mirror of
https://github.com/gmemstr/pogo.git
synced 2024-09-20 01:31:09 +01:00
commit
ddc9ff3aeb
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -19,7 +19,6 @@ podcasts/
|
|||
|
||||
feed\.rss
|
||||
|
||||
assets/static/custom\.css
|
||||
|
||||
config\.json
|
||||
vendor/
|
||||
|
||||
assets/config/users\.db
|
||||
|
|
30
CONTRIBUTING.md
Normal file
30
CONTRIBUTING.md
Normal 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
14
Godeps/Godeps.json
generated
|
@ -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"
|
||||
|
|
82
README.md
82
README.md
|
@ -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
|
||||
|
||||
To produce a product that is easy to deploy and easier to use when hosting a podcast from ones own servers.
|
||||
|
||||
## Features
|
||||
|
||||
* 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
|
||||
- Automatic RSS and JSON feed generation
|
||||
- Frontend for listening and publishing episodes
|
||||
- Multiple user support
|
||||
- Custom CSS themes
|
||||
- Docker support
|
||||
|
||||
## Requirements
|
||||
## Running
|
||||
|
||||
[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)
|
258
admin/admin.go
258
admin/admin.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
8
assets/config/config.json
Normal file
8
assets/config/config.json
Normal 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
BIN
assets/config/users.db
Normal file
Binary file not shown.
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"admin": "$2a$04$ZAf88Bao4Q768vKfCaKBlOqtPumwKwFhrcpBCdfMWWFX69wyhgTqi",
|
||||
"gabriel": "$2a$04$KrhZ1q6FpOGqs0FVKMYhQ.BTYeVXztnjrM9RbK.0buI1OHfmyNEAy"
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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 */
|
||||
|
||||
|
|
BIN
assets/web/static/logo-large.png
Normal file
BIN
assets/web/static/logo-large.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
BIN
assets/web/static/logo-med.png
Normal file
BIN
assets/web/static/logo-med.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
assets/web/static/logo-sm.png
Normal file
BIN
assets/web/static/logo-sm.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
BIN
assets/web/static/logo-xs.png
Normal file
BIN
assets/web/static/logo-xs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
|
@ -12,13 +12,68 @@ body {
|
|||
h1,h2,h3,h4,h5 {
|
||||
font-weight: 400;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gmemstr/feeds"
|
||||
"github.com/gorilla/feeds"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
|
|
@ -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,22 +161,33 @@ 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 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,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if dbun == username && bcrypt.CompareHashAndPassword([]byte(dbhsh), []byte(password)) == nil {
|
||||
c, err := auth.CreateSession(&common.User{
|
||||
Username: k,
|
||||
Username: username,
|
||||
})
|
||||
if err != nil {
|
||||
return &common.HTTPError{
|
||||
|
@ -163,9 +200,10 @@ func loginHandler() common.Handler {
|
|||
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!",
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue