Initial
This commit is contained in:
commit
cf3e84202b
|
|
@ -0,0 +1 @@
|
||||||
|
gin-bin
|
||||||
|
|
@ -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}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
const Secret = "BLAHBLAHLBAH"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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=
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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}}
|
||||||
|
|
@ -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}}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
Execute(w, "login.gotmpl", nil)
|
||||||
|
}
|
||||||
|
|
@ -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}}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Register(w http.ResponseWriter, r *http.Request) {
|
||||||
|
Execute(w, "register.gotmpl", nil)
|
||||||
|
}
|
||||||
|
|
@ -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}}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
type UserData struct {
|
||||||
|
Username string
|
||||||
|
PassHash []byte
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue