Compare commits

...

9 Commits

Author SHA1 Message Date
Teajey dcba801dde
feat: user deletion and get user as json 2024-11-22 10:48:41 +09:00
Teajey 20761920d3
feat: page not found 2024-11-21 20:17:04 +09:00
Teajey b48140e9c3
fix: correctly assign member 2024-11-21 20:17:04 +09:00
Teajey 98a39f8e4f
feat: group members always initialized 2024-11-21 20:17:04 +09:00
Teajey 271163a889
feat: groups json interface 2024-11-21 20:17:04 +09:00
Teajey 5a4097f4fe
feat: remove register json post endpoint 2024-11-21 20:17:04 +09:00
Teajey 994f4ee64a
feat: json login support 2024-11-21 20:17:04 +09:00
Teajey fac92511ee
feat: list users json endpoint 2024-11-21 20:17:04 +09:00
Teajey d2fb0fa707
feat: register via json
also lots of refactoring (sorry)
2024-11-21 20:17:03 +09:00
24 changed files with 592 additions and 257 deletions

85
server/api/login.go Normal file
View File

@ -0,0 +1,85 @@
package api
import (
"lishwist/db"
"lishwist/templates"
"log"
"golang.org/x/crypto/bcrypt"
)
type LoginProps struct {
GeneralError string
SuccessfulRegistration bool
Username templates.InputProps
Password templates.InputProps
}
func NewLoginProps(username, password string) *LoginProps {
return &LoginProps{
Username: templates.InputProps{
Name: "username",
Required: true,
Value: username,
},
Password: templates.InputProps{
Name: "password",
Type: "password",
Required: true,
Value: password,
},
}
}
func (p *LoginProps) Validate() (valid bool) {
valid = true
if !p.Username.Validate() {
valid = false
}
if !p.Password.Validate() {
valid = false
}
return
}
func Login(username, password string) *LoginProps {
props := NewLoginProps(username, password)
valid := props.Validate()
props.Password.Value = ""
if !valid {
log.Printf("Invalid props: %#v\n", props)
return props
}
user, err := db.GetUserByName(username)
if err != nil {
log.Printf("Failed to fetch user: %s\n", err)
props.GeneralError = "Username or password invalid"
return props
}
if user == nil {
log.Printf("User not found by name: %q\n", username)
props.GeneralError = "Username or password invalid"
return props
}
passHash, err := user.GetPassHash()
if err != nil {
log.Println("Failed to get password hash: " + err.Error())
props.GeneralError = "Something went wrong. Error code: Momo"
return props
}
err = bcrypt.CompareHashAndPassword(passHash, []byte(password))
if err != nil {
log.Println("Username or password invalid: " + err.Error())
props.GeneralError = "Username or password invalid"
return props
}
return nil
}

View File

@ -17,6 +17,8 @@ type RegisterProps struct {
}
func (p *RegisterProps) Validate() (valid bool) {
valid = true
if p.Password.Value != p.ConfirmPassword.Value {
p.ConfirmPassword.Error = "Passwords didn't match"
valid = false
@ -69,24 +71,27 @@ func Register(username, newPassword, confirmPassword string) *RegisterProps {
props.Password.Value = ""
props.ConfirmPassword.Value = ""
if !valid {
log.Printf("Invalid props: %#v\n", props)
return props
}
existingUser, _ := db.GetUserByName(username)
if existingUser != nil {
log.Printf("Username is taken: %q\n", username)
props.Username.Error = "Username is taken"
return props
}
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
if err != nil {
log.Printf("Failed to hash password: %s\n", err)
props.GeneralError = "Something went wrong. Error code: Aang"
return props
}
_, err = db.CreateUser(username, hashedPasswordBytes)
if err != nil {
log.Println("Registration error:", err)
log.Printf("Failed to create user: %s\n", err)
props.GeneralError = "Something went wrong. Error code: Ozai"
return props
}

View File

@ -1,61 +1,121 @@
package db
import "database/sql"
import (
"database/sql"
"fmt"
"strconv"
)
type Group struct {
Id string
Name string
Reference string
Members []User
}
func (g *Group) MemberIndex(userId string) int {
for i, u := range g.Members {
if u.Id == userId {
return i
}
}
return -1
}
func queryForGroup(query string, args ...any) (*Group, error) {
var id string
var name string
var reference string
err := database.QueryRow(query, args...).Scan(&id, &name, &reference)
var group Group
err := database.QueryRow(query, args...).Scan(&group.Id, &group.Name, &group.Reference)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
members, err := queryForGroupMembers(group.Id)
if err != nil {
return nil, err
}
group.Members = members
return &group, nil
}
func queryForGroups(query string, args ...any) ([]Group, error) {
groups := []Group{}
rows, err := database.Query(query)
if err != nil {
return groups, err
}
defer rows.Close()
for rows.Next() {
var group Group
err := rows.Scan(&group.Id, &group.Name, &group.Reference)
if err != nil {
return groups, err
}
members, err := queryForGroupMembers(group.Id)
if err != nil {
return groups, err
}
group.Members = members
groups = append(groups, group)
}
err = rows.Err()
if err != nil {
return groups, err
}
return groups, nil
}
func queryForGroupMembers(groupId string) ([]User, error) {
query := "SELECT user.id, user.name, user.reference, user.is_admin, user.is_live FROM v_user AS user JOIN group_member ON group_member.user_id = user.id JOIN [group] ON [group].id = group_member.group_id WHERE [group].id = ? ORDER BY group_member.user_id"
members, err := queryForUsers(query, groupId)
if err != nil {
return members, fmt.Errorf("Failed to get members: %w", err)
}
return members, nil
}
func GetGroupByReference(reference string) (*Group, error) {
query := "SELECT [group].id, [group].name, [group].reference FROM [group] WHERE [group].reference = ?"
return queryForGroup(query, reference)
}
func GetAllGroups() ([]Group, error) {
query := "SELECT id, name, reference FROM [group];"
return queryForGroups(query)
}
func CreateGroup(name string, reference string) (*Group, error) {
stmt := "INSERT INTO [group] (name, reference) VALUES (?, ?)"
result, err := database.Exec(stmt, name, reference)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
group := Group{
Id: id,
Id: strconv.FormatInt(id, 10),
Name: name,
Reference: reference,
}
return &group, nil
}
func GetGroupByReference(reference string) (*Group, error) {
stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] WHERE [group].reference = ?"
return queryForGroup(stmt, reference)
func (g *Group) AddUser(userId string) error {
stmt := "INSERT INTO group_member (group_id, user_id) VALUES (?, ?)"
_, err := database.Exec(stmt, g.Id, userId)
if err != nil {
return err
}
return nil
}
func (g *Group) GetMembers() ([]User, error) {
stmt := "SELECT user.id, user.name, user.reference FROM user JOIN group_member ON group_member.user_id = user.id JOIN [group] ON [group].id = group_member.group_id WHERE [group].id = ?"
rows, err := database.Query(stmt, g.Id)
users := []User{}
func (g *Group) RemoveUser(userId string) error {
stmt := "DELETE FROM group_member WHERE group_id = ? AND user_id = ?"
_, err := database.Exec(stmt, g.Id, userId)
if err != nil {
return users, err
return err
}
defer rows.Close()
for rows.Next() {
var id string
var name string
var reference string
err := rows.Scan(&id, &name, &reference)
if err != nil {
return users, err
}
users = append(users, User{
Id: id,
Name: name,
Reference: reference,
})
}
err = rows.Err()
if err != nil {
return users, err
}
return users, nil
return nil
}

View File

@ -3,8 +3,10 @@ CREATE TABLE IF NOT EXISTS "user" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL UNIQUE,
"reference" TEXT NOT NULL UNIQUE,
"motto" TEXT NOT NULL,
"motto" TEXT NOT NULL DEFAULT "",
"password_hash" TEXT NOT NULL,
"is_admin" INTEGER NOT NULL DEFAULT 0,
"is_live" INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "gift" (
@ -37,4 +39,15 @@ CREATE TABLE IF NOT EXISTS "session" (
"value" TEXT NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
DROP VIEW IF EXISTS "v_user";
CREATE VIEW "v_user"
AS
SELECT * FROM user WHERE user.is_live = 1;
-- DROP VIEW IF EXISTS "v_wish";
-- CREATE VIEW "v_wish"
-- AS
-- SELECT gift.id, gift.name, gift.sent FROM gift JOIN user AS recipient;
COMMIT;

22
server/db/migration/1.sql Normal file
View File

@ -0,0 +1,22 @@
BEGIN TRANSACTION;
ALTER TABLE user ADD COLUMN "is_live" INTEGER NOT NULL DEFAULT 1;
ALTER TABLE user RENAME TO old_user;
CREATE TABLE "user" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL UNIQUE,
"reference" TEXT NOT NULL UNIQUE,
"motto" TEXT NOT NULL DEFAULT "",
"password_hash" TEXT NOT NULL,
"is_admin" INTEGER NOT NULL DEFAULT 0,
"is_live" INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY("id" AUTOINCREMENT)
);
INSERT INTO user SELECT * FROM old_user;
DROP TABLE "old_user";
COMMIT;

View File

@ -11,6 +11,8 @@ type User struct {
Id string
Name string
Reference string
IsAdmin bool
IsLive bool
}
type Gift struct {
@ -27,35 +29,75 @@ type Gift struct {
}
func queryForUser(query string, args ...any) (*User, error) {
var id string
var name string
var reference string
err := database.QueryRow(query, args...).Scan(&id, &name, &reference)
var u User
err := database.QueryRow(query, args...).Scan(&u.Id, &u.Name, &u.Reference, &u.IsAdmin, &u.IsLive)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, err
}
user := User{
Id: id,
Name: name,
Reference: reference,
return &u, nil
}
func queryForUsers(query string, args ...any) ([]User, error) {
rows, err := database.Query(query, args...)
if err != nil {
return nil, err
}
return &user, nil
defer rows.Close()
users := []User{}
for rows.Next() {
var u User
err = rows.Scan(&u.Id, &u.Name, &u.Reference, &u.IsAdmin, &u.IsLive)
if err != nil {
return nil, err
}
users = append(users, u)
}
err = rows.Err()
if err != nil {
return nil, err
}
return users, nil
}
func GetAllUsers() ([]User, error) {
stmt := "SELECT id, name, reference, is_admin, is_live FROM user"
return queryForUsers(stmt)
}
func GetUser(id string) (*User, error) {
stmt := "SELECT id, name, reference, is_admin, is_live FROM v_user WHERE id = ?"
return queryForUser(stmt, id)
}
func GetUserByName(username string) (*User, error) {
stmt := "SELECT user.id, user.name, user.reference FROM user WHERE user.name = ?"
stmt := "SELECT id, name, reference, is_admin, is_live FROM v_user WHERE name = ?"
return queryForUser(stmt, username)
}
func GetUserByReference(reference string) (*User, error) {
stmt := "SELECT user.id, user.name, user.reference FROM user WHERE user.reference = ?"
stmt := "SELECT id, name, reference, is_admin, is_live FROM v_user WHERE reference = ?"
return queryForUser(stmt, reference)
}
func GetAnyUserByReference(reference string) (*User, error) {
stmt := "SELECT id, name, reference, is_admin, is_live FROM user WHERE reference = ?"
return queryForUser(stmt, reference)
}
func (u *User) SetLive(setting bool) error {
query := "UPDATE user SET is_live = ? WHERE reference = ?"
_, err := database.Exec(query, setting, u.Reference)
if err != nil {
return err
}
u.IsLive = setting
return err
}
func CreateUser(username string, passHash []byte) (*User, error) {
stmt := "INSERT INTO user (name, motto, reference, password_hash) VALUES (?, '', ?, ?)"
stmt := "INSERT INTO user (name, reference, password_hash) VALUES (?, ?, ?)"
reference, err := uuid.NewRandom()
if err != nil {
return nil, err
@ -76,7 +118,7 @@ func CreateUser(username string, passHash []byte) (*User, error) {
}
func (u *User) GetPassHash() ([]byte, error) {
stmt := "SELECT user.password_hash FROM user WHERE user.id = ?"
stmt := "SELECT password_hash FROM v_user WHERE id = ?"
var passHash string
err := database.QueryRow(stmt, u.Id).Scan(&passHash)
if err != nil {
@ -86,7 +128,7 @@ func (u *User) GetPassHash() ([]byte, error) {
}
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 = ?"
stmt := "SELECT COUNT(gift.id) AS gift_count FROM gift WHERE gift.creator_id = ?1 AND gift.recipient_id = ?1"
var giftCount int
err := database.QueryRow(stmt, u.Id).Scan(&giftCount)
if err != nil {
@ -96,7 +138,7 @@ func (u *User) CountGifts() (int, 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, gift.sent FROM gift WHERE gift.creator_id = ?1 AND gift.recipient_id = ?1"
rows, err := database.Query(stmt, u.Id)
if err != nil {
return nil, err
@ -106,19 +148,15 @@ func (u *User) GetGifts() ([]Gift, error) {
for rows.Next() {
var id string
var name string
var claimantId sql.NullString
var claimantName sql.NullString
var sent bool
err = rows.Scan(&id, &name, &claimantId, &claimantName, &sent)
err = rows.Scan(&id, &name, &sent)
if err != nil {
return nil, err
}
gift := Gift{
Id: id,
Name: name,
ClaimantId: claimantId.String,
ClaimantName: claimantName.String,
Sent: sent,
Id: id,
Name: name,
Sent: sent,
}
gifts = append(gifts, gift)
}
@ -132,15 +170,15 @@ func (u *User) GetGifts() ([]Gift, error) {
func (u *User) GetOtherUserGifts(otherUserReference string) ([]Gift, error) {
otherUser, err := GetUserByReference(otherUserReference)
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to get other user: %w", err)
}
if otherUser.Id == u.Id {
return nil, fmt.Errorf("Not allowed to view own foreign wishlist")
}
stmt := "SELECT gift.id, gift.name, claimant.id, claimant.name, gift.sent, gift.creator_id, creator.name, gift.recipient_id FROM gift JOIN user ON gift.recipient_id = user.id LEFT JOIN user AS claimant ON gift.claimant_id = claimant.id LEFT JOIN user AS creator ON gift.creator_id = creator.id WHERE user.id = ?"
stmt := "SELECT gift.id, gift.name, claimant.id, claimant.name, gift.sent, gift.creator_id, creator.name, gift.recipient_id FROM gift JOIN v_user AS user ON gift.recipient_id = user.id LEFT JOIN v_user AS claimant ON gift.claimant_id = claimant.id LEFT JOIN v_user AS creator ON gift.creator_id = creator.id WHERE user.id = ?"
rows, err := database.Query(stmt, otherUser.Id)
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to execute query: %w", err)
}
defer rows.Close()
gifts := []Gift{}
@ -155,7 +193,7 @@ func (u *User) GetOtherUserGifts(otherUserReference string) ([]Gift, error) {
var recipientId string
err = rows.Scan(&id, &name, &claimantId, &claimantName, &sent, &creatorId, &creatorName, &recipientId)
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to scan row: %w", err)
}
gift := Gift{
Id: id,
@ -171,13 +209,13 @@ func (u *User) GetOtherUserGifts(otherUserReference string) ([]Gift, error) {
}
err = rows.Err()
if err != nil {
return nil, err
return nil, fmt.Errorf("Rows returned an error: %w", err)
}
return gifts, nil
}
func (u *User) GetTodo() ([]Gift, error) {
stmt := "SELECT gift.id, gift.name, gift.sent, recipient.name, recipient.reference FROM gift JOIN user ON gift.claimant_id = user.id JOIN user AS recipient ON gift.recipient_id = recipient.id WHERE user.id = ? ORDER BY gift.sent ASC, gift.name"
stmt := "SELECT gift.id, gift.name, gift.sent, recipient.name, recipient.reference FROM gift JOIN v_user AS user ON gift.claimant_id = user.id JOIN v_user AS recipient ON gift.recipient_id = recipient.id WHERE user.id = ? ORDER BY gift.sent ASC, gift.name"
rows, err := database.Query(stmt, u.Id)
if err != nil {
return nil, err
@ -367,7 +405,7 @@ func (u *User) AddGiftToUser(otherUserReference string, giftName string) error {
}
func (u *User) GetGroups() ([]Group, error) {
stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] JOIN group_member ON group_member.group_id = [group].id JOIN user ON user.id = group_member.user_id WHERE user.id = ?"
stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] JOIN group_member ON group_member.group_id = [group].id JOIN v_user AS user ON user.id = group_member.user_id WHERE user.id = ?"
rows, err := database.Query(stmt, u.Id)
if err != nil {
return nil, err
@ -395,35 +433,6 @@ func (u *User) GetGroups() ([]Group, error) {
return groups, nil
}
func (u *User) GetPeers(groupId string) ([]User, error) {
stmt := "SELECT user.id, user.name, user.reference FROM user JOIN group_member ON group_member.user_id = user.id JOIN [group] ON [group].id = group_member.group_id WHERE [group].id = ? AND user.id != ?"
rows, err := database.Query(stmt, groupId, u.Id)
if err != nil {
return nil, err
}
defer rows.Close()
users := []User{}
for rows.Next() {
var id string
var name string
var reference string
err := rows.Scan(&id, &name, &reference)
if err != nil {
return nil, err
}
users = append(users, User{
Id: id,
Name: name,
Reference: reference,
})
}
err = rows.Err()
if err != nil {
return nil, err
}
return users, nil
}
func (u *User) GetGroupByReference(reference string) (*Group, error) {
stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] JOIN group_member ON [group].id == group_member.group_id WHERE [group].reference = ? AND group_member.user_id = ?"
return queryForGroup(stmt, reference, u.Id)

View File

@ -11,9 +11,9 @@ type pageProps struct {
}
func Page(w http.ResponseWriter, publicMessage string, status int, err error) {
w.WriteHeader(status)
if err != nil {
log.Printf("%s --- %s\n", publicMessage, err)
}
templates.Execute(w, "error_page.gotmpl", pageProps{publicMessage})
http.Error(w, "", http.StatusInternalServerError)
}

View File

@ -14,7 +14,7 @@ import (
func main() {
gob.Register(&api.RegisterProps{})
gob.Register(&routing.LoginProps{})
gob.Register(&api.LoginProps{})
err := db.Open()
if err != nil {
@ -27,7 +27,7 @@ func main() {
store, err := db.NewSessionStore()
if err != nil {
log.Fatalf("Failed to ")
log.Fatalf("Failed to initialize session store: %s\n", err)
}
store.Options.MaxAge = 86_400
store.Options.Secure = !env.InDev
@ -44,14 +44,23 @@ func main() {
r.Html.Public.HandleFunc("GET /list/{userReference}", route.PublicWishlist)
r.Html.Public.HandleFunc("GET /group/{groupReference}", route.PublicGroupPage)
r.Html.Private.HandleFunc("GET /{$}", route.Home)
r.Html.Private.HandleFunc("POST /{$}", route.HomePost)
r.Html.Private.HandleFunc("GET /list/{userReference}", route.ForeignWishlist)
r.Html.Private.HandleFunc("POST /list/{userReference}", route.ForeignWishlistPost)
r.Html.Private.HandleFunc("GET /group/{groupReference}", route.GroupPage)
r.Html.Private.Handle("GET /{$}", route.ExpectUser(route.Home))
r.Html.Private.Handle("POST /{$}", route.ExpectUser(route.HomePost))
r.Html.Private.Handle("GET /list/{userReference}", route.ExpectUser(route.ForeignWishlist))
r.Html.Private.Handle("POST /list/{userReference}", route.ExpectUser(route.ForeignWishlistPost))
r.Html.Private.Handle("GET /group/{groupReference}", route.ExpectUser(route.GroupPage))
r.Html.Private.HandleFunc("POST /logout", route.LogoutPost)
r.Html.Private.HandleFunc("GET /", routing.NotFound)
r.Json.Public.HandleFunc("POST /register", route.RegisterPostJson)
r.Json.Public.HandleFunc("GET /", routing.NotFoundJson)
r.Json.Private.Handle("GET /users", route.ExpectUser(route.UsersJson))
r.Json.Private.Handle("GET /users/{userReference}", route.ExpectUser(route.User))
r.Json.Private.Handle("POST /users/{userReference}", route.ExpectUser(route.UserPost))
r.Json.Private.Handle("GET /groups", route.ExpectUser(route.GroupsJson))
r.Json.Private.Handle("POST /groups/{groupReference}", route.ExpectUser(route.GroupPost))
r.Json.Private.Handle("GET /groups/{groupReference}", route.ExpectUser(route.Group))
r.Json.Private.HandleFunc("GET /", routing.NotFoundJson)
http.Handle("/", r)

View File

@ -2,6 +2,7 @@ package router
import (
"net/http"
"strings"
"github.com/Teajey/sqlstore"
)
@ -29,10 +30,10 @@ type Router struct {
}
func (s *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
accept := r.Header.Get("Accept")
switch contentType {
case "application/json":
switch {
case strings.HasPrefix(accept, "application/json"):
s.Json.ServeHTTP(w, r)
default:
s.Html.ServeHTTP(w, r)

View File

@ -18,16 +18,23 @@ func NewContext(store *sqlstore.Store) *Context {
}
}
func (auth *Context) ExpectUser(r *http.Request) *db.User {
session, _ := auth.store.Get(r, "lishwist_user")
username, ok := session.Values["username"].(string)
if !ok {
log.Fatalln("Failed to get username")
}
func (ctx *Context) ExpectUser(next func(*db.User, http.ResponseWriter, *http.Request)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := ctx.store.Get(r, "lishwist_user")
username, ok := session.Values["username"].(string)
if !ok {
log.Println("Failed to get username")
http.Error(w, "", http.StatusInternalServerError)
return
}
user, err := db.GetUserByName(username)
if err != nil {
log.Fatalf("Failed to get user: %s\n", err)
}
return user
user, err := db.GetUserByName(username)
if err != nil {
log.Printf("Failed to get user: %s\n", err)
http.Error(w, "", http.StatusInternalServerError)
return
}
next(user, w, r)
})
}

View File

@ -2,13 +2,16 @@ package routing
import (
"fmt"
"log"
"net/http"
"strings"
)
func writeGeneralError(w http.ResponseWriter, msg string, status int) {
func writeGeneralErrorJson(w http.ResponseWriter, status int, format string, a ...any) {
msg := fmt.Sprintf(format, a...)
log.Printf("General error: %s\n", msg)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(status)
escapedMsg := strings.ReplaceAll(msg, `"`, `\"`)
_, _ = w.Write([]byte(fmt.Sprintf(`{"GeneralError":"%s"}`, escapedMsg)))
w.Header().Add("Content-Type", "application/json")
}

View File

@ -14,10 +14,9 @@ type foreignWishlistProps struct {
Gifts []db.Gift
}
func (ctx *Context) ForeignWishlist(w http.ResponseWriter, r *http.Request) {
func (ctx *Context) ForeignWishlist(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
userReference := r.PathValue("userReference")
user := ctx.ExpectUser(r)
if user.Reference == userReference {
if currentUser.Reference == userReference {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
@ -30,12 +29,12 @@ func (ctx *Context) ForeignWishlist(w http.ResponseWriter, r *http.Request) {
error.Page(w, "User not found", http.StatusNotFound, err)
return
}
gifts, err := user.GetOtherUserGifts(userReference)
gifts, err := currentUser.GetOtherUserGifts(userReference)
if err != nil {
error.Page(w, "An error occurred while fetching this user's wishlist :(", http.StatusInternalServerError, err)
return
}
p := foreignWishlistProps{CurrentUserId: user.Id, CurrentUserName: user.Name, Username: otherUser.Name, Gifts: gifts}
p := foreignWishlistProps{CurrentUserId: currentUser.Id, CurrentUserName: currentUser.Name, Username: otherUser.Name, Gifts: gifts}
templates.Execute(w, "foreign_wishlist.gotmpl", p)
}

View File

@ -1,7 +1,9 @@
package routing
import (
"encoding/json"
"net/http"
"slices"
"lishwist/db"
"lishwist/error"
@ -9,15 +11,13 @@ import (
)
type GroupProps struct {
Name string
Members []db.User
Group *db.Group
CurrentUsername string
}
func (ctx *Context) GroupPage(w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
func (ctx *Context) GroupPage(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
groupReference := r.PathValue("groupReference")
group, err := user.GetGroupByReference(groupReference)
group, err := currentUser.GetGroupByReference(groupReference)
if err != nil {
error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err)
return
@ -26,15 +26,11 @@ func (ctx *Context) GroupPage(w http.ResponseWriter, r *http.Request) {
error.Page(w, "Group not found. (It might be because you're not a member)", http.StatusNotFound, nil)
return
}
peers, err := user.GetPeers(group.Id)
if err != nil {
error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err)
return
}
index := group.MemberIndex(currentUser.Id)
group.Members = slices.Delete(group.Members, index, index+1)
p := GroupProps{
Name: group.Name,
Members: peers,
CurrentUsername: user.Name,
Group: group,
CurrentUsername: currentUser.Name,
}
templates.Execute(w, "group_page.gotmpl", p)
}
@ -46,14 +42,114 @@ func (ctx *Context) PublicGroupPage(w http.ResponseWriter, r *http.Request) {
error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err)
return
}
members, err := group.GetMembers()
if err != nil {
error.Page(w, "An error occurred while fetching this group :(", http.StatusInternalServerError, err)
return
}
p := GroupProps{
Name: group.Name,
Members: members,
Group: group,
}
templates.Execute(w, "public_group_page.gotmpl", p)
}
func (ctx *Context) GroupPost(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if !currentUser.IsAdmin {
NotFoundJson(w, r)
return
}
if err := r.ParseForm(); err != nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "Failed to parse form: "+err.Error())
return
}
var group *db.Group
reference := r.PathValue("groupReference")
name := r.Form.Get("name")
addUsers := r.Form["addUser"]
removeUsers := r.Form["removeUser"]
if name != "" {
createdGroup, err := db.CreateGroup(name, reference)
if err != nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "Failed to create group: "+err.Error())
return
}
group = createdGroup
} else {
existingGroup, err := db.GetGroupByReference(reference)
if err != nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "Failed to get group: "+err.Error())
return
}
if existingGroup == nil {
writeGeneralErrorJson(w, http.StatusNotFound, "Group not found")
return
}
group = existingGroup
for _, userId := range removeUsers {
index := group.MemberIndex(userId)
if index == -1 {
writeGeneralErrorJson(w, http.StatusBadRequest, "Group %q does not contain a user with id %s", reference, userId)
return
}
err = group.RemoveUser(userId)
if err != nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "On group %q failed to remove user with id %s: %s", reference, userId, err)
return
}
group.Members = slices.Delete(group.Members, index, index+1)
}
}
for _, userId := range addUsers {
user, err := db.GetUser(userId)
if err != nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "Groups exists, but a user with id %s could not be fetched: %s", userId, err)
return
}
if user == nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "Groups exists, but a user with id %s does not exist", userId)
return
}
err = group.AddUser(user.Id)
if err != nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "Groups exists, but failed to add user with id %s: %s", userId, err)
return
}
group.Members = append(group.Members, *user)
}
_ = json.NewEncoder(w).Encode(group)
}
func (ctx *Context) GroupsJson(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if !currentUser.IsAdmin {
NotFoundJson(w, r)
return
}
groups, err := db.GetAllGroups()
if err != nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "Failed to get groups: "+err.Error())
return
}
_ = json.NewEncoder(w).Encode(groups)
}
func (ctx *Context) Group(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if !currentUser.IsAdmin {
NotFoundJson(w, r)
return
}
groupReference := r.PathValue("groupReference")
group, err := db.GetGroupByReference(groupReference)
if err != nil {
writeGeneralErrorJson(w, http.StatusBadRequest, "Couldn't get group: %s", err)
return
}
if group == nil {
writeGeneralErrorJson(w, http.StatusNotFound, "Group not found.")
return
}
_ = json.NewEncoder(w).Encode(group)
}

View File

@ -18,41 +18,40 @@ type HomeProps struct {
Groups []db.Group
}
func (ctx *Context) Home(w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
gifts, err := user.GetGifts()
func (ctx *Context) Home(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
gifts, err := currentUser.GetGifts()
if err != nil {
error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err)
return
}
todo, err := user.GetTodo()
todo, err := currentUser.GetTodo()
if err != nil {
error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err)
return
}
groups, err := user.GetGroups()
groups, err := currentUser.GetGroups()
if err != nil {
error.Page(w, "An error occurred while fetching your wishlist :(", http.StatusInternalServerError, err)
return
}
p := HomeProps{Username: user.Name, Gifts: gifts, Todo: todo, Reference: user.Reference, HostUrl: env.HostUrl.String(), Groups: groups}
p := HomeProps{Username: currentUser.Name, Gifts: gifts, Todo: todo, Reference: currentUser.Reference, HostUrl: env.HostUrl.String(), Groups: groups}
templates.Execute(w, "home.gotmpl", p)
}
func (ctx *Context) HomePost(w http.ResponseWriter, r *http.Request) {
func (ctx *Context) HomePost(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "Couldn't parse form", http.StatusBadRequest)
return
}
switch r.Form.Get("intent") {
case "add_idea":
ctx.WishlistAdd(w, r)
ctx.WishlistAdd(currentUser, w, r)
return
case "delete_idea":
ctx.WishlistDelete(w, r)
ctx.WishlistDelete(currentUser, w, r)
return
default:
ctx.TodoUpdate(w, r)
ctx.TodoUpdate(currentUser, w, r)
return
}
}

View File

@ -1,13 +0,0 @@
package routing
import (
"encoding/json"
"net/http"
)
func decodeJsonParams(r *http.Request, v any) error {
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
err := dec.Decode(&v)
return err
}

View File

@ -1,41 +1,18 @@
package routing
import (
"lishwist/db"
"encoding/json"
"lishwist/api"
sesh "lishwist/session"
"lishwist/templates"
"log"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"
)
type LoginProps struct {
GeneralError string
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 (ctx *Context) Login(w http.ResponseWriter, r *http.Request) {
session, _ := ctx.store.Get(r, "lishwist_user")
props := NewLoginProps()
props := api.NewLoginProps("", "")
flash, err := sesh.GetFirstFlash(w, r, session, "login_props")
if err != nil {
@ -43,7 +20,7 @@ func (ctx *Context) Login(w http.ResponseWriter, r *http.Request) {
return
}
flashProps, ok := flash.(*LoginProps)
flashProps, ok := flash.(*api.LoginProps)
if ok {
props.Username.Value = flashProps.Username.Value
@ -75,28 +52,10 @@ func (ctx *Context) LoginPost(w http.ResponseWriter, r *http.Request) {
username := r.Form.Get("username")
password := r.Form.Get("password")
props := NewLoginProps()
props.Username.Value = username
user, err := db.GetUserByName(username)
if user == nil || err != nil {
time.Sleep(time.Second)
props.GeneralError = "Username or password invalid"
ctx.RedirectWithFlash(w, r, "/", "login_props", &props)
return
}
passHash, err := user.GetPassHash()
if err != nil {
props.GeneralError = "Something went wrong. Error code: Momo"
ctx.RedirectWithFlash(w, r, "/", "login_props", &props)
return
}
err = bcrypt.CompareHashAndPassword(passHash, []byte(password))
if err != nil {
props.GeneralError = "Username or password invalid"
props := api.Login(username, password)
if props != nil {
ctx.RedirectWithFlash(w, r, "/", "login_props", &props)
_ = json.NewEncoder(w).Encode(props)
return
}

View File

@ -0,0 +1,17 @@
package routing
import (
"net/http"
"lishwist/error"
)
func NotFoundJson(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"GeneralError":"Not Found"}`))
}
func NotFound(w http.ResponseWriter, r *http.Request) {
error.Page(w, "404 -- Page not found", http.StatusNotFound, nil)
}

View File

@ -13,6 +13,7 @@ func (ctx *Context) Register(w http.ResponseWriter, r *http.Request) {
session, _ := ctx.store.Get(r, "lishwist_user")
if flashes := session.Flashes("register_props"); len(flashes) > 0 {
log.Printf("Register found flashes: %#v\n", flashes)
flashProps, _ := flashes[0].(*api.RegisterProps)
props.Username.Value = flashProps.Username.Value
@ -44,27 +45,9 @@ func (ctx *Context) RegisterPost(w http.ResponseWriter, r *http.Request) {
if props != nil {
ctx.RedirectWithFlash(w, r, "/register", "register_props", &props)
_ = json.NewEncoder(w).Encode(props)
return
}
ctx.RedirectWithFlash(w, r, "/", "successful_registration", true)
}
type jsonParams struct {
Username string
NewPassword string
ConfirmPassword string
}
func (ctx *Context) RegisterPostJson(w http.ResponseWriter, r *http.Request) {
var params jsonParams
err := decodeJsonParams(r, &params)
if err != nil {
writeGeneralError(w, "Failed to decode json params: "+err.Error(), http.StatusBadRequest)
return
}
props := api.Register(params.Username, params.NewPassword, params.ConfirmPassword)
_ = json.NewEncoder(w).Encode(props)
}

View File

@ -1,12 +1,12 @@
package routing
import (
"lishwist/db"
"log"
"net/http"
)
func (ctx *Context) TodoUpdate(w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
func (ctx *Context) TodoUpdate(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@ -14,14 +14,14 @@ func (ctx *Context) TodoUpdate(w http.ResponseWriter, r *http.Request) {
switch r.Form.Get("intent") {
case "unclaim_todo":
unclaims := r.Form["gift"]
err := user.ClaimGifts([]string{}, unclaims)
err := currentUser.ClaimGifts([]string{}, unclaims)
if err != nil {
http.Error(w, "Failed to update claim...", http.StatusInternalServerError)
return
}
case "complete_todo":
claims := r.Form["gift"]
err := user.CompleteGifts(claims)
err := currentUser.CompleteGifts(claims)
if err != nil {
log.Printf("Failed to complete gifts: %s\n", err)
http.Error(w, "Failed to complete gifts...", http.StatusInternalServerError)
@ -29,6 +29,7 @@ func (ctx *Context) TodoUpdate(w http.ResponseWriter, r *http.Request) {
}
default:
http.Error(w, "Invalid intent", http.StatusBadRequest)
return
}
http.Redirect(w, r, "/", http.StatusSeeOther)
}

82
server/routing/users.go Normal file
View File

@ -0,0 +1,82 @@
package routing
import (
"encoding/json"
"lishwist/db"
"net/http"
)
func (ctx *Context) UsersJson(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if !currentUser.IsAdmin {
NotFoundJson(w, r)
return
}
users, err := db.GetAllUsers()
if err != nil {
writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to get users: "+err.Error())
return
}
_ = json.NewEncoder(w).Encode(users)
}
func (ctx *Context) User(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if !currentUser.IsAdmin {
NotFoundJson(w, r)
return
}
reference := r.PathValue("userReference")
user, err := db.GetUserByReference(reference)
if err != nil {
writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to get user: %s", err)
return
}
if user == nil {
writeGeneralErrorJson(w, http.StatusNotFound, "User not found")
return
}
_ = json.NewEncoder(w).Encode(user)
}
func (ctx *Context) UserPost(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if !currentUser.IsAdmin {
NotFoundJson(w, r)
return
}
if err := r.ParseForm(); err != nil {
writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to parse form: %s", err)
return
}
reference := r.PathValue("userReference")
if reference == currentUser.Reference {
writeGeneralErrorJson(w, http.StatusForbidden, "You cannot delete yourself.")
return
}
user, err := db.GetAnyUserByReference(reference)
if err != nil {
writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to get user: %s", err)
return
}
if user == nil {
writeGeneralErrorJson(w, http.StatusNotFound, "User not found")
return
}
intent := r.Form.Get("intent")
if intent != "" {
err = user.SetLive(intent != "delete")
if err != nil {
writeGeneralErrorJson(w, http.StatusInternalServerError, "Failed to delete user: "+err.Error())
return
}
}
_ = json.NewEncoder(w).Encode(user)
}

View File

@ -1,18 +1,18 @@
package routing
import (
"lishwist/db"
"lishwist/error"
"net/http"
)
func (ctx *Context) WishlistAdd(w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
func (ctx *Context) WishlistAdd(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
newGiftName := r.Form.Get("gift_name")
err := user.AddGift(newGiftName)
err := currentUser.AddGift(newGiftName)
if err != nil {
error.Page(w, "Failed to add gift.", http.StatusInternalServerError, err)
return
@ -20,14 +20,13 @@ func (ctx *Context) WishlistAdd(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (ctx *Context) WishlistDelete(w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
func (ctx *Context) WishlistDelete(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
targets := r.Form["gift"]
err := user.RemoveGifts(targets...)
err := currentUser.RemoveGifts(targets...)
if err != nil {
error.Page(w, "Failed to remove gifts.", http.StatusInternalServerError, err)
return
@ -35,8 +34,7 @@ func (ctx *Context) WishlistDelete(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request) {
user := ctx.ExpectUser(r)
func (ctx *Context) ForeignWishlistPost(currentUser *db.User, w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
error.Page(w, "Failed to parse form...", http.StatusBadRequest, err)
return
@ -46,14 +44,14 @@ func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request)
case "claim":
claims := r.Form["unclaimed"]
unclaims := r.Form["claimed"]
err := user.ClaimGifts(claims, unclaims)
err := currentUser.ClaimGifts(claims, unclaims)
if err != nil {
error.Page(w, "Failed to update claim...", http.StatusInternalServerError, err)
return
}
case "complete":
claims := r.Form["claimed"]
err := user.CompleteGifts(claims)
err := currentUser.CompleteGifts(claims)
if err != nil {
error.Page(w, "Failed to complete gifts...", http.StatusInternalServerError, nil)
return
@ -64,7 +62,7 @@ func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request)
error.Page(w, "Gift name not provided", http.StatusBadRequest, nil)
return
}
err := user.AddGiftToUser(userReference, giftName)
err := currentUser.AddGiftToUser(userReference, giftName)
if err != nil {
error.Page(w, "Failed to add gift idea to other user...", http.StatusInternalServerError, err)
return
@ -73,7 +71,7 @@ func (ctx *Context) ForeignWishlistPost(w http.ResponseWriter, r *http.Request)
claims := r.Form["unclaimed"]
unclaims := r.Form["claimed"]
gifts := append(claims, unclaims...)
err := user.RemoveGifts(gifts...)
err := currentUser.RemoveGifts(gifts...)
if err != nil {
error.Page(w, "Failed to remove gift idea for other user...", http.StatusInternalServerError, err)
return

View File

@ -30,8 +30,8 @@
<div class="container py-5">
<section class="card">
<div class="card-body">
<h2><em>{{.Name}}</em> group members</h2>
{{with .Members}}
<h2><em>{{.Group.Name}}</em> group members</h2>
{{with .Group.Members}}
<ul class="list-group">
{{range .}}
<li class="list-group-item">

View File

@ -83,7 +83,7 @@
</ul>
<button id="unclaimSubmit" class="btn btn-warning" type="submit" name="intent" value="unclaim_todo"
disabled>Unclaim</button>
<button id="completeSubmit" class="btn btn-success" type="submit" name="mode" value="complete_todo"
<button id="completeSubmit" class="btn btn-success" type="submit" name="intent" value="complete_todo"
disabled>Complete</button>
</form>
{{else}}

View File

@ -17,9 +17,9 @@
<div class="container py-5">
<section class="card">
<div class="card-body">
<h2><em>{{.Name}}</em> group members</h2>
<h2><em>{{.Group.Name}}</em> group members</h2>
<p>{{template "login_prompt"}} to see your groups</p>
{{with .Members}}
{{with .Group.Members}}
<ul class="list-group">
{{range .}}
<li class="list-group-item">
@ -34,4 +34,4 @@
</section>
</div>
</div>
{{end}}
{{end}}