Compare commits

...

7 Commits

Author SHA1 Message Date
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
40 changed files with 955 additions and 891 deletions

View File

@ -1,5 +1,13 @@
package lishwist
type Admin struct {
user *User
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
}
}

View File

@ -1,3 +1,18 @@
module lishwist/core
go 1.23
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 (
"fmt"
"lishwist/http/normalize"
"lishwist/core/internal/db"
"strconv"
"strings"
)
type Group struct {
@ -24,7 +25,7 @@ func (g *Group) MemberIndex(userId string) int {
func queryManyGroups(query string, args ...any) ([]Group, error) {
groups := []Group{}
rows, err := database.Query(query, args...)
rows, err := db.Connection.Query(query, args...)
if err != nil {
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"
members, err := queryManyUsers(query, groupId)
if err != nil {
return members, fmt.Errorf("Failed to get members: %w", err)
return members, err
}
return members, nil
}
func GetGroupByReference(reference string) (*Group, error) {
query := "SELECT [group].id, [group].name, [group].reference FROM [group] WHERE [group].reference = ?"
return queryOneGroup(query, reference)
func (s *Session) 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, 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];"
return queryManyGroups(query)
}
func CreateGroup(name string, reference string) (*Group, error) {
name = normalize.Trim(name)
func (a *Admin) CreateGroup(name string, reference string) (*Group, error) {
name = strings.TrimSpace(name)
reference = strings.TrimSpace(reference)
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 {
return nil, err
}
@ -98,20 +105,26 @@ func CreateGroup(name string, reference string) (*Group, error) {
return &group, nil
}
func (g *Group) AddUser(userId string) error {
stmt := "INSERT INTO group_member (group_id, user_id) VALUES (?, ?)"
_, err := database.Exec(stmt, g.Id, userId)
func (a *Admin) AddUserToGroup(userId, groupId string) error {
stmt := "INSERT INTO group_member (user_id, group_id) VALUES (?, ?)"
_, err := db.Connection.Exec(stmt, userId, groupId)
if err != nil {
return err
}
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 = ?"
_, err := database.Exec(stmt, g.Id, userId)
_, err := db.Connection.Exec(stmt, userId, groupId)
if err != nil {
return err
}
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)
}

View File

@ -6,13 +6,14 @@ import (
"database/sql"
"fmt"
_ "github.com/glebarez/go-sqlite"
_ "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("sqlite", dataSourceName)
db, err := sql.Open("sqlite3", dataSourceName)
if err != nil {
return fmt.Errorf("Failed to open db connection: %w", err)
}

View File

@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS "user" (
"is_live" INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "gift" (
CREATE TABLE IF NOT EXISTS "wish" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"recipient_id" INTEGER NOT NULL,
@ -37,9 +37,8 @@ CREATE TABLE IF NOT EXISTS "group_member" (
);
CREATE TABLE IF NOT EXISTS "session" (
"id" INTEGER NOT NULL UNIQUE,
"user_id" INTEGER NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("user_id") REFERENCES "user"("id")
"value" TEXT NOT NULL,
PRIMARY KEY("id" AUTOINCREMENT)
);
DROP VIEW IF EXISTS "v_user";
@ -50,6 +49,6 @@ 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;
-- SELECT wish.id, wish.name, wish.sent FROM wish JOIN user AS recipient;
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

@ -4,11 +4,7 @@ import (
"strings"
)
func Trim(s string) string {
return strings.Trim(s, " \t")
}
func Name(name string) string {
name = Trim(name)
name = strings.TrimSpace(name)
return strings.ToLower(name)
}

View File

@ -6,13 +6,15 @@ import (
"golang.org/x/crypto/bcrypt"
)
func (sm *SessionManager) Login(username, password string) (*Session, error) {
type ErrorInvalidCredentials error
func Login(username, password string) (*Session, error) {
user, err := getUserByName(username)
if err != nil {
return nil, fmt.Errorf("Failed to fetch user: %w", err)
return nil, ErrorInvalidCredentials(fmt.Errorf("Failed to fetch user: %w", err))
}
if user == nil {
return nil, fmt.Errorf("User not found by name: %s", username)
return nil, ErrorInvalidCredentials(fmt.Errorf("User not found by name: %s", username))
}
passHash, err := user.getPassHash()
@ -22,13 +24,8 @@ func (sm *SessionManager) Login(username, password string) (*Session, error) {
err = bcrypt.CompareHashAndPassword(passHash, []byte(password))
if err != nil {
return nil, err
return nil, ErrorInvalidCredentials(fmt.Errorf("Password compare failed: %w", err))
}
session, err := sm.createSession(user)
if err != nil {
return nil, fmt.Errorf("Couldn't create session: %w", err)
}
return session, nil
return &Session{*user}, nil
}

View File

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

View File

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

View File

@ -1,64 +1,15 @@
package lishwist
import (
"crypto/rand"
"encoding/base64"
"fmt"
"lishwist/core/internal/db"
"time"
)
import "fmt"
type Session struct {
Id string
Token string
User *User
ExpiresAt time.Time
CreatedAt time.Time
User
}
type SessionManager struct {
sessionDuration time.Duration
sessionTokenLength uint
}
func NewSessionManager(sessionDuration time.Duration, sessionTokenLength uint) SessionManager {
return SessionManager{
sessionDuration,
sessionTokenLength,
}
}
func generateSecureToken(size uint) (string, error) {
bytes := make([]byte, size)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
func (sm *SessionManager) createSession(user *User) (*Session, error) {
stmt := "INSERT INTO session (user_id) VALUES (?);"
result, err := db.Connection.Exec(stmt, user.Id)
func SessionFromUsername(username string) (*Session, error) {
user, err := getUserByName(username)
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to get user: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
token, err := generateSecureToken(sm.sessionTokenLength)
if err != nil {
return nil, fmt.Errorf("Failed to generate secure token: %w", err)
}
session := Session{
Id: fmt.Sprintf("%s", id),
Token: token,
User: user,
ExpiresAt: time.Now().Add(sm.sessionDuration),
CreatedAt: time.Now(),
}
return &session, nil
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
}

View File

@ -10,7 +10,8 @@ import (
)
type User struct {
Id string
Id string
// TODO: rename to DisplayName
NormalName string
Name string
Reference string
@ -51,34 +52,26 @@ func queryOneUser(query string, args ...any) (*User, error) {
return &users[0], nil
}
func (u *User) GetAdmin() *Admin {
if u.IsAdmin {
return &Admin{u}
} else {
return 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) (*User, error) {
func createUser(name string, passHash []byte, isAdmin bool) (*User, error) {
username := normalize.Name(name)
stmt := "INSERT INTO user (name, display_name, reference, password_hash) VALUES (?, ?, ?, ?)"
stmt := "INSERT INTO user (name, display_name, reference, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)"
reference, err := uuid.NewRandom()
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to generate reference: %w", err)
}
result, err := db.Connection.Exec(stmt, username, name, reference, passHash)
result, err := db.Connection.Exec(stmt, username, name, reference, passHash, isAdmin)
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to execute query: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
return nil, fmt.Errorf("Failed to get last insert id: %w", err)
}
user := User{
Id: fmt.Sprintf("%d", id),
@ -96,3 +89,77 @@ func (u *User) getPassHash() ([]byte, error) {
}
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")
}

11
go.work
View File

@ -1,7 +1,6 @@
go 1.23
go 1.23.3
toolchain go1.23.3
use ./core
use ./http
use (
./core
./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/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=
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/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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=
lukechampine.com/adiantum v1.1.1/go.mod h1:LrAYVnTYLnUtE/yMp5bQr0HstAf060YUF8nM0B6+rUw=
lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=

View File

@ -1,62 +0,0 @@
//go:generate go run gen_init_sql.go
package db
import (
"database/sql"
"fmt"
"lishwist/http/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,30 +0,0 @@
//go:build ignore
package main
import (
"log"
"os"
"text/template"
)
var initTemplate = template.Must(template.New("").Parse("// Code generated DO NOT EDIT.\n" +
"package db\n" +
"\n" +
"const initQuery = `{{.}}`\n",
))
func main() {
initStmt, err := os.ReadFile("./init.sql")
if err != nil {
log.Fatal(err)
}
f, err := os.Create("./init_sql.go")
if err != nil {
log.Fatal(err)
}
defer f.Close()
initTemplate.Execute(f, string(initStmt))
}

View File

@ -1,54 +0,0 @@
BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "user" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL UNIQUE,
"display_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)
);
CREATE TABLE IF NOT EXISTS "gift" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL,
"recipient_id" INTEGER NOT NULL,
"claimant_id" INTEGER,
"creator_id" INTEGER NOT NULL,
"sent" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY("id" AUTOINCREMENT),
FOREIGN KEY("recipient_id") REFERENCES "user"("id"),
FOREIGN KEY("creator_id") REFERENCES "user"("id"),
FOREIGN KEY("claimant_id") REFERENCES "user"("id")
);
CREATE TABLE IF NOT EXISTS "group" (
"id" INTEGER NOT NULL UNIQUE,
"name" TEXT NOT NULL UNIQUE,
"reference" TEXT NOT NULL UNIQUE,
PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "group_member" (
"group_id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
UNIQUE("user_id","group_id"),
FOREIGN KEY("group_id") REFERENCES "group"("id"),
FOREIGN KEY("user_id") REFERENCES "user"("id")
);
CREATE TABLE IF NOT EXISTS "session" (
"id" INTEGER NOT NULL UNIQUE,
"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;

View File

@ -1,22 +0,0 @@
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

@ -1,419 +0,0 @@
package db
import (
"database/sql"
"fmt"
"lishwist/http/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)
}

View File

@ -1,11 +1,10 @@
package api
import (
"lishwist/http/api/db"
"lishwist/http/templates"
"log"
"golang.org/x/crypto/bcrypt"
lishwist "lishwist/core"
"lishwist/http/templates"
)
type LoginProps struct {
@ -55,31 +54,19 @@ func Login(username, password string) *LoginProps {
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
_, err := lishwist.Login(props.Username.Value, props.Password.Value)
if err == nil {
return nil
}
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())
switch err.(type) {
case lishwist.ErrorInvalidCredentials:
log.Printf("Invalid credentials: %w\n", err)
props.GeneralError = "Username or password invalid"
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
import (
"log"
"lishwist/http/api/db"
"lishwist/http/templates"
"golang.org/x/crypto/bcrypt"
)
type RegisterProps struct {
@ -64,37 +59,37 @@ func NewRegisterProps(usernameVal, passwordVal, confirmPassVal string) *Register
}
}
func Register(username, newPassword, confirmPassword string) *RegisterProps {
props := NewRegisterProps(username, newPassword, confirmPassword)
// func Register(username, newPassword, confirmPassword string) *RegisterProps {
// props := NewRegisterProps(username, newPassword, confirmPassword)
valid := props.Validate()
props.Password.Value = ""
props.ConfirmPassword.Value = ""
if !valid {
log.Printf("Invalid props: %#v\n", props)
return props
}
// valid := props.Validate()
// 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", existingUser.NormalName)
props.Username.Error = "Username is taken"
return props
}
// existingUser, _ := db.GetUserByName(username)
// if existingUser != nil {
// log.Printf("Username is taken: %q\n", existingUser.NormalName)
// 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
}
// 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.Printf("Failed to create user: %s\n", err)
props.GeneralError = "Something went wrong. Error code: Ozai"
return props
}
// _, err = db.CreateUser(username, hashedPasswordBytes)
// if err != nil {
// log.Printf("Failed to create user: %s\n", err)
// props.GeneralError = "Something went wrong. Error code: Ozai"
// return props
// }
return nil
}
// return nil
// }

View File

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

View File

@ -1,12 +1,12 @@
package routing
import (
"lishwist/http/api/db"
lishwist "lishwist/core"
"lishwist/http/rsvp"
"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 {
session := r.GetSession()
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")
}
user, err := db.GetUserByName(username)
appSession, err := lishwist.SessionFromUsername(username)
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,7 +1,7 @@
package routing
import (
"lishwist/http/api/db"
lishwist "lishwist/core"
"lishwist/http/rsvp"
"net/http"
)
@ -10,26 +10,26 @@ type foreignWishlistProps struct {
CurrentUserId string
CurrentUserName 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")
if currentUser.Reference == userReference {
if app.User.Reference == userReference {
return rsvp.SeeOther("/")
}
otherUser, err := db.GetUserByReference(userReference)
otherUser, err := lishwist.GetUserByReference(userReference)
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)
}
if otherUser == nil {
return rsvp.Error(http.StatusInternalServerError, "User not found")
}
gifts, err := currentUser.GetOtherUserGifts(userReference)
wishes, err := app.GetOthersWishes(userReference)
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)
}
@ -40,14 +40,14 @@ type publicWishlistProps struct {
func PublicWishlist(h http.Header, r *rsvp.Request) rsvp.Response {
userReference := r.PathValue("userReference")
otherUser, err := db.GetUserByReference(userReference)
otherUser, err := lishwist.GetUserByReference(userReference)
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)
}
if otherUser == nil {
return rsvp.Error(http.StatusInternalServerError, "User not found")
}
giftCount, err := otherUser.CountGifts()
giftCount, err := otherUser.WishCount()
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)
}

View File

@ -4,59 +4,59 @@ import (
"net/http"
"slices"
"lishwist/http/api/db"
lishwist "lishwist/core"
"lishwist/http/rsvp"
)
type GroupProps struct {
Group *db.Group
Group *lishwist.Group
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")
group, err := db.GetGroupByReference(reference)
group, err := app.GetGroupByReference(reference)
if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Couldn't get group: %s", err)
}
if group == nil {
return rsvp.Error(http.StatusNotFound, "Group not found")
}
if !currentUser.IsAdmin {
index := group.MemberIndex(currentUser.Id)
if !app.User.IsAdmin {
index := group.MemberIndex(app.User.Id)
group.Members = slices.Delete(group.Members, index, index+1)
}
p := GroupProps{
Group: group,
CurrentUsername: currentUser.Name,
CurrentUsername: app.User.Name,
}
return rsvp.Data("group_page.gotmpl", p)
}
func Group(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response {
if currentUser.IsAdmin {
return AdminGroup(currentUser, h, r)
func Group(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
if app.User.IsAdmin {
return AdminGroup(app, h, r)
}
groupReference := r.PathValue("groupReference")
group, err := currentUser.GetGroupByReference(groupReference)
group, err := app.GetGroupByReference(groupReference)
if err != nil {
return rsvp.Error(http.StatusInternalServerError, "An error occurred while fetching this group :(").Log("Couldn't get group: %s", err)
}
if group == nil {
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)
p := GroupProps{
Group: group,
CurrentUsername: currentUser.Name,
CurrentUsername: app.User.Name,
}
return rsvp.Data("group_page.gotmpl", p)
}
func PublicGroup(h http.Header, r *rsvp.Request) rsvp.Response {
groupReference := r.PathValue("groupReference")
group, err := db.GetGroupByReference(groupReference)
group, err := lishwist.GetGroupByReference(groupReference)
if err != nil {
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)
}
func GroupPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response {
if !currentUser.IsAdmin {
func GroupPost(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
admin := app.Admin()
if admin == nil {
return NotFound(h, r)
}
form := r.ParseForm()
var group *db.Group
var group *lishwist.Group
reference := r.PathValue("groupReference")
name := form.Get("name")
@ -80,13 +81,13 @@ func GroupPost(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Respon
removeUsers := form["removeUser"]
if name != "" {
createdGroup, err := db.CreateGroup(name, reference)
createdGroup, err := admin.CreateGroup(name, reference)
if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to create group: %s", err)
}
group = createdGroup
} else {
existingGroup, err := db.GetGroupByReference(reference)
existingGroup, err := lishwist.GetGroupByReference(reference)
if err != nil {
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 {
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 {
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 {
user, err := db.GetUser(userId)
user, err := admin.GetUser(userId)
if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Groups exists, but a user with id %s could not be fetched: %s", userId, err)
}
if user == nil {
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 {
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)
}
func Groups(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response {
if !currentUser.IsAdmin {
func Groups(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
admin := app.Admin()
if admin == nil {
return NotFound(h, r)
}
groups, err := db.GetAllGroups()
groups, err := admin.ListGroups()
if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to get groups: %s", err)
}

View File

@ -3,45 +3,45 @@ package routing
import (
"net/http"
"lishwist/http/api/db"
lishwist "lishwist/core"
"lishwist/http/env"
"lishwist/http/rsvp"
)
type HomeProps struct {
Username string
Gifts []db.Gift
Todo []db.Gift
Gifts []lishwist.Wish
Todo []lishwist.Wish
Reference string
HostUrl string
Groups []db.Group
Groups []lishwist.Group
}
func Home(currentUser *db.User, h http.Header, r *rsvp.Request) rsvp.Response {
gifts, err := currentUser.GetGifts()
func Home(app *lishwist.Session, h http.Header, r *rsvp.Request) rsvp.Response {
gifts, err := app.GetWishes()
if err != nil {
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 {
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 {
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)
}
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()
switch form.Get("intent") {
case "add_idea":
return WishlistAdd(currentUser, h, r)
return WishlistAdd(app, h, r)
case "delete_idea":
return WishlistDelete(currentUser, h, r)
return WishlistDelete(app, h, r)
default:
return TodoUpdate(currentUser, h, r)
return TodoUpdate(app, h, r)
}
}

View File

@ -1,6 +1,7 @@
package routing
import (
lishwist "lishwist/core"
"lishwist/http/api"
"lishwist/http/rsvp"
"net/http"
@ -37,15 +38,32 @@ func LoginPost(h http.Header, r *rsvp.Request) rsvp.Response {
username := form.Get("username")
password := form.Get("password")
props := api.Login(username, password)
if props != nil {
props := api.NewLoginProps(username, password)
valid := props.Validate()
props.Password.Value = ""
if !valid {
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.SetValue("authorized", true)
session.SetValue("username", username)
session.SetValue("username", app.User.Name)
return rsvp.SeeOther(r.URL().Path).SaveSession(session)
}

View File

@ -1,6 +1,8 @@
package routing
import (
"errors"
lishwist "lishwist/core"
"lishwist/http/api"
"lishwist/http/rsvp"
"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 {
form := r.ParseForm()
s := r.GetSession()
username := form.Get("username")
newPassword := form.Get("newPassword")
confirmPassword := form.Get("confirmPassword")
props := api.Register(username, newPassword, confirmPassword)
props := api.NewRegisterProps(username, newPassword, confirmPassword)
s := r.GetSession()
if props != nil {
valid := props.Validate()
props.Password.Value = ""
props.ConfirmPassword.Value = ""
if !valid {
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)

View File

@ -1,24 +1,24 @@
package routing
import (
"lishwist/http/api/db"
lishwist "lishwist/core"
"lishwist/http/rsvp"
"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()
switch form.Get("intent") {
case "unclaim_todo":
unclaims := form["gift"]
err := currentUser.ClaimGifts([]string{}, unclaims)
err := app.ClaimWishes([]string{}, unclaims)
if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to update claim...").LogError(err)
}
case "complete_todo":
claims := form["gift"]
err := currentUser.CompleteGifts(claims)
err := app.CompleteWishes(claims)
if err != nil {
return rsvp.Error(http.StatusInternalServerError, "Failed to complete gifts...").LogError(err)
}

View File

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

View File

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