~ols/mudliag

c4e1744e79ec58752245e67c8d24a6c46294abff — Oliver Leaver-Smith 2 years ago 147b511 master
add auth
M .gitignore => .gitignore +3 -1
@@ 1,2 1,4 @@
submissions/
go.sum
users/
mudliag
nohup.out

M go.mod => go.mod +2 -0
@@ 3,10 3,12 @@ module mudliag
go 1.13

require (
	github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
	github.com/gernest/front v0.0.0-20181129160812-ed80ca338b88
	github.com/gorilla/mux v1.8.0
	github.com/microcosm-cc/bluemonday v1.0.4
	github.com/russross/blackfriday v2.0.0+incompatible
	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
	golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect
	gopkg.in/yaml.v2 v2.4.0 // indirect
)

A go.sum => go.sum +33 -0
@@ 0,0 1,33 @@
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU=
github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/gernest/front v0.0.0-20181129160812-ed80ca338b88 h1:fqfzqvgJfq5Sw7VZyb+OoiOKQzI27pkwhvf/V46XEl8=
github.com/gernest/front v0.0.0-20181129160812-ed80ca338b88/go.mod h1:FwEMwQ5+xky8tbzDLj72k2RAqXnFByLNwxg+9UZDtqU=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/microcosm-cc/bluemonday v1.0.4 h1:p0L+CTpo/PLFdkoPcJemLXG+fpMD7pYOoDEq1axMbGg=
github.com/microcosm-cc/bluemonday v1.0.4/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w=
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

M main.go => main.go +545 -56
@@ 1,6 1,7 @@
package main

