This commit is contained in:
Teajey 2024-05-04 14:47:03 +12:00
commit cf3e84202b
Signed by: Teajey
GPG Key ID: 970E790FE834A713
23 changed files with 529 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
gin-bin

47
auth/auth.go Normal file
View File

@ -0,0 +1,47 @@
package auth
import (
"log"
"net/http"
"lishwist/db"
"lishwist/env"
"lishwist/types"
"github.com/gorilla/sessions"
)
type AuthMiddleware struct {
Store *sessions.CookieStore
protectedHandler http.Handler
publicHandler http.Handler
}
func (auth *AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
session, _ := auth.Store.Get(r, "lishwist_user")
authorized, _ := session.Values["authorized"].(bool)
if !authorized {
auth.publicHandler.ServeHTTP(w, r)
} else {
auth.protectedHandler.ServeHTTP(w, r)
}
}
func (auth *AuthMiddleware) ExpectUser(r *http.Request) *types.UserData {
session, _ := auth.Store.Get(r, "lishwist_user")
username, ok := session.Values["username"].(string)
if !ok {
log.Fatalln("Failed to get username")
}
user := db.GetUser(username)
if user == nil {
log.Fatalln("Failed to get user")
}
return user
}
func NewAuthMiddleware(protectedHandler http.Handler, publicHandler http.Handler) *AuthMiddleware {
store := sessions.NewCookieStore([]byte(env.Secret))
return &AuthMiddleware{store, protectedHandler, publicHandler}
}

49
auth/login.go Normal file
View File

@ -0,0 +1,49 @@
package auth
import (
"lishwist/db"
"lishwist/types"
"log"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"
)
func (auth *AuthMiddleware) LoginPost(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Couldn't parse form", http.StatusBadRequest)
return
}
username := r.Form.Get("username")
password := r.Form.Get("password")
user, ok := db.Get("user:" + username).(types.UserData)
if !ok {
time.Sleep(2 * time.Second)
http.Error(w, "Username or password invalid", http.StatusUnauthorized)
return
}
err := bcrypt.CompareHashAndPassword(user.PassHash, []byte(password))
if err != nil {
http.Error(w, "Username or password invalid", http.StatusUnauthorized)
return
}
session, err := auth.Store.Get(r, "lishwist_user")
if err != nil {
http.Error(w, "Something went wrong. Error code: Sokka", http.StatusInternalServerError)
return
}
session.Values["authorized"] = true
session.Values["username"] = username
if err := session.Save(r, w); err != nil {
log.Println("Couldn't save session:", err)
http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError)
return
}
http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
}

20
auth/logout.go Normal file
View File

@ -0,0 +1,20 @@
package auth
import (
"net/http"
)
func (auth *AuthMiddleware) LogoutPost(w http.ResponseWriter, r *http.Request) {
session, err := auth.Store.Get(r, "lishwist_user")
if err != nil {
http.Error(w, "Something went wrong. Error code: Iroh", http.StatusInternalServerError)
return
}
session.Values = nil
if err := session.Save(r, w); err != nil {
http.Error(w, "Something went wrong. Error code: Azula", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}

44
auth/register.go Normal file
View File

@ -0,0 +1,44 @@
package auth
import (
"net/http"
"lishwist/db"
"lishwist/types"
"golang.org/x/crypto/bcrypt"
)
func (auth *AuthMiddleware) RegisterPost(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Couldn't parse form", http.StatusBadRequest)
return
}
username := r.Form.Get("username")
newPassword := r.Form.Get("newPassword")
confirmPassword := r.Form.Get("confirmPassword")
if db.Exists("user:" + username) {
http.Error(w, "Username is taken", http.StatusBadRequest)
return
}
if newPassword != confirmPassword {
http.Error(w, "passwords didn't match", http.StatusBadRequest)
return
}
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
if err != nil {
http.Error(w, "Something went wrong. Error code: Aang", http.StatusInternalServerError)
return
}
db.Set("user:"+username, types.UserData{
Username: username,
PassHash: hashedPasswordBytes,
})
http.Redirect(w, r, "/", http.StatusSeeOther)
}

49
context/context.go Normal file
View File

@ -0,0 +1,49 @@
package context
import (
"lishwist/auth"
"lishwist/db"
"net/http"
"slices"
)
type Context struct {
Auth *auth.AuthMiddleware
}
func (ctx *Context) WishlistAdd(w http.ResponseWriter, r *http.Request) {
user := ctx.Auth.ExpectUser(r)
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
items := db.GetUserItems(user.Username)
newItem := r.Form.Get("item")
if newItem != "" {
items = append(items, newItem)
}
db.SetUserItems(user.Username, items)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (ctx *Context) WishlistDelete(w http.ResponseWriter, r *http.Request) {
user := ctx.Auth.ExpectUser(r)
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
items := db.GetUserItems(user.Username)
target := r.Form.Get("item")
if target == "" {
http.Error(w, "Item not provided"+target, http.StatusBadRequest)
return
}
idx := slices.Index(items, target)
if idx < 0 {
http.Error(w, "Couldn't find item: "+target, http.StatusBadRequest)
return
}
items = append(items[:idx], items[idx+1:]...)
db.SetUserItems(user.Username, items)
http.Redirect(w, r, "/", http.StatusSeeOther)
}

View File

@ -0,0 +1,27 @@
package context
import (
"lishwist/db"
"lishwist/templates"
"net/http"
)
type ForeignWishlistProps struct {
Username string
Items []string
}
func (ctx *Context) ViewForeignWishlist(w http.ResponseWriter, r *http.Request) {
otherUsername := r.PathValue("username")
user := ctx.Auth.ExpectUser(r)
if user.Username == otherUsername {
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
items := db.GetUserItems(otherUsername)
if items == nil {
http.Error(w, "User not found", http.StatusNotFound)
}
p := ForeignWishlistProps{Username: otherUsername, Items: items}
templates.Execute(w, "foreign_wishlist.gotmpl", p)
}

19
context/home.go Normal file
View File

@ -0,0 +1,19 @@
package context
import (
"net/http"
"lishwist/db"
"lishwist/templates"
)
type HomeProps struct {
Items []string
}
func (ctx *Context) Home(w http.ResponseWriter, r *http.Request) {
user := ctx.Auth.ExpectUser(r)
items := db.GetUserItems(user.Username)
p := HomeProps{Items: items}
templates.Execute(w, "home.gotmpl", p)
}

40
db/db.go Normal file
View File

@ -0,0 +1,40 @@
package db
import "fmt"
var database map[string]any = map[string]any{}
func Add(key string, value any) error {
_, existing := database[key]
if existing {
return fmt.Errorf("A value already exists under '%s'", key)
}
database[key] = value
return nil
}
func Set(key string, value any) {
database[key] = value
}
func Get(key string) any {
value, existing := database[key]
if !existing {
return fmt.Errorf("No value under '%s'", key)
}
return value
}
func Remove(key string) any {
value, existing := database[key]
if !existing {
return fmt.Errorf("No value under '%s'", key)
}
delete(database, key)
return value
}
func Exists(key string) bool {
_, existing := database[key]
return existing
}

39
db/user.go Normal file
View File

@ -0,0 +1,39 @@
package db
import (
"fmt"
"lishwist/types"
)
func GetUser(username string) *types.UserData {
user, ok := Get("user:" + username).(types.UserData)
if !ok {
return nil
}
return &user
}
func GetUserItems(username string) []string {
user := GetUser(username)
if user == nil {
return nil
}
items, ok := Get("user_items:" + user.Username).([]string)
if !ok {
return nil
}
return items
}
func SetUserItems(username string, items []string) error {
user := GetUser(username)
if user == nil {
return fmt.Errorf("Didn't find user")
}
Set("user_items:"+user.Username, items)
return nil
}

3
env/env.go vendored Normal file
View File

@ -0,0 +1,3 @@
package env
const Secret = "BLAHBLAHLBAH"

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module lishwist
go 1.22.0
require (
github.com/gorilla/sessions v1.2.2
golang.org/x/crypto v0.22.0
)
require github.com/gorilla/securecookie v1.1.2 // indirect

8
go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=

38
main.go Normal file
View File

@ -0,0 +1,38 @@
package main
import (
"net/http"
"lishwist/auth"
"lishwist/context"
"lishwist/templates"
)
func main() {
publicMux := http.NewServeMux()
protectedMux := http.NewServeMux()
authMiddleware := auth.NewAuthMiddleware(protectedMux, publicMux)
ctx := context.Context{
Auth: authMiddleware,
}
publicMux.HandleFunc("GET /register", templates.Register)
publicMux.HandleFunc("POST /register", authMiddleware.RegisterPost)
publicMux.HandleFunc("GET /", templates.Login)
publicMux.HandleFunc("POST /", authMiddleware.LoginPost)
protectedMux.HandleFunc("GET /", ctx.Home)
protectedMux.HandleFunc("GET /{username}", ctx.ViewForeignWishlist)
protectedMux.HandleFunc("POST /wishlist/add", ctx.WishlistAdd)
protectedMux.HandleFunc("POST /wishlist/delete", ctx.WishlistDelete)
protectedMux.HandleFunc("POST /logout", authMiddleware.LogoutPost)
// TODO: Remove me
protectedMux.HandleFunc("GET /logout", authMiddleware.LogoutPost)
http.Handle("/", authMiddleware)
http.ListenAndServe(":4000", nil)
}

13
templates/base.gotmpl Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Lishwist</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
{{template "body" .}}
</body>
</html>

View File

@ -0,0 +1,16 @@
{{define "body"}}
<nav>
<ul>
<li>
<a href="/">Home</a>
</li>
</ul>
</nav>
<h1>Lishwist</h1>
<h2>{{.Username}}'s list</h2>
<ul>
{{range .Items}}
<li>{{.}}</li>
{{end}}
</ul>
{{end}}

21
templates/home.gotmpl Normal file
View File

@ -0,0 +1,21 @@
{{define "body"}}
<form method="post" action="/logout">
<input type="submit" value="Logout">
</form>
<h1>Lishwist</h1>
<h2>Your list</h2>
<ul>
{{range .Items}}
<li>{{.}}
<form method="post" action="wishlist/delete">
<input type="hidden" name="item" value="{{.}}">
<input type="submit" value="Delete">
</form>
</li>
{{end}}
</ul>
<form method="post" action="/wishlist/add">
<input name="item" required>
<input type="submit">
</form>
{{end}}

9
templates/login.go Normal file
View File

@ -0,0 +1,9 @@
package templates
import (
"net/http"
)
func Login(w http.ResponseWriter, r *http.Request) {
Execute(w, "login.gotmpl", nil)
}

14
templates/login.gotmpl Normal file
View File

@ -0,0 +1,14 @@
{{define "body"}}
<form method="post">
<label>
Username
<input name="username" required>
</label>
<label>
Password
<input name="password" type="password" required>
</label>
<input type="submit" value="Login">
</form>
<a href="/register">Register</a>
{{end}}

9
templates/register.go Normal file
View File

@ -0,0 +1,9 @@
package templates
import (
"net/http"
)
func Register(w http.ResponseWriter, r *http.Request) {
Execute(w, "register.gotmpl", nil)
}

17
templates/register.gotmpl Normal file
View File

@ -0,0 +1,17 @@
{{define "body"}}
<form method="post" autocomplete="off">
<label>
Username
<input name="username" required>
</label>
<label>
Password
<input name="newPassword" type="password" required>
</label>
<label>
Confirm password
<input name="confirmPassword" type="password" required>
</label>
<input type="submit" value="Register">
</form>
{{end}}

30
templates/templates.go Normal file
View File

@ -0,0 +1,30 @@
package templates
import (
"log"
"net/http"
"text/template"
)
var tmpls map[string]*template.Template = loadTemplates()
func Execute(w http.ResponseWriter, name string, data any) {
err := tmpls[name].Execute(w, data)
if err != nil {
log.Printf("Failed to execute '%s' template: %s\n", name, err)
return
}
}
func loadTemplates() map[string]*template.Template {
homeTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/home.gotmpl"))
loginTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/login.gotmpl"))
registerTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/register.gotmpl"))
foreignTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/foreign_wishlist.gotmpl"))
return map[string]*template.Template{
"home.gotmpl": homeTmpl,
"login.gotmpl": loginTmpl,
"register.gotmpl": registerTmpl,
"foreign_wishlist.gotmpl": foreignTmpl,
}
}

6
types/user.go Normal file
View File

@ -0,0 +1,6 @@
package types
type UserData struct {
Username string
PassHash []byte
}