feat: improvements

- better error presentation
 - public wishlist view
This commit is contained in:
Teajey 2024-10-26 12:30:11 +09:00
parent fb658e16fa
commit 0cc6abe03b
Signed by: Teajey
GPG Key ID: 970E790FE834A713
22 changed files with 377 additions and 74 deletions

View File

@ -1,6 +1,7 @@
package auth package auth
import ( import (
"encoding/gob"
"log" "log"
"net/http" "net/http"
@ -41,6 +42,8 @@ func (auth *AuthMiddleware) ExpectUser(r *http.Request) *db.User {
} }
func NewAuthMiddleware(protectedHandler http.Handler, publicHandler http.Handler) *AuthMiddleware { func NewAuthMiddleware(protectedHandler http.Handler, publicHandler http.Handler) *AuthMiddleware {
gob.Register(&RegisterProps{})
gob.Register(&LoginProps{})
store := sessions.NewCookieStore([]byte(env.JwtSecret)) store := sessions.NewCookieStore([]byte(env.JwtSecret))
store.Options.MaxAge = 86_400 store.Options.MaxAge = 86_400
return &AuthMiddleware{store, protectedHandler, publicHandler} return &AuthMiddleware{store, protectedHandler, publicHandler}

View File

@ -1,28 +1,62 @@
package auth package auth
import ( import (
sesh "lishwist/session"
"lishwist/templates" "lishwist/templates"
"log"
"net/http" "net/http"
) )
type LoginGetProps struct { type LoginProps struct {
GeneralError string
SuccessfulRegistration bool SuccessfulRegistration bool
Username templates.InputProps
Password templates.InputProps
}
func NewLoginProps() LoginProps {
return LoginProps{
Username: templates.InputProps{
Name: "username",
Required: true,
},
Password: templates.InputProps{
Name: "password",
Type: "password",
Required: true,
},
}
} }
func (auth *AuthMiddleware) Login(w http.ResponseWriter, r *http.Request) { func (auth *AuthMiddleware) Login(w http.ResponseWriter, r *http.Request) {
session, _ := auth.Store.Get(r, "lishwist_user") session, _ := auth.Store.Get(r, "lishwist_user")
successfulReg, ok := session.Values["successful_registration"].(bool)
if ok { props := NewLoginProps()
delete(session.Values, "successful_registration")
if err := session.Save(r, w); err != nil { flash, err := sesh.GetFirstFlash(w, r, session, "login_props")
log.Println("Couldn't save session:", err) if err != nil {
http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError) http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError)
return return
}
} }
templates.Execute(w, "login.gotmpl", LoginGetProps{ flashProps, ok := flash.(*LoginProps)
SuccessfulRegistration: successfulReg, if ok {
}) props.Username.Value = flashProps.Username.Value
props.GeneralError = flashProps.GeneralError
props.Username.Error = flashProps.Username.Error
props.Password.Error = flashProps.Password.Error
}
flash, err = sesh.GetFirstFlash(w, r, session, "successful_registration")
if err != nil {
http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError)
return
}
successfulReg, _ := flash.(bool)
if successfulReg {
props.SuccessfulRegistration = true
}
templates.Execute(w, "login.gotmpl", props)
} }

View File

@ -18,29 +18,36 @@ func (auth *AuthMiddleware) LoginPost(w http.ResponseWriter, r *http.Request) {
username := r.Form.Get("username") username := r.Form.Get("username")
password := r.Form.Get("password") password := r.Form.Get("password")
props := NewLoginProps()
props.Username.Value = username
user, err := db.GetUserByName(username) user, err := db.GetUserByName(username)
if user == nil || err != nil { if user == nil || err != nil {
time.Sleep(time.Second) time.Sleep(time.Second)
http.Error(w, "Username or password invalid", http.StatusUnauthorized) props.GeneralError = "Username or password invalid"
auth.RedirectWithFlash(w, r, "/", "login_props", &props)
return return
} }
passHash, err := user.GetPassHash() passHash, err := user.GetPassHash()
if err != nil { if err != nil {
http.Error(w, "Something went wrong. Error code: Momo", http.StatusInternalServerError) props.GeneralError = "Something went wrong. Error code: Momo"
auth.RedirectWithFlash(w, r, "/", "login_props", &props)
return return
} }
err = bcrypt.CompareHashAndPassword(passHash, []byte(password)) err = bcrypt.CompareHashAndPassword(passHash, []byte(password))
if err != nil { if err != nil {
http.Error(w, "Username or password invalid", http.StatusUnauthorized) props.GeneralError = "Username or password invalid"
auth.RedirectWithFlash(w, r, "/", "login_props", &props)
return return
} }
session, err := auth.Store.Get(r, "lishwist_user") session, err := auth.Store.Get(r, "lishwist_user")
if err != nil { if err != nil {
log.Println("Couldn't get jwt:", err) log.Println("Couldn't get jwt:", err)
http.Error(w, "Something went wrong. Error code: Sokka", http.StatusInternalServerError) props.GeneralError = "Something went wrong. Error code: Sokka"
auth.RedirectWithFlash(w, r, "/", "login_props", &props)
return return
} }
session.Values["authorized"] = true session.Values["authorized"] = true

View File

@ -0,0 +1,17 @@
package auth
import (
"log"
"net/http"
)
func (auth *AuthMiddleware) RedirectWithFlash(w http.ResponseWriter, r *http.Request, url string, key string, flash any) {
session, _ := auth.Store.Get(r, "lishwist_user")
session.AddFlash(flash, key)
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, url, http.StatusSeeOther)
}

View File