import (
	b64 "encoding/base64"
	"fmt"
	"html"
	"html/template"


@@ 8,6 9,7 @@ import (
	"log"
	"math/rand"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"


@@ 16,15 18,115 @@ import (
	"strings"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/gernest/front"
	"github.com/gorilla/mux"
	"github.com/microcosm-cc/bluemonday"
	"github.com/russross/blackfriday"
	"golang.org/x/crypto/bcrypt"
	"gopkg.in/yaml.v2"
)

var SECRET_KEY = []byte(os.Getenv("JWT_TOKEN"))

var redir string

type Credentials struct {
	Username string `yaml:"username"`
	Password string `yaml:"password"`
}

type Claims struct {
	Username string `json:"username"`
	Name     string `json:"name"`
	Email    string `json:"email"`
	Status   string `jsoin:"status"`
	Auth     bool   `json:"authenticated"`
	jwt.StandardClaims
}

type UserInfo struct {
	Username string `yaml:"username"`
	Name     string `yaml:"name"`
	Email    string `yaml:"email"`
	Social   string `yaml:"social"`
	Homepage string `yaml:"homepage"`
	Git      string `yaml:"git"`
	Status   string `yaml:"status"`
	Auth     bool
}

type Data struct {
	UserInfo    UserInfo
	Comments    []Comment
	Submissions []Submission
	Error       ReturnError
}

type ReturnError struct {
	Title string
	Body  template.HTML
	Auth  bool
}

var NotImplemented = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	tmpl := template.Must(template.New("not.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseFiles("not.html"))
	err := tmpl.ExecuteTemplate(w, "not.html", nil)
	userInfo := getLoggedInInfo(r)
	tmpl := template.Must(template.New("not.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("views/*.html"))
	returnedData := Data{
		UserInfo: UserInfo{
			Auth: userInfo.Auth,
		},
	}
	err := tmpl.ExecuteTemplate(w, "not.html", returnedData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
})

func getLoggedInInfo(r *http.Request) UserInfo {
	cookie, _ := r.Cookie("token")
	if cookie != nil {
		token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
			if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
				return nil, fmt.Errorf("Error parsing token")
			}
			return SECRET_KEY, nil
		})

		if err != nil {
			log.Println(err)
		}

		if token.Valid {
			username := token.Claims.(jwt.MapClaims)["username"]
			name := token.Claims.(jwt.MapClaims)["name"]
			email := token.Claims.(jwt.MapClaims)["email"]
			auth := token.Claims.(jwt.MapClaims)["authenticated"]
			if auth == nil {
				auth = false
			}
			userInfo := UserInfo{
				Username: username.(string),
				Name:     name.(string),
				Email:    email.(string),
				Auth:     auth.(bool),
			}
			return userInfo
		}
	}
	userInfo := UserInfo{}
	return userInfo
}

var loginPage = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userInfo := getLoggedInInfo(r)
	tmpl := template.Must(template.New("login.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
	returnedData := Data{
		UserInfo: UserInfo{
			Auth: userInfo.Auth,
		},
	}
	err := tmpl.ExecuteTemplate(w, "login.html", returnedData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}


@@ 33,10 135,11 @@ var NotImplemented = http.HandlerFunc(func(w http.ResponseWriter, r *http.Reques
var Scores = map[string]uint64{}

type Submission struct {
	Title, Url, Body, Slug, Submitter string
	Author, Visible                   bool
	Score                             uint64
	Created                           string
	Title, Host, Scheme, Body, Slug, Submitter string
	Url                                        template.URL
	Author, Visible                            bool
	Score                                      uint64
	Created                                    string
	// Created                     time.Time
	Tags []string
}


@@ 49,10 152,75 @@ type Comment struct {
type ByScore []Submission

func (a ByScore) Len() int           { return len(a) }
func (a ByScore) Less(i, j int) bool { return a[i].Score > a[j].Score }
func (a ByScore) Less(i, j int) bool { return a[i].Created > a[j].Created }
func (a ByScore) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

var displaySubmissionsBySource = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userInfo := getLoggedInInfo(r)
	vars := mux.Vars(r)
	source := vars["source"]
	m := front.NewMatter()
	submissions := LoadSubmissions("./submissions")
	var returnedSubmissions []Submission
	for _, submission := range submissions {

		rawSubmission, _ := os.Open(submission)
		m.Handle("---", front.YAMLHandler)
		fm, bd, err := m.Parse(rawSubmission)
		if err != nil {
			log.Println(err)
		}

		u, _ := url.Parse(fm["url"].(string))
		if u.Host == source {

			author, _ := strconv.ParseBool(fm["author"].(string))
			visible, _ := strconv.ParseBool(fm["visible"].(string))

			// Assert front-matter tags as an []interface{}
			// so that we can iterate over it and add to a []string
			tagSlice := fm["tags"].([]interface{})
			var submissionTags []string
			for _, eachTag := range tagSlice {
				submissionTags = append(submissionTags, eachTag.(string))
			}

			decodedBody, _ := b64.StdEncoding.DecodeString(bd)

			scheme := u.Scheme
			data := Submission{
				Title:     fm["title"].(string),
				Submitter: fm["submitter"].(string),
				Url:       template.URL(fm["url"].(string)),
				Author:    author,
				Score:     Scores[fm["slug"].(string)],
				Created:   fm["created"].(string),
				Body:      string(decodedBody),
				Slug:      fm["slug"].(string),
				Tags:      submissionTags,
				Visible:   visible,
				Host:      u.Host,
				Scheme:    scheme,
			}
			returnedSubmissions = append(returnedSubmissions, data)
		}
	}
	returnedData := Data{
		Submissions: returnedSubmissions,
		UserInfo: UserInfo{
			Auth: userInfo.Auth,
		},
	}
	sort.Sort(ByScore(returnedSubmissions))
	tmpl := template.Must(template.New("index.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("views/*.html"))
	err := tmpl.ExecuteTemplate(w, "index.html", returnedData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
})

var displaySubmissions = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userInfo := getLoggedInInfo(r)
	m := front.NewMatter()
	submissions := LoadSubmissions("./submissions")
	var returnedSubmissions []Submission


@@ 76,30 244,42 @@ var displaySubmissions = http.HandlerFunc(func(w http.ResponseWriter, r *http.Re
			submissionTags = append(submissionTags, eachTag.(string))
		}

		decodedBody, _ := b64.StdEncoding.DecodeString(bd)

		u, _ := url.Parse(fm["url"].(string))
		scheme := u.Scheme
		data := Submission{
			Title:     fm["title"].(string),
			Submitter: fm["submitter"].(string),
			Url:       fm["url"].(string),
			Url:       template.URL(fm["url"].(string)),
			Author:    author,
			Score:     Scores[fm["slug"].(string)],
			Created:   fm["created"].(string),
			Body:      bd,
			Body:      string(decodedBody),
			Slug:      fm["slug"].(string),
			Tags:      submissionTags,
			Visible:   visible,
			Host:      u.Host,
			Scheme:    scheme,
		}

		returnedSubmissions = append(returnedSubmissions, data)
	}
	returnedData := Data{
		Submissions: returnedSubmissions,
		UserInfo: UserInfo{
			Auth: userInfo.Auth,
		},
	}
	sort.Sort(ByScore(returnedSubmissions))
	tmpl := template.Must(template.New("index.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseFiles("index.html"))
	err := tmpl.ExecuteTemplate(w, "index.html", returnedSubmissions)
	tmpl := template.Must(template.New("index.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("views/*.html"))
	err := tmpl.ExecuteTemplate(w, "index.html", returnedData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
})

var individualSubmission = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userInfo := getLoggedInInfo(r)
	m := front.NewMatter()
	vars := mux.Vars(r)
	slug := vars["slug"]


@@ 122,35 302,52 @@ var individualSubmission = http.HandlerFunc(func(w http.ResponseWriter, r *http.
		submissionTags = append(submissionTags, eachTag.(string))
	}

	decodedBody, _ := b64.StdEncoding.DecodeString(bd)

	data := Submission{
		Title:     fm["title"].(string),
		Submitter: fm["submitter"].(string),
		Url:       fm["url"].(string),
		Url:       template.URL(fm["url"].(string)),
		Author:    author,
		Score:     Scores[fm["slug"].(string)],
		Created:   fm["created"].(string),
		Body:      bd,
		Body:      string(decodedBody),
		Slug:      fm["slug"].(string),
		Tags:      submissionTags,
		Visible:   visible,
	}

	tmpl := template.Must(template.New("post.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseFiles("post.html"))
	err = tmpl.ExecuteTemplate(w, "post.html", data)
	returnedSubmissions := []Submission{data}

	returnedData := Data{
		Submissions: returnedSubmissions,
		UserInfo: UserInfo{
			Auth:     userInfo.Auth,
			Username: userInfo.Username,
		},
	}

	tmpl := template.Must(template.New("post.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("views/*.html"))
	err = tmpl.ExecuteTemplate(w, "post.html", returnedData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
})

var submitPage = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	tmpl := template.Must(template.New("submit.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseFiles("submit.html"))
	err := tmpl.ExecuteTemplate(w, "submit.html", nil)
	userInfo := getLoggedInInfo(r)
	tmpl := template.Must(template.New("submit.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("views/*.html"))
	data := Data{
		UserInfo: userInfo,
	}
	err := tmpl.ExecuteTemplate(w, "submit.html", data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
})

var submissionComments = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userInfo := getLoggedInInfo(r)
	m := front.NewMatter()
	vars := mux.Vars(r)
	slug := vars["slug"]


@@ 175,8 372,16 @@ var submissionComments = http.HandlerFunc(func(w http.ResponseWriter, r *http.Re
		submissionComments = append(submissionComments, comment)
	}

	tmpl := template.Must(template.New("comments.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseFiles("comments.html"))
	err = tmpl.ExecuteTemplate(w, "comments.html", submissionComments)
	returnedData := Data{
		Comments: submissionComments,
		UserInfo: UserInfo{
			Auth:     userInfo.Auth,
			Username: userInfo.Username,
		},
	}

	tmpl := template.Must(template.New("comments.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("views/*.html"))
	err = tmpl.ExecuteTemplate(w, "comments.html", returnedData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}


@@ 253,19 458,14 @@ var upSubmission = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request)
})

var addComment = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userInfo := getLoggedInInfo(r)
	vars := mux.Vars(r)
	slug := vars["slug"]
	reg, err := regexp.Compile(`[^a-zA-Z0-9 '"?!,.()$&]+`)
	if err != nil {
		log.Fatal(err)
	}
	name := html.EscapeString(reg.ReplaceAllString(r.FormValue("name"), ""))
	if len(name) < 1 {
		name = "Coward"
	}
	if len(name) > 50 {
		name = "Troublemaker"
	}
	name := userInfo.Username
	comment := html.EscapeString(reg.ReplaceAllString(r.FormValue("comment"), ""))
	if len(comment) < 1 {
		w.Write([]byte("You need a comment"))


@@ 305,13 505,14 @@ func StringWithCharset(length int, charset string) string {
}

var actuallySubmit = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userInfo := getLoggedInInfo(r)
	reg, err := regexp.Compile(`[^a-zA-Z0-9 '"?!,.()$&]+`)
	if err != nil {
		log.Fatal(err)
	}
	title := html.EscapeString(reg.ReplaceAllString(r.FormValue("title"), ""))
	url := html.EscapeString(r.FormValue("url"))
	submitter := html.EscapeString(reg.ReplaceAllString(r.FormValue("submitter"), ""))
	submitter := userInfo.Username
	author := r.FormValue("author")
	var authorBool bool
	if author == "on" {


@@ 332,21 533,21 @@ var actuallySubmit = http.HandlerFunc(func(w http.ResponseWriter, r *http.Reques

	submissionTags := strings.Split(tags, ",")

	body := b64.StdEncoding.EncodeToString([]byte(r.FormValue("context")))

	data := Submission{
		Title:     title,
		Submitter: submitter,
		Url:       url,
		Url:       template.URL(url),
		Author:    authorBool,
		Score:     1,
		Created:   string(currentTime.Format("2006-01-02")),
		Body:      "",
		Created:   string(currentTime.Format("2006-01-02 15:04:05")),
		Body:      body,
		Slug:      slug,
		Tags:      submissionTags,
		Visible:   true,
	}

	log.Println(data)

	log.Printf("Would submit %s (%s) from %s (author %t) with tags %s", title, url, submitter, author, tags)

	t, err := template.ParseFiles("submission.yaml")


@@ 371,43 572,331 @@ var actuallySubmit = http.HandlerFunc(func(w http.ResponseWriter, r *http.Reques
	http.Redirect(w, r, redirPath, 302)
})

func main() {
func isAuthorised(endpoint func(http.ResponseWriter, *http.Request)) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cookie, _ := r.Cookie("token")
		if cookie != nil {
			token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
				if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
					return nil, fmt.Errorf("Error parsing token")
				}
				return SECRET_KEY, nil
			})

			if err != nil {
				fmt.Fprintf(w, err.Error())
			}

	InitialiseScoreMap()

	ticker := time.NewTicker(30 * time.Second)
	quit := make(chan struct{})
	go func() {
		for {
			select {
			case <-ticker.C:
				WriteScores()
			case <-quit:
				ticker.Stop()
				return
			if token.Valid {
				endpoint(w, r)
			}
		} else {
			returnError := ReturnError{
				Title: "Access denied",
				Body:  template.HTML(`You don't have permission to see this page. Try <a href="/login">logging in</a>.`),
				Auth:  false,
			}
			data := Data{
				UserInfo: UserInfo{
					Auth: false,
				},
				Error: returnError,
			}
			tmpl := template.Must(template.New("error.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
			err := tmpl.ExecuteTemplate(w, "error.html", data)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
			}
			return
		}
	})
}

var logout = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	c := &http.Cookie{
		Name:    "token",
		Value:   "",
		Path:    "/",
		Expires: time.Unix(0, 0),

		HttpOnly: true,
	}

	http.SetCookie(w, c)
	http.Redirect(w, r, "/", 302)
})

var userPage = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	userInfo := getLoggedInInfo(r)
	auth := userInfo.Auth
	vars := mux.Vars(r)
	user := vars["user"]
	re := regexp.MustCompile("^[a-zA-Z0-9_]*$")
	if !re.MatchString(user) {
		log.Printf("Not attempting to read file for %s", user)
		returnError := ReturnError{
			Title: "Error displaying user",
			Body:  template.HTML("Unable to display an Account page for " + user),
			Auth:  auth,
		}
		tmpl := template.Must(template.New("error.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
		err := tmpl.ExecuteTemplate(w, "error.html", returnError)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	userPageInfo := UserInfo{}
	file, err := ioutil.ReadFile("./users/" + user + ".yaml")
	if err != nil {
		log.Printf("File read error for %s: %s", user, err)
		returnError := ReturnError{
			Title: "Error displaying user",
			Body:  template.HTML("Unable to display an Account page for " + user),
			Auth:  auth,
		}
	}()
		log.Printf("File read error for %s: %s", user, err)
		tmpl := template.Must(template.New("error.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
		err := tmpl.ExecuteTemplate(w, "error.html", returnError)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	err = yaml.Unmarshal([]byte(file), &userPageInfo)
	if err != nil {
		log.Printf("YAML unmarshal error for %s: %s", user, err)
		returnError := ReturnError{
			Title: "Error displaying user",
			Body:  template.HTML("Unable to display an Account page for " + user),
			Auth:  auth,
		}
		log.Printf("File read error for %s: %s", user, err)
		tmpl := template.Must(template.New("error.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
		err := tmpl.ExecuteTemplate(w, "error.html", returnError)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	userPageInfo = UserInfo{
		Username: userPageInfo.Username,
		Name:     userPageInfo.Name,
		Status:   userPageInfo.Status,
		Social:   userPageInfo.Social,
		Homepage: userPageInfo.Homepage,
		Git:      userPageInfo.Git,
		Auth:     auth,
	}
	returnedData := Data{
		UserInfo: userPageInfo,
	}
	tmpl := template.Must(template.New("profile.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
	err = tmpl.ExecuteTemplate(w, "profile.html", returnedData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
	log.Printf("%s opened account page for %s", userInfo.Username, userPageInfo.Username)
})

var account = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	user := getLoggedInInfo(r)
	username := user.Username
	auth := user.Auth
	userInfo := UserInfo{}
	file, err := ioutil.ReadFile("./users/" + username + ".yaml")
	if err != nil {
		returnError := ReturnError{
			Title: "Error displaying user",
			Body:  template.HTML("Unable to display an Account page for " + username),
		}
		log.Printf("File read error for %s: %s", user, err)
		tmpl := template.Must(template.New("error.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
		err := tmpl.ExecuteTemplate(w, "error.html", returnError)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	err = yaml.Unmarshal([]byte(file), &userInfo)
	if err != nil {
		log.Printf("YAML unmarshal error for %s: %s", user, err)
		returnError := ReturnError{
			Title: "Error displaying user",
			Body:  template.HTML("Unable to display an Account page for " + username),
			Auth:  auth,
		}
		data := Data{
			UserInfo: UserInfo{
				Auth: auth,
			},
			Error: returnError,
		}
		log.Printf("File read error for %s: %s", user, err)
		tmpl := template.Must(template.New("error.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
		err := tmpl.ExecuteTemplate(w, "error.html", data)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	userInfo = UserInfo{
		Username: userInfo.Username,
		Name:     userInfo.Name,
		Email:    userInfo.Email,
		Status:   userInfo.Status,
		Social:   userInfo.Social,
		Homepage: userInfo.Homepage,
		Git:      userInfo.Git,
		Auth:     auth,
	}
	returnedData := Data{
		UserInfo: userInfo,
	}
	tmpl := template.Must(template.New("account.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
	err = tmpl.ExecuteTemplate(w, "account.html", returnedData)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
	log.Printf("%s opened account page", userInfo.Username)
})

var login = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	user := r.FormValue("username")
	re := regexp.MustCompile("^[a-zA-Z0-9_]*$")
	if !re.MatchString(user) {
		log.Printf("Not attempting to read file for %s", user)
		returnError := ReturnError{
			Title: "Access denied",
			Body:  template.HTML(""),
			Auth:  false,
		}
		tmpl := template.Must(template.New("error.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
		err := tmpl.ExecuteTemplate(w, "error.html", returnError)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	password := []byte(r.FormValue("password"))
	hashedPassword, err := getHashedPassword(user)
	if err != nil {
		log.Printf("Credentials error for %s: %s", user, err)
		tmpl := template.Must(template.New("denied.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
		err := tmpl.ExecuteTemplate(w, "denied.html", nil)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}
	passErr := bcrypt.CompareHashAndPassword(hashedPassword, password)
	if passErr != nil {
		log.Printf("Credentials error for %s: %s", user, passErr)
		tmpl := template.Must(template.New("denied.html").Funcs(template.FuncMap{"markDown": markDowner}).ParseGlob("./views/*.html"))
		err := tmpl.ExecuteTemplate(w, "denied.html", nil)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	} else {
		log.Printf("%s logged in", user)
		jwt, expirationTime, _ := generateJWT(user)
		http.SetCookie(w, &http.Cookie{
			Name:    "token",
			Value:   jwt,
			Expires: expirationTime,
		})
		http.Redirect(w, r, redir, 302)
	}
})

func getHashedPassword(user string) ([]byte, error) {
	credentials := Credentials{}
	log.Printf("Attempting to load user file for %s", user)
	file, err := ioutil.ReadFile("./users/" + user + ".yaml")
	if err != nil {
		log.Printf("File read error for %s: %s", user, err)
		return []byte(""), err
	}
	err = yaml.Unmarshal([]byte(file), &credentials)
	if err != nil {
		log.Printf("YAML unmarshal error for %s: %s", user, err)
		return []byte(""), err
	}
	return []byte(credentials.Password), nil
}

func generateJWT(user string) (string, time.Time, error) {
	userInfo := UserInfo{}
	file, err := ioutil.ReadFile("./users/" + user + ".yaml")
	if err != nil {
		log.Printf("File read error for %s: %s", user, err)
		return "", time.Now(), err
	}
	err = yaml.Unmarshal([]byte(file), &userInfo)
	if err != nil {
		log.Printf("YAML unmarshal error for %s: %s", user, err)
		return "", time.Now(), err
	}
	expirationTime := time.Now().Add(48 * time.Hour)
	claims := &Claims{
		Username: user,
		Name:     userInfo.Name,
		Email:    userInfo.Email,
		Auth:     true,
		StandardClaims: jwt.StandardClaims{
			ExpiresAt: expirationTime.Unix(),
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString(SECRET_KEY)
	if err != nil {
		log.Println("Error in JWT token generation")
		return "", time.Now(), err
	}
	return tokenString, expirationTime, nil
}

func main() {

	//	InitialiseScoreMap()
	//
	//	ticker := time.NewTicker(30 * time.Second)
	//	quit := make(chan struct{})
	//	go func() {
	//		for {
	//			select {
	//			case <-ticker.C:
	//				WriteScores()
	//			case <-quit:
	//				ticker.Stop()
	//				return
	//			}
	//		}
	//	}()

	r := mux.NewRouter()

	r.Handle("/", displaySubmissions).Methods("GET")
	r.Handle("/up/{slug}", upSubmission).Methods("GET")
	r.Handle("/comment/{slug}", addComment).Methods("POST")
	r.Handle("/actuallysubmit", actuallySubmit).Methods("POST")
	r.Handle("/up/{slug}", isAuthorised(upSubmission)).Methods("GET")
	r.Handle("/comment/{slug}", isAuthorised(addComment)).Methods("POST")
	r.Handle("/actuallysubmit", isAuthorised(actuallySubmit)).Methods("POST")

	r.Handle("/s/{slug}", individualSubmission).Methods("GET")
	r.Handle("/comments/{slug}", submissionComments).Methods("GET")
	r.Handle("/status", NotImplemented).Methods("GET")

	r.Handle("/recent", NotImplemented).Methods("GET")
	r.Handle("/submit", submitPage).Methods("GET")
	r.Handle("/saved", NotImplemented).Methods("GET")
	r.Handle("/search", NotImplemented).Methods("GET")
	r.Handle("/messages", NotImplemented).Methods("GET")
	r.Handle("/account", NotImplemented).Methods("GET")
	r.Handle("/submit", isAuthorised(submitPage)).Methods("GET")
	r.HandleFunc("/account", isAuthorised(account)).Methods("GET")
	r.Handle("/source", NotImplemented).Methods("GET")
	r.Handle("/source/{source}", displaySubmissionsBySource).Methods("GET")
	r.Handle("/tags", NotImplemented).Methods("GET")
	r.Handle("/tags/{tag}", NotImplemented).Methods("GET")
	r.HandleFunc("/logout", isAuthorised(logout)).Methods("GET")
	r.Handle("/u/{user}", userPage).Methods("GET")

	r.Handle("/login", loginPage).Methods("GET")
	r.Handle("/login", login).Methods("POST")

	r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))


M static/style.css => static/style.css +8 -12
@@ 1,18 1,14 @@
a{color:blue;}
body{font-family:sans-serif;margin-top:50px;}
h3,p{display:inline;}
.link{word-break:break-all;}
body{font-family:sans-serif;margin:1rem;}
ul,ul li{list-style:none;padding:0;margin:0;}
ul li{padding:5px 0px;}
ul li{padding:1rem;}
.post{border:solid 2px blue;margin:1rem 0;}
header ul, header ul li{font-weight:600;margin:0;padding:0;list-style:none;display:inline-block;}
header{border-bottom:solid 3px blue;margin:20px 0px;}
header a{text-decoration:none;color:black;}
header a:hover{color:red;}
#container{width:60%;margin:0 auto;}
.tags a{color:#999;font-size:0.7em;}
.content{display:block;}
.content p{display:block;}
a.up{text-decoration:none;}
@media (max-width: 40em) {
body{margin-top:10px;}
#container{width:99%;}
}
#container{max-width:930px;margin:0 auto;}
h3{margin-top:0;}
p{margin:0;}
.context{border-left: solid 2px blue;padding-left:1rem;margin-top:1rem;}

M submission.yaml => submission.yaml +1 -0
@@ 13,3 13,4 @@ tags:
{{- end }}
comments:
---
{{ .Body }}

A views/account.html => views/account.html +29 -0
@@ 0,0 1,29 @@
{{ template "header.html" . }}
                <h1>Account</h1>
		<p><a href="/">home</a> | <a href="/logout">logout</a></p>
		 <table>
<tr>
<td class="bold">Name</td>
<td><input type="text" value="{{ .UserInfo.Name }}"></td>
</tr>
<td class="bold">Email</td>
<td><input type="text" value="{{ .UserInfo.Email }}"></td>
</tr>
<tr>
<td class="bold">Homepage</td>
<td><input type="text" value="{{ .UserInfo.Homepage }}"></a></td>
</tr>
<tr>
<td class="bold">Git</td>
<td><input type="text" value="{{ .UserInfo.Git }}"></a></td>
</tr>
<tr>
<td class="bold">Social</td>
<td><input type="text" value="{{ .UserInfo.Social }}"></a></td>
</tr>
</table>
<input type="Submit" value="Save changes"><span class="subtle">(this does nothing yet)</span>

</div>
        </body>
        </html>

A views/comments.html => views/comments.html +10 -0
@@ 0,0 1,10 @@
<ul>
{{ range .Comments }}
<li>
{{ if .Visible }}
<strong>{{ .Author }}</strong><br>{{ .Content }}</li>
{{ else }}
<i><strong>[deleted]</strong><br>[removed]</li></i>
{{ end }}
{{ end }}
</ul>

A views/denied.html => views/denied.html +6 -0
@@ 0,0 1,6 @@
{{ template "header.html" . }}
		<h1>Access denied</h1>
		<p><a href="/">home</a></p>
</div>
	</body>
	</html>

A views/error.html => views/error.html +7 -0
@@ 0,0 1,7 @@
{{ template "header.html" . }}
	<h1>{{ .Error.Title }}</h1>
		<p><a href="/">home</a></p>
		<p>{{ .Error.Body }}</p>
</div>
	</body>
	</html>

A views/header.html => views/header.html +22 -0
@@ 0,0 1,22 @@
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<link rel="stylesheet" href="/static/style.css">
		<title>Links</title>
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
	</head>
	<body>
	<div id="container">
		<header>
			<ul>
				<li><a href="/">Links</a></li>
				{{ if .UserInfo.Auth }}
				<li><a href="/submit">Submit</a></li>
				<li><a href="/account">Account</a></li>
				<li><a href="/logout">Logout</a></li>
				{{ else }}
				<li><a href="/login">Login</a></li>
				{{ end }}
			</ul>
		</header>

A views/index.html => views/index.html +18 -0
@@ 0,0 1,18 @@
{{ template "header.html" . }}
{{ template "intro.html" . }}
<ul>
{{ range .Submissions }}
{{ if .Visible }}
<li class="post">
<h3>{{ .Title }}</h3>
<p><a class="link" href="{{ .Url }}">{{ .Url }}</a> [<a href="/source/{{ .Host }}">{{ .Host }}</a>]</p>
<p>{{ if .Author }}by{{ else }}via{{ end }} <a href="/u/{{ .Submitter }}">{{ .Submitter }}</a> {{ .Created }} (<a href="/s/{{ .Slug }}">permalink</a>)</p>
<p class="tags">tags: {{ range .Tags }}<a href="/tags/{{ . }}">{{ . }}</a> {{ end }}</p>
{{ if .Body }}<p class="context">{{ .Body }}</p>{{ end }}
{{ end }}
</li>
{{ end }}
</ul>
</div>
</body>
</html>

A views/intro.html => views/intro.html +1 -0
@@ 0,0 1,1 @@
<p>Interesting links and resources from around the Internet</p>

A views/login.html => views/login.html +13 -0
@@ 0,0 1,13 @@
{{ template "header.html" . }}
{{ if .UserInfo.Username }}
You are already logged in as <a href="/account">{{ .UserInfo.Username }}</a>. <a href="/logout">Logout</a>?
{{ else }}
<form action="/login" method="POST">
	Username: <input type="text" name="username" required><br>
	Password: <input type="password" name="password" required><br>
<input type="submit" value="Login">
</form>
{{ end }}
</div>
</body>
</html>

A views/not.html => views/not.html +4 -0
@@ 0,0 1,4 @@
{{ template "header.html" . }}
<h3>Not implemented</h3>
</body>
</html>

A views/post.html => views/post.html +44 -0
@@ 0,0 1,44 @@
{{ template "header.html" . }}
{{ range .Submissions }}
{{ if .Visible }}
<h3><a href="{{ .Url }}">{{ .Title }}</a></h3><br>
<p>{{ if .Author }}by{{ else }}via{{ end }} <a href="/u/{{ .Submitter }}">{{ .Submitter }}</a> {{ .Created }}</p>
<p><a href="/s/{{ .Slug }}">comments</a></hp>
<p class="tags">{{ range .Tags }}<a href="/tags/{{ . }}">{{ . }}</a> {{ end }}</p>
<div class="content">{{ .Body | markDown }}</div>
{{ end }}
{{ end }}
<hr>
{{ if .UserInfo.Auth }}
<h3>Leave a comment</h3>
{{ range .Submissions }}
<form id="commentForm" action="/comment/{{ .Slug }}" method="POST">
{{ end }}
	{{ .UserInfo.Username }}<br>
	Comment: <input type="text" name="comment"><br>
	<input type="submit" value="Comment">
</form>
{{ end }}
<hr>
<h3>Comments:</h3>
<div id="comments">
</div>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script>
	$(document).ready(function(){
{{ range .Submissions }}
		$("#comments").load("/comments/{{ .Slug }}");
{{ end }}
	});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.form/4.3.0/jquery.form.min.js" integrity="sha384-qlmct0AOBiA2VPZkMY3+2WqkHtIQ9lSdAsAn5RUJD/3vA5MKDgSGcdmIv4ycVxyn" crossorigin="anonymous"></script>
<script>
$(function() {
		$("#commentForm").ajaxForm(function(){
				location.reload();
			});
});
</script>
</body>
</html>

A views/profile.html => views/profile.html +31 -0
@@ 0,0 1,31 @@
{{ template "header.html" . }}
	<h1>User Profile</h1>
		<p><a href="/">home</a></p>
		<h4>{{ .UserInfo.Username }} <span style="font-weight:400;">({{ .UserInfo.Status }})</span></h4>
		<table>
			<tr>
				<td class="bold">Name</td>
				<td>{{ .UserInfo.Name }}</td>
			</tr>
		{{ if .UserInfo.Homepage }}
			<tr>
				<td class="bold">Homepage</td>
				<td><a href="{{ .UserInfo.Homepage }}">{{ .UserInfo.Homepage }}</a></td>
			</tr>
				{{ end }}
		{{ if .UserInfo.Git }}
			<tr>
				<td class="bold">Git</td>
				<td><a href="{{ .UserInfo.Git }}">{{ .UserInfo.Git }}</a></td>
			</tr>
				{{ end }}
		{{ if .UserInfo.Social }}
			<tr>
				<td class="bold">Social</td>
				<td><a href="{{ .UserInfo.Social }}">{{ .UserInfo.Social }}</a></td>
			</tr>
				{{ end }}
				</table>
		</div>
	</body>
	</html>

A views/secret.html => views/secret.html +16 -0
@@ 0,0 1,16 @@
{{ template "header.html" . }}
		<h1>Well done {{ .Name }}!</h1>
		<p><a href="/">home</a></p>
		<p>You found the secret page, a list of things to do:</p>
		<ul>
			<li>refresh tokens</li>
			<li>salt passwords</li>
			<li>package up as a library</li>
			<li>style it up nice</li>
			<li>store metadata and sessions somewhere (probably in the <code>&lt;user&gt;.yaml</code>)</li>
			<li>integrate with yaml link aggregator site</li>
			<li>allow registration</li>
		</ul>
</div>
	</body>
	</html>

A views/submit.html => views/submit.html +22 -0
@@ 0,0 1,22 @@
{{ template "header.html" . }}
<h3>Submit</h3>
<form id="commentForm" action="/actuallysubmit" method="POST">
	Title: <input type="text" name="title"><br>
	URL: <input type="text" name="url"><br>
	Author? <input type="checkbox" name="author"><br>
	Tags: <input type="text" name="tags"><br>
	Body: <br><textarea name="context"></textarea><br>
	<input type="submit" value="Submit">
</form>
</div>
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.form/4.3.0/jquery.form.min.js" integrity="sha384-qlmct0AOBiA2VPZkMY3+2WqkHtIQ9lSdAsAn5RUJD/3vA5MKDgSGcdmIv4ycVxyn" crossorigin="anonymous"></script>
<script>
$(function() {
		$("#submitForm").ajaxForm(function(){
				location.reload();
			});
});
</script>
</body>
</html>