Compare commits

...

12 Commits

Author SHA1 Message Date
Thomas Williams 4494932246 Merge pull request 'Core separation' (#13) from core-separation into main
Reviewed-on: #13
2025-06-27 01:14:19 +12:00
Teajey 8cdbfe0439
feat: migrations 2025-06-26 22:09:00 +09:00
Teajey b2f8ef19be
feat: log before anything else 2025-06-26 21:48:16 +09:00
Teajey cc0409d1dc
feat: use core library 2025-06-26 21:03:11 +09:00
Teajey b2e9bab55d
feat: session store 2025-06-22 22:18:21 +09:00
Teajey e44f299d5d
feat: admin inherits session 2025-06-22 22:18:02 +09:00
Teajey 439d4a1844
feat: groups 2025-06-22 21:15:47 +09:00
Teajey bba5136cca
fix: warnings 2025-06-19 20:36:11 +09:00
Teajey 5769d44576
feat: first user is an admin 2025-06-19 20:32:52 +09:00
Teajey 5c13893f23
feat: wish making 2025-06-19 19:52:24 +09:00
Teajey bffa68c9f7
feat: register and login 2025-06-19 00:52:08 +09:00
Teajey d89b855299
refac: rename current module to http 2024-12-29 23:50:12 +13:00
66 changed files with 1198 additions and 704 deletions

2
.gitignore vendored
View File

@ -2,5 +2,5 @@
gin-bin gin-bin
*lishwist.db *lishwist.db
.env*.local .env*.local
server/api/db/init_sql.go init_sql.go
.ignored/ .ignored/

13
core/admin.go Normal file
View File

@ -0,0 +1,13 @@
package lishwist
type Admin struct {
session *Session
}
func (s *Session) Admin() *Admin {
if s.User.IsAdmin {
return &Admin{s}
} else {
return nil
}
}

55
core/debug.go Normal file
View File

@ -0,0 +1,55 @@
package lishwist
import (
"database/sql"
"fmt"
"log"
)
func PrintViews(d *sql.DB) {
rows, err := d.Query("SELECT name FROM sqlite_master WHERE type = 'view';")
if err != nil {
log.Println("Query failed: %w", err)
return
}
defer rows.Close()
fmt.Println("Printing view names...")
for rows.Next() {
var name string
err = rows.Scan(&name)
if err != nil {
log.Println("Scan failed: %w", err)
return
}
fmt.Printf("name: %s\n", name)
}
err = rows.Err()
if err != nil {
log.Println("Rows returned error: %w", err)
return
}
}
func PrintTables(d *sql.DB) {
rows, err := d.Query("SELECT name FROM sqlite_master WHERE type = 'table';")
if err != nil {
log.Println("Query failed: %w", err)
return
}
defer rows.Close()
fmt.Println("Printing table names...")
for rows.Next() {
var name string
err = rows.Scan(&name)
if err != nil {
fmt.Println("Scan failed: %w", err)
return
}
fmt.Printf("name: %s\n", name)
}
err = rows.Err()
if err != nil {
log.Println("Rows returned error: %w", err)
return
}
}

18
core/go.mod Normal file
View File

@ -0,0 +1,18 @@
module lishwist/core
go 1.23.0
toolchain go1.23.3
require golang.org/x/crypto v0.39.0
require (
github.com/google/uuid v1.6.0
github.com/ncruces/go-sqlite3 v0.26.1
)
require (
github.com/ncruces/julianday v1.0.0 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
golang.org/x/sys v0.33.0 // indirect
)

14
core/go.sum Normal file
View File

@ -0,0 +1,14 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ncruces/go-sqlite3 v0.26.1 h1:lBXmbmucH1Bsj57NUQR6T84UoMN7jnNImhF+ibEITJU=
github.com/ncruces/go-sqlite3 v0.26.1/go.mod h1:XFTPtFIo1DmGCh+XVP8KGn9b/o2f+z0WZuT09x2N6eo=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=

View File

@ -1,9 +1,10 @@
package db package lishwist
import ( import (
"fmt" "fmt"
"lishwist/normalize" "lishwist/core/internal/db"
"strconv" "strconv"
"strings"
) )
type Group struct { type Group struct {
@ -24,7 +25,7 @@ func (g *Group) MemberIndex(userId string) int {
func queryManyGroups(query string, args ...any) ([]Group, error) { func queryManyGroups(query string, args ...any) ([]Group, error) {
groups := []Group{} groups := []Group{}
rows, err := database.Query(query, args...) rows, err := db.Connection.Query(query, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("Query failed: %w", err) return nil, fmt.Errorf("Query failed: %w", err)
} }
@ -64,25 +65,31 @@ func queryManyGroupMembers(groupId string) ([]User, error) {
query := "SELECT user.id, user.name, user.display_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" query := "SELECT user.id, user.name, user.display_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 := queryManyUsers(query, groupId) members, err := queryManyUsers(query, groupId)
if err != nil { if err != nil {
return members, fmt.Errorf("Failed to get members: %w", err) return members, err
} }
return members, nil return members, nil
} }
func GetGroupByReference(reference string) (*Group, error) { func (s *Session) GetGroupByReference(reference string) (*Group, error) {
query := "SELECT [group].id, [group].name, [group].reference FROM [group] WHERE [group].reference = ?" 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 queryOneGroup(query, reference) return queryOneGroup(stmt, reference, s.User.Id)
} }
func GetAllGroups() ([]Group, error) { func GetGroupByReference(reference string) (*Group, error) {
stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] WHERE [group].reference = ?;"
return queryOneGroup(stmt, reference)
}
func (a *Admin) ListGroups() ([]Group, error) {
query := "SELECT id, name, reference FROM [group];" query := "SELECT id, name, reference FROM [group];"
return queryManyGroups(query) return queryManyGroups(query)
} }
func CreateGroup(name string, reference string) (*Group, error) { func (a *Admin) CreateGroup(name string, reference string) (*Group, error) {
name = normalize.Trim(name) name = strings.TrimSpace(name)
reference = strings.TrimSpace(reference)
stmt := "INSERT INTO [group] (name, reference) VALUES (?, ?)" stmt := "INSERT INTO [group] (name, reference) VALUES (?, ?)"
result, err := database.Exec(stmt, name, reference) result, err := db.Connection.Exec(stmt, name, reference)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -98,20 +105,26 @@ func CreateGroup(name string, reference string) (*Group, error) {
return &group, nil return &group, nil
} }
func (g *Group) AddUser(userId string) error { func (a *Admin) AddUserToGroup(userId, groupId string) error {
stmt := "INSERT INTO group_member (group_id, user_id) VALUES (?, ?)" stmt := "INSERT INTO group_member (user_id, group_id) VALUES (?, ?)"
_, err := database.Exec(stmt, g.Id, userId) _, err := db.Connection.Exec(stmt, userId, groupId)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (g *Group) RemoveUser(userId string) error { func (a *Admin) RemoveUserFromGroup(userId, groupId string) error {
stmt := "DELETE FROM group_member WHERE group_id = ? AND user_id = ?" stmt := "DELETE FROM group_member WHERE group_id = ? AND user_id = ?"
_, err := database.Exec(stmt, g.Id, userId) _, err := db.Connection.Exec(stmt, userId, groupId)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
// Get the groups the session user belongs to
func (u *Session) GetGroups() ([]Group, error) {
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 = ?"
return queryManyGroups(stmt, u.Id)
}

42
core/group_test.go Normal file
View File

@ -0,0 +1,42 @@
package lishwist_test
import (
"testing"
lishwist "lishwist/core"
"lishwist/core/internal/fixtures"
)
func TestCreateGroup(t *testing.T) {
s := fixtures.Login(t, "thomas", "123")
group, err := s.Admin().CreateGroup(" My Friends ", " my-friends ")
fixtures.FailIfErr(t, err, "Failed to create group")
fixtures.AssertEq(t, "Number of users", "My Friends", group.Name)
fixtures.AssertEq(t, "Number of users", "my-friends", group.Reference)
}
func TestCantSeeSelfInGroup(t *testing.T) {
s := fixtures.Login(t, "thomas", "123")
caleb, err := lishwist.Register("caleb", "123")
fixtures.FailIfErr(t, err, "Failed to register caleb")
group, err := s.Admin().CreateGroup(" My Friends ", " my-friends ")
fixtures.FailIfErr(t, err, "Failed to create group")
err = s.Admin().AddUserToGroup(s.User.Id, group.Id)
fixtures.FailIfErr(t, err, "Failed to add self to group")
err = s.Admin().AddUserToGroup(caleb.Id, group.Id)
fixtures.FailIfErr(t, err, "Failed to add caleb to group")
group, err = s.GetGroupByReference("my-friends")
fixtures.FailIfErr(t, err, "Failed to get group")
fixtures.FatalAssert(t, "Group not nil", group != nil)
fixtures.AssertEq(t, "Group contains 2 users", 2, len(group.Members))
fixtures.AssertEq(t, "Group user 1 is thomas", "thomas", group.Members[0].Name)
fixtures.AssertEq(t, "Group user 2 is caleb", "caleb", group.Members[1].Name)
}

9
core/init.go Normal file
View File

@ -0,0 +1,9 @@
package lishwist
import (
"lishwist/core/internal/db"
)
func Init(dataSourceName string) error {
return db.Init(dataSourceName)
}

29
core/internal/db/db.go Normal file
View File

@ -0,0 +1,29 @@
//go:generate go run gen_init_sql.go
package db
import (
"database/sql"
"fmt"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
var Connection *sql.DB
func Init(dataSourceName string) error {
db, err := sql.Open("sqlite3", dataSourceName)
if err != nil {
return fmt.Errorf("Failed to open db connection: %w", err)
}
_, err = db.Exec(initQuery)
if err != nil {
return fmt.Errorf("Failed to initialize db: %w", err)
}
Connection = db
return nil
}

View File

@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS "user" (
"is_live" INTEGER NOT NULL DEFAULT 1, "is_live" INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY("id" AUTOINCREMENT) PRIMARY KEY("id" AUTOINCREMENT)
); );
CREATE TABLE IF NOT EXISTS "gift" ( CREATE TABLE IF NOT EXISTS "wish" (
"id" INTEGER NOT NULL UNIQUE, "id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"recipient_id" INTEGER NOT NULL, "recipient_id" INTEGER NOT NULL,
@ -49,6 +49,6 @@ SELECT * FROM user WHERE user.is_live = 1;
-- DROP VIEW IF EXISTS "v_wish"; -- DROP VIEW IF EXISTS "v_wish";
-- CREATE VIEW "v_wish" -- CREATE VIEW "v_wish"
-- AS -- AS
-- SELECT gift.id, gift.name, gift.sent FROM gift JOIN user AS recipient; -- SELECT wish.id, wish.name, wish.sent FROM wish JOIN user AS recipient;
COMMIT; COMMIT;

View File

@ -0,0 +1,6 @@
BEGIN TRANSACTION;
ALTER TABLE gift RENAME TO wish;
COMMIT;

View File

@ -0,0 +1,27 @@
package fixtures
import "testing"
func AssertEq[C comparable](t *testing.T, context string, expected, actual C) {
if expected != actual {
t.Errorf("%s: %#v != %#v", context, expected, actual)
}
}
func Assert(t *testing.T, context string, condition bool) {
if !condition {
t.Errorf("%s", context)
}
}
func FatalAssert(t *testing.T, context string, condition bool) {
if !condition {
t.Fatalf("%s", context)
}
}
func FailIfErr(t *testing.T, err error, context string) {
if err != nil {
t.Fatalf("%s: %s\n", context, err)
}
}

View File

@ -0,0 +1,35 @@
package fixtures
import (
"log"
"testing"
lishwist "lishwist/core"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)
func TestInit(t *testing.T) error {
uri := memdb.TestDB(t)
return lishwist.Init(uri)
}
func Login(t *testing.T, username, password string) *lishwist.Session {
uri := memdb.TestDB(t)
err := lishwist.Init(uri)
if err != nil {
log.Fatalf("Failed to init db: %s\n", err)
}
_, err = lishwist.Register(username, password)
if err != nil {
log.Fatalf("Failed to register on login fixture: %s\n", err)
}
session, err := lishwist.Login(username, password)
if err != nil {
log.Fatalf("Failed to login on fixture: %s\n", err)
}
return session
}

View File

@ -0,0 +1,10 @@
package normalize
import (
"strings"
)
func Name(name string) string {
name = strings.TrimSpace(name)
return strings.ToLower(name)
}

31
core/login.go Normal file
View File

@ -0,0 +1,31 @@
package lishwist
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
type ErrorInvalidCredentials error
func Login(username, password string) (*Session, error) {
user, err := getUserByName(username)
if err != nil {
return nil, ErrorInvalidCredentials(fmt.Errorf("Failed to fetch user: %w", err))
}
if user == nil {
return nil, ErrorInvalidCredentials(fmt.Errorf("User not found by name: %s", username))
}
passHash, err := user.getPassHash()
if err != nil {
return nil, fmt.Errorf("Failed to get password hash: %w", err)
}
err = bcrypt.CompareHashAndPassword(passHash, []byte(password))
if err != nil {
return nil, ErrorInvalidCredentials(fmt.Errorf("Password compare failed: %w", err))
}
return &Session{*user}, nil
}

25
core/login_test.go Normal file
View File

@ -0,0 +1,25 @@
package lishwist_test
import (
"testing"
lishwist "lishwist/core"
"lishwist/core/internal/fixtures"
)
func TestLogin(t *testing.T) {
err := fixtures.TestInit(t)
if err != nil {
t.Fatalf("Failed to init db: %s\n", err)
}
_, err = lishwist.Register("thomas", "123")
if err != nil {
t.Fatalf("Failed to register: %s\n", err)
}
_, err = lishwist.Login("thomas", "123")
if err != nil {
t.Fatalf("Failed to login: %s\n", err)
}
}

41
core/register.go Normal file
View File

@ -0,0 +1,41 @@
package lishwist
import (
"errors"
"fmt"
"golang.org/x/crypto/bcrypt"
)
var ErrorUsernameTaken = errors.New("Username is taken")
func Register(username, newPassword string) (*User, error) {
if username == "" {
return nil, errors.New("Username required")
}
if newPassword == "" {
return nil, errors.New("newPassword required")
}
existingUser, _ := getUserByName(username)
if existingUser != nil {
return nil, ErrorUsernameTaken
}
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
if err != nil {
return nil, fmt.Errorf("Failed to hash password: %w", err)
}
usersExist, err := hasUsers()
if err != nil {
return nil, fmt.Errorf("Failed to count users: %w", err)
}
user, err := createUser(username, hashedPasswordBytes, !usersExist)
if err != nil {
return nil, fmt.Errorf("Failed to create user: %w\n", err)
}
return user, nil
}

20
core/sesh Normal file
View File

@ -0,0 +1,20 @@
{
"__meta__": {
"about": "xh session file",
"xh": "0.24.1"
},
"auth": {
"type": null,
"raw_auth": null
},
"cookies": [
{
"name": "lishwist_user",
"value": "MTc1MDg2NDE2N3xCQXdBQVRjPXw8gaasdVy--TC-_fUb-3ZL58n8UVakTqDm_0_7c50cYA==",
"expires": 1750950567,
"path": "/lists",
"domain": "127.0.0.1"
}
],
"headers": []
}

15
core/session.go Normal file
View File

@ -0,0 +1,15 @@
package lishwist
import "fmt"
type Session struct {
User
}
func SessionFromUsername(username string) (*Session, error) {
user, err := getUserByName(username)
if err != nil {
return nil, fmt.Errorf("Failed to get user: %w", err)
}
return &Session{*user}, nil
}

39
core/session/store.go Normal file
View File

@ -0,0 +1,39 @@
package session
import (
"fmt"
"lishwist/core/internal/db"
"github.com/Teajey/sqlstore"
)
func NewStore(keyPairs ...[]byte) (*sqlstore.Store, error) {
deleteStmt, err := db.Connection.Prepare("DELETE FROM session WHERE id = ?;")
if err != nil {
return nil, fmt.Errorf("Failed to prepare delete statement: %w", err)
}
insertStmt, err := db.Connection.Prepare("INSERT INTO session (value) VALUES (?);")
if err != nil {
return nil, fmt.Errorf("Failed to prepare insert statement: %w", err)
}
selectStmt, err := db.Connection.Prepare("SELECT value FROM session WHERE id = ?;")
if err != nil {
return nil, fmt.Errorf("Failed to prepare select statement: %w", err)
}
updateStmt, err := db.Connection.Prepare("UPDATE session SET value = ?2 WHERE id = ?1;")
if err != nil {
return nil, fmt.Errorf("Failed to prepare update statement: %w", err)
}
s := sqlstore.NewSqlStore(db.Connection, sqlstore.Statements{
Delete: deleteStmt,
Insert: insertStmt,
Select: selectStmt,
Update: updateStmt,
}, keyPairs...)
return s, nil
}

165
core/user.go Normal file
View File

@ -0,0 +1,165 @@
package lishwist
import (
"fmt"
"github.com/google/uuid"
"lishwist/core/internal/db"
"lishwist/core/internal/normalize"
)
type User struct {
Id string
// TODO: rename to DisplayName
NormalName string
Name string
Reference string
IsAdmin bool
IsLive bool
}
func queryManyUsers(query string, args ...any) ([]User, error) {
rows, err := db.Connection.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
users := []User{}
for rows.Next() {
var u User
err = rows.Scan(&u.Id, &u.NormalName, &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 queryOneUser(query string, args ...any) (*User, error) {
users, err := queryManyUsers(query, args...)
if err != nil {
return nil, err
}
if len(users) < 1 {
return nil, nil
}
return &users[0], nil
}
func getUserByName(username string) (*User, error) {
username = normalize.Name(username)
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE name = ?"
return queryOneUser(stmt, username)
}
func createUser(name string, passHash []byte, isAdmin bool) (*User, error) {
username := normalize.Name(name)
stmt := "INSERT INTO user (name, display_name, reference, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)"
reference, err := uuid.NewRandom()
if err != nil {
return nil, fmt.Errorf("Failed to generate reference: %w", err)
}
result, err := db.Connection.Exec(stmt, username, name, reference, passHash, isAdmin)
if err != nil {
return nil, fmt.Errorf("Failed to execute query: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return nil, fmt.Errorf("Failed to get last insert id: %w", err)
}
user := User{
Id: fmt.Sprintf("%d", id),
Name: name,
}
return &user, nil
}
func (u *User) getPassHash() ([]byte, error) {
stmt := "SELECT password_hash FROM v_user WHERE id = ?"
var passHash string
err := db.Connection.QueryRow(stmt, u.Id).Scan(&passHash)
if err != nil {
return nil, err
}
return []byte(passHash), nil
}
func getUserByReference(reference string) (*User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE reference = ?"
return queryOneUser(stmt, reference)
}
func getUserById(id string) (*User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE id = ?"
return queryOneUser(stmt, id)
}
func hasUsers() (bool, error) {
stmt := "SELECT COUNT(id) FROM v_user LIMIT 1"
var userCount uint
err := db.Connection.QueryRow(stmt).Scan(&userCount)
if err != nil {
return false, err
}
return userCount > 0, nil
}
func (*Admin) ListUsers() ([]User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM user"
return queryManyUsers(stmt)
}
func (*Admin) GetUser(id string) (*User, error) {
return getUserById(id)
}
func GetUserByReference(reference string) (*User, error) {
return getUserByReference(reference)
}
func (u *User) GetTodo() ([]Wish, error) {
stmt := "SELECT wish.id, wish.name, wish.sent, recipient.name, recipient.reference FROM wish JOIN v_user AS user ON wish.claimant_id = user.id JOIN v_user AS recipient ON wish.recipient_id = recipient.id WHERE user.id = ? ORDER BY wish.sent ASC, wish.name"
rows, err := db.Connection.Query(stmt, u.Id)
if err != nil {
return nil, err
}
defer rows.Close()
wishes := []Wish{}
for rows.Next() {
var id string
var name string
var sent bool
var recipientName string
var recipientRef string
_ = rows.Scan(&id, &name, &sent, &recipientName, &recipientRef)
wish := Wish{
Id: id,
Name: name,
Sent: sent,
RecipientName: recipientName,
RecipientRef: recipientRef,
}
wishes = append(wishes, wish)
}
err = rows.Err()
if err != nil {
return nil, err
}
return wishes, nil
}
func (u *Admin) UserSetLive(userReference string, setting bool) error {
query := "UPDATE user SET is_live = ? WHERE reference = ?"
_, err := db.Connection.Exec(query, setting, userReference)
if err != nil {
return err
}
// u.IsLive = setting
return err
}

22
core/user_test.go Normal file
View File

@ -0,0 +1,22 @@
package lishwist_test
import (
"testing"
lishwist "lishwist/core"
"lishwist/core/internal/fixtures"
)
func TestFirstUserIsAdmin(t *testing.T) {
s := fixtures.Login(t, "thomas", "123")
_, err := lishwist.Register("caleb", "123")
fixtures.FailIfErr(t, err, "Failed to register caleb")
users, err := s.Admin().ListUsers()
fixtures.FailIfErr(t, err, "Failed to list users")
fixtures.AssertEq(t, "Number of users", 2, len(users))
fixtures.Assert(t, "User 1 is admin", users[0].IsAdmin)
fixtures.Assert(t, "User 2 is not admin", !users[1].IsAdmin)
}

295
core/wish.go Normal file
View File

@ -0,0 +1,295 @@
package lishwist
import (
"database/sql"
"errors"
"fmt"
"strings"
"lishwist/core/internal/db"
)
type Wish struct {
Id string
Name string
ClaimantId string `json:",omitempty"`
ClaimantName string `json:",omitempty"`
Sent bool
RecipientId string `json:",omitempty"`
RecipientName string `json:",omitempty"`
RecipientRef string `json:",omitempty"`
CreatorId string `json:",omitempty"`
CreatorName string `json:",omitempty"`
}
func (s *Session) GetWishes() ([]Wish, error) {
stmt := "SELECT wish.id, wish.name, wish.sent FROM wish WHERE wish.creator_id = ?1 AND wish.recipient_id = ?1"
rows, err := db.Connection.Query(stmt, s.User.Id)
if err != nil {
return nil, fmt.Errorf("Query execution failed: %w", err)
}
defer rows.Close()
wishs := []Wish{}
for rows.Next() {
var id string
var name string
var sent bool
err = rows.Scan(&id, &name, &sent)
if err != nil {
return nil, fmt.Errorf("Failed to scan a row: %w", err)
}
wish := Wish{
Id: id,
Name: name,
Sent: sent,
}
wishs = append(wishs, wish)
}
err = rows.Err()
if err != nil {
return nil, fmt.Errorf("Rows returned an error: %w", err)
}
return wishs, nil
}
func (s *Session) MakeWish(name string) error {
stmt := "INSERT INTO wish (name, recipient_id, creator_id) VALUES (?, ?, ?)"
_, err := db.Connection.Exec(stmt, strings.TrimSpace(name), s.User.Id, s.User.Id)
if err != nil {
return fmt.Errorf("Query execution failed: %w", err)
}
return nil
}
func (u *Session) deleteWishes(tx *sql.Tx, ids []string) error {
stmt := "DELETE FROM wish WHERE wish.creator_id = ? AND wish.id = ?"
for _, id := range ids {
r, err := tx.Exec(stmt, u.Id, id)
if err != nil {
return err
}
rE, err := r.RowsAffected()
if err != nil {
return err
}
if rE < 1 {
return fmt.Errorf("Wish deletion failed for '%s'", id)
}
}
return nil
}
func (s *Session) RevokeWishes(ids ...string) error {
if len(ids) < 1 {
return fmt.Errorf("Attempt to remove zero wishes")
}
tx, err := db.Connection.Begin()
if err != nil {
return err
}
err = s.deleteWishes(tx, ids)
if err != nil {
rollBackErr := tx.Rollback()
if rollBackErr != nil {
return err
}
return err
}
err = tx.Commit()
return err
}
func (s *Session) GetOthersWishes(userReference string) ([]Wish, error) {
otherUser, err := getUserByReference(userReference)
if err != nil {
return nil, fmt.Errorf("Failed to get other user: %w", err)
}
if otherUser.Id == s.User.Id {
return nil, errors.New("Use (s *Session) GetWishes() to view your own wishes")
}
stmt := "SELECT wish.id, wish.name, claimant.id, claimant.name, wish.sent, wish.creator_id, creator.name, wish.recipient_id FROM wish JOIN v_user AS user ON wish.recipient_id = user.id LEFT JOIN v_user AS claimant ON wish.claimant_id = claimant.id LEFT JOIN v_user AS creator ON wish.creator_id = creator.id WHERE user.id = ?"
rows, err := db.Connection.Query(stmt, otherUser.Id)
if err != nil {
return nil, fmt.Errorf("Failed to execute query: %w", err)
}
defer rows.Close()
wishes := []Wish{}
for rows.Next() {
var id string
var name string
var claimantId sql.NullString
var claimantName sql.NullString
var sent bool
var creatorId string
var creatorName string
var recipientId string
err = rows.Scan(&id, &name, &claimantId, &claimantName, &sent, &creatorId, &creatorName, &recipientId)
if err != nil {
return nil, fmt.Errorf("Failed to scan a row: %w", err)
}
wish := Wish{
Id: id,
Name: name,
ClaimantId: claimantId.String,
ClaimantName: claimantName.String,
Sent: sent,
CreatorId: creatorId,
CreatorName: creatorName,
RecipientId: recipientId,
}
wishes = append(wishes, wish)
}
err = rows.Err()
if err != nil {
return nil, fmt.Errorf("Rows returned an error: %w", err)
}
return wishes, nil
}
// NOTE: This could just be a field on the user... but only if we get this often
func (u *User) WishCount() (int, error) {
stmt := "SELECT COUNT(wish.id) AS wish_count FROM wish WHERE wish.creator_id = ?1 AND wish.recipient_id = ?1"
var wishCount int
err := db.Connection.QueryRow(stmt, u.Id).Scan(&wishCount)
if err != nil {
return 0, err
}
return wishCount, nil
}
func (s *Session) executeClaims(tx *sql.Tx, claims, unclaims []string) error {
claimStmt := "UPDATE wish SET claimant_id = ? WHERE id = ?"
unclaimStmt := "UPDATE wish SET claimant_id = NULL WHERE id = ?"
for _, id := range claims {
r, err := tx.Exec(claimStmt, s.Id, id)
if err != nil {
return err
}
rE, err := r.RowsAffected()
if err != nil {
return err
}
if rE < 1 {
return fmt.Errorf("Wish claim failed for '%s'", id)
}
}
for _, id := range unclaims {
r, err := tx.Exec(unclaimStmt, id)
if err != nil {
return err
}
rE, err := r.RowsAffected()
if err != nil {
return err
}
if rE < 1 {
return fmt.Errorf("Wish unclaim failed for '%s'", id)
}
}
return nil
}
// Undertake or abandon wishes made by other users
func (s *Session) ClaimWishes(claims, unclaims []string) error {
if len(claims) < 1 && len(unclaims) < 1 {
return fmt.Errorf("Attempt to claim/unclaim zero wishes")
}
tx, err := db.Connection.Begin()
if err != nil {
return err
}
err = s.executeClaims(tx, claims, unclaims)
if err != nil {
rollBackErr := tx.Rollback()
if rollBackErr != nil {
return err
}
return err
}
err = tx.Commit()
return err
}
func executeCompletions(tx *sql.Tx, claims []string) error {
claimStmt := "UPDATE wish SET sent = 1 WHERE id = ?"
for _, id := range claims {
r, err := tx.Exec(claimStmt, id)
if err != nil {
return err
}
rE, err := r.RowsAffected()
if err != nil {
return err
}
if rE < 1 {
return fmt.Errorf("Wish completion failed for '%s'", id)
}
}
return nil
}
// TODO: User ought not be able to interact with wishes outside their group network
func (s *Session) CompleteWishes(claims []string) error {
if len(claims) < 1 {
return fmt.Errorf("Attempt to complete zero wishes")
}
tx, err := db.Connection.Begin()
if err != nil {
return err
}
err = executeCompletions(tx, claims)
if err != nil {
rollBackErr := tx.Rollback()
if rollBackErr != nil {
return err
}
return err
}
err = tx.Commit()
return err
}
func (u *Session) SuggestWishForUser(otherUserReference string, wishName string) error {
otherUser, err := GetUserByReference(otherUserReference)
if err != nil {
return err
}
stmt := "INSERT INTO wish (name, recipient_id, creator_id) VALUES (?, ?, ?)"
_, err = db.Connection.Exec(stmt, wishName, otherUser.Id, u.Id)
if err != nil {
return err
}
return nil
}
func (s *Session) RecindWishesForUser(ids ...string) error {
if len(ids) < 1 {
return fmt.Errorf("Attempt to remove zero wishes")
}
tx, err := db.Connection.Begin()
if err != nil {
return err
}
err = s.deleteWishes(tx, ids)
if err != nil {
rollBackErr := tx.Rollback()
if rollBackErr != nil {
return err
}
return err
}
err = tx.Commit()
return err
}

28
core/wish_test.go Normal file
View File

@ -0,0 +1,28 @@
package lishwist_test
import (
"testing"
"lishwist/core/internal/fixtures"
)
func TestMakeWish(t *testing.T) {
s := fixtures.Login(t, "thomas", "123")
if err := s.MakeWish("apple"); err != nil {
t.Fatalf("Failed to make wish 1: %s\n", err)
}
if err := s.MakeWish(" A car "); err != nil {
t.Fatalf("Failed to make wish 2: %s\n", err)
}
wishes, err := s.GetWishes()
if err != nil {
t.Fatalf("Failed to get wishes: %s\n", err)
}
fixtures.AssertEq(t, "Number of wishes", 2, len(wishes))
fixtures.AssertEq(t, "Wish 1 name", wishes[0].Name, "apple")
fixtures.AssertEq(t, "Wish 2 name", wishes[1].Name, "A car")
}

View File

@ -1,5 +1,6 @@
go 1.23 go 1.23.3
toolchain go1.23.3 use (
./core
use ./server ./http
)

View File

@ -1,12 +1,16 @@
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= github.com/ncruces/sort v0.1.5/go.mod h1:obJToO4rYr6VWP0Uw5FYymgYGt3Br4RXcs/JdKaXAPk=
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=

View File

@ -1,11 +1,10 @@
package api package api
import ( import (
"lishwist/api/db"
"lishwist/templates"
"log" "log"
"golang.org/x/crypto/bcrypt" lishwist "lishwist/core"
"lishwist/http/templates"
) )
type LoginProps struct { type LoginProps struct {
@ -55,31 +54,19 @@ func Login(username, password string) *LoginProps {
return props return props
} }
user, err := db.GetUserByName(username) _, err := lishwist.Login(props.Username.Value, props.Password.Value)
if err != nil { if err == nil {
log.Printf("Failed to fetch user: %s\n", err) return nil
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() switch err.(type) {
if err != nil { case lishwist.ErrorInvalidCredentials:
log.Println("Failed to get password hash: " + err.Error()) log.Printf("Invalid credentials: %w\n", err)
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" props.GeneralError = "Username or password invalid"
return props return props
default:
log.Printf("Login error: %w\n", err)
props.GeneralError = "Something went wrong."
return props
} }
return nil
} }

View File

@ -1,12 +1,7 @@
package api package api
import ( import (
"log" "lishwist/http/templates"
"lishwist/api/db"
"lishwist/templates"
"golang.org/x/crypto/bcrypt"
) )
type RegisterProps struct { type RegisterProps struct {
@ -64,37 +59,37 @@ func NewRegisterProps(usernameVal, passwordVal, confirmPassVal string) *Register
} }
} }
func Register(username, newPassword, confirmPassword string) *RegisterProps { // func Register(username, newPassword, confirmPassword string) *RegisterProps {
props := NewRegisterProps(username, newPassword, confirmPassword) // props := NewRegisterProps(username, newPassword, confirmPassword)
valid := props.Validate() // valid := props.Validate()
props.Password.Value = "" // props.Password.Value = ""
props.ConfirmPassword.Value = "" // props.ConfirmPassword.Value = ""
if !valid { // if !valid {
log.Printf("Invalid props: %#v\n", props) // log.Printf("Invalid props: %#v\n", props)
return props // return props
} // }
existingUser, _ := db.GetUserByName(username) // existingUser, _ := db.GetUserByName(username)
if existingUser != nil { // if existingUser != nil {
log.Printf("Username is taken: %q\n", existingUser.NormalName) // log.Printf("Username is taken: %q\n", existingUser.NormalName)
props.Username.Error = "Username is taken" // props.Username.Error = "Username is taken"
return props // return props
} // }
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost) // hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost)
if err != nil { // if err != nil {
log.Printf("Failed to hash password: %s\n", err) // log.Printf("Failed to hash password: %s\n", err)
props.GeneralError = "Something went wrong. Error code: Aang" // props.GeneralError = "Something went wrong. Error code: Aang"
return props // return props
} // }
_, err = db.CreateUser(username, hashedPasswordBytes) // _, err = db.CreateUser(username, hashedPasswordBytes)
if err != nil { // if err != nil {
log.Printf("Failed to create user: %s\n", err) // log.Printf("Failed to create user: %s\n", err)
props.GeneralError = "Something went wrong. Error code: Ozai" // props.GeneralError = "Something went wrong. Error code: Ozai"
return props // return props
} // }
return nil // return nil
} // }

View File

@ -1,4 +1,4 @@
module lishwist module lishwist/http
go 1.23 go 1.23

View File

@ -5,28 +5,24 @@ import (
"log" "log"
"net/http" "net/http"
"lishwist/api" lishwist "lishwist/core"
// TODO: lishwist/api/db ought not to be used outside lishwist/api "lishwist/core/session"
"lishwist/api/db" "lishwist/http/api"
"lishwist/env" "lishwist/http/env"
"lishwist/router" "lishwist/http/router"
"lishwist/routing" "lishwist/http/routing"
) )
func main() { func main() {
gob.Register(&api.RegisterProps{}) gob.Register(&api.RegisterProps{})
gob.Register(&api.LoginProps{}) gob.Register(&api.LoginProps{})
err := db.Open() err := lishwist.Init(env.DatabaseFile)
if err != nil { if err != nil {
log.Fatalf("Failed to open DB: %s\n", err) log.Fatalf("Failed to init Lishwist: %s\n", err)
}
err = db.Init()
if err != nil {
log.Fatalf("Failed to init DB: %s\n", err)
} }
store, err := db.NewSessionStore() store, err := session.NewStore([]byte(env.SessionSecret))
if err != nil { if err != nil {
log.Fatalf("Failed to initialize session store: %s\n", err) log.Fatalf("Failed to initialize session store: %s\n", err)
} }
@ -38,27 +34,29 @@ func main() {
r.Public.HandleFunc("GET /", routing.Login) r.Public.HandleFunc("GET /", routing.Login)
r.Public.HandleFunc("GET /groups/{groupReference}", routing.PublicGroup) r.Public.HandleFunc("GET /groups/{groupReference}", routing.PublicGroup)
r.Public.HandleFunc("GET /list/{userReference}", routing.PublicWishlist) r.Public.HandleFunc("GET /lists/{userReference}", routing.PublicWishlist)
r.Public.HandleFunc("GET /register", routing.Register) r.Public.HandleFunc("GET /register", routing.Register)
r.Public.HandleFunc("POST /", routing.LoginPost) r.Public.HandleFunc("POST /", routing.LoginPost)
r.Public.HandleFunc("POST /register", routing.RegisterPost) r.Public.HandleFunc("POST /register", routing.RegisterPost)
r.Private.HandleFunc("GET /", routing.NotFound) r.Private.HandleFunc("GET /", routing.NotFound)
r.Private.HandleFunc("GET /groups", routing.ExpectUser(routing.Groups)) r.Private.HandleFunc("GET /groups", routing.ExpectAppSession(routing.Groups))
r.Private.HandleFunc("GET /groups/{groupReference}", routing.ExpectUser(routing.Group)) r.Private.HandleFunc("GET /groups/{groupReference}", routing.ExpectAppSession(routing.Group))
r.Private.HandleFunc("GET /list/{userReference}", routing.ExpectUser(routing.ForeignWishlist)) r.Private.HandleFunc("GET /lists/{userReference}", routing.ExpectAppSession(routing.ForeignWishlist))
r.Private.HandleFunc("GET /users", routing.ExpectUser(routing.Users)) r.Private.HandleFunc("GET /users", routing.ExpectAppSession(routing.Users))
r.Private.HandleFunc("GET /users/{userReference}", routing.ExpectUser(routing.User)) r.Private.HandleFunc("GET /users/{userReference}", routing.ExpectAppSession(routing.User))
r.Private.HandleFunc("GET /{$}", routing.ExpectUser(routing.Home)) r.Private.HandleFunc("GET /{$}", routing.ExpectAppSession(routing.Home))
r.Private.HandleFunc("POST /groups/{groupReference}", routing.ExpectUser(routing.GroupPost)) r.Private.HandleFunc("POST /groups/{groupReference}", routing.ExpectAppSession(routing.GroupPost))
r.Private.HandleFunc("POST /list/{userReference}", routing.ExpectUser(routing.ForeignWishlistPost)) r.Private.HandleFunc("POST /list/{userReference}", routing.ExpectAppSession(routing.ForeignWishlistPost))
r.Private.HandleFunc("POST /logout", routing.LogoutPost) r.Private.HandleFunc("POST /logout", routing.LogoutPost)
r.Private.HandleFunc("POST /users/{userReference}", routing.ExpectUser(routing.UserPost)) r.Private.HandleFunc("POST /users/{userReference}", routing.ExpectAppSession(routing.UserPost))
r.Private.HandleFunc("POST /{$}", routing.ExpectUser(routing.HomePost)) r.Private.HandleFunc("POST /{$}", routing.ExpectAppSession(routing.HomePost))
// Deprecated // Deprecated
r.Private.HandleFunc("GET /group/{groupReference}", routing.ExpectAppSession(routing.Group))
r.Private.HandleFunc("GET /list/{userReference}", routing.ExpectAppSession(routing.ForeignWishlist))
r.Public.HandleFunc("GET /group/{groupReference}", routing.PublicGroup) r.Public.HandleFunc("GET /group/{groupReference}", routing.PublicGroup)
r.Private.HandleFunc("GET /group/{groupReference}", routing.ExpectUser(routing.Group)) r.Public.HandleFunc("GET /list/{userReference}", routing.PublicWishlist)
http.Handle("/", r) http.Handle("/", r)

View File

@ -1,7 +1,7 @@
package router package router
import ( import (
"lishwist/rsvp" "lishwist/http/rsvp"
"net/http" "net/http"
"github.com/Teajey/sqlstore" "github.com/Teajey/sqlstore"

View File

@ -1,12 +1,12 @@
package routing package routing
import ( import (
"lishwist/api/db" lishwist "lishwist/core"
"lishwist/rsvp" "lishwist/http/rsvp"
"net/http" "net/http"
) )
func ExpectUser(next func(*db.User, http.Header, *rsvp.Request) rsvp.Response) rsvp.HandlerFunc { func ExpectAppSession(next func(*lishwist.Session, http.Header, *rsvp.Request) rsvp.Response) rsvp.HandlerFunc {
return func(w http.Header, r *rsvp.Request) rsvp.Response { return func(w http.Header, r *rsvp.Request) rsvp.Response {
session := r.GetSession() session := r.GetSession()
username, ok := session.GetValue("username").(string) username, ok := session.GetValue("username").(string)
@ -14,11 +14,11 @@ func ExpectUser(next func(*db.User, http.Header, *rsvp.Request) rsvp.Response) r
return rsvp.Error(http.StatusInternalServerError, "Something went wrong.").Log("Failed to get username from session") return rsvp.Error(http.StatusInternalServerError, "Something went wrong.").Log("Failed to get username from session")
} }
user, err := db.GetUserByName(username) appSession, err := lishwist.SessionFromUsername(username)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Something went wrong.").Log("Failed to get user %q: %s", username, err) return rsvp.Error(http.StatusInternalServerError, "Something went wrong.").Log("Failed to get session by username %q: %s", username, err)
} }
return next(user, w, r) return next(appSession, w, r)
} }
} }

View File

@ -1,8 +1,8 @@
package routing package routing
import ( import (
"lishwist/api/db" lishwist "lishwist/core"
"lishwist/rsvp" "lishwist/http/rsvp"
"net/http" "net/http"
) )
@ -10,26 +10,26 @@ type foreignWishlistProps struct {
CurrentUserId string CurrentUserId string
CurrentUserName string CurrentUserName string
Username string Username string
Gifts []db.Gift Gifts []lishwist.Wish
} }
func ForeignWishlist(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func ForeignWishlist(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
userReference := r.PathValue("userReference") userReference := r.PathValue("userReference")
if currentUser.Reference == userReference { if app.User.Reference == userReference {
return rsvp.SeeOther("/") return rsvp.SeeOther("/")
} }
otherUser, err := db.GetUserByReference(userReference) otherUser, err := lishwist.GetUserByReference(userReference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get user by reference %q: %s", userReference, err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get user by reference %q: %s", userReference, err)
} }
if otherUser == nil { if otherUser == nil {
return rsvp.Error(http.StatusInternalServerError, "User not found") return rsvp.Error(http.StatusInternalServerError, "User not found")
} }
gifts, err := currentUser.GetOtherUserGifts(userReference) wishes, err := app.GetOthersWishes(userReference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("%q couldn't get wishes of other user %q: %s", currentUser.Name, otherUser.Name, err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("%q couldn't get wishes of other user %q: %s", app.User.Name, otherUser.Name, err)
} }
p := foreignWishlistProps{CurrentUserId: currentUser.Id, CurrentUserName: currentUser.Name, Username: otherUser.Name, Gifts: gifts} p := foreignWishlistProps{CurrentUserId: app.User.Id, CurrentUserName: app.User.Name, Username: otherUser.Name, Gifts: wishes}
return rsvp.Data("foreign_wishlist.gotmpl", p) return rsvp.Data("foreign_wishlist.gotmpl", p)
} }
@ -40,14 +40,14 @@ type publicWishlistProps struct {
func PublicWishlist(h http.Header, r *rsvp.Request) rsvp.Response { func PublicWishlist(h http.Header, r *rsvp.Request) rsvp.Response {
userReference := r.PathValue("userReference") userReference := r.PathValue("userReference")
otherUser, err := db.GetUserByReference(userReference) otherUser, err := lishwist.GetUserByReference(userReference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get user by reference %q on public wishlist: %s", userReference, err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get user by reference %q on public wishlist: %s", userReference, err)
} }
if otherUser == nil { if otherUser == nil {
return rsvp.Error(http.StatusInternalServerError, "User not found") return rsvp.Error(http.StatusInternalServerError, "User not found")
} }
giftCount, err := otherUser.CountGifts() giftCount, err := otherUser.WishCount()
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get wishes of user %q on public wishlist: %s", otherUser.Name, err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this user :(").Log("Couldn't get wishes of user %q on public wishlist: %s", otherUser.Name, err)
} }

View File

@ -4,59 +4,59 @@ import (
"net/http" "net/http"
"slices" "slices"
"lishwist/api/db" lishwist "lishwist/core"
"lishwist/rsvp" "lishwist/http/rsvp"
) )
type GroupProps struct { type GroupProps struct {
Group *db.Group Group *lishwist.Group
CurrentUsername string CurrentUsername string
} }
func AdminGroup(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func AdminGroup(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
reference := r.PathValue("groupReference") reference := r.PathValue("groupReference")
group, err := db.GetGroupByReference(reference) group, err := app.GetGroupByReference(reference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Couldn't get group: %s", err) return rsvp.Error(http.StatusInternalServerError, "Couldn't get group: %s", err)
} }
if group == nil { if group == nil {
return rsvp.Error(http.StatusNotFound, "Group not found") return rsvp.Error(http.StatusNotFound, "Group not found")
} }
if !currentUser.IsAdmin { if !app.User.IsAdmin {
index := group.MemberIndex(currentUser.Id) index := group.MemberIndex(app.User.Id)
group.Members = slices.Delete(group.Members, index, index+1) group.Members = slices.Delete(group.Members, index, index+1)
} }
p := GroupProps{ p := GroupProps{
Group: group, Group: group,
CurrentUsername: currentUser.Name, CurrentUsername: app.User.Name,
} }
return rsvp.Data("group_page.gotmpl", p) return rsvp.Data("group_page.gotmpl", p)
} }
func Group(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func Group(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
if currentUser.IsAdmin { if app.User.IsAdmin {
return AdminGroup(currentUser, h, r) return AdminGroup(app, h, r)
} }
groupReference := r.PathValue("groupReference") groupReference := r.PathValue("groupReference")
group, err := currentUser.GetGroupByReference(groupReference) group, err := app.GetGroupByReference(groupReference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err)
} }
if group == nil { if group == nil {
return rsvp.Error(http.StatusNotFound, "Group not found. (It might be because you're not a member)") return rsvp.Error(http.StatusNotFound, "Group not found. (It might be because you're not a member)")
} }
index := group.MemberIndex(currentUser.Id) index := group.MemberIndex(app.User.Id)
group.Members = slices.Delete(group.Members, index, index+1) group.Members = slices.Delete(group.Members, index, index+1)
p := GroupProps{ p := GroupProps{
Group: group, Group: group,
CurrentUsername: currentUser.Name, CurrentUsername: app.User.Name,
} }
return rsvp.Data("group_page.gotmpl", p) return rsvp.Data("group_page.gotmpl", p)
} }
func PublicGroup(h http.Header, r *rsvp.Request) rsvp.Response { func PublicGroup(h http.Header, r *rsvp.Request) rsvp.Response {
groupReference := r.PathValue("groupReference") groupReference := r.PathValue("groupReference")
group, err := db.GetGroupByReference(groupReference) group, err := lishwist.GetGroupByReference(groupReference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err)
} }
@ -66,13 +66,14 @@ func PublicGroup(h http.Header, r *rsvp.Request) rsvp.Response {
return rsvp.Data("public_group_page.gotmpl", p) return rsvp.Data("public_group_page.gotmpl", p)
} }
func GroupPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func GroupPost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
if !currentUser.IsAdmin { admin := app.Admin()
if admin == nil {
return NotFound(h, r) return NotFound(h, r)
} }
form := r.ParseForm() form := r.ParseForm()
var group *db.Group var group *lishwist.Group
reference := r.PathValue("groupReference") reference := r.PathValue("groupReference")
name := form.Get("name") name := form.Get("name")
@ -80,13 +81,13 @@ func GroupPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Respon
removeUsers := form["removeUser"] removeUsers := form["removeUser"]
if name != "" { if name != "" {
createdGroup, err := db.CreateGroup(name, reference) createdGroup, err := admin.CreateGroup(name, reference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to create group: %s", err) return rsvp.Error(http.StatusInternalServerError, "Failed to create group: %s", err)
} }
group = createdGroup group = createdGroup
} else { } else {
existingGroup, err := db.GetGroupByReference(reference) existingGroup, err := lishwist.GetGroupByReference(reference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to get group: %s", err) return rsvp.Error(http.StatusInternalServerError, "Failed to get group: %s", err)
} }
@ -100,7 +101,7 @@ func GroupPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Respon
if index == -1 { if index == -1 {
return rsvp.Error(http.StatusBadRequest, "Group %q does not contain a user with id %s", reference, userId) return rsvp.Error(http.StatusBadRequest, "Group %q does not contain a user with id %s", reference, userId)
} }
err = group.RemoveUser(userId) err = admin.RemoveUserFromGroup(userId, group.Id)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "On group %q failed to remove user with id %s: %s", reference, userId, err) return rsvp.Error(http.StatusInternalServerError, "On group %q failed to remove user with id %s: %s", reference, userId, err)
} }
@ -109,14 +110,14 @@ func GroupPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Respon
} }
for _, userId := range addUsers { for _, userId := range addUsers {
user, err := db.GetUser(userId) user, err := admin.GetUser(userId)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s could not be fetched: %s", userId, err) return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s could not be fetched: %s", userId, err)
} }
if user == nil { if user == nil {
return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s does not exist", userId) return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s does not exist", userId)
} }
err = group.AddUser(user.Id) err = admin.AddUserToGroup(user.Id, group.Id)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Groups exists, but failed to add user with id %s: %s", userId, err) return rsvp.Error(http.StatusInternalServerError, "Groups exists, but failed to add user with id %s: %s", userId, err)
} }
@ -126,12 +127,13 @@ func GroupPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Respon
return rsvp.Data("", group) return rsvp.Data("", group)
} }
func Groups(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func Groups(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
if !currentUser.IsAdmin { admin := app.Admin()
if admin == nil {
return NotFound(h, r) return NotFound(h, r)
} }
groups, err := db.GetAllGroups() groups, err := admin.ListGroups()
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to get groups: %s", err) return rsvp.Error(http.StatusInternalServerError, "Failed to get groups: %s", err)
} }

View File

@ -3,45 +3,45 @@ package routing
import ( import (
"net/http" "net/http"
"lishwist/api/db" lishwist "lishwist/core"
"lishwist/env" "lishwist/http/env"
"lishwist/rsvp" "lishwist/http/rsvp"
) )
type HomeProps struct { type HomeProps struct {
Username string Username string
Gifts []db.Gift Gifts []lishwist.Wish
Todo []db.Gift Todo []lishwist.Wish
Reference string Reference string
HostUrl string HostUrl string
Groups []db.Group Groups []lishwist.Group
} }
func Home(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func Home(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
gifts, err := currentUser.GetGifts() gifts, err := app.GetWishes()
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get gifts: %s", err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get gifts: %s", err)
} }
todo, err := currentUser.GetTodo() todo, err := app.GetTodo()
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get todo: %s", err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get todo: %s", err)
} }
groups, err := currentUser.GetGroups() groups, err := app.GetGroups()
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get groups: %s", err) return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching your wishlist :(").Log("Failed to get groups: %s", err)
} }
p := HomeProps{Username: currentUser.Name, Gifts: gifts, Todo: todo, Reference: currentUser.Reference, HostUrl: env.HostUrl.String(), Groups: groups} p := HomeProps{Username: app.User.Name, Gifts: gifts, Todo: todo, Reference: app.User.Reference, HostUrl: env.HostUrl.String(), Groups: groups}
return rsvp.Data("home.gotmpl", p) return rsvp.Data("home.gotmpl", p)
} }
func HomePost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func HomePost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
form := r.ParseForm() form := r.ParseForm()
switch form.Get("intent") { switch form.Get("intent") {
case "add_idea": case "add_idea":
return WishlistAdd(currentUser, h, r) return WishlistAdd(app, h, r)
case "delete_idea": case "delete_idea":
return WishlistDelete(currentUser, h, r) return WishlistDelete(app, h, r)
default: default:
return TodoUpdate(currentUser, h, r) return TodoUpdate(app, h, r)
} }
} }

View File

@ -1,8 +1,9 @@
package routing package routing
import ( import (
"lishwist/api" lishwist "lishwist/core"
"lishwist/rsvp" "lishwist/http/api"
"lishwist/http/rsvp"
"net/http" "net/http"
) )
@ -37,15 +38,32 @@ func LoginPost(h http.Header, r *rsvp.Request) rsvp.Response {
username := form.Get("username") username := form.Get("username")
password := form.Get("password") password := form.Get("password")
props := api.Login(username, password) props := api.NewLoginProps(username, password)
if props != nil {
valid := props.Validate()
props.Password.Value = ""
if !valid {
session.FlashSet(&props) session.FlashSet(&props)
return rsvp.SeeOther("/").SaveSession(session) return rsvp.SeeOther("/").SaveSession(session).Log("Invalid props: %#v\n", props)
}
app, err := lishwist.Login(username, password)
if err != nil {
switch err.(type) {
case lishwist.ErrorInvalidCredentials:
props.GeneralError = "Username or password invalid"
session.FlashSet(&props)
return rsvp.SeeOther("/").SaveSession(session).Log("Invalid credentials: %#v\n", props)
default:
props.GeneralError = "Something went wrong."
session.FlashSet(&props)
return rsvp.SeeOther("/").SaveSession(session).Log("Login error: %s\n", err)
}
} }
session.SetID("") session.SetID("")
session.SetValue("authorized", true) session.SetValue("authorized", true)
session.SetValue("username", username) session.SetValue("username", app.User.Name)
return rsvp.SeeOther(r.URL().Path).SaveSession(session) return rsvp.SeeOther(r.URL().Path).SaveSession(session)
} }

View File

@ -1,7 +1,7 @@
package routing package routing
import ( import (
"lishwist/rsvp" "lishwist/http/rsvp"
"net/http" "net/http"
) )

View File

@ -3,7 +3,7 @@ package routing
import ( import (
"net/http" "net/http"
"lishwist/rsvp" "lishwist/http/rsvp"
) )
func NotFound(h http.Header, r *rsvp.Request) rsvp.Response { func NotFound(h http.Header, r *rsvp.Request) rsvp.Response {

View File

@ -1,8 +1,10 @@
package routing package routing
import ( import (
"lishwist/api" "errors"
"lishwist/rsvp" lishwist "lishwist/core"
"lishwist/http/api"
"lishwist/http/rsvp"
"net/http" "net/http"
) )
@ -26,18 +28,31 @@ func Register(h http.Header, r *rsvp.Request) rsvp.Response {
func RegisterPost(h http.Header, r *rsvp.Request) rsvp.Response { func RegisterPost(h http.Header, r *rsvp.Request) rsvp.Response {
form := r.ParseForm() form := r.ParseForm()
s := r.GetSession()
username := form.Get("username") username := form.Get("username")
newPassword := form.Get("newPassword") newPassword := form.Get("newPassword")
confirmPassword := form.Get("confirmPassword") confirmPassword := form.Get("confirmPassword")
props := api.Register(username, newPassword, confirmPassword) props := api.NewRegisterProps(username, newPassword, confirmPassword)
s := r.GetSession() valid := props.Validate()
props.Password.Value = ""
if props != nil { props.ConfirmPassword.Value = ""
if !valid {
s.FlashSet(&props) s.FlashSet(&props)
return rsvp.SeeOther("/register").SaveSession(s) return rsvp.SeeOther("/").SaveSession(s).Log("Invalid props: %#v\n", props)
}
_, err := lishwist.Register(username, newPassword)
if err != nil {
if errors.Is(err, lishwist.ErrorUsernameTaken) {
props.Username.Error = "Username is taken"
} else {
props.GeneralError = "Something went wrong."
}
s.FlashSet(&props)
return rsvp.SeeOther("/register").SaveSession(s).Log("Registration failed: %s\n", err)
} }
s.FlashSet(true) s.FlashSet(true)

View File

@ -1,24 +1,24 @@
package routing package routing
import ( import (
"lishwist/api/db" lishwist "lishwist/core"
"lishwist/rsvp" "lishwist/http/rsvp"
"net/http" "net/http"
) )
func TodoUpdate(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func TodoUpdate(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
form := r.ParseForm() form := r.ParseForm()
switch form.Get("intent") { switch form.Get("intent") {
case "unclaim_todo": case "unclaim_todo":
unclaims := form["gift"] unclaims := form["gift"]
err := currentUser.ClaimGifts([]string{}, unclaims) err := app.ClaimWishes([]string{}, unclaims)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err) return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err)
} }
case "complete_todo": case "complete_todo":
claims := form["gift"] claims := form["gift"]
err := currentUser.CompleteGifts(claims) err := app.CompleteWishes(claims)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err) return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err)
} }

View File

@ -1,17 +1,18 @@
package routing package routing
import ( import (
"lishwist/api/db" lishwist "lishwist/core"
"lishwist/rsvp" "lishwist/http/rsvp"
"net/http" "net/http"
) )
func Users(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func Users(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
if !currentUser.IsAdmin { admin := app.Admin()
if admin == nil {
return NotFound(h, r) return NotFound(h, r)
} }
users, err := db.GetAllUsers() users, err := admin.ListUsers()
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to get users: %s", err) return rsvp.Error(http.StatusInternalServerError, "Failed to get users: %s", err)
} }
@ -19,14 +20,15 @@ func Users(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response {
return rsvp.Data("", users) return rsvp.Data("", users)
} }
func User(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func User(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
if !currentUser.IsAdmin { admin := app.Admin()
if admin == nil {
return NotFound(h, r) return NotFound(h, r)
} }
reference := r.PathValue("userReference") reference := r.PathValue("userReference")
user, err := db.GetUserByReference(reference) user, err := lishwist.GetUserByReference(reference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to get user: %s", err) return rsvp.Error(http.StatusInternalServerError, "Failed to get user: %s", err)
} }
@ -37,19 +39,20 @@ func User(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response {
return rsvp.Data("", user) return rsvp.Data("", user)
} }
func UserPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func UserPost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
if !currentUser.IsAdmin { admin := app.Admin()
if admin == nil {
return NotFound(h, r) return NotFound(h, r)
} }
form := r.ParseForm() form := r.ParseForm()
reference := r.PathValue("userReference") reference := r.PathValue("userReference")
if reference == currentUser.Reference { if reference == app.User.Reference {
return rsvp.Error(http.StatusForbidden, "You cannot delete yourself.") return rsvp.Error(http.StatusForbidden, "You cannot delete yourself.")
} }
user, err := db.GetAnyUserByReference(reference) user, err := lishwist.GetUserByReference(reference)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to get user: %s", err) return rsvp.Error(http.StatusInternalServerError, "Failed to get user: %s", err)
} }
@ -60,7 +63,7 @@ func UserPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Respons
intent := form.Get("intent") intent := form.Get("intent")
if intent != "" { if intent != "" {
err = user.SetLive(intent != "delete") err = admin.UserSetLive(reference, intent != "delete")
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to delete user: %s", err) return rsvp.Error(http.StatusInternalServerError, "Failed to delete user: %s", err)
} }

View File

@ -1,32 +1,32 @@
package routing package routing
import ( import (
"lishwist/api/db" lishwist "lishwist/core"
"lishwist/rsvp" "lishwist/http/rsvp"
"net/http" "net/http"
) )
func WishlistAdd(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func WishlistAdd(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
form := r.ParseForm() form := r.ParseForm()
newGiftName := form.Get("gift_name") newGiftName := form.Get("gift_name")
err := currentUser.AddGift(newGiftName) err := app.MakeWish(newGiftName)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to add gift.").LogError(err) return rsvp.Error(http.StatusInternalServerError, "Failed to add gift.").LogError(err)
} }
return rsvp.SeeOther("/") return rsvp.SeeOther("/")
} }
func WishlistDelete(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func WishlistDelete(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
form := r.ParseForm() form := r.ParseForm()
targets := form["gift"] targets := form["gift"]
err := currentUser.RemoveGifts(targets...) err := app.RevokeWishes(targets...)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to remove gifts.").LogError(err) return rsvp.Error(http.StatusInternalServerError, "Failed to remove gifts.").LogError(err)
} }
return rsvp.SeeOther("/") return rsvp.SeeOther("/")
} }
func ForeignWishlistPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response { func ForeignWishlistPost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
form := r.ParseForm() form := r.ParseForm()
userReference := r.PathValue("userReference") userReference := r.PathValue("userReference")
intent := form.Get("intent") intent := form.Get("intent")
@ -34,22 +34,22 @@ func ForeignWishlistPost(currentUser *db.User, h http.Header, r *rsvp.Request) r
case "claim": case "claim":
claims := form["unclaimed"] claims := form["unclaimed"]
unclaims := form["claimed"] unclaims := form["claimed"]
err := currentUser.ClaimGifts(claims, unclaims) err := app.ClaimWishes(claims, unclaims)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err) return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err)
} }
case "complete": case "complete":
claims := form["claimed"] claims := form["claimed"]
err := currentUser.CompleteGifts(claims) err := app.CompleteWishes(claims)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err) return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err)
} }
case "add": case "add":
giftName := form.Get("gift_name") wishName := form.Get("gift_name")
if giftName == "" { if wishName == "" {
return rsvp.Error(http.StatusBadRequest, "Gift name not provided") return rsvp.Error(http.StatusBadRequest, "Gift name not provided")
} }
err := currentUser.AddGiftToUser(userReference, giftName) err := app.SuggestWishForUser(userReference, wishName)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to add gift idea to other user...").LogError(err) return rsvp.Error(http.StatusInternalServerError, "Failed to add gift idea to other user...").LogError(err)
} }
@ -57,7 +57,7 @@ func ForeignWishlistPost(currentUser *db.User, h http.Header, r *rsvp.Request) r
claims := form["unclaimed"] claims := form["unclaimed"]
unclaims := form["claimed"] unclaims := form["claimed"]
gifts := append(claims, unclaims...) gifts := append(claims, unclaims...)
err := currentUser.RemoveGifts(gifts...) err := app.RecindWishesForUser(gifts...)
if err != nil { if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to remove gift idea for other user...").LogError(err) return rsvp.Error(http.StatusInternalServerError, "Failed to remove gift idea for other user...").LogError(err)
} }

View File

@ -4,7 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"lishwist/templates" "lishwist/http/templates"
"log" "log"
"net/http" "net/http"
"strings" "strings"
@ -20,6 +20,10 @@ type Response struct {
} }
func (res *Response) Write(w http.ResponseWriter, r *http.Request) error { func (res *Response) Write(w http.ResponseWriter, r *http.Request) error {
if res.LogMessage != "" {
log.Printf("%s --- %s\n", res.Data, res.LogMessage)
}
if res.Session != nil { if res.Session != nil {
err := res.Session.inner.Save(r, w) err := res.Session.inner.Save(r, w)
if err != nil { if err != nil {
@ -44,10 +48,6 @@ func (res *Response) Write(w http.ResponseWriter, r *http.Request) error {
bodyBytes := bytes.NewBuffer([]byte{}) bodyBytes := bytes.NewBuffer([]byte{})
accept := r.Header.Get("Accept") accept := r.Header.Get("Accept")
if res.LogMessage != "" {
log.Printf("%s --- %s\n", res.Data, res.LogMessage)
}
if res.Status != 0 { if res.Status != 0 {
w.WriteHeader(res.Status) w.WriteHeader(res.Status)
} }

View File

@ -1,62 +0,0 @@
//go:generate go run gen_init_sql.go
package db
import (
"database/sql"
"fmt"
"lishwist/env"
"github.com/Teajey/sqlstore"
_ "github.com/glebarez/go-sqlite"
)
var database *sql.DB
func Open() error {
db, err := sql.Open("sqlite", env.DatabaseFile)
if err != nil {
return err
}
database = db
return nil
}
func Init() error {
_, err := database.Exec(initQuery)
if err != nil {
return err
}
return nil
}
func NewSessionStore() (*sqlstore.Store, error) {
deleteStmt, err := database.Prepare("DELETE FROM session WHERE id = ?;")
if err != nil {
return nil, fmt.Errorf("Failed to prepare delete statement: %w", err)
}
insertStmt, err := database.Prepare("INSERT INTO session (value) VALUES (?);")
if err != nil {
return nil, fmt.Errorf("Failed to prepare insert statement: %w", err)
}
selectStmt, err := database.Prepare("SELECT value FROM session WHERE id = ?;")
if err != nil {
return nil, fmt.Errorf("Failed to prepare select statement: %w", err)
}
updateStmt, err := database.Prepare("UPDATE session SET value = ?2 WHERE id = ?1;")
if err != nil {
return nil, fmt.Errorf("Failed to prepare update statement: %w", err)
}
s := sqlstore.NewSqlStore(database, sqlstore.Statements{
Delete: deleteStmt,
Insert: insertStmt,
Select: selectStmt,
Update: updateStmt,
}, []byte(env.SessionSecret))
return s, nil
}

View File

@ -1,419 +0,0 @@
package db
import (
"database/sql"
"fmt"
"lishwist/normalize"
"github.com/google/uuid"
)
type User struct {
Id string
NormalName string
Name string
Reference string
IsAdmin bool
IsLive bool
}
type Gift struct {
Id string
Name string
ClaimantId string `json:",omitempty"`
ClaimantName string `json:",omitempty"`
Sent bool
RecipientId string `json:",omitempty"`
RecipientName string `json:",omitempty"`
RecipientRef string `json:",omitempty"`
CreatorId string `json:",omitempty"`
CreatorName string `json:",omitempty"`
}
func queryManyUsers(query string, args ...any) ([]User, error) {
rows, err := database.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
users := []User{}
for rows.Next() {
var u User
err = rows.Scan(&u.Id, &u.NormalName, &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 queryOneUser(query string, args ...any) (*User, error) {
users, err := queryManyUsers(query, args...)
if err != nil {
return nil, err
}
if len(users) < 1 {
return nil, nil
}
return &users[0], nil
}
func GetAllUsers() ([]User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM user"
return queryManyUsers(stmt)
}
func GetUser(id string) (*User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE id = ?"
return queryOneUser(stmt, id)
}
func GetUserByName(username string) (*User, error) {
username = normalize.Name(username)
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE name = ?"
return queryOneUser(stmt, username)
}
func GetUserByReference(reference string) (*User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM v_user WHERE reference = ?"
return queryOneUser(stmt, reference)
}
func GetAnyUserByReference(reference string) (*User, error) {
stmt := "SELECT id, name, display_name, reference, is_admin, is_live FROM user WHERE reference = ?"
return queryOneUser(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(name string, passHash []byte) (*User, error) {
username := normalize.Name(name)
stmt := "INSERT INTO user (name, display_name, reference, password_hash) VALUES (?, ?, ?, ?)"
reference, err := uuid.NewRandom()
if err != nil {
return nil, err
}
result, err := database.Exec(stmt, username, name, reference, passHash)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
user := User{
Id: fmt.Sprintf("%d", id),
Name: name,
}
return &user, nil
}
func (u *User) GetPassHash() ([]byte, error) {
stmt := "SELECT password_hash FROM v_user WHERE id = ?"
var passHash string
err := database.QueryRow(stmt, u.Id).Scan(&passHash)
if err != nil {
return nil, err
}
return []byte(passHash), nil
}
func (u *User) CountGifts() (int, error) {
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 {
return 0, err
}
return giftCount, nil
}
func (u *User) GetGifts() ([]Gift, error) {
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
}
defer rows.Close()
gifts := []Gift{}
for rows.Next() {
var id string
var name string
var sent bool
err = rows.Scan(&id, &name, &sent)
if err != nil {
return nil, err
}
gift := Gift{
Id: id,
Name: name,
Sent: sent,
}
gifts = append(gifts, gift)
}
err = rows.Err()
if err != nil {
return nil, err
}
return gifts, nil
}
func (u *User) GetOtherUserGifts(otherUserReference string) ([]Gift, error) {
otherUser, err := GetUserByReference(otherUserReference)
if err != nil {
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 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, fmt.Errorf("Failed to execute query: %w", err)
}
defer rows.Close()
gifts := []Gift{}
for rows.Next() {
var id string
var name string
var claimantId sql.NullString
var claimantName sql.NullString
var sent bool
var creatorId string
var creatorName string
var recipientId string
err = rows.Scan(&id, &name, &claimantId, &claimantName, &sent, &creatorId, &creatorName, &recipientId)
if err != nil {
return nil, fmt.Errorf("Failed to scan row: %w", err)
}
gift := Gift{
Id: id,
Name: name,
ClaimantId: claimantId.String,
ClaimantName: claimantName.String,
Sent: sent,
CreatorId: creatorId,
CreatorName: creatorName,
RecipientId: recipientId,
}
gifts = append(gifts, gift)
}
err = rows.Err()
if err != nil {
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 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
}
defer rows.Close()
gifts := []Gift{}
for rows.Next() {
var id string
var name string
var sent bool
var recipientName string
var recipientRef string
_ = rows.Scan(&id, &name, &sent, &recipientName, &recipientRef)
gift := Gift{
Id: id,
Name: name,
Sent: sent,
RecipientName: recipientName,
RecipientRef: recipientRef,
}
gifts = append(gifts, gift)
}
err = rows.Err()
if err != nil {
return nil, err
}
return gifts, nil
}
func (u *User) AddGift(name string) error {
stmt := "INSERT INTO gift (name, recipient_id, creator_id) VALUES (?, ?, ?)"
_, err := database.Exec(stmt, name, u.Id, u.Id)
if err != nil {
return err
}
return nil
}
func (u *User) deleteGifts(tx *sql.Tx, ids []string) error {
stmt := "DELETE FROM gift WHERE gift.creator_id = ? AND gift.id = ?"
for _, id := range ids {
r, err := tx.Exec(stmt, u.Id, id)
if err != nil {
return err
}
rE, err := r.RowsAffected()
if err != nil {
return err
}
if rE < 1 {
return fmt.Errorf("Gift deletion failed for '%s'", id)
}
}
return nil
}
func (u *User) RemoveGifts(ids ...string) error {
if len(ids) < 1 {
return fmt.Errorf("Attempt to remove zero gifts")
}
tx, err := database.Begin()
if err != nil {
return err
}
err = u.deleteGifts(tx, ids)
if err != nil {
rollBackErr := tx.Rollback()
if rollBackErr != nil {
return err
}
return err
}
err = tx.Commit()
return err
}
func (u *User) executeClaims(tx *sql.Tx, claims, unclaims []string) error {
claimStmt := "UPDATE gift SET claimant_id = ? WHERE id = ?"
unclaimStmt := "UPDATE gift SET claimant_id = NULL WHERE id = ?"
for _, id := range claims {
r, err := tx.Exec(claimStmt, u.Id, id)
if err != nil {
return err
}
rE, err := r.RowsAffected()
if err != nil {
return err
}
if rE < 1 {
return fmt.Errorf("Gift claim failed for '%s'", id)
}
}
for _, id := range unclaims {
r, err := tx.Exec(unclaimStmt, id)
if err != nil {
return err
}
rE, err := r.RowsAffected()
if err != nil {
return err
}
if rE < 1 {
return fmt.Errorf("Gift unclaim failed for '%s'", id)
}
}
return nil
}
func (u *User) ClaimGifts(claims, unclaims []string) error {
if len(claims) < 1 && len(unclaims) < 1 {
return fmt.Errorf("Attempt to claim/unclaim zero gifts")
}
tx, err := database.Begin()
if err != nil {
return err
}
err = u.executeClaims(tx, claims, unclaims)
if err != nil {
rollBackErr := tx.Rollback()
if rollBackErr != nil {
return err
}
return err
}
err = tx.Commit()
return err
}
func (u *User) executeCompletions(tx *sql.Tx, claims []string) error {
claimStmt := "UPDATE gift SET sent = 1 WHERE id = ?"
for _, id := range claims {
r, err := tx.Exec(claimStmt, id)
if err != nil {
return err
}
rE, err := r.RowsAffected()
if err != nil {
return err
}
if rE < 1 {
return fmt.Errorf("Gift completion failed for '%s'", id)
}
}
return nil
}
func (u *User) CompleteGifts(claims []string) error {
if len(claims) < 1 {
return fmt.Errorf("Attempt to complete zero gifts")
}
tx, err := database.Begin()
if err != nil {
return err
}
err = u.executeCompletions(tx, claims)
if err != nil {
rollBackErr := tx.Rollback()
if rollBackErr != nil {
return err
}
return err
}
err = tx.Commit()
return err
}
func (u *User) AddGiftToUser(otherUserReference string, giftName string) error {
otherUser, err := GetUserByReference(otherUserReference)
if err != nil {
return err
}
stmt := "INSERT INTO gift (name, recipient_id, creator_id) VALUES (?, ?, ?)"
_, err = database.Exec(stmt, giftName, otherUser.Id, u.Id)
if err != nil {
return err
}
return nil
}
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 v_user AS user ON user.id = group_member.user_id WHERE user.id = ?"
return queryManyGroups(stmt, u.Id)
}
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 queryOneGroup(stmt, reference, u.Id)
}