@ -5,10 +5,61 @@ import (
"net/http" "net/http"
"lishwist/db" "lishwist/db"
"lishwist/templates"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
type RegisterProps struct {
GeneralError string
Username templates.InputProps
Password templates.InputProps
ConfirmPassword templates.InputProps
}
func NewRegisterProps() RegisterProps {
return RegisterProps{
GeneralError: "",
Username: templates.InputProps{
Name: "username",
Required: true,
},
Password: templates.InputProps{
Type: "password",
Name: "newPassword",
Required: true,
MinLength: 5,
},
ConfirmPassword: templates.InputProps{
Type: "password",
Name: "confirmPassword",
Required: true,
},
}
}
func (auth *AuthMiddleware) Register(w http.ResponseWriter, r *http.Request) {
props := NewRegisterProps()
session, _ := auth.Store.Get(r, "lishwist_user")
if flashes := session.Flashes("register_props"); len(flashes) > 0 {
flashProps, _ := flashes[0].(*RegisterProps)
props.Username.Value = flashProps.Username.Value
props.GeneralError = flashProps.GeneralError
props.Username.Error = flashProps.Username.Error
props.ConfirmPassword.Error = flashProps.ConfirmPassword.Error
}
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
}
templates.Execute(w, "register.gotmpl", props)
}
func (auth *AuthMiddleware) RegisterPost(w http.ResponseWriter, r *http.Request) { func (auth *AuthMiddleware) RegisterPost(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Error(w, "Couldn't parse form", http.StatusBadRequest) http.Error(w, "Couldn't parse form", http.StatusBadRequest)
@ -19,31 +70,40 @@ func (auth *AuthMiddleware) RegisterPost(w http.ResponseWriter, r *http.Request)
newPassword := r.Form.Get("newPassword") newPassword := r.Form.Get("newPassword")
confirmPassword := r.Form.Get("confirmPassword") confirmPassword := r.Form.Get("confirmPassword")
props := NewRegisterProps()
props.Username.Value = username
props.Password.Value = newPassword
props.ConfirmPassword.Value = confirmPassword
existingUser, _ := db.GetUserByName(username) existingUser, _ := db.GetUserByName(username)
if existingUser != nil { if existingUser != nil {
http.Error(w, "Username is taken", http.StatusBadRequest) props.Username.Error = "Username is taken"
auth.RedirectWithFlash(w, r, "/register", "register_props", &props)
return return
} }
if newPassword != confirmPassword { if newPassword != confirmPassword {
http.Error(w, "passwords didn't match", http.StatusBadRequest) props.ConfirmPassword.Error = "Password didn't match"
auth.RedirectWithFlash(w, r, "/register", "register_props", &props)
return return
} }
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost) hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
if err != nil { if err != nil {
http.Error(w, "Something went wrong. Error code: Aang", http.StatusInternalServerError) props.GeneralError = "Something went wrong. Error code: Aang"
auth.RedirectWithFlash(w, r, "/register", "register_props", &props)
return return
} }
_, err = db.CreateUser(username, hashedPasswordBytes) _, err = db.CreateUser(username, hashedPasswordBytes)
if err != nil { if err != nil {
http.Error(w, "Something went wrong. Error code: Ozai", http.StatusInternalServerError) props.GeneralError = "Something went wrong. Error code: Ozai"
auth.RedirectWithFlash(w, r, "/register", "register_props", &props)
return return
} }
session, _ := auth.Store.Get(r, "lishwist_user") session, _ := auth.Store.Get(r, "lishwist_user")
session.Values["successful_registration"] = true session.AddFlash(true, "successful_registration")
if err := session.Save(r, w); err != nil { if err := session.Save(r, w); err != nil {
log.Println("Couldn't save session:", err) log.Println("Couldn't save session:", err)
http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError) http.Error(w, "Something went wrong. Error code: Zuko", http.StatusInternalServerError)

View File

@ -2,7 +2,7 @@ package context
import ( import (
"lishwist/auth" "lishwist/auth"
"log" "lishwist/error"
"net/http" "net/http"
) )
@ -19,8 +19,7 @@ func (ctx *Context) WishlistAdd(w http.ResponseWriter, r *http.Request) {
newGiftName := r.Form.Get("gift_name") newGiftName := r.Form.Get("gift_name")
err := user.AddGift(newGiftName) err := user.AddGift(newGiftName)
if err != nil { if err != nil {
log.Printf("Failed to add gift: %s\n", err) error.Page(w, "Failed to add gift.", http.StatusInternalServerError, err)
http.Error(w, "Failed to add gift.", http.StatusInternalServerError)
return return
} }
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
@ -35,8 +34,7 @@ func (ctx *Context) WishlistDelete(w http.ResponseWriter, r *http.Request) {
targets := r.Form["gift"] targets := r.Form["gift"]
err := user.RemoveGifts(targets...) err := user.RemoveGifts(targets...)
if err != nil { if err != nil {
log.Printf("Failed to remove gifts: %s\n", err) error.Page(w, "Failed to remove gifts.", http.StatusInternalServerError, err)
http.Error(w, "Failed to remove gifts.", http.StatusInternalServerError)
return return
} }
http.Redirect(w, r, "/", http.StatusSeeOther) http.Redirect(w, r, "/", http.StatusSeeOther)
@ -45,7 +43,7 @@ func (ctx *Context) WishlistDelete(w http.ResponseWriter, r *http.Request) {
func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request) { func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request) {
user := ctx.Auth.ExpectUser(r) user := ctx.Auth.ExpectUser(r)
if err := r.ParseForm(); err != nil { if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) error.Page(w, "Failed to parse form...", http.StatusBadRequest, err)
return return
} }
userReference := r.PathValue("userReference") userReference := r.PathValue("userReference")
@ -55,27 +53,25 @@ func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request)
unclaims := r.Form["claimed"] unclaims := r.Form["claimed"]
err := user.ClaimGifts(claims, unclaims) err := user.ClaimGifts(claims, unclaims)
if err != nil { if err != nil {
http.Error(w, "Failed to update claim...", http.StatusInternalServerError) error.Page(w, "Failed to update claim...", http.StatusInternalServerError, err)
return return
} }
case "complete": case "complete":
claims := r.Form["claimed"] claims := r.Form["claimed"]
err := user.CompleteGifts(claims) err := user.CompleteGifts(claims)
if err != nil { if err != nil {
log.Printf("Failed to complete gifts: %s\n", err) error.Page(w, "Failed to complete gifts...", http.StatusInternalServerError, nil)
http.Error(w, "Failed to complete gifts...", http.StatusInternalServerError)
return return
} }
case "add": case "add":
giftName := r.Form.Get("gift_name") giftName := r.Form.Get("gift_name")
if giftName == "" { if giftName == "" {
http.Error(w, "Gift name not provided", http.StatusBadRequest) error.Page(w, "Gift name not provided", http.StatusBadRequest, nil)
return return
} }
err := user.AddGiftToUser(userReference, giftName) err := user.AddGiftToUser(userReference, giftName)
if err != nil { if err != nil {
log.Printf("Failed to add gift idea to other user: %s\n", err) error.Page(w, "Failed to add gift idea to other user...", http.StatusInternalServerError, err)
http.Error(w, "Failed to add gift idea to other user...", http.StatusInternalServerError)
return return
} }
case "delete": case "delete":
@ -84,8 +80,7 @@ func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request)
gifts := append(claims, unclaims...) gifts := append(claims, unclaims...)
err := user.RemoveGifts(gifts...) err := user.RemoveGifts(gifts...)
if err != nil { if err != nil {
log.Printf("Failed to remove gift idea for other user: %s\n", err) error.Page(w, "Failed to remove gift idea for other user...", http.StatusInternalServerError, err)
http.Error(w, "Failed to remove gift idea for other user...", http.StatusInternalServerError)
return return
} }
default: default:

View File

@ -2,16 +2,15 @@ package context
import ( import (
"lishwist/db" "lishwist/db"
"lishwist/error"
"lishwist/templates" "lishwist/templates"
"log"
"net/http" "net/http"
) )
type ForeignWishlistProps struct { type foreignWishlistProps struct {
CurrentUserId string CurrentUserId string
CurrentUserName string CurrentUserName string
Username string Username string
UserReference string
Gifts []db.Gift Gifts []db.Gift
} }
@ -19,25 +18,48 @@ func (ctx *Context) ForeignWishlist(w http.ResponseWriter, r *http.Request) {
userReference := r.PathValue("userReference") userReference := r.PathValue("userReference")
user := ctx.Auth.ExpectUser(r) user := ctx.Auth.ExpectUser(r)
if user.Reference == userReference { if user.Reference == userReference {
http.Error(w, "You can't view your own list, silly ;)", http.StatusForbidden) http.Redirect(w, r, "/", http.StatusSeeOther)
return return
} }
otherUser, err := db.GetUserByReference(userReference) otherUser, err := db.GetUserByReference(userReference)
if err != nil { if err != nil {
log.Printf("An error occurred while fetching a user: %s\n", err) error.Page(w, "An error occurred while fetching this user :(", http.StatusInternalServerError, err)
http.Error(w, "An error occurred while fetching this user :(", http.StatusInternalServerError)
return return
} }
if otherUser == nil { if otherUser == nil {
http.Error(w, "User not found", http.StatusNotFound) error.Page(w, "User not found", http.StatusNotFound, err)
return return
} }
gifts, err := user.GetOtherUserGifts(userReference) gifts, err := user.GetOtherUserGifts(userReference)
if err != nil { if err != nil {
log.Printf("An error occurred while fetching %s's wishlist: %s\n", otherUser.Name, err) error.Page(w, "An error occurred while fetching this user's wishlist :(", http.StatusInternalServerError, err)
http.Error(w, "An error occurred while fetching this user's wishlist :(", http.StatusInternalServerError)
return return
} }
p := ForeignWishlistProps{CurrentUserId: user.Id, CurrentUserName: user.Name, Username: otherUser.Name, UserReference: userReference, Gifts: gifts} p := foreignWishlistProps{CurrentUserId: user.Id, CurrentUserName: user.Name, Username: otherUser.Name, Gifts: gifts}
templates.Execute(w, "foreign_wishlist.gotmpl", p) templates.Execute(w, "foreign_wishlist.gotmpl", p)
} }
type publicForeignWishlistProps struct {
Username string
GiftCount int
}
func (ctx *Context) PublicForeignWishlist(w http.ResponseWriter, r *http.Request) {
userReference := r.PathValue("userReference")
otherUser, err := db.GetUserByReference(userReference)
if err != nil {
error.Page(w, "An error occurred while fetching this user :(", http.StatusInternalServerError, err)
return
}
if otherUser == nil {
error.Page(w, "User not found", http.StatusNotFound, err)
return
}
giftCount, err := otherUser.CountGifts()
if err != nil {
error.Page(w, "An error occurred while fetching data about this user :(", http.StatusInternalServerError, err)
return
}
p := publicForeignWishlistProps{Username: otherUser.Name, GiftCount: giftCount}
templates.Execute(w, "public_foreign_wishlist.gotmpl", p)
}

View File

@ -5,6 +5,7 @@ import (
"lishwist/db" "lishwist/db"
"lishwist/env" "lishwist/env"
"lishwist/error"
"lishwist/templates" "lishwist/templates"
) )
@ -20,12 +21,12 @@ func (ctx *Context) Home(w http.ResponseWriter, r *http.Request) {
user := ctx.Auth.ExpectUser(r) user := ctx.Auth.ExpectUser(r)
gifts, err := user.GetGifts() gifts, err := user.GetGifts()
if err != nil { if err != nil {
http.Error(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError) error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err)
return return
} }
todo, err := user.GetTodo() todo, err := user.GetTodo()
if err != nil { if err != nil {
http.Error(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError) error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err)
return return
} }
p := HomeProps{Username: user.Name, Gifts: gifts, Todo: todo, Reference: user.Reference, HostUrl: env.HostUrl.String()} p := HomeProps{Username: user.Name, Gifts: gifts, Todo: todo, Reference: user.Reference, HostUrl: env.HostUrl.String()}

View File

@ -85,6 +85,16 @@ func (u *User) GetPassHash() ([]byte, error) {
return []byte(passHash), nil return []byte(passHash), nil
} }
func (u *User) CountGifts() (int, error) {
stmt := "SELECT COUNT(gift.id) AS gift_count FROM gift JOIN user ON gift.recipient_id = user.id LEFT JOIN user AS claimant ON gift.claimant_id = claimant.id WHERE gift.creator_id = user.id AND user.id = ?"
var giftCount int
err := database.QueryRow(stmt, u.Id).Scan(&giftCount)
if err != nil {
return 0, err
}
return giftCount, nil
}
func (u *User) GetGifts() ([]Gift, error) { func (u *User) GetGifts() ([]Gift, error) {
stmt := "SELECT gift.id, gift.name, claimant.id, claimant.name, gift.sent FROM gift JOIN user ON gift.recipient_id = user.id LEFT JOIN user AS claimant ON gift.claimant_id = claimant.id WHERE gift.creator_id = user.id AND user.id = ?" stmt := "SELECT gift.id, gift.name, claimant.id, claimant.name, gift.sent FROM gift JOIN user ON gift.recipient_id = user.id LEFT JOIN user AS claimant ON gift.claimant_id = claimant.id WHERE gift.creator_id = user.id AND user.id = ?"
rows, err := database.Query(stmt, u.Id) rows, err := database.Query(stmt, u.Id)

17
error/page.go Normal file
View File

@ -0,0 +1,17 @@
package error
import (
"lishwist/templates"
"log"
"net/http"
)
type pageProps struct {
Message string
}
func Page(w http.ResponseWriter, publicMessage string, status int, err error) {
log.Printf("%s --- %s\n", publicMessage, err)
templates.Execute(w, "error_page.gotmpl", pageProps{publicMessage})
http.Error(w, "", http.StatusInternalServerError)
}

23
hashpword/main.go Normal file
View File

@ -0,0 +1,23 @@
package main
import (
"fmt"
"log"
"os"
"strconv"
"golang.org/x/crypto/bcrypt"
)
func main() {
password := []byte(os.Args[1])
cost, err := strconv.ParseInt(os.Args[2], 10, 0)
if err != nil {
log.Fatalln("Failed to parse cost: ", err)
}
hash, err := bcrypt.GenerateFromPassword(password, int(cost))
if err != nil {
log.Fatalln("Failed to hash: ", err)
}
fmt.Println(string(hash))
}

View File

@ -8,7 +8,6 @@ import (
"lishwist/context" "lishwist/context"
"lishwist/db" "lishwist/db"
"lishwist/env" "lishwist/env"
"lishwist/templates"
) )
func main() { func main() {
@ -30,10 +29,11 @@ func main() {
Auth: authMiddleware, Auth: authMiddleware,
} }
publicMux.HandleFunc("GET /register", templates.Register) publicMux.HandleFunc("GET /register", authMiddleware.Register)
publicMux.HandleFunc("POST /register", authMiddleware.RegisterPost) publicMux.HandleFunc("POST /register", authMiddleware.RegisterPost)
publicMux.HandleFunc("GET /", authMiddleware.Login) publicMux.HandleFunc("GET /", authMiddleware.Login)
publicMux.HandleFunc("POST /", authMiddleware.LoginPost) publicMux.HandleFunc("POST /", authMiddleware.LoginPost)
publicMux.HandleFunc("GET /list/{userReference}", ctx.PublicForeignWishlist)
protectedMux.HandleFunc("GET /{$}", ctx.Home) protectedMux.HandleFunc("GET /{$}", ctx.Home)
protectedMux.HandleFunc("POST /{$}", ctx.HomePost) protectedMux.HandleFunc("POST /{$}", ctx.HomePost)

25
session/session.go Normal file
View File

@ -0,0 +1,25 @@
package sesh
import (
"log"
"net/http"
"github.com/gorilla/sessions"
)
func GetFirstFlash(w http.ResponseWriter, r *http.Request, session *sessions.Session, key ...string) (any, error) {
flashes := session.Flashes(key...)
if len(flashes) < 1 {
return nil, nil
}
flash := flashes[0]
if err := session.Save(r, w); err != nil {
log.Println("Couldn't save session:", err)
return nil, err
}
return flash, nil
}

View File

@ -1,3 +1,17 @@
{{define "input"}}
<input style="padding: .375rem .75rem;" class="form-control{{if .Error}} is-invalid{{end}}" {{if .Type}}type="{{.Type}}" {{end}}name="{{.Name}}"
value="{{.Value}}" {{if .Required}}required {{end}}aria-describedby="{{if .Error}}{{.Name}}Error{{end}}">
{{with .Error}}
<div id="{{$.Name}}Error" class="invalid-feedback">
{{.}}
</div>
{{else}}
<div style="margin-top: .25rem; font-size: .875em;">
</div>
{{end}}
{{end}}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>

View File

@ -0,0 +1,17 @@
{{define "navbar"}}
<nav>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
</ul>
</nav>
{{end}}
{{define "body"}}
<div class="container d-flex flex-grow-1 justify-content-center align-items-center flex-column">
<div class="alert alert-danger" role="alert">
<p class="mb-0">{{.Message}}</p>
</div>
</div>
{{end}}

View File

@ -84,7 +84,7 @@
{{end}} {{end}}
<form method="post"> <form method="post">
<div class="input-group mt-3"> <div class="input-group mt-3">
<input class="form-control" name="gift_name" required aria-describedby="gift_name_help"> <input class="form-control" name="gift_name" required aria-describedby="gift_name_help" placeholder="Write a gift idea here" autofocus>
<button class="btn btn-primary" type="submit" name="intent" value="add">Add gift idea</button> <button class="btn btn-primary" type="submit" name="intent" value="add">Add gift idea</button>
</div> </div>
<div id="gift_name_help" class="form-text">This will be invisible to {{.Username}}, but everyone else will be <div id="gift_name_help" class="form-text">This will be invisible to {{.Username}}, but everyone else will be

View File

@ -47,7 +47,7 @@
{{end}} {{end}}
<form method="post"> <form method="post">
<div class="input-group"> <div class="input-group">
<input class="form-control" name="gift_name" required> <input class="form-control" name="gift_name" required placeholder="Write a gift idea here" autofocus>
<button class="btn btn-primary" type="submit" name="intent" value="add_idea">Add gift idea</button> <button class="btn btn-primary" type="submit" name="intent" value="add_idea">Add gift idea</button>
</div> </div>
</form> </form>

View File

@ -8,17 +8,22 @@
<p class="mb-0">Registration successful. Now you can login.</p> <p class="mb-0">Registration successful. Now you can login.</p>
</div> </div>
{{end}} {{end}}
{{with .GeneralError}}
<div class="alert alert-danger" role="alert">
<p class="mb-0">{{.}}</p>
</div>
{{end}}
<form method="post"> <form method="post">
<div class="d-flex flex-column gap-3"> <div class="d-flex flex-column">
<label> <label>
Username Username
<input class="form-control" name="username" required> {{template "input" .Username}}
</label> </label>
<label> <label>
Password Password
<input class="form-control" name="password" type="password" required> {{template "input" .Password}}
</label> </label>
<div> <div class="mb-3">
<a href="/register">Register</a> <a href="/register">Register</a>
</div> </div>
<input class="btn btn-primary" type="submit" value="Login"> <input class="btn btn-primary" type="submit" value="Login">

View File

@ -0,0 +1,37 @@
{{define "navbar"}}
<nav>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
</ul>
</nav>
{{end}}
{{define "login_prompt"}}
<a href="/">Login</a> or <a href="/register">register</a>
{{end}}
{{define "body"}}
<div class="overflow-y-scroll flex-grow-1">
<div class="container py-5">
<section class="card">
<div class="card-body">
<h2>{{.Username}}'s list</h2>
{{if eq .GiftCount 0}}
<p>{{.Username}} doesn't have any gift ideas!</p>
<p>{{template "login_prompt"}} to add some! :^)</p>
{{else}}
{{if eq .GiftCount 1}}
<p>{{.Username}} only has one gift idea.</p>
<p>{{template "login_prompt"}} to claim it, or add more! :^)</p>
{{else}}
<p>{{.Username}} has {{.GiftCount}} gift ideas.</p>
<p>{{template "login_prompt"}} to claim an idea, or add more! :^)</p>
{{end}}
{{end}}
</div>
</section>
</div>
</div>
{{end}}

View File

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

View File

@ -1,4 +1,11 @@
{{define "navbar"}} {{define "navbar"}}
<nav>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
</ul>
</nav>
{{end}} {{end}}
{{define "body"}} {{define "body"}}
@ -7,19 +14,24 @@
<p>Your password will be stored in a safe, responsible manner; but don't trust my programming skills!</p> <p>Your password will be stored in a safe, responsible manner; but don't trust my programming skills!</p>
<p class="mb-0">Maybe use a password here that you don't use for important things...</p> <p class="mb-0">Maybe use a password here that you don't use for important things...</p>
</div> </div>
{{with .GeneralError}}
<div class="alert alert-danger" role="alert">
<p class="mb-0">{{.}}</p>
</div>
{{end}}
<form method="post"> <form method="post">
<div class="d-flex flex-column gap-3"> <div class="d-flex flex-column">
<label> <label>
Username Username
<input class="form-control" name="username" required> {{template "input" .Username}}
</label> </label>
<label> <label>
Password Password
<input class="form-control" name="newPassword" type="password" required minlength="5"> {{template "input" .Password}}
</label> </label>
<label> <label>
Confirm password Confirm password
<input class="form-control" name="confirmPassword" type="password" required minlength="5"> {{template "input" .ConfirmPassword}}
</label> </label>
<input class="btn btn-primary" type="submit" value="Register"> <input class="btn btn-primary" type="submit" value="Register">
</div> </div>

View File

@ -6,6 +6,15 @@ import (
"text/template" "text/template"
) )
type InputProps struct {
Type string
Name string
Required bool
Value string
Error string
MinLength uint
}
var tmpls map[string]*template.Template = loadTemplates() var tmpls map[string]*template.Template = loadTemplates()
func Execute(w http.ResponseWriter, name string, data any) { func Execute(w http.ResponseWriter, name string, data any) {
@ -21,10 +30,14 @@ func loadTemplates() map[string]*template.Template {
loginTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/login.gotmpl")) loginTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/login.gotmpl"))
registerTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/register.gotmpl")) registerTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/register.gotmpl"))
foreignTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/foreign_wishlist.gotmpl")) foreignTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/foreign_wishlist.gotmpl"))
publicForeignTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/public_foreign_wishlist.gotmpl"))
errorTmpl := template.Must(template.ParseFiles("templates/base.gotmpl", "templates/error_page.gotmpl"))
return map[string]*template.Template{ return map[string]*template.Template{
"home.gotmpl": homeTmpl, "home.gotmpl": homeTmpl,
"login.gotmpl": loginTmpl, "login.gotmpl": loginTmpl,
"register.gotmpl": registerTmpl, "register.gotmpl": registerTmpl,
"foreign_wishlist.gotmpl": foreignTmpl, "foreign_wishlist.gotmpl": foreignTmpl,
"public_foreign_wishlist.gotmpl": publicForeignTmpl,
"error_page.gotmpl": errorTmpl,
} }
} }