feat: groups

This commit is contained in:
Teajey 2025-06-22 21:15:47 +09:00
parent bba5136cca
commit 439d4a1844
Signed by: Teajey
GPG Key ID: 970E790FE834A713
15 changed files with 295 additions and 47 deletions

55
core/debug.go Normal file
View File

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

View File

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

14
core/go.sum Normal file
View File

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

130
core/group.go Normal file
View File

@ -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
}

42
core/group_test.go Normal file
View File

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

View File

@ -6,13 +6,14 @@ import (
"database/sql"
"fmt"
_ "github.com/glebarez/go-sqlite"
_ "github.com/ncruces/go-sqlite3/driver"
_ "github.com/ncruces/go-sqlite3/embed"
)
var Connection *sql.DB
func Init(dataSourceName string) error {
db, err := sql.Open("sqlite", dataSourceName)
db, err := sql.Open("sqlite3", dataSourceName)
if err != nil {
return fmt.Errorf("Failed to open db connection: %w", err)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -1,7 +0,0 @@
go 1.23
toolchain go1.23.3
use ./core
use ./http

View File

@ -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=