From d89b85529975e5e853396022f411a318637f641f Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Sun, 29 Dec 2024 23:50:12 +1300 Subject: [PATCH 01/11] refac: rename current module to http --- .gitignore | 2 +- go.work | 2 +- {server => http}/api/db/db.go | 2 +- {server => http}/api/db/gen_init_sql.go | 0 {server => http}/api/db/group.go | 2 +- {server => http}/api/db/init.sql | 0 {server => http}/api/db/migration/1.sql | 0 {server => http}/api/db/user.go | 2 +- {server => http}/api/login.go | 4 ++-- {server => http}/api/register.go | 4 ++-- {server => http}/env/env.go | 0 {server => http}/go.mod | 2 +- {server => http}/go.sum | 0 {server => http}/hashpword/main.go | 0 {server => http}/main.go | 12 ++++++------ {server => http}/normalize/name.go | 0 {server => http}/router/router.go | 2 +- {server => http}/routing/context.go | 4 ++-- {server => http}/routing/error.go | 0 {server => http}/routing/foreign_wishlist.go | 4 ++-- {server => http}/routing/groups.go | 4 ++-- {server => http}/routing/home.go | 6 +++--- {server => http}/routing/login.go | 4 ++-- {server => http}/routing/logout.go | 2 +- {server => http}/routing/not_found.go | 2 +- {server => http}/routing/register.go | 4 ++-- {server => http}/routing/todo.go | 4 ++-- {server => http}/routing/users.go | 4 ++-- {server => http}/routing/wishlist.go | 4 ++-- {server => http}/rsvp/handler.go | 0 {server => http}/rsvp/request.go | 0 {server => http}/rsvp/response.go | 2 +- {server => http}/rsvp/session.go | 0 {server => http}/session/session.go | 0 {server => http}/templates/base.gotmpl | 0 {server => http}/templates/error_page.gotmpl | 0 {server => http}/templates/foreign_wishlist.gotmpl | 0 {server => http}/templates/group_page.gotmpl | 0 {server => http}/templates/home.gotmpl | 0 {server => http}/templates/login.gotmpl | 0 .../templates/public_foreign_wishlist.gotmpl | 0 {server => http}/templates/public_group_page.gotmpl | 0 {server => http}/templates/register.gotmpl | 0 {server => http}/templates/templates.go | 0 44 files changed, 39 insertions(+), 39 deletions(-) rename {server => http}/api/db/db.go (98%) rename {server => http}/api/db/gen_init_sql.go (100%) rename {server => http}/api/db/group.go (99%) rename {server => http}/api/db/init.sql (100%) rename {server => http}/api/db/migration/1.sql (100%) rename {server => http}/api/db/user.go (99%) rename {server => http}/api/login.go (97%) rename {server => http}/api/register.go (97%) rename {server => http}/env/env.go (100%) rename {server => http}/go.mod (96%) rename {server => http}/go.sum (100%) rename {server => http}/hashpword/main.go (100%) rename {server => http}/main.go (92%) rename {server => http}/normalize/name.go (100%) rename {server => http}/router/router.go (96%) rename {server => http}/routing/context.go (93%) rename {server => http}/routing/error.go (100%) rename {server => http}/routing/foreign_wishlist.go (97%) rename {server => http}/routing/groups.go (98%) rename {server => http}/routing/home.go (95%) rename {server => http}/routing/login.go (96%) rename {server => http}/routing/logout.go (91%) rename {server => http}/routing/not_found.go (88%) rename {server => http}/routing/register.go (95%) rename {server => http}/routing/todo.go (93%) rename {server => http}/routing/users.go (97%) rename {server => http}/routing/wishlist.go (97%) rename {server => http}/rsvp/handler.go (100%) rename {server => http}/rsvp/request.go (100%) rename {server => http}/rsvp/response.go (98%) rename {server => http}/rsvp/session.go (100%) rename {server => http}/session/session.go (100%) rename {server => http}/templates/base.gotmpl (100%) rename {server => http}/templates/error_page.gotmpl (100%) rename {server => http}/templates/foreign_wishlist.gotmpl (100%) rename {server => http}/templates/group_page.gotmpl (100%) rename {server => http}/templates/home.gotmpl (100%) rename {server => http}/templates/login.gotmpl (100%) rename {server => http}/templates/public_foreign_wishlist.gotmpl (100%) rename {server => http}/templates/public_group_page.gotmpl (100%) rename {server => http}/templates/register.gotmpl (100%) rename {server => http}/templates/templates.go (100%) diff --git a/.gitignore b/.gitignore index d862823..2c382ac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ gin-bin *lishwist.db .env*.local -server/api/db/init_sql.go +http/api/db/init_sql.go .ignored/ diff --git a/go.work b/go.work index bd31234..72e8c6b 100644 --- a/go.work +++ b/go.work @@ -2,4 +2,4 @@ go 1.23 toolchain go1.23.3 -use ./server +use ./http diff --git a/server/api/db/db.go b/http/api/db/db.go similarity index 98% rename from server/api/db/db.go rename to http/api/db/db.go index b564228..f4fb502 100644 --- a/server/api/db/db.go +++ b/http/api/db/db.go @@ -5,7 +5,7 @@ package db import ( "database/sql" "fmt" - "lishwist/env" + "lishwist/http/env" "github.com/Teajey/sqlstore" _ "github.com/glebarez/go-sqlite" diff --git a/server/api/db/gen_init_sql.go b/http/api/db/gen_init_sql.go similarity index 100% rename from server/api/db/gen_init_sql.go rename to http/api/db/gen_init_sql.go diff --git a/server/api/db/group.go b/http/api/db/group.go similarity index 99% rename from server/api/db/group.go rename to http/api/db/group.go index b672c98..eee6785 100644 --- a/server/api/db/group.go +++ b/http/api/db/group.go @@ -2,7 +2,7 @@ package db import ( "fmt" - "lishwist/normalize" + "lishwist/http/normalize" "strconv" ) diff --git a/server/api/db/init.sql b/http/api/db/init.sql similarity index 100% rename from server/api/db/init.sql rename to http/api/db/init.sql diff --git a/server/api/db/migration/1.sql b/http/api/db/migration/1.sql similarity index 100% rename from server/api/db/migration/1.sql rename to http/api/db/migration/1.sql diff --git a/server/api/db/user.go b/http/api/db/user.go similarity index 99% rename from server/api/db/user.go rename to http/api/db/user.go index 34d2ba8..b1beb15 100644 --- a/server/api/db/user.go +++ b/http/api/db/user.go @@ -3,7 +3,7 @@ package db import ( "database/sql" "fmt" - "lishwist/normalize" + "lishwist/http/normalize" "github.com/google/uuid" ) diff --git a/server/api/login.go b/http/api/login.go similarity index 97% rename from server/api/login.go rename to http/api/login.go index d4d9341..64c12f5 100644 --- a/server/api/login.go +++ b/http/api/login.go @@ -1,8 +1,8 @@ package api import ( - "lishwist/api/db" - "lishwist/templates" + "lishwist/http/api/db" + "lishwist/http/templates" "log" "golang.org/x/crypto/bcrypt" diff --git a/server/api/register.go b/http/api/register.go similarity index 97% rename from server/api/register.go rename to http/api/register.go index 1d72fa5..1a4eca2 100644 --- a/server/api/register.go +++ b/http/api/register.go @@ -3,8 +3,8 @@ package api import ( "log" - "lishwist/api/db" - "lishwist/templates" + "lishwist/http/api/db" + "lishwist/http/templates" "golang.org/x/crypto/bcrypt" ) diff --git a/server/env/env.go b/http/env/env.go similarity index 100% rename from server/env/env.go rename to http/env/env.go diff --git a/server/go.mod b/http/go.mod similarity index 96% rename from server/go.mod rename to http/go.mod index cebcb93..ca1fb6a 100644 --- a/server/go.mod +++ b/http/go.mod @@ -1,4 +1,4 @@ -module lishwist +module lishwist/http go 1.23 diff --git a/server/go.sum b/http/go.sum similarity index 100% rename from server/go.sum rename to http/go.sum diff --git a/server/hashpword/main.go b/http/hashpword/main.go similarity index 100% rename from server/hashpword/main.go rename to http/hashpword/main.go diff --git a/server/main.go b/http/main.go similarity index 92% rename from server/main.go rename to http/main.go index d77f133..640577b 100644 --- a/server/main.go +++ b/http/main.go @@ -5,12 +5,12 @@ import ( "log" "net/http" - "lishwist/api" - // TODO: lishwist/api/db ought not to be used outside lishwist/api - "lishwist/api/db" - "lishwist/env" - "lishwist/router" - "lishwist/routing" + "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" ) func main() { diff --git a/server/normalize/name.go b/http/normalize/name.go similarity index 100% rename from server/normalize/name.go rename to http/normalize/name.go diff --git a/server/router/router.go b/http/router/router.go similarity index 96% rename from server/router/router.go rename to http/router/router.go index 56049d2..8891f68 100644 --- a/server/router/router.go +++ b/http/router/router.go @@ -1,7 +1,7 @@ package router import ( - "lishwist/rsvp" + "lishwist/http/rsvp" "net/http" "github.com/Teajey/sqlstore" diff --git a/server/routing/context.go b/http/routing/context.go similarity index 93% rename from server/routing/context.go rename to http/routing/context.go index 1d48290..b9ec3da 100644 --- a/server/routing/context.go +++ b/http/routing/context.go @@ -1,8 +1,8 @@ package routing import ( - "lishwist/api/db" - "lishwist/rsvp" + "lishwist/http/api/db" + "lishwist/http/rsvp" "net/http" ) diff --git a/server/routing/error.go b/http/routing/error.go similarity index 100% rename from server/routing/error.go rename to http/routing/error.go diff --git a/server/routing/foreign_wishlist.go b/http/routing/foreign_wishlist.go similarity index 97% rename from server/routing/foreign_wishlist.go rename to http/routing/foreign_wishlist.go index 0eb0123..6363661 100644 --- a/server/routing/foreign_wishlist.go +++ b/http/routing/foreign_wishlist.go @@ -1,8 +1,8 @@ package routing import ( - "lishwist/api/db" - "lishwist/rsvp" + "lishwist/http/api/db" + "lishwist/http/rsvp" "net/http" ) diff --git a/server/routing/groups.go b/http/routing/groups.go similarity index 98% rename from server/routing/groups.go rename to http/routing/groups.go index a6db5e5..89f6bb6 100644 --- a/server/routing/groups.go +++ b/http/routing/groups.go @@ -4,8 +4,8 @@ import ( "net/http" "slices" - "lishwist/api/db" - "lishwist/rsvp" + "lishwist/http/api/db" + "lishwist/http/rsvp" ) type GroupProps struct { diff --git a/server/routing/home.go b/http/routing/home.go similarity index 95% rename from server/routing/home.go rename to http/routing/home.go index 7f05b79..b8dbf54 100644 --- a/server/routing/home.go +++ b/http/routing/home.go @@ -3,9 +3,9 @@ package routing import ( "net/http" - "lishwist/api/db" - "lishwist/env" - "lishwist/rsvp" + "lishwist/http/api/db" + "lishwist/http/env" + "lishwist/http/rsvp" ) type HomeProps struct { diff --git a/server/routing/login.go b/http/routing/login.go similarity index 96% rename from server/routing/login.go rename to http/routing/login.go index dffb9b1..d5214bd 100644 --- a/server/routing/login.go +++ b/http/routing/login.go @@ -1,8 +1,8 @@ package routing import ( - "lishwist/api" - "lishwist/rsvp" + "lishwist/http/api" + "lishwist/http/rsvp" "net/http" ) diff --git a/server/routing/logout.go b/http/routing/logout.go similarity index 91% rename from server/routing/logout.go rename to http/routing/logout.go index 5b4b19f..dc1bb66 100644 --- a/server/routing/logout.go +++ b/http/routing/logout.go @@ -1,7 +1,7 @@ package routing import ( - "lishwist/rsvp" + "lishwist/http/rsvp" "net/http" ) diff --git a/server/routing/not_found.go b/http/routing/not_found.go similarity index 88% rename from server/routing/not_found.go rename to http/routing/not_found.go index 6b2ed65..43a0efd 100644 --- a/server/routing/not_found.go +++ b/http/routing/not_found.go @@ -3,7 +3,7 @@ package routing import ( "net/http" - "lishwist/rsvp" + "lishwist/http/rsvp" ) func NotFound(h http.Header, r *rsvp.Request) rsvp.Response { diff --git a/server/routing/register.go b/http/routing/register.go similarity index 95% rename from server/routing/register.go rename to http/routing/register.go index c9118b3..de68d0b 100644 --- a/server/routing/register.go +++ b/http/routing/register.go @@ -1,8 +1,8 @@ package routing import ( - "lishwist/api" - "lishwist/rsvp" + "lishwist/http/api" + "lishwist/http/rsvp" "net/http" ) diff --git a/server/routing/todo.go b/http/routing/todo.go similarity index 93% rename from server/routing/todo.go rename to http/routing/todo.go index d856a3e..8680659 100644 --- a/server/routing/todo.go +++ b/http/routing/todo.go @@ -1,8 +1,8 @@ package routing import ( - "lishwist/api/db" - "lishwist/rsvp" + "lishwist/http/api/db" + "lishwist/http/rsvp" "net/http" ) diff --git a/server/routing/users.go b/http/routing/users.go similarity index 97% rename from server/routing/users.go rename to http/routing/users.go index 826f14b..abee64f 100644 --- a/server/routing/users.go +++ b/http/routing/users.go @@ -1,8 +1,8 @@ package routing import ( - "lishwist/api/db" - "lishwist/rsvp" + "lishwist/http/api/db" + "lishwist/http/rsvp" "net/http" ) diff --git a/server/routing/wishlist.go b/http/routing/wishlist.go similarity index 97% rename from server/routing/wishlist.go rename to http/routing/wishlist.go index d8b2c4e..88b3bc2 100644 --- a/server/routing/wishlist.go +++ b/http/routing/wishlist.go @@ -1,8 +1,8 @@ package routing import ( - "lishwist/api/db" - "lishwist/rsvp" + "lishwist/http/api/db" + "lishwist/http/rsvp" "net/http" ) diff --git a/server/rsvp/handler.go b/http/rsvp/handler.go similarity index 100% rename from server/rsvp/handler.go rename to http/rsvp/handler.go diff --git a/server/rsvp/request.go b/http/rsvp/request.go similarity index 100% rename from server/rsvp/request.go rename to http/rsvp/request.go diff --git a/server/rsvp/response.go b/http/rsvp/response.go similarity index 98% rename from server/rsvp/response.go rename to http/rsvp/response.go index c72536f..1a36408 100644 --- a/server/rsvp/response.go +++ b/http/rsvp/response.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "lishwist/templates" + "lishwist/http/templates" "log" "net/http" "strings" diff --git a/server/rsvp/session.go b/http/rsvp/session.go similarity index 100% rename from server/rsvp/session.go rename to http/rsvp/session.go diff --git a/server/session/session.go b/http/session/session.go similarity index 100% rename from server/session/session.go rename to http/session/session.go diff --git a/server/templates/base.gotmpl b/http/templates/base.gotmpl similarity index 100% rename from server/templates/base.gotmpl rename to http/templates/base.gotmpl diff --git a/server/templates/error_page.gotmpl b/http/templates/error_page.gotmpl similarity index 100% rename from server/templates/error_page.gotmpl rename to http/templates/error_page.gotmpl diff --git a/server/templates/foreign_wishlist.gotmpl b/http/templates/foreign_wishlist.gotmpl similarity index 100% rename from server/templates/foreign_wishlist.gotmpl rename to http/templates/foreign_wishlist.gotmpl diff --git a/server/templates/group_page.gotmpl b/http/templates/group_page.gotmpl similarity index 100% rename from server/templates/group_page.gotmpl rename to http/templates/group_page.gotmpl diff --git a/server/templates/home.gotmpl b/http/templates/home.gotmpl similarity index 100% rename from server/templates/home.gotmpl rename to http/templates/home.gotmpl diff --git a/server/templates/login.gotmpl b/http/templates/login.gotmpl similarity index 100% rename from server/templates/login.gotmpl rename to http/templates/login.gotmpl diff --git a/server/templates/public_foreign_wishlist.gotmpl b/http/templates/public_foreign_wishlist.gotmpl similarity index 100% rename from server/templates/public_foreign_wishlist.gotmpl rename to http/templates/public_foreign_wishlist.gotmpl diff --git a/server/templates/public_group_page.gotmpl b/http/templates/public_group_page.gotmpl similarity index 100% rename from server/templates/public_group_page.gotmpl rename to http/templates/public_group_page.gotmpl diff --git a/server/templates/register.gotmpl b/http/templates/register.gotmpl similarity index 100% rename from server/templates/register.gotmpl rename to http/templates/register.gotmpl diff --git a/server/templates/templates.go b/http/templates/templates.go similarity index 100% rename from server/templates/templates.go rename to http/templates/templates.go From bffa68c9f725f835a5eba14f4137d81d057fc784 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 19 Jun 2025 00:52:08 +0900 Subject: [PATCH 02/11] feat: register and login --- .gitignore | 2 +- core/admin.go | 5 ++ core/go.mod | 3 + core/init.go | 9 +++ core/internal/db/db.go | 28 +++++++++ core/internal/db/gen_init_sql.go | 30 ++++++++++ core/internal/db/init.sql | 55 ++++++++++++++++++ core/internal/normalize/name.go | 14 +++++ core/login.go | 34 +++++++++++ core/login_test.go | 27 +++++++++ core/register.go | 33 +++++++++++ core/session.go | 64 +++++++++++++++++++++ core/user.go | 98 ++++++++++++++++++++++++++++++++ go.work | 2 + 14 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 core/admin.go create mode 100644 core/go.mod create mode 100644 core/init.go create mode 100644 core/internal/db/db.go create mode 100644 core/internal/db/gen_init_sql.go create mode 100644 core/internal/db/init.sql create mode 100644 core/internal/normalize/name.go create mode 100644 core/login.go create mode 100644 core/login_test.go create mode 100644 core/register.go create mode 100644 core/session.go create mode 100644 core/user.go diff --git a/.gitignore b/.gitignore index 2c382ac..7e3ee04 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ gin-bin *lishwist.db .env*.local -http/api/db/init_sql.go +init_sql.go .ignored/ diff --git a/core/admin.go b/core/admin.go new file mode 100644 index 0000000..133cde3 --- /dev/null +++ b/core/admin.go @@ -0,0 +1,5 @@ +package lishwist + +type Admin struct { + user *User +} diff --git a/core/go.mod b/core/go.mod new file mode 100644 index 0000000..6b195fd --- /dev/null +++ b/core/go.mod @@ -0,0 +1,3 @@ +module lishwist/core + +go 1.23 diff --git a/core/init.go b/core/init.go new file mode 100644 index 0000000..b1d7786 --- /dev/null +++ b/core/init.go @@ -0,0 +1,9 @@ +package lishwist + +import ( + "lishwist/core/internal/db" +) + +func Init(dataSourceName string) error { + return db.Init(dataSourceName) +} diff --git a/core/internal/db/db.go b/core/internal/db/db.go new file mode 100644 index 0000000..79517d3 --- /dev/null +++ b/core/internal/db/db.go @@ -0,0 +1,28 @@ +//go:generate go run gen_init_sql.go + +package db + +import ( + "database/sql" + "fmt" + + _ "github.com/glebarez/go-sqlite" +) + +var Connection *sql.DB + +func Init(dataSourceName string) error { + db, err := sql.Open("sqlite", 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 +} diff --git a/core/internal/db/gen_init_sql.go b/core/internal/db/gen_init_sql.go new file mode 100644 index 0000000..0e3da1b --- /dev/null +++ b/core/internal/db/gen_init_sql.go @@ -0,0 +1,30 @@ +//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)) +} diff --git a/core/internal/db/init.sql b/core/internal/db/init.sql new file mode 100644 index 0000000..8200404 --- /dev/null +++ b/core/internal/db/init.sql @@ -0,0 +1,55 @@ +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, + "user_id" INTEGER NOT NULL, + PRIMARY KEY("id" AUTOINCREMENT), + FOREIGN KEY("user_id") REFERENCES "user"("id") +); + +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; diff --git a/core/internal/normalize/name.go b/core/internal/normalize/name.go new file mode 100644 index 0000000..ccd8574 --- /dev/null +++ b/core/internal/normalize/name.go @@ -0,0 +1,14 @@ +package normalize + +import ( + "strings" +) + +func Trim(s string) string { + return strings.Trim(s, " \t") +} + +func Name(name string) string { + name = Trim(name) + return strings.ToLower(name) +} diff --git a/core/login.go b/core/login.go new file mode 100644 index 0000000..5cfc45f --- /dev/null +++ b/core/login.go @@ -0,0 +1,34 @@ +package lishwist + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +func (sm *SessionManager) Login(username, password string) (*Session, error) { + user, err := getUserByName(username) + if err != nil { + return nil, fmt.Errorf("Failed to fetch user: %w", err) + } + if user == nil { + return nil, 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, err + } + + session, err := sm.createSession(user) + if err != nil { + return nil, fmt.Errorf("Couldn't create session: %w", err) + } + + return session, nil +} diff --git a/core/login_test.go b/core/login_test.go new file mode 100644 index 0000000..b5a107c --- /dev/null +++ b/core/login_test.go @@ -0,0 +1,27 @@ +package lishwist_test + +import ( + "testing" + "time" + + lishwist "lishwist/core" +) + +func TestLogin(t *testing.T) { + err := lishwist.Init(":memory:") + if err != nil { + t.Fatalf("Failed to init db: %s\n", err) + } + + lw := lishwist.NewSessionManager(time.Second*10, 32) + + err = lishwist.Register("thomas", "123") + if err != nil { + t.Fatalf("Failed to register: %s\n", err) + } + + _, err = lw.Login("thomas", "123") + if err != nil { + t.Fatalf("Failed to login: %s\n", err) + } +} diff --git a/core/register.go b/core/register.go new file mode 100644 index 0000000..ff7fa6b --- /dev/null +++ b/core/register.go @@ -0,0 +1,33 @@ +package lishwist + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +func Register(username, newPassword string) error { + if username == "" { + return fmt.Errorf("Username required") + } + if newPassword == "" { + return fmt.Errorf("newPassword required") + } + + existingUser, _ := getUserByName(username) + if existingUser != nil { + return fmt.Errorf("Username is taken") + } + + hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost) + if err != nil { + return fmt.Errorf("Failed to hash password: %w", err) + } + + _, err = createUser(username, hashedPasswordBytes) + if err != nil { + return fmt.Errorf("Failed to create user: %w\n", err) + } + + return nil +} diff --git a/core/session.go b/core/session.go new file mode 100644 index 0000000..181853c --- /dev/null +++ b/core/session.go @@ -0,0 +1,64 @@ +package lishwist + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "lishwist/core/internal/db" + "time" +) + +type Session struct { + Id string + Token string + User *User + ExpiresAt time.Time + CreatedAt time.Time +} + +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) + if err != nil { + return nil, 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 +} diff --git a/core/user.go b/core/user.go new file mode 100644 index 0000000..e776f42 --- /dev/null +++ b/core/user.go @@ -0,0 +1,98 @@ +package lishwist + +import ( + "fmt" + + "github.com/google/uuid" + + "lishwist/core/internal/db" + "lishwist/core/internal/normalize" +) + +type User struct { + Id string + 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 (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) { + 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 := db.Connection.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 := db.Connection.QueryRow(stmt, u.Id).Scan(&passHash) + if err != nil { + return nil, err + } + return []byte(passHash), nil +} diff --git a/go.work b/go.work index 72e8c6b..4e0d860 100644 --- a/go.work +++ b/go.work @@ -2,4 +2,6 @@ go 1.23 toolchain go1.23.3 +use ./core + use ./http From 5c13893f23feb647948b2969efd378c5bf196218 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 19 Jun 2025 19:52:24 +0900 Subject: [PATCH 03/11] feat: wish making --- core/internal/db/init.sql | 4 +- core/internal/fixtures/assert.go | 9 +++ core/internal/fixtures/login.go | 29 ++++++++ core/internal/normalize/name.go | 6 +- core/user.go | 5 ++ core/wish.go | 109 +++++++++++++++++++++++++++++++ core/wish_test.go | 28 ++++++++ 7 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 core/internal/fixtures/assert.go create mode 100644 core/internal/fixtures/login.go create mode 100644 core/wish.go create mode 100644 core/wish_test.go diff --git a/core/internal/db/init.sql b/core/internal/db/init.sql index 8200404..8da1d9b 100644 --- a/core/internal/db/init.sql +++ b/core/internal/db/init.sql @@ -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, @@ -50,6 +50,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; diff --git a/core/internal/fixtures/assert.go b/core/internal/fixtures/assert.go new file mode 100644 index 0000000..3d654d8 --- /dev/null +++ b/core/internal/fixtures/assert.go @@ -0,0 +1,9 @@ +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) + } +} diff --git a/core/internal/fixtures/login.go b/core/internal/fixtures/login.go new file mode 100644 index 0000000..99583de --- /dev/null +++ b/core/internal/fixtures/login.go @@ -0,0 +1,29 @@ +package fixtures + +import ( + "log" + "time" + + lishwist "lishwist/core" +) + +func Login(username, password string) *lishwist.Session { + err := lishwist.Init(":memory:") + if err != nil { + log.Fatalf("Failed to init db: %s\n", err) + } + + lw := lishwist.NewSessionManager(time.Second*10, 32) + + err = lishwist.Register("thomas", "123") + if err != nil { + log.Fatalf("Failed to register: %s\n", err) + } + + session, err := lw.Login("thomas", "123") + if err != nil { + log.Fatalf("Failed to login: %s\n", err) + } + + return session +} diff --git a/core/internal/normalize/name.go b/core/internal/normalize/name.go index ccd8574..54a1c59 100644 --- a/core/internal/normalize/name.go +++ b/core/internal/normalize/name.go @@ -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) } diff --git a/core/user.go b/core/user.go index e776f42..6b8b5f6 100644 --- a/core/user.go +++ b/core/user.go @@ -96,3 +96,8 @@ 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) +} diff --git a/core/wish.go b/core/wish.go new file mode 100644 index 0000000..43befd1 --- /dev/null +++ b/core/wish.go @@ -0,0 +1,109 @@ +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 (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 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 := 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 +} diff --git a/core/wish_test.go b/core/wish_test.go new file mode 100644 index 0000000..b867b7f --- /dev/null +++ b/core/wish_test.go @@ -0,0 +1,28 @@ +package lishwist_test + +import ( + "testing" + + "lishwist/core/internal/fixtures" +) + +func TestMakeWish(t *testing.T) { + s := fixtures.Login("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") +} From 5769d445769f8eac1ca64665516ca6889f74731a Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:32:52 +0900 Subject: [PATCH 04/11] feat: first user is an admin --- core/admin.go | 8 ++++++++ core/internal/fixtures/assert.go | 12 +++++++++++ core/internal/fixtures/login.go | 8 ++++---- core/register.go | 14 +++++++++---- core/user.go | 35 +++++++++++++++++++------------- core/user_test.go | 22 ++++++++++++++++++++ 6 files changed, 77 insertions(+), 22 deletions(-) create mode 100644 core/user_test.go diff --git a/core/admin.go b/core/admin.go index 133cde3..51ab1a1 100644 --- a/core/admin.go +++ b/core/admin.go @@ -3,3 +3,11 @@ package lishwist type Admin struct { user *User } + +func (s *Session) Admin() *Admin { + if s.User.IsAdmin { + return &Admin{s.User} + } else { + return nil + } +} diff --git a/core/internal/fixtures/assert.go b/core/internal/fixtures/assert.go index 3d654d8..35d288f 100644 --- a/core/internal/fixtures/assert.go +++ b/core/internal/fixtures/assert.go @@ -7,3 +7,15 @@ func AssertEq[C comparable](t *testing.T, context string, expected, actual C) { t.Errorf("%s: %#v != %#v", context, expected, actual) } } + +func Assert(t *testing.T, context string, condition bool) { + if !condition { + t.Errorf("%s", context) + } +} + +func FailIfErr(t *testing.T, err error, context string) { + if err != nil { + t.Fatalf("%s: %s\n", context, err) + } +} diff --git a/core/internal/fixtures/login.go b/core/internal/fixtures/login.go index 99583de..abff84c 100644 --- a/core/internal/fixtures/login.go +++ b/core/internal/fixtures/login.go @@ -15,14 +15,14 @@ func Login(username, password string) *lishwist.Session { lw := lishwist.NewSessionManager(time.Second*10, 32) - err = lishwist.Register("thomas", "123") + err = lishwist.Register(username, password) if err != nil { - log.Fatalf("Failed to register: %s\n", err) + log.Fatalf("Failed to register on login fixture: %s\n", err) } - session, err := lw.Login("thomas", "123") + session, err := lw.Login(username, password) if err != nil { - log.Fatalf("Failed to login: %s\n", err) + log.Fatalf("Failed to login on fixture: %s\n", err) } return session diff --git a/core/register.go b/core/register.go index ff7fa6b..5ec8432 100644 --- a/core/register.go +++ b/core/register.go @@ -1,6 +1,7 @@ package lishwist import ( + "errors" "fmt" "golang.org/x/crypto/bcrypt" @@ -8,15 +9,15 @@ import ( func Register(username, newPassword string) error { if username == "" { - return fmt.Errorf("Username required") + return errors.New("Username required") } if newPassword == "" { - return fmt.Errorf("newPassword required") + return errors.New("newPassword required") } existingUser, _ := getUserByName(username) if existingUser != nil { - return fmt.Errorf("Username is taken") + return errors.New("Username is taken") } hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost) @@ -24,7 +25,12 @@ func Register(username, newPassword string) error { return fmt.Errorf("Failed to hash password: %w", err) } - _, err = createUser(username, hashedPasswordBytes) + usersExist, err := hasUsers() + if err != nil { + return fmt.Errorf("Failed to count users: %w", err) + } + + _, err = createUser(username, hashedPasswordBytes, !usersExist) if err != nil { return fmt.Errorf("Failed to create user: %w\n", err) } diff --git a/core/user.go b/core/user.go index 6b8b5f6..33edcd5 100644 --- a/core/user.go +++ b/core/user.go @@ -51,34 +51,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") } - 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") } id, err := result.LastInsertId() if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to get last insert id: %w") } user := User{ Id: fmt.Sprintf("%d", id), @@ -101,3 +93,18 @@ 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 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) +} diff --git a/core/user_test.go b/core/user_test.go new file mode 100644 index 0000000..6d53d5b --- /dev/null +++ b/core/user_test.go @@ -0,0 +1,22 @@ +package lishwist_test + +import ( + "testing" + + lishwist "lishwist/core" + "lishwist/core/internal/fixtures" +) + +func TestFirstUserIsAdmin(t *testing.T) { + s := fixtures.Login("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) +} From bba5136ccae4ac62ff1ddad3b8dbff72fd232fc7 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:36:11 +0900 Subject: [PATCH 05/11] fix: warnings --- core/session.go | 2 +- core/user.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/session.go b/core/session.go index 181853c..08f7702 100644 --- a/core/session.go +++ b/core/session.go @@ -53,7 +53,7 @@ func (sm *SessionManager) createSession(user *User) (*Session, error) { } session := Session{ - Id: fmt.Sprintf("%s", id), + Id: fmt.Sprintf("%d", id), Token: token, User: user, ExpiresAt: time.Now().Add(sm.sessionDuration), diff --git a/core/user.go b/core/user.go index 33edcd5..7fa7fac 100644 --- a/core/user.go +++ b/core/user.go @@ -62,15 +62,15 @@ func createUser(name string, passHash []byte, isAdmin bool) (*User, error) { 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") + 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") + 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") + return nil, fmt.Errorf("Failed to get last insert id: %w", err) } user := User{ Id: fmt.Sprintf("%d", id), From 439d4a184457c85ed944991242fbd42305aede57 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Sun, 22 Jun 2025 21:15:47 +0900 Subject: [PATCH 06/11] feat: groups --- core/debug.go | 55 +++++++++++++ core/go.mod | 17 +++- core/go.sum | 14 ++++ core/group.go | 130 +++++++++++++++++++++++++++++++ core/group_test.go | 42 ++++++++++ core/internal/db/db.go | 5 +- core/internal/fixtures/assert.go | 6 ++ core/internal/fixtures/login.go | 15 +++- core/login_test.go | 5 +- core/register.go | 18 ++--- core/session.go | 4 +- core/user_test.go | 4 +- core/wish_test.go | 2 +- go.work | 7 -- go.work.sum | 18 ----- 15 files changed, 295 insertions(+), 47 deletions(-) create mode 100644 core/debug.go create mode 100644 core/go.sum create mode 100644 core/group.go create mode 100644 core/group_test.go delete mode 100644 go.work delete mode 100644 go.work.sum diff --git a/core/debug.go b/core/debug.go new file mode 100644 index 0000000..c0139e4 --- /dev/null +++ b/core/debug.go @@ -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 + } +} diff --git a/core/go.mod b/core/go.mod index 6b195fd..d1b9821 100644 --- a/core/go.mod +++ b/core/go.mod @@ -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 +) diff --git a/core/go.sum b/core/go.sum new file mode 100644 index 0000000..f2fefe5 --- /dev/null +++ b/core/go.sum @@ -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= diff --git a/core/group.go b/core/group.go new file mode 100644 index 0000000..86b1f80 --- /dev/null +++ b/core/group.go @@ -0,0 +1,130 @@ +package lishwist + +import ( + "fmt" + "lishwist/core/internal/db" + "log" + "strconv" + "strings" +) + +type Group struct { + Id string + Name string + Reference string + Members []User +} + +func (g *Group) MemberIndex(userId string) int { + for i, u := range g.Members { + if u.Id == userId { + return i + } + } + return -1 +} + +func queryManyGroups(query string, args ...any) ([]Group, error) { + groups := []Group{} + // PrintTables() + // PrintViews() + log.Println(query, args) + rows, err := db.Connection.Query(query, args...) + // PrintTables() + // PrintViews() + if err != nil { + return nil, fmt.Errorf("Query failed: %w", err) + } + defer rows.Close() + for rows.Next() { + var group Group + err := rows.Scan(&group.Id, &group.Name, &group.Reference) + if err != nil { + return nil, fmt.Errorf("Failed to scan row: %w", err) + } + members, err := queryManyGroupMembers(group.Id) + if err != nil { + return nil, fmt.Errorf("Failed to query for group members: %w", err) + } + group.Members = members + groups = append(groups, group) + } + err = rows.Err() + if err != nil { + return nil, fmt.Errorf("Rows error: %w", err) + } + return groups, nil +} + +func queryOneGroup(query string, args ...any) (*Group, error) { + groups, err := queryManyGroups(query, args...) + if err != nil { + return nil, err + } + if len(groups) < 1 { + return nil, nil + } + return &groups[0], nil +} + +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, err + } + return members, nil +} + +func (a *Admin) 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 (a *Admin) ListGroups() ([]Group, error) { + query := "SELECT id, name, reference FROM [group];" + return queryManyGroups(query) +} + +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 := db.Connection.Exec(stmt, name, reference) + if err != nil { + return nil, err + } + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + group := Group{ + Id: strconv.FormatInt(id, 10), + Name: name, + Reference: reference, + } + return &group, nil +} + +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 (a *Admin) RemoveUserFromGroup(userId, groupId string) error { + stmt := "DELETE FROM group_member WHERE group_id = ? AND user_id = ?" + _, err := db.Connection.Exec(stmt, userId, groupId) + if err != nil { + return err + } + return nil +} diff --git a/core/group_test.go b/core/group_test.go new file mode 100644 index 0000000..de96b63 --- /dev/null +++ b/core/group_test.go @@ -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) +} diff --git a/core/internal/db/db.go b/core/internal/db/db.go index 79517d3..938dc12 100644 --- a/core/internal/db/db.go +++ b/core/internal/db/db.go @@ -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) } diff --git a/core/internal/fixtures/assert.go b/core/internal/fixtures/assert.go index 35d288f..39ea2a1 100644 --- a/core/internal/fixtures/assert.go +++ b/core/internal/fixtures/assert.go @@ -14,6 +14,12 @@ func Assert(t *testing.T, context string, condition bool) { } } +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) diff --git a/core/internal/fixtures/login.go b/core/internal/fixtures/login.go index abff84c..d2e015e 100644 --- a/core/internal/fixtures/login.go +++ b/core/internal/fixtures/login.go @@ -2,20 +2,29 @@ package fixtures import ( "log" + "testing" "time" lishwist "lishwist/core" + + "github.com/ncruces/go-sqlite3/vfs/memdb" ) -func Login(username, password string) *lishwist.Session { - err := lishwist.Init(":memory:") +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) } lw := lishwist.NewSessionManager(time.Second*10, 32) - err = lishwist.Register(username, password) + _, err = lishwist.Register(username, password) if err != nil { log.Fatalf("Failed to register on login fixture: %s\n", err) } diff --git a/core/login_test.go b/core/login_test.go index b5a107c..eca5ac0 100644 --- a/core/login_test.go +++ b/core/login_test.go @@ -5,17 +5,18 @@ import ( "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) } diff --git a/core/register.go b/core/register.go index 5ec8432..bc33707 100644 --- a/core/register.go +++ b/core/register.go @@ -7,33 +7,33 @@ import ( "golang.org/x/crypto/bcrypt" ) -func Register(username, newPassword string) error { +func Register(username, newPassword string) (*User, error) { if username == "" { - return errors.New("Username required") + return nil, errors.New("Username required") } if newPassword == "" { - return errors.New("newPassword required") + return nil, errors.New("newPassword required") } existingUser, _ := getUserByName(username) if existingUser != nil { - return errors.New("Username is taken") + return nil, errors.New("Username is taken") } 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) } usersExist, err := hasUsers() if err != nil { - return fmt.Errorf("Failed to count users: %w", err) + return nil, fmt.Errorf("Failed to count users: %w", err) } - _, err = createUser(username, hashedPasswordBytes, !usersExist) + user, err := createUser(username, hashedPasswordBytes, !usersExist) if err != nil { - return fmt.Errorf("Failed to create user: %w\n", err) + return nil, fmt.Errorf("Failed to create user: %w\n", err) } - return nil + return user, nil } diff --git a/core/session.go b/core/session.go index 08f7702..5a02181 100644 --- a/core/session.go +++ b/core/session.go @@ -40,11 +40,11 @@ func (sm *SessionManager) createSession(user *User) (*Session, error) { stmt := "INSERT INTO session (user_id) VALUES (?);" result, err := db.Connection.Exec(stmt, user.Id) 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) } token, err := generateSecureToken(sm.sessionTokenLength) diff --git a/core/user_test.go b/core/user_test.go index 6d53d5b..978ec7c 100644 --- a/core/user_test.go +++ b/core/user_test.go @@ -8,9 +8,9 @@ import ( ) func TestFirstUserIsAdmin(t *testing.T) { - s := fixtures.Login("thomas", "123") + s := fixtures.Login(t, "thomas", "123") - err := lishwist.Register("caleb", "123") + _, err := lishwist.Register("caleb", "123") fixtures.FailIfErr(t, err, "Failed to register caleb") users, err := s.Admin().ListUsers() diff --git a/core/wish_test.go b/core/wish_test.go index b867b7f..b7a950c 100644 --- a/core/wish_test.go +++ b/core/wish_test.go @@ -7,7 +7,7 @@ import ( ) func TestMakeWish(t *testing.T) { - s := fixtures.Login("thomas", "123") + s := fixtures.Login(t, "thomas", "123") if err := s.MakeWish("apple"); err != nil { t.Fatalf("Failed to make wish 1: %s\n", err) diff --git a/go.work b/go.work deleted file mode 100644 index 4e0d860..0000000 --- a/go.work +++ /dev/null @@ -1,7 +0,0 @@ -go 1.23 - -toolchain go1.23.3 - -use ./core - -use ./http diff --git a/go.work.sum b/go.work.sum deleted file mode 100644 index 2c1030c..0000000 --- a/go.work.sum +++ /dev/null @@ -1,18 +0,0 @@ -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= -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/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -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= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= From e44f299d5d851d099d52cf1c4550e3c1948532ba Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Sun, 22 Jun 2025 22:18:02 +0900 Subject: [PATCH 07/11] feat: admin inherits session --- core/admin.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin.go b/core/admin.go index 51ab1a1..8f87b2a 100644 --- a/core/admin.go +++ b/core/admin.go @@ -1,12 +1,12 @@ package lishwist type Admin struct { - user *User + session *Session } func (s *Session) Admin() *Admin { if s.User.IsAdmin { - return &Admin{s.User} + return &Admin{s} } else { return nil } From b2e9bab55db847888d461d8377150452e49eafb1 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Sun, 22 Jun 2025 22:18:21 +0900 Subject: [PATCH 08/11] feat: session store --- core/session/store.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 core/session/store.go diff --git a/core/session/store.go b/core/session/store.go new file mode 100644 index 0000000..3045fc3 --- /dev/null +++ b/core/session/store.go @@ -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 +} From cc0409d1dcd23d29ca7f987b49964b5f722c59ad Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 26 Jun 2025 21:03:11 +0900 Subject: [PATCH 09/11] feat: use core library --- core/group.go | 22 +- core/internal/db/init.sql | 5 +- core/internal/fixtures/login.go | 5 +- core/login.go | 17 +- core/login_test.go | 5 +- core/register.go | 4 +- core/sesh | 20 ++ core/session.go | 61 +---- core/user.go | 57 ++++- core/wish.go | 188 +++++++++++++- go.work | 6 + go.work.sum | 22 ++ http/api/db/db.go | 62 ----- http/api/db/gen_init_sql.go | 30 --- http/api/db/group.go | 117 --------- http/api/db/init.sql | 54 ---- http/api/db/migration/1.sql | 22 -- http/api/db/user.go | 419 ------------------------------- http/api/login.go | 37 +-- http/api/register.go | 63 +++-- http/main.go | 40 ++- http/routing/context.go | 10 +- http/routing/foreign_wishlist.go | 20 +- http/routing/groups.go | 52 ++-- http/routing/home.go | 26 +- http/routing/login.go | 26 +- http/routing/register.go | 25 +- http/routing/todo.go | 8 +- http/routing/users.go | 27 +- http/routing/wishlist.go | 24 +- 30 files changed, 510 insertions(+), 964 deletions(-) create mode 100644 core/sesh create mode 100644 go.work create mode 100644 go.work.sum delete mode 100644 http/api/db/db.go delete mode 100644 http/api/db/gen_init_sql.go delete mode 100644 http/api/db/group.go delete mode 100644 http/api/db/init.sql delete mode 100644 http/api/db/migration/1.sql delete mode 100644 http/api/db/user.go diff --git a/core/group.go b/core/group.go index 86b1f80..230c475 100644 --- a/core/group.go +++ b/core/group.go @@ -3,7 +3,6 @@ package lishwist import ( "fmt" "lishwist/core/internal/db" - "log" "strconv" "strings" ) @@ -26,12 +25,7 @@ func (g *Group) MemberIndex(userId string) int { func queryManyGroups(query string, args ...any) ([]Group, error) { groups := []Group{} - // PrintTables() - // PrintViews() - log.Println(query, args) rows, err := db.Connection.Query(query, args...) - // PrintTables() - // PrintViews() if err != nil { return nil, fmt.Errorf("Query failed: %w", err) } @@ -76,16 +70,16 @@ func queryManyGroupMembers(groupId string) ([]User, error) { return members, nil } -func (a *Admin) 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 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) @@ -128,3 +122,9 @@ func (a *Admin) RemoveUserFromGroup(userId, groupId string) error { } 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) +} diff --git a/core/internal/db/init.sql b/core/internal/db/init.sql index 8da1d9b..e6d94b6 100644 --- a/core/internal/db/init.sql +++ b/core/internal/db/init.sql @@ -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"; diff --git a/core/internal/fixtures/login.go b/core/internal/fixtures/login.go index d2e015e..c730027 100644 --- a/core/internal/fixtures/login.go +++ b/core/internal/fixtures/login.go @@ -3,7 +3,6 @@ package fixtures import ( "log" "testing" - "time" lishwist "lishwist/core" @@ -22,14 +21,12 @@ func Login(t *testing.T, username, password string) *lishwist.Session { log.Fatalf("Failed to init db: %s\n", err) } - lw := lishwist.NewSessionManager(time.Second*10, 32) - _, err = lishwist.Register(username, password) if err != nil { log.Fatalf("Failed to register on login fixture: %s\n", err) } - session, err := lw.Login(username, password) + session, err := lishwist.Login(username, password) if err != nil { log.Fatalf("Failed to login on fixture: %s\n", err) } diff --git a/core/login.go b/core/login.go index 5cfc45f..47659a8 100644 --- a/core/login.go +++ b/core/login.go @@ -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 } diff --git a/core/login_test.go b/core/login_test.go index eca5ac0..9603a09 100644 --- a/core/login_test.go +++ b/core/login_test.go @@ -2,7 +2,6 @@ package lishwist_test import ( "testing" - "time" lishwist "lishwist/core" "lishwist/core/internal/fixtures" @@ -14,14 +13,12 @@ func TestLogin(t *testing.T) { t.Fatalf("Failed to init db: %s\n", err) } - lw := lishwist.NewSessionManager(time.Second*10, 32) - _, 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) } diff --git a/core/register.go b/core/register.go index bc33707..d8148fc 100644 --- a/core/register.go +++ b/core/register.go @@ -7,6 +7,8 @@ import ( "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") @@ -17,7 +19,7 @@ func Register(username, newPassword string) (*User, error) { existingUser, _ := getUserByName(username) if existingUser != nil { - return nil, errors.New("Username is taken") + return nil, ErrorUsernameTaken } hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost) diff --git a/core/sesh b/core/sesh new file mode 100644 index 0000000..392f621 --- /dev/null +++ b/core/sesh @@ -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": [] +} diff --git a/core/session.go b/core/session.go index 5a02181..1a7445b 100644 --- a/core/session.go +++ b/core/session.go @@ -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, fmt.Errorf("Failed to execute query: %w", err) + return nil, fmt.Errorf("Failed to get user: %w", err) } - id, err := result.LastInsertId() - if err != nil { - return nil, fmt.Errorf("Failed to get last insert id: %w", 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("%d", id), - Token: token, - User: user, - ExpiresAt: time.Now().Add(sm.sessionDuration), - CreatedAt: time.Now(), - } - - return &session, nil + return &Session{*user}, nil } diff --git a/core/user.go b/core/user.go index 7fa7fac..b0207cb 100644 --- a/core/user.go +++ b/core/user.go @@ -10,7 +10,8 @@ import ( ) type User struct { - Id string + Id string + // TODO: rename to DisplayName NormalName string Name string Reference string @@ -94,6 +95,11 @@ func getUserByReference(reference string) (*User, error) { 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 @@ -108,3 +114,52 @@ 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 +} diff --git a/core/wish.go b/core/wish.go index 43befd1..3b7dffb 100644 --- a/core/wish.go +++ b/core/wish.go @@ -61,6 +61,47 @@ func (s *Session) MakeWish(name string) error { 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 { @@ -69,7 +110,7 @@ func (s *Session) GetOthersWishes(userReference string) ([]Wish, error) { if otherUser.Id == s.User.Id { return nil, errors.New("Use (s *Session) GetWishes() to view your own wishes") } - 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 = ?" + 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) @@ -107,3 +148,148 @@ func (s *Session) GetOthersWishes(userReference string) ([]Wish, error) { } 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 +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..7328ab8 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.23.3 + +use ( + ./core + ./http +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..dbc3473 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,22 @@ +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= +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/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= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE= diff --git a/http/api/db/db.go b/http/api/db/db.go deleted file mode 100644 index f4fb502..0000000 --- a/http/api/db/db.go +++ /dev/null @@ -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 -} diff --git a/http/api/db/gen_init_sql.go b/http/api/db/gen_init_sql.go deleted file mode 100644 index 0e3da1b..0000000 --- a/http/api/db/gen_init_sql.go +++ /dev/null @@ -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)) -} diff --git a/http/api/db/group.go b/http/api/db/group.go deleted file mode 100644 index eee6785..0000000 --- a/http/api/db/group.go +++ /dev/null @@ -1,117 +0,0 @@ -package db - -import ( - "fmt" - "lishwist/http/normalize" - "strconv" -) - -type Group struct { - Id string - Name string - Reference string - Members []User -} - -func (g *Group) MemberIndex(userId string) int { - for i, u := range g.Members { - if u.Id == userId { - return i - } - } - return -1 -} - -func queryManyGroups(query string, args ...any) ([]Group, error) { - groups := []Group{} - rows, err := database.Query(query, args...) - if err != nil { - return nil, fmt.Errorf("Query failed: %w", err) - } - defer rows.Close() - for rows.Next() { - var group Group - err := rows.Scan(&group.Id, &group.Name, &group.Reference) - if err != nil { - return nil, fmt.Errorf("Failed to scan row: %w", err) - } - members, err := queryManyGroupMembers(group.Id) - if err != nil { - return nil, fmt.Errorf("Failed to query for group members: %w", err) - } - group.Members = members - groups = append(groups, group) - } - err = rows.Err() - if err != nil { - return nil, fmt.Errorf("Rows error: %w", err) - } - return groups, nil -} - -func queryOneGroup(query string, args ...any) (*Group, error) { - groups, err := queryManyGroups(query, args...) - if err != nil { - return nil, err - } - if len(groups) < 1 { - return nil, nil - } - return &groups[0], nil -} - -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, 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 GetAllGroups() ([]Group, error) { - query := "SELECT id, name, reference FROM [group];" - return queryManyGroups(query) -} - -func CreateGroup(name string, reference string) (*Group, error) { - name = normalize.Trim(name) - stmt := "INSERT INTO [group] (name, reference) VALUES (?, ?)" - result, err := database.Exec(stmt, name, reference) - if err != nil { - return nil, err - } - id, err := result.LastInsertId() - if err != nil { - return nil, err - } - group := Group{ - Id: strconv.FormatInt(id, 10), - Name: name, - Reference: reference, - } - 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) - if err != nil { - return err - } - return nil -} - -func (g *Group) RemoveUser(userId string) error { - stmt := "DELETE FROM group_member WHERE group_id = ? AND user_id = ?" - _, err := database.Exec(stmt, g.Id, userId) - if err != nil { - return err - } - return nil -} diff --git a/http/api/db/init.sql b/http/api/db/init.sql deleted file mode 100644 index 2780036..0000000 --- a/http/api/db/init.sql +++ /dev/null @@ -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; diff --git a/http/api/db/migration/1.sql b/http/api/db/migration/1.sql deleted file mode 100644 index c286b74..0000000 --- a/http/api/db/migration/1.sql +++ /dev/null @@ -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; diff --git a/http/api/db/user.go b/http/api/db/user.go deleted file mode 100644 index b1beb15..0000000 --- a/http/api/db/user.go +++ /dev/null @@ -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) -} diff --git a/http/api/login.go b/http/api/login.go index 64c12f5..9a8b24c 100644 --- a/http/api/login.go +++ b/http/api/login.go @@ -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 } diff --git a/http/api/register.go b/http/api/register.go index 1a4eca2..c8e4301 100644 --- a/http/api/register.go +++ b/http/api/register.go @@ -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 +// } diff --git a/http/main.go b/http/main.go index 640577b..ad53076 100644 --- a/http/main.go +++ b/http/main.go @@ -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) diff --git a/http/routing/context.go b/http/routing/context.go index b9ec3da..a45b9f1 100644 --- a/http/routing/context.go +++ b/http/routing/context.go @@ -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) } } diff --git a/http/routing/foreign_wishlist.go b/http/routing/foreign_wishlist.go index 6363661..9a91c88 100644 --- a/http/routing/foreign_wishlist.go +++ b/http/routing/foreign_wishlist.go @@ -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) } diff --git a/http/routing/groups.go b/http/routing/groups.go index 89f6bb6..eefdac3 100644 --- a/http/routing/groups.go +++ b/http/routing/groups.go @@ -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) } diff --git a/http/routing/home.go b/http/routing/home.go index b8dbf54..4e4229c 100644 --- a/http/routing/home.go +++ b/http/routing/home.go @@ -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) } } diff --git a/http/routing/login.go b/http/routing/login.go index d5214bd..b738b39 100644 --- a/http/routing/login.go +++ b/http/routing/login.go @@ -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) } diff --git a/http/routing/register.go b/http/routing/register.go index de68d0b..b7a8d4c 100644 --- a/http/routing/register.go +++ b/http/routing/register.go @@ -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) diff --git a/http/routing/todo.go b/http/routing/todo.go index 8680659..4f6764a 100644 --- a/http/routing/todo.go +++ b/http/routing/todo.go @@ -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) } diff --git a/http/routing/users.go b/http/routing/users.go index abee64f..becb2e3 100644 --- a/http/routing/users.go +++ b/http/routing/users.go @@ -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) } diff --git a/http/routing/wishlist.go b/http/routing/wishlist.go index 88b3bc2..dec0cf3 100644 --- a/http/routing/wishlist.go +++ b/http/routing/wishlist.go @@ -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) } From b2f8ef19be6ff91085c056fcd44b9103513f30b5 Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 26 Jun 2025 21:48:16 +0900 Subject: [PATCH 10/11] feat: log before anything else --- http/rsvp/response.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/http/rsvp/response.go b/http/rsvp/response.go index 1a36408..2c380b5 100644 --- a/http/rsvp/response.go +++ b/http/rsvp/response.go @@ -20,6 +20,10 @@ type Response struct { } 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 { err := res.Session.inner.Save(r, w) if err != nil { @@ -44,10 +48,6 @@ func (res *Response) Write(w http.ResponseWriter, r *http.Request) error { bodyBytes := bytes.NewBuffer([]byte{}) accept := r.Header.Get("Accept") - if res.LogMessage != "" { - log.Printf("%s --- %s\n", res.Data, res.LogMessage) - } - if res.Status != 0 { w.WriteHeader(res.Status) } From 8cdbfe04391db09efb917796a98e81a93952977b Mon Sep 17 00:00:00 2001 From: Teajey <21069848+Teajey@users.noreply.github.com> Date: Thu, 26 Jun 2025 22:09:00 +0900 Subject: [PATCH 11/11] feat: migrations --- core/internal/db/migration/1.sql | 22 ++++++++++++++++++++++ core/internal/db/migration/2.sql | 6 ++++++ 2 files changed, 28 insertions(+) create mode 100644 core/internal/db/migration/1.sql create mode 100644 core/internal/db/migration/2.sql diff --git a/core/internal/db/migration/1.sql b/core/internal/db/migration/1.sql new file mode 100644 index 0000000..c286b74 --- /dev/null +++ b/core/internal/db/migration/1.sql @@ -0,0 +1,22 @@ +BEGIN TRANSACTION; + +ALTER TABLE user ADD COLUMN "is_live" INTEGER NOT NULL DEFAULT 1; + +ALTER TABLE user RENAME TO old_user; + +CREATE TABLE "user" ( + "id" INTEGER NOT NULL UNIQUE, + "name" TEXT NOT NULL UNIQUE, + "reference" TEXT NOT NULL UNIQUE, + "motto" TEXT NOT NULL DEFAULT "", + "password_hash" TEXT NOT NULL, + "is_admin" INTEGER NOT NULL DEFAULT 0, + "is_live" INTEGER NOT NULL DEFAULT 1, + PRIMARY KEY("id" AUTOINCREMENT) +); + +INSERT INTO user SELECT * FROM old_user; + +DROP TABLE "old_user"; + +COMMIT; diff --git a/core/internal/db/migration/2.sql b/core/internal/db/migration/2.sql new file mode 100644 index 0000000..78168a6 --- /dev/null +++ b/core/internal/db/migration/2.sql @@ -0,0 +1,6 @@ +BEGIN TRANSACTION; + +ALTER TABLE gift RENAME TO wish; + +COMMIT; +