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
assets/static/custom\.css
config\.json
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"
},
{
"ImportPath": "github.com/gmemstr/feeds",
"Rev": "c8f8657f3a1f60cff88624dc069d369d77c658a1"
"ImportPath": "github.com/gorilla/feeds",
"Comment": "v1.0.0",
"Rev": "4b936b5221c53c99fcd4b15ac0b8c38ff490ab89"
},
{
"ImportPath": "github.com/gorilla/mux",
"Comment": "v1.4.0-10-g18fca31",
"Rev": "18fca31550181693b3a834a15b74b564b3605876"
},
{
"ImportPath": "github.com/mattn/go-sqlite3",
"Comment": "v1.3.0",
"Rev": "5160b48509cf5c877bc22c11c373f8c7738cdb38"
},
{
"ImportPath": "golang.org/x/crypto/bcrypt",
"Rev": "9419663f5a44be8b34ca85f08abc5fe1be11f8a3"
@ -25,6 +31,10 @@
"ImportPath": "golang.org/x/crypto/blowfish",
"Rev": "9419663f5a44be8b34ca85f08abc5fe1be11f8a3"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "3da985ce5951d99de868be4385f21ea6c2b22f24"
},
{
"ImportPath": "golang.org/x/sys/unix",
"Rev": "0b25a408a50076fbbcae6b7ac0ea5fbb0b085e79"

View file

@ -1,48 +1,68 @@
# Pogo
## Podcast RSS feed generator and CMS in Go.
<img src="https://cdn.rawgit.com/gmemstr/pogo/users/assets/web/static/logo-sm.png" alt="Pogo logo" align="right">
## 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)
## 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
* Basic frontend for listening to episodes
* Flat-file directory structure
* Human readable files
* Self publishing interface w/ password protection
* Custom CSS and themeing capabilities
* JSON feed generation for easier parsing
* 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)
1. [Download the latest release](https://github.com/gmemstr/pogo/releases/latest)
2. Unzip somewhere safe
3. [Edit the config](https://github.com/gmemstr/pogo/wiki/Configuration)
4. Run `pogo`
5. Navigate to your instance (`localhost:3000` by default)
6. Login to the admin interface (default: **admin**, **password1**)
7. **CHANGE YOUR PASSWORD**
## 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
# Build
go build
# Set environment variable
export POGO_SECRET=secret
# Windows
# set POGO_SECRET=secret
./podcast
# Run
./pogo
```
## 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
separated by underscores (`YYYY-MM-DD_TITLE.mp3`).
"Shownote" files are markdown formatted and simply append `_SHOWNOTES.md` to the existing filename (sans .mp3 of course).
- [Golang](https://golang.org/)
- [gorilla/mux](http://github.com/gorilla/mux)
- [gorilla/feeds](http://github.com/gorilla/feeds)
- [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"
"os"
"strings"
"encoding/json"
"golang.org/x/crypto/bcrypt"
"database/sql"
_ "github.com/mattn/go-sqlite3"
"github.com/gorilla/mux"
"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
@ -95,7 +351,7 @@ func EditEpisode() common.Handler {
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
}
}

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>
<div class="container">
<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>
<router-view></router-view>
</div>
</div>
<footer>
<p>Pogo licensed under the GPLv3</p>
</footer>
</div>
<script src="/assets/vue.js"></script>
<script src="https://unpkg.com/vue-router@2.7.0/dist/vue-router.js"></script>
<script src="/assets/app.js"></script>

View file

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

View file

@ -1,9 +1,42 @@
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 = {
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>',
const message = {
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() {
return {
loading: false,
@ -25,12 +58,151 @@ const episodemanagement = {
this.error = this.items = []
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
if (err) {
this.error = err.toString()
} else {
console.log(items);
var t = JSON.parse(items).items
for (var i = t.length - 1; i >= 0; i--) {
this.items.push({
@ -46,7 +218,20 @@ const episodemanagement = {
}
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() {
return {
loading: false,
@ -68,7 +253,7 @@ const episodeedit = {
this.error = this.episode = {}
this.loading = true
getEpisodes((err, items) => {
get("/json", (err, items) => {
this.loading = false
if (err) {
this.error = err.toString()
@ -96,7 +281,14 @@ const episodeedit = {
}
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() {
return {
loading: false,
@ -118,7 +310,7 @@ const customcss = {
this.error = this.css = null
this.loading = true
getCss((err, css) => {
get("/admin/css", (err, css) => {
this.loading = false
if (err) {
this.error = err.toString()
@ -131,10 +323,15 @@ const customcss = {
}
const routes = [
{path: '/', redirect: '/publish'},
{ path: '/publish', component: episodepublishform },
{ path: '/manage', component: episodemanagement },
{ 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({
@ -148,22 +345,12 @@ const app = new Vue({
}
}).$mount('#app')
function getCss(callback) {
function get(url,callback) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200)
callback(null, xmlHttp.responseText)
}
xmlHttp.open("GET", "/admin/css", 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.open("GET", url, true);
xmlHttp.send(null);
}

View file

@ -8,7 +8,27 @@
.container {} /* Basic container from styles.css */
.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 */

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 {
font-weight: 400;
}
.container {
margin: 0 auto;
padding: 0 2.0rem;
position: relative;
width: 60vw;
.podcastlist {
padding-bottom: 10%;
}
.container {
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 {
width:70%;
}
@ -39,3 +94,27 @@ hr {
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"
"github.com/fsnotify/fsnotify"
"github.com/gmemstr/feeds"
"github.com/gorilla/feeds"
)
type Config struct {

View file

@ -8,6 +8,9 @@ import (
"net/http"
"strings"
"golang.org/x/crypto/bcrypt"
"database/sql"
_ "github.com/mattn/go-sqlite3"
"github.com/gorilla/mux"
"github.com/gmemstr/pogo/admin"
@ -80,6 +83,19 @@ func Init() *mux.Router {
admin.CreateEpisode(),
)).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(
auth.RequireAuthorization(),
admin.EditEpisode(),
@ -95,6 +111,16 @@ func Init() *mux.Router {
admin.CustomCss(),
)).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(
serveSetup(),
)).Methods("GET", "POST")
@ -104,6 +130,16 @@ func Init() *mux.Router {
func loginHandler() 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,
}
}
statement, err := db.Prepare("SELECT * FROM users WHERE username=?")
if _, err := auth.DecryptCookie(r); err == nil {
http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect)
@ -115,17 +151,7 @@ func loginHandler() common.Handler {
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()
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in parsing form: %v", err),
@ -135,37 +161,49 @@ func loginHandler() common.Handler {
username := r.Form.Get("username")
password := r.Form.Get("password")
rows, err := statement.Query(username)
if username == "" || password == "" {
return &common.HTTPError{
Message: "username or password is empty",
StatusCode: http.StatusBadRequest,
}
}
var u map[string]string
err = json.Unmarshal(d, &u) // Unmarshal into interface
// Iterate through map until we find matching username
for k, v := range u {
if k == username && bcrypt.CompareHashAndPassword([]byte(v), []byte(password)) == nil {
// Create a cookie here because the credentials are correct
c, err := auth.CreateSession(&common.User{
Username: k,
})
if err != nil {
return &common.HTTPError{
Message: err.Error(),
StatusCode: http.StatusInternalServerError,
}
var id int
var dbun string
var dbhsh string
var dbrn string
var dbem string
for rows.Next() {
err := rows.Scan(&id,&dbun,&dbhsh,&dbrn,&dbem)
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in decoding sql data", err),
StatusCode: http.StatusBadRequest,
}
// 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{
Message: "Invalid credentials!",

View file

@ -23,6 +23,6 @@ func main() {
// Define routes
// We're live
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))
}