From 970cf0274ee2e426cf97f1a05812503444adba46 Mon Sep 17 00:00:00 2001 From: Ethan Wellenreiter Date: Mon, 5 May 2025 22:07:37 -0400 Subject: [PATCH] Partial implementation of the SQL version of the user storage interface Signed-off-by: Ethan Wellenreiter --- backend/internal/storage/sql-users.go | 194 +++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 7 deletions(-) diff --git a/backend/internal/storage/sql-users.go b/backend/internal/storage/sql-users.go index ad143c6..f274d01 100644 --- a/backend/internal/storage/sql-users.go +++ b/backend/internal/storage/sql-users.go @@ -3,6 +3,7 @@ package storage import ( "context" "database/sql" + "fmt" "time" ) @@ -11,21 +12,175 @@ type SQLUsersStore struct { } func (s *SQLUsersStore) GetByID(ctx context.Context, id int64) (*User, error) { - return nil, nil + query := `SELECT users.id, users.username, users.email, users.password, users.created_at, roles.* + FROM users + JOIN roles ON (users.role_id = roles.id) + WHERE users.id = $1 AND is_active = true` + + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + user := &User{} + err := s.db.QueryRowContext( + ctx, + query, + id, + ).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.Password.hash, + &user.CreatedAt, + &user.Role.ID, + &user.Role.Name, + &user.Role.Level, + &user.Role.Description, + ) + if err != nil { + switch err { + case sql.ErrNoRows: + return nil, ErrNotFound + default: + return nil, err + } + } + + return user, nil } -func (s *SQLUsersStore) GetByEmail(context.Context, string) (*User, error) { - return nil, nil +func (s *SQLUsersStore) GetByEmail(ctx context.Context, email string) (*User, error) { + query := `SELECT users.id, users.username, users.email, users.password, users.created_at, roles.* + FROM users + JOIN roles ON (users.role_id = roles.id) + WHERE users.email = $1 AND is_active = true` + + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + user := &User{} + err := s.db.QueryRowContext( + ctx, + query, + email, + ).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.Password.hash, + &user.CreatedAt, + &user.Role.ID, + &user.Role.Name, + &user.Role.Level, + &user.Role.Description, + ) + if err != nil { + switch err { + case sql.ErrNoRows: + return nil, ErrNotFound + default: + return nil, err + } + } + + return user, nil } -func (s *SQLUsersStore) GetByUsername(context.Context, string) (*User, error) { - return nil, nil +func (s *SQLUsersStore) GetByUsername(ctx context.Context, username string) (*User, error) { + query := `SELECT users.id, users.username, users.email, users.password, users.created_at, roles.* + FROM users + JOIN roles ON (users.role_id = roles.id) + WHERE users.username = $1 AND is_active = true` + + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + user := &User{} + err := s.db.QueryRowContext( + ctx, + query, + username, + ).Scan( + &user.ID, + &user.Username, + &user.Email, + &user.Password.hash, + &user.CreatedAt, + &user.Role.ID, + &user.Role.Name, + &user.Role.Level, + &user.Role.Description, + ) + if err != nil { + switch err { + case sql.ErrNoRows: + return nil, ErrNotFound + default: + return nil, err + } + } + + return user, nil } -func (s *SQLUsersStore) Create(context.Context, *User) error { // create a non-exported create function which does take in the tx +func (s *SQLUsersStore) create(ctx context.Context, user *User, tx *sql.Tx) error { // creates the personal group and binds them + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + query := `INSERT INTO groups (name, owner) VALUES ($1, $2) RETURNING id` + + err := tx.QueryRowContext(ctx, query, fmt.Sprintf("User-%d-Personal-Group", user.ID), user.ID).Scan(&user.PersonalGroup) + if err != nil { + return err + } + + query = `INSERT INTO users (username, password, email, role_id, personalgroup) VALUES + ($1, $2, $3, (SELECT id FROM roles WHERE name = $4), $5) + RETURNING id, created_at` + + role := user.Role.Name + if role == "" { + role = "user" + } + + err = tx.QueryRowContext( + ctx, + query, + user.Username, + user.Password, + user.Email, + role, + user.PersonalGroup, + ).Scan( + &user.ID, + &user.CreatedAt, + ) + if err != nil { + switch { + case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`: + return ErrDuplicateEmail + case err.Error() == `pq: duplicate key value violates unique constraint "users_username_key"`: + return ErrDuplicateUsername + default: + return err + } + } + + query = `INSERT INTO groupMembership (groupid, userid, moderator) VALUES ($1, $2, $3)` + + _, err = tx.ExecContext(ctx, query, user.PersonalGroup, user.ID, true) + if err != nil { + return err + } + return nil } +func (s *SQLUsersStore) Create(ctx context.Context, user *User) error { // create a non-exported create function which does take in the tx + return withTx(s.db, ctx, func(tx *sql.Tx) error { + return s.create(ctx, user, tx) + }) +} + func (s *SQLUsersStore) CreateAndInvite(ctx context.Context, user *User, token string, exp time.Duration) error { // figure this out return nil } @@ -34,10 +189,35 @@ func (s *SQLUsersStore) Activate(context.Context, string) error { // what does t return nil } -func (s *SQLUsersStore) Delete(ctx context.Context, id int64) error { +func (s *SQLUsersStore) delete(ctx context.Context, id int64, tx *sql.Tx) error { + query := `DELETE FROM users WHERE id = $1` + + ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration) + defer cancel() + + res, err := s.db.ExecContext(ctx, query, id) + if err != nil { + return err + } + + rows, err := res.RowsAffected() + if err != nil { + return err + } + + if rows == 0 { + return ErrNotFound + } + return nil } +func (s *SQLUsersStore) Delete(ctx context.Context, id int64) error { + return withTx(s.db, ctx, func(tx *sql.Tx) error { + return s.delete(ctx, id, tx) + }) +} + func (s *SQLUsersStore) UpdateUserPass(ctx context.Context, user string, oldPassword string, newPass string) error { return nil }