package main import ( "database/sql" "errors" "fmt" "strings" "fn-registry/functions/infra" ) type User struct { ID string `json:"id"` Username string `json:"username"` DisplayName string `json:"display_name"` Color string `json:"color"` CreatedAt string `json:"created_at"` } var ( errUserNotFound = errors.New("user not found") errUserAlreadyExists = errors.New("username already exists") errInvalidCredentials = errors.New("invalid credentials") ) func (db *DB) CreateUser(username, password, displayName string) (*User, error) { username = strings.TrimSpace(strings.ToLower(username)) if username == "" { return nil, fmt.Errorf("username required") } if len(password) < 4 { return nil, fmt.Errorf("password must be at least 4 characters") } hash, err := infra.PasswordHash(password, 0) if err != nil { return nil, fmt.Errorf("hash: %w", err) } u := User{ID: newID(), Username: username, DisplayName: displayName, CreatedAt: nowRFC3339()} _, err = db.conn.Exec( `INSERT INTO users (id, username, password_hash, display_name, created_at) VALUES (?, ?, ?, ?, ?)`, u.ID, u.Username, hash, u.DisplayName, u.CreatedAt, ) if err != nil { if strings.Contains(err.Error(), "UNIQUE") { return nil, errUserAlreadyExists } return nil, err } return &u, nil } func (db *DB) GetUserByID(id string) (*User, error) { var u User err := db.conn.QueryRow( `SELECT id, username, display_name, color, created_at FROM users WHERE id=?`, id, ).Scan(&u.ID, &u.Username, &u.DisplayName, &u.Color, &u.CreatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, errUserNotFound } if err != nil { return nil, err } return &u, nil } func (db *DB) GetUserByUsername(username string) (*User, string, error) { username = strings.TrimSpace(strings.ToLower(username)) var u User var hash string err := db.conn.QueryRow( `SELECT id, username, display_name, color, created_at, password_hash FROM users WHERE username=?`, username, ).Scan(&u.ID, &u.Username, &u.DisplayName, &u.Color, &u.CreatedAt, &hash) if errors.Is(err, sql.ErrNoRows) { return nil, "", errUserNotFound } if err != nil { return nil, "", err } return &u, hash, nil } func (db *DB) ListUsers() ([]User, error) { rows, err := db.conn.Query(`SELECT id, username, display_name, color, created_at FROM users ORDER BY username`) if err != nil { return nil, err } defer rows.Close() out := []User{} for rows.Next() { var u User if err := rows.Scan(&u.ID, &u.Username, &u.DisplayName, &u.Color, &u.CreatedAt); err != nil { return nil, err } out = append(out, u) } return out, rows.Err() } func (db *DB) Authenticate(username, password string) (*User, error) { u, hash, err := db.GetUserByUsername(username) if err != nil { if errors.Is(err, errUserNotFound) { return nil, errInvalidCredentials } return nil, err } if err := infra.PasswordVerify(password, hash); err != nil { return nil, errInvalidCredentials } return u, nil } func (db *DB) CountUsers() (int, error) { var n int if err := db.conn.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&n); err != nil { return 0, err } return n, nil } func (db *DB) UpdateUserColor(id, color string) error { _, err := db.conn.Exec(`UPDATE users SET color=? WHERE id=?`, color, id) return err } func (db *DB) DeleteSessionByToken(token string) error { _, err := db.conn.Exec(`DELETE FROM sessions WHERE token=?`, token) return err }