Merge pull request #16 from gmemstr/users

Users
This commit is contained in:
Gabriel Simmer 2017-11-23 09:20:32 -08:00 committed by GitHub
commit ddc9ff3aeb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 753 additions and 107 deletions

5
.gitignore vendored
View file

@ -19,7 +19,6 @@ podcasts/
feed\.rss feed\.rss
assets/static/custom\.css
config\.json
vendor/ vendor/
assets/config/users\.db

30
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,30 @@
# Contributing
Generally, any contributions to Pogo are more than welcome, but we'd like it if you follow a couple guidelines. We'll also point out a couple of tricks for ease of use.
First, fork the repository and clone it locally
```
git clone git@github.com:your-username/pogo.git
```
Set up Go and install the dependencies
```
cd pogo
go get github.com/tools/godep
godep restore
```
Then make your changes. If you use Sublime Text 3, please check out [our snippets](https://gist.github.com/gmemstr/60831109f0ae6c40861c1751a367524e) to add some shortcuts to make your life easier.
The platform is divided into two main parts: The main Go app, which does everything from generating RSS to serving up webpages, and the Vue.js app portion, which currently handles the admin interface but will be implemented into the main frontend as well.
Once you've made your changes, please make sure the app can build
```
go build
```
Once you've verified your addition works, push to your repository and [create a pull request](https://github.com/gmemstr/pogo/compare). During the review your PR will also pass through our TravisCI testing as well.

14
Godeps/Godeps.json generated
View file

@ -9,14 +9,20 @@
"Rev": "4da3e2cfbabc9f751898f250b49f2439785783a1" "Rev": "4da3e2cfbabc9f751898f250b49f2439785783a1"
}, },
{ {
"ImportPath": "github.com/gmemstr/feeds", "ImportPath": "github.com/gorilla/feeds",
"Rev": "c8f8657f3a1f60cff88624dc069d369d77c658a1" "Comment": "v1.0.0",
"Rev": "4b936b5221c53c99fcd4b15ac0b8c38ff490ab89"
}, },
{ {
"ImportPath": "github.com/gorilla/mux", "ImportPath": "github.com/gorilla/mux",
"Comment": "v1.4.0-10-g18fca31", "Comment": "v1.4.0-10-g18fca31",
"Rev": "18fca31550181693b3a834a15b74b564b3605876" "Rev": "18fca31550181693b3a834a15b74b564b3605876"
}, },
{
"ImportPath": "github.com/mattn/go-sqlite3",
"Comment": "v1.3.0",
"Rev": "5160b48509cf5c877bc22c11c373f8c7738cdb38"
},
{ {
"ImportPath": "golang.org/x/crypto/bcrypt", "ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "9419663f5a44be8b34ca85f08abc5fe1be11f8a3" "Rev": "9419663f5a44be8b34ca85f08abc5fe1be11f8a3"
@ -25,6 +31,10 @@
"ImportPath": "golang.org/x/crypto/blowfish", "ImportPath": "golang.org/x/crypto/blowfish",
"Rev": "9419663f5a44be8b34ca85f08abc5fe1be11f8a3" "Rev": "9419663f5a44be8b34ca85f08abc5fe1be11f8a3"
}, },
{
"ImportPath": "golang.org/x/net/context",
"Rev": "3da985ce5951d99de868be4385f21ea6c2b22f24"
},
{ {
"ImportPath": "golang.org/x/sys/unix", "ImportPath": "golang.org/x/sys/unix",
"Rev": "0b25a408a50076fbbcae6b7ac0ea5fbb0b085e79" "Rev": "0b25a408a50076fbbcae6b7ac0ea5fbb0b085e79"

View file

@ -1,48 +1,68 @@
# Pogo <img src="https://cdn.rawgit.com/gmemstr/pogo/users/assets/web/static/logo-sm.png" alt="Pogo logo" align="right">
## Podcast RSS feed generator and CMS in Go.
## Pogo
Podcast RSS feed generator and CMS in Go.
## Getting Started
There are a couple options for getting Pogo up and running.
- [Download the latest release](https://github.com/gmemstr/pogo/releases/latest)
- [Clone the repo and build](#building)
## Status
[![Build Status](https://travis-ci.org/gmemstr/pogo.svg?branch=master)](https://travis-ci.org/gmemstr/pogo) [![gitgalaxy](https://img.shields.io/badge/website-gitgalaxy.com-blue.svg)](https://gitgalaxy.com) [![live branch](https://img.shields.io/badge/live-podcast.gitgalaxy.com-green.svg)](https://podcast.gitgalaxy.com) [![follow](https://img.shields.io/twitter/follow/gitgalaxy.svg?style=social&label=Follow)](https://twitter.com/gitgalaxy) [![Build Status](https://travis-ci.org/gmemstr/pogo.svg?branch=master)](https://travis-ci.org/gmemstr/pogo) [![gitgalaxy](https://img.shields.io/badge/website-gitgalaxy.com-blue.svg)](https://gitgalaxy.com) [![live branch](https://img.shields.io/badge/live-podcast.gitgalaxy.com-green.svg)](https://podcast.gitgalaxy.com) [![follow](https://img.shields.io/twitter/follow/gitgalaxy.svg?style=social&label=Follow)](https://twitter.com/gitgalaxy)
## Goal ## Features
To produce a product that is easy to deploy and easier to use when hosting a podcast from ones own servers. - Automatic RSS and JSON feed generation
- Frontend for listening and publishing episodes
- Multiple user support
- Custom CSS themes
- Docker support
## Features ## Running
* Auto-generate rss feed 1. [Download the latest release](https://github.com/gmemstr/pogo/releases/latest)
* Basic frontend for listening to episodes 2. Unzip somewhere safe
* Flat-file directory structure 3. [Edit the config](https://github.com/gmemstr/pogo/wiki/Configuration)
* Human readable files 4. Run `pogo`
* Self publishing interface w/ password protection 5. Navigate to your instance (`localhost:3000` by default)
* Custom CSS and themeing capabilities 6. Login to the admin interface (default: **admin**, **password1**)
* JSON feed generation for easier parsing 7. **CHANGE YOUR PASSWORD**
* Docker support
## Requirements
[github.com/gmemstr/feeds](https://github.com/gmemstr/feeds) _this branch contains some fixes for "podcast specific" tags_
[github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify)
[github.com/gorilla/mux](https://github.com/gorilla/mux)
## Building ## Building
_Note: [This requires a valid Go enviornment setup!](https://golang.org/doc/install)_
``` ```
# Go get the repository
go get github.com/gmemstr/pogo
# Go to directory
cd $GOPATH/src/github.com/gmemstr/pogo
# Get godep
go get github.com/tools/godep
# Install Go dependencies
godep restore godep restore
# Build
go build go build
# Set environment variable
export POGO_SECRET=secret # Run
# Windows ./pogo
# set POGO_SECRET=secret
./podcast
``` ```
## File format ## Credits
Pogo uses a flat file structure for managing podcast episodes. As such, files have a special naming convention. Pogo depends on several other open source projects to function.
For podcast audio files, filenames take the form of YEAR-MONTH-DAY followed by the title. The two values are - [Golang](https://golang.org/)
separated by underscores (`YYYY-MM-DD_TITLE.mp3`). - [gorilla/mux](http://github.com/gorilla/mux)
- [gorilla/feeds](http://github.com/gorilla/feeds)
"Shownote" files are markdown formatted and simply append `_SHOWNOTES.md` to the existing filename (sans .mp3 of course). - [fsnotify/fsnotify](http://github.com/fsnotify/fsnotify)
- [mattn/go-sqlite3](http://github.com/mattn/go-sqlite3)

View file

@ -13,9 +13,265 @@ import (
"net/http" "net/http"
"os" "os"
"strings" "strings"
"encoding/json"
"golang.org/x/crypto/bcrypt"
"database/sql"
_ "github.com/mattn/go-sqlite3"
"github.com/gorilla/mux"
"github.com/gmemstr/pogo/common" "github.com/gmemstr/pogo/common"
) )
type User struct {
Id int `json:"id"`
Dbun string `json:"username"`
Dbrn string `json:"realname"`
Dbem string `json:"email"`
}
type UserList struct {
Users []User
}
/*
* The following is a set of admin commands
* that the average user probably shouldn't be
* able to have access to, mostly user management.
*/
func AddUser() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
db, err := sql.Open("sqlite3", "assets/config/users.db")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error opening sqlite3 file: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
statement, err := db.Prepare("INSERT INTO users(username,hash,realname,email) VALUES (?,?,?,?)")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error preparing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
err = r.ParseMultipartForm(32 << 20)
if err != nil {
return &common.HTTPError{
Message: err.Error(),
StatusCode: http.StatusBadRequest,
}
}
username := strings.Join(r.Form["username"], "")
password := strings.Join(r.Form["password"], "")
realname := strings.Join(r.Form["realname"], "")
email := strings.Join(r.Form["email"], "")
hash, err := bcrypt.GenerateFromPassword([]byte(password), 4)
_, err = statement.Exec(username,hash,realname,email)
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error executing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
w.Write([]byte("<script>window.location = '/admin#/users/added';</script>"))
db.Close()
return nil
}
}
func EditUser() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
db, err := sql.Open("sqlite3", "assets/config/users.db")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in reading user database: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
err = r.ParseMultipartForm(32 << 20)
if err != nil {
return &common.HTTPError{
Message: err.Error(),
StatusCode: http.StatusBadRequest,
}
}
id := strings.Join(r.Form["id"], "")
username := strings.Join(r.Form["username"], "")
password := strings.Join(r.Form["oldpw"], "")
newpassword := strings.Join(r.Form["newpw1"], "")
realname := strings.Join(r.Form["realname"], "")
email := strings.Join(r.Form["email"], "")
pwhash, err := bcrypt.GenerateFromPassword([]byte(password), 4)
statement, err := db.Prepare("UPDATE users SET username=?, hash=?, realname=?, email=? WHERE id=?")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error preparing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
pwstatement, err := db.Prepare("SELECT hash FROM users WHERE id=?")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error preparing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
tmp, err := pwstatement.Query(id)
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error executing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
var hash []byte
for tmp.Next() {
err = tmp.Scan(&hash)
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error executing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
}
fmt.Println(hash)
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) != nil {
fmt.Println("Passwords do not match")
w.Write([]byte("<script>window.location = '/admin#/users/editerror';</script>"))
db.Close()
return nil
}
if newpassword != "" {
pwhash, err = bcrypt.GenerateFromPassword([]byte(newpassword), 4)
}
_, err = statement.Exec(username,pwhash,realname,email,id)
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error executing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
w.Write([]byte("<script>window.location = '/admin#/users/edited';</script>"))
db.Close()
return nil
}
}
func DeleteUser() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
db, err := sql.Open("sqlite3", "assets/config/users.db")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error opening sqlite3 file: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
statement, err := db.Prepare("DELETE FROM users WHERE id=?")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error preparing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
if err != nil {
return &common.HTTPError{
Message: err.Error(),
StatusCode: http.StatusBadRequest,
}
}
vars := mux.Vars(r)
id := vars["id"]
if id == "1" {
w.Write([]byte("<script>window.location = '/admin#/msg/Cannot%20Delete%20Admin%20User';</script>"))
db.Close()
return nil
}
_, err = statement.Exec(id)
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error executing sqlite3 statement: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
w.Write([]byte("<script>window.location = '/admin#/msg/Deleted%20User';</script>"))
db.Close()
return nil
}
}
func ListUsers() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
db, err := sql.Open("sqlite3", "assets/config/users.db")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in reading user database: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
// NEVER SELECT hash ENTRY
statement, err := db.Prepare("SELECT id,username,realname,email FROM users")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in reading user database: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
rows, err := statement.Query()
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in executing user SELECT: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
res := []User{}
for rows.Next() {
var u User
err := rows.Scan(&u.Id, &u.Dbun, &u.Dbrn, &u.Dbem)
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in decoding sql data", err),
StatusCode: http.StatusBadRequest,
}
}
res = append(res, u)
}
fin, err := json.Marshal(res)
w.Write(fin)
db.Close()
return nil
}
}
/*************************************
* End of "sensitive" admin functions
* ***********************************/
// Write custom CSS to disk or send it back to the client if GET // Write custom CSS to disk or send it back to the client if GET
@ -95,7 +351,7 @@ func EditEpisode() common.Handler {
StatusCode: http.StatusBadRequest, StatusCode: http.StatusBadRequest,
} }
} }
w.Write([]byte("<script>window.location = '/admin#published';</script>")) w.Write([]byte("<script>window.location = '/admin#/msg/Episode%20Published!';</script>"))
return nil return nil
} }
} }

View file

@ -0,0 +1,8 @@
{
"Name": "Pogo Test Feed",
"Host": "Gabriel Simmer",
"Email": "admin@localhost",
"Description": "Discussion about open source projects on the internet.",
"Image": "localhost:3000/assets/logo-xs.png",
"PodcastUrl": "http://localhost:3000"
}

BIN
assets/config/users.db Normal file

Binary file not shown.

View file

@ -1,4 +0,0 @@
{
"admin": "$2a$04$ZAf88Bao4Q768vKfCaKBlOqtPumwKwFhrcpBCdfMWWFX69wyhgTqi",
"gabriel": "$2a$04$KrhZ1q6FpOGqs0FVKMYhQ.BTYeVXztnjrM9RbK.0buI1OHfmyNEAy"
}

View file

@ -9,14 +9,15 @@
<body> <body>
<div class="container"> <div class="container">
<div id="app"> <div id="app">
<router-link to="/publish">Publish</router-link> <router-link to="/theme">Theme</router-link> <router-link to="/manage">Manage</router-link> <nav>
<router-link to="/publish">Publish</router-link> <router-link to="/manage">Episodes</router-link> <router-link to="/theme">Theme</router-link> <router-link to="/users">Users</router-link></nav>
<h1>{{ header }}</h1> <h1>{{ header }}</h1>
<router-view></router-view> <router-view></router-view>
</div> </div>
</div>
<footer> <footer>
<p>Pogo licensed under the GPLv3</p> <p>Pogo licensed under the GPLv3</p>
</footer> </footer>
</div>
<script src="/assets/vue.js"></script> <script src="/assets/vue.js"></script>
<script src="https://unpkg.com/vue-router@2.7.0/dist/vue-router.js"></script> <script src="https://unpkg.com/vue-router@2.7.0/dist/vue-router.js"></script>
<script src="/assets/app.js"></script> <script src="/assets/app.js"></script>

View file

@ -12,12 +12,14 @@
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="main">
<h1 id="title" class="title">Loading</h1> <h1 id="title" class="title">Loading</h1>
<h3><a href="/admin" class="adminlink">Admin</a></h3> <h3><a href="/admin" class="adminlink">Admin</a></h3>
<div id="podcasts" class="podcastlist"> <div id="podcasts" class="podcastlist">
</div> </div>
</div>
<footer> <footer>
<p>Pogo licensed under the GPLv3 | <a href="/rss">RSS Feed</a> <p>Pogo licensed under the GPLv3 | <a href="/rss">RSS Feed</a>
</footer> </footer>

View file

@ -1,9 +1,42 @@
const episodepublishform = { const episodepublishform = {
template: '<div><h3>Publish Episode</h3><form enctype="multipart/form-data" action="/admin/publish" method="post"><label for="title">Episode Title</label><input type="text" id="title" name="title"><label for="description">Episode Description</label><textarea name="description" id="description" cols="100" rows="20" style="resize: none;"></textarea><label for="file">Media File</label><input type="file" id="file" name="file"><label for="date">Publish Date</label><input type="date" id="date" name="date"><input type="submit" value="Publish"></form></div>' template: `<div>
<h3>Publish Episode</h3>
<form enctype="multipart/form-data" action="/admin/publish" method="post" class="publish">
<label for="title">Episode Title</label>
<input type="text" id="title" name="title">
<label for="description">Episode Description</label>
<textarea name="description" id="description" style="resize: none;" class="epdesc"></textarea>
<label for="file">Media File</label>
<input type="file" id="file" name="file">
<label for="date">Publish Date</label>
<input type="date" id="date" name="date"><br /><br />
<input type="submit" value="Publish" class="button">
</form>
</div>`
} }
const episodemanagement = { const message = {
template: '<div><table style="width:100%"><tr><th>Title</th><th>URL</th><th>Actions</th></tr><tr v-for="item in items"><td>{{ item.id }}: {{ item.title }}</td><td>{{ item.url }}</td><td><router-link :to="\'edit/\' + item.id">Edit</router-link></td></tr></table></div>', template: `<div><h3>{{ this.$route.params.message }}</h3></div>`
}
const userlist = {
template: `<div>
<router-link :to="\'users/new\'" tag="button">New</router-link>
<table>
<tr>
<th>Username</th>
<th>Email</th>
<th></th>
</tr>
<tr v-for="item in items">
<td>{{ item.username }}</td>
<td>{{ item.email }}</td>
<td>
<router-link :to="\'user/\' + item.id" class="button">Edit</router-link>
</td>
</tr>
</table>
</div>`,
data() { data() {
return { return {
loading: false, loading: false,
@ -25,12 +58,151 @@ const episodemanagement = {
this.error = this.items = [] this.error = this.items = []
this.loading = true this.loading = true
getEpisodes((err, items) => { get("/admin/listusers", (err, items) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
var t = JSON.parse(items).reverse();
for (var i = t.length - 1; i >= 0; i--) {
this.items.push({
id: t[i].id,
username: t[i].username,
email: t[i].email,
})
}
}
})
}
}
}
const usernew = {
template: `<div>
<div>
<h3>New User</h3>
<form enctype="multipart/form-data" action="/admin/adduser" method="post">
<label for="username">Username</label>
<input type="text" id="username" name="username">
<label for="email">Email</label>
<input type="text" id="email" name="email">
<label for="realname">Real Name</label>
<input type="text" id="realname" name="realname">
<label for="password">New Password</label>
<input type="password" id="password" name="password">
<br /><br />
<input type="submit" class="button" value="Save"></form>
</div>
</div>`
}
const useredit = {
template: `<div>
<div>
<h3>Edit User</h3>
<form enctype="multipart/form-data" action="/admin/edituser" method="post">
<label for="username">Username</label>
<input type="text" id="username" name="username" :value="user.username">
<label for="email">Email</label>
<input type="text" id="email" name="email" :value="user.email">
<label for="realname">Real Name</label>
<input type="text" id="realname" name="realname" :value="user.realname">
<label for="newpw1">New Password</label>
<input type="password" id="newpw1" name="newpw1">
<label for="newpw2">Repeat New Password</label>
<input type="password" id="newpw2" name="newpw2">
<label for="oldpw">Old Password</label>
<input type="password" id="oldpw" name="oldpw">
<input name="id" id="id" :value="user.id" type="hidden">
<br /><br />
<input type="submit" class="button" value="Save" class="button"></form>
<a v-bind:href="'/admin/deleteuser/'+user.id+''" class="button">Delete User</a>
</div>
</div>`,
data() {
return {
loading: false,
user: null,
error: null
}
},
created() {
// fetch the data when the view is created and the data is
// already being observed
this.fetchData()
},
watch: {
// call again the method if the route changes
'$route': 'fetchData'
},
methods: {
fetchData() {
this.error = this.user = []
this.loading = true
get("/admin/listusers", (err, items) => {
this.loading = false
if (err) {
this.error = err.toString()
} else {
var t = JSON.parse(items)
for (var i = t.length - 1; i >= 0; i--) {
if (t[i].id == this.$route.params.id) {
this.user = {
id: t[i].id,
username: t[i].username,
email: t[i].email,
realname: t[i].realname
}
}
}
}
})
}
}
}
const episodemanagement = {
template: `<div>
<table style="width:100%">
<tr>
<th>Title</th>
<th>URL</th>
<th></th>
</tr>
<tr v-for="item in items">
<td>{{ item.id }}: {{ item.title }}</td><td>{{ item.url }}</td><td><router-link class="button" :to="\'edit/\' + item.id">Edit</router-link></td>
</tr>
</table>
</div>`,
data() {
return {
loading: false,
items: null,
error: null
}
},
created() {
// fetch the data when the view is created and the data is
// already being observed
this.fetchData()
},
watch: {
// call again the method if the route changes
'$route': 'fetchData'
},
methods: {
fetchData() {
this.error = this.items = []
this.loading = true
get("/json", (err, items) => {
this.loading = false this.loading = false
if (err) { if (err) {
this.error = err.toString() this.error = err.toString()
} else { } else {
console.log(items);
var t = JSON.parse(items).items var t = JSON.parse(items).items
for (var i = t.length - 1; i >= 0; i--) { for (var i = t.length - 1; i >= 0; i--) {
this.items.push({ this.items.push({
@ -46,7 +218,20 @@ const episodemanagement = {
} }
const episodeedit = { const episodeedit = {
template: '<div><div><h3>Edit Episode</h3><form enctype="multipart/form-data" action="/admin/edit" method="post"><label for="title">Episode Title</label><input type="text" id="title" name="title" :value="episode.title"><label for="description">Episode Description</label><textarea name="description" id="description" cols="100" rows="20" style="resize: none;">{{ episode.description }}</textarea><label for="date">Publish Date</label><input type="date" id="date" name="date" :value="episode.time"><input name="previousfilename" id="previousfilename" :value="episode.previousfilename" type="hidden"><input type="submit" value="Publish"></form></div></div>', template: `<div>
<div>
<h3>Edit Episode</h3>
<form enctype="multipart/form-data" action="/admin/edit" method="post">
<label for="title">Episode Title</label>
<input type="text" id="title" name="title" :value="episode.title">
<label for="description">Episode Description</label>
<textarea name="description" id="description" cols="100" rows="20" style="resize: none;">{{ episode.description }}</textarea>
<label for="date">Publish Date</label>
<input type="date" id="date" name="date" :value="episode.time">
<input name="previousfilename" id="previousfilename" :value="episode.previousfilename" type="hidden">
<input type="submit" class="button" value="Publish"></form>
</div>
</div>`,
data() { data() {
return { return {
loading: false, loading: false,
@ -68,7 +253,7 @@ const episodeedit = {
this.error = this.episode = {} this.error = this.episode = {}
this.loading = true this.loading = true
getEpisodes((err, items) => { get("/json", (err, items) => {
this.loading = false this.loading = false
if (err) { if (err) {
this.error = err.toString() this.error = err.toString()
@ -96,7 +281,14 @@ const episodeedit = {
} }
const customcss = { const customcss = {
template: '<div><h3>Edit CSS</h3><form action="/admin/css" method="post" enctype="multipart/form-data"><label for="css">Custom CSS</label><textarea name="css" id="css" cols="120" rows="20">{{ css }}</textarea><br /><input type="submit" value="Submit"></form></div>', template: `<div>
<h3>Theme</h3>
<form action="/admin/css" method="post" enctype="multipart/form-data">
<textarea spellcheck="false" name="css" id="css" cols="120" rows="20" class="css">{{ css }}</textarea>
<br /><br />
<input type="submit" class="button" value="Submit" class="button">
</form>
</div>`,
data() { data() {
return { return {
loading: false, loading: false,
@ -118,7 +310,7 @@ const customcss = {
this.error = this.css = null this.error = this.css = null
this.loading = true this.loading = true
getCss((err, css) => { get("/admin/css", (err, css) => {
this.loading = false this.loading = false
if (err) { if (err) {
this.error = err.toString() this.error = err.toString()
@ -131,10 +323,15 @@ const customcss = {
} }
const routes = [ const routes = [
{path: '/', redirect: '/publish'},
{ path: '/publish', component: episodepublishform }, { path: '/publish', component: episodepublishform },
{ path: '/manage', component: episodemanagement }, { path: '/manage', component: episodemanagement },
{ path: '/theme', component: customcss }, { path: '/theme', component: customcss },
{ path: '/edit/:id', component: episodeedit } { path: '/edit/:id', component: episodeedit },
{ path: '/users/', component: userlist },
{ path: '/msg/:message', component: message },
{ path: '/user/:id', component: useredit },
{ path: '/users/new', component: usernew }
] ]
const router = new VueRouter({ const router = new VueRouter({
@ -148,22 +345,12 @@ const app = new Vue({
} }
}).$mount('#app') }).$mount('#app')
function getCss(callback) { function get(url,callback) {
var xmlHttp = new XMLHttpRequest(); var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() { xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
callback(null, xmlHttp.responseText) callback(null, xmlHttp.responseText)
} }
xmlHttp.open("GET", "/admin/css", true); xmlHttp.open("GET", url, true);
xmlHttp.send(null);
}
function getEpisodes(callback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
callback(null, xmlHttp.responseText)
}
xmlHttp.open("GET", "/json", true);
xmlHttp.send(null); xmlHttp.send(null);
} }

View file

@ -8,7 +8,27 @@
.container {} /* Basic container from styles.css */ .container {} /* Basic container from styles.css */
.title {} /* Page title */ .title {} /* Page title */
.adminlink {} /* Link to admin interface */ .adminlink { /* Link to admin interface */
margin-left:100%;
display:inline-block;
margin-bottom: 10px;
background-color: #397AD6;
border: none;
color: white;
padding: 5px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
-webkit-transition:.52s;
-moz-transition: .5s;
-o-transition: .5s;
-ms-transition: .5s;
transition:.5s;
}
.adminlink:hover {
background-color: #50B7D5;
}
.podcastlist {} /* Chronological podcast list */ .podcastlist {} /* Chronological podcast list */

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View file

@ -12,13 +12,68 @@ body {
h1,h2,h3,h4,h5 { h1,h2,h3,h4,h5 {
font-weight: 400; font-weight: 400;
} }
.container { .podcastlist {
margin: 0 auto; padding-bottom: 10%;
padding: 0 2.0rem; }
position: relative; .container {
width: 60vw; margin: 0 auto;
padding: 0 2.0rem;
position: relative;
width: 60vw;
height: 100%;
}
button, .button {
margin-bottom: 10px;
background-color: #397AD6;
border: none;
color: white;
padding: 5px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
-webkit-transition:.52s;
-moz-transition: .5s;
-o-transition: .5s;
-ms-transition: .5s;
transition:.5s;
}
button:hover, .button:hover {
background-color: #50B7D5;
}
footer {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: .25rem;
color: #f9f9f9;
background-color: #397AD6;
text-align: center;
}
table {
text-align: left;
width: 100%;
}
nav a {
margin-bottom: 10px;
background-color: #397AD6;
border: none;
color: white;
padding: 5px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
-webkit-transition:.52s;
-moz-transition: .5s;
-o-transition: .5s;
-ms-transition: .5s;
transition:.5s;
}
nav a:hover {
background-color: #50B7D5;
} }
.podcast { .podcast {
width:70%; width:70%;
} }
@ -39,3 +94,27 @@ hr {
text-align: center; text-align: center;
} }
.css {
font-family: Monospace;
}
input[type=text], input[type=date], input[type=file], input[type=password],textarea {
padding:10px;
border-radius: 4px;
box-sizing: border-box;
border: 1px solid #397AD6;
}
.publish [type=text],
.publish input[type=date],
.publish input[type=file],
.publish input[type=password],
.publish textarea {
width: 100%;
}
.publish textarea {
height: 30vh;
}
.epdesc {
font-family: 'Muli', sans-serif;
}

View file

@ -17,7 +17,7 @@ import (
"encoding/json" "encoding/json"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/gmemstr/feeds" "github.com/gorilla/feeds"
) )
type Config struct { type Config struct {

View file

@ -8,6 +8,9 @@ import (
"net/http" "net/http"
"strings" "strings"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"database/sql"
_ "github.com/mattn/go-sqlite3"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gmemstr/pogo/admin" "github.com/gmemstr/pogo/admin"
@ -80,6 +83,19 @@ func Init() *mux.Router {
admin.CreateEpisode(), admin.CreateEpisode(),
)).Methods("POST") )).Methods("POST")
r.Handle("/admin/edituser", Handle(
auth.RequireAuthorization(),
admin.EditUser(),
)).Methods("POST")
r.Handle("/admin/newuser", Handle(
auth.RequireAuthorization(),
admin.AddUser(),
)).Methods("POST")
r.Handle("/admin/deleteuser/{id}", Handle(
auth.RequireAuthorization(),
admin.DeleteUser(),
)).Methods("GET")
r.Handle("/admin/edit", Handle( r.Handle("/admin/edit", Handle(
auth.RequireAuthorization(), auth.RequireAuthorization(),
admin.EditEpisode(), admin.EditEpisode(),
@ -95,6 +111,16 @@ func Init() *mux.Router {
admin.CustomCss(), admin.CustomCss(),
)).Methods("GET", "POST") )).Methods("GET", "POST")
r.Handle("/admin/adduser", Handle(
auth.RequireAuthorization(),
admin.AddUser(),
)).Methods("POST")
r.Handle("/admin/listusers", Handle(
auth.RequireAuthorization(),
admin.ListUsers(),
)).Methods("GET")
r.Handle("/setup", Handle( r.Handle("/setup", Handle(
serveSetup(), serveSetup(),
)).Methods("GET", "POST") )).Methods("GET", "POST")
@ -104,6 +130,16 @@ func Init() *mux.Router {
func loginHandler() common.Handler { func loginHandler() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError { return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
db, err := sql.Open("sqlite3", "assets/config/users.db")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in reading user database: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
statement, err := db.Prepare("SELECT * FROM users WHERE username=?")
if _, err := auth.DecryptCookie(r); err == nil { if _, err := auth.DecryptCookie(r); err == nil {
http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect) http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect)
@ -115,17 +151,7 @@ func loginHandler() common.Handler {
return common.ReadAndServeFile("assets/web/login.html", w) return common.ReadAndServeFile("assets/web/login.html", w)
} }
d, err := ioutil.ReadFile("assets/config/users.json")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in reading users.json: %v", err),
StatusCode: http.StatusInternalServerError,
}
}
err = r.ParseForm() err = r.ParseForm()
if err != nil { if err != nil {
return &common.HTTPError{ return &common.HTTPError{
Message: fmt.Sprintf("error in parsing form: %v", err), Message: fmt.Sprintf("error in parsing form: %v", err),
@ -135,37 +161,49 @@ func loginHandler() common.Handler {
username := r.Form.Get("username") username := r.Form.Get("username")
password := r.Form.Get("password") password := r.Form.Get("password")
rows, err := statement.Query(username)
if username == "" || password == "" { if username == "" || password == "" {
return &common.HTTPError{ return &common.HTTPError{
Message: "username or password is empty", Message: "username or password is empty",
StatusCode: http.StatusBadRequest, StatusCode: http.StatusBadRequest,
} }
} }
var id int
var u map[string]string var dbun string
err = json.Unmarshal(d, &u) // Unmarshal into interface var dbhsh string
var dbrn string
// Iterate through map until we find matching username var dbem string
for k, v := range u { for rows.Next() {
if k == username && bcrypt.CompareHashAndPassword([]byte(v), []byte(password)) == nil { err := rows.Scan(&id,&dbun,&dbhsh,&dbrn,&dbem)
// Create a cookie here because the credentials are correct if err != nil {
c, err := auth.CreateSession(&common.User{ return &common.HTTPError{
Username: k, Message: fmt.Sprintf("error in decoding sql data", err),
}) StatusCode: http.StatusBadRequest,
if err != nil {
return &common.HTTPError{
Message: err.Error(),
StatusCode: http.StatusInternalServerError,
}
} }
// r.AddCookie(c)
w.Header().Add("Set-Cookie", c.String())
// And now redirect the user to admin page
http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect)
return nil
} }
} }
// Create a cookie here because the credentials are correct
if dbun == username && bcrypt.CompareHashAndPassword([]byte(dbhsh), []byte(password)) == nil {
c, err := auth.CreateSession(&common.User{
Username: username,
})
if err != nil {
return &common.HTTPError{
Message: err.Error(),
StatusCode: http.StatusInternalServerError,
}
}
// r.AddCookie(c)
w.Header().Add("Set-Cookie", c.String())
// And now redirect the user to admin page
http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect)
db.Close()
return nil
}
return &common.HTTPError{ return &common.HTTPError{
Message: "Invalid credentials!", Message: "Invalid credentials!",

View file

@ -23,6 +23,6 @@ func main() {
// Define routes // Define routes
// We're live // We're live
r := router.Init() r := router.Init()
fmt.Println("Listening on port :3000") fmt.Println("Your Pogo instance is live on port :3000")
log.Fatal(http.ListenAndServe(":3000", r)) log.Fatal(http.ListenAndServe(":3000", r))
} }