Foundation: config, schema/migrations, auth, stable IDs, metadata probing

This commit is contained in:
2026-08-22 23:29:16 -05:00
commit d4ea7a4ca9
40 changed files with 3874 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
package database
import (
"database/sql"
"errors"
"time"
"git.noroutes.net/NoRoutes/vibetunes/internal/idgen"
)
type User struct {
ID string
Username string
Email string
PasswordHash sql.NullString
IsAdmin bool
Disabled bool
ExternalAuth bool
CreatedAt time.Time
LastLoginAt sql.NullTime
}
var ErrNotFound = errors.New("not found")
func CountUsers(db *sql.DB) (int, error) {
var n int
err := db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&n)
return n, err
}
// CreateUser inserts a new user. passwordHash may be empty for
// external-auth-only accounts.
func CreateUser(db *sql.DB, username, email, passwordHash string, isAdmin bool) (*User, error) {
id := idgen.New("us")
var hash sql.NullString
if passwordHash != "" {
hash = sql.NullString{String: passwordHash, Valid: true}
}
_, err := db.Exec(`INSERT INTO users (id, username, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)`,
id, username, email, nullStringVal(hash), boolInt(isAdmin))
if err != nil {
return nil, err
}
return GetUserByID(db, id)
}
// CreateExternalUser inserts a user with no local password, created on
// first login via a trusted reverse-proxy identity header.
func CreateExternalUser(db *sql.DB, username string) (*User, error) {
id := idgen.New("us")
_, err := db.Exec(`INSERT INTO users (id, username, external_auth) VALUES (?, ?, 1)`, id, username)
if err != nil {
return nil, err
}
return GetUserByID(db, id)
}
func GetUserByID(db *sql.DB, id string) (*User, error) {
return scanUser(db.QueryRow(`SELECT id, username, email, password_hash, is_admin, disabled, external_auth, created_at, last_login_at FROM users WHERE id = ?`, id))
}
func GetUserByUsername(db *sql.DB, username string) (*User, error) {
return scanUser(db.QueryRow(`SELECT id, username, email, password_hash, is_admin, disabled, external_auth, created_at, last_login_at FROM users WHERE username = ?`, username))
}
func scanUser(row *sql.Row) (*User, error) {
var u User
var isAdmin, disabled, external int
var createdAt string
var lastLogin sql.NullString
err := row.Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &isAdmin, &disabled, &external, &createdAt, &lastLogin)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
u.IsAdmin = isAdmin != 0
u.Disabled = disabled != 0
u.ExternalAuth = external != 0
u.CreatedAt, _ = parseTime(createdAt)
if lastLogin.Valid {
if t, err := parseTime(lastLogin.String); err == nil {
u.LastLoginAt = sql.NullTime{Time: t, Valid: true}
}
}
return &u, nil
}
func ListUsers(db *sql.DB) ([]*User, error) {
rows, err := db.Query(`SELECT id, username, email, password_hash, is_admin, disabled, external_auth, created_at, last_login_at FROM users ORDER BY username COLLATE NOCASE`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*User
for rows.Next() {
var u User
var isAdmin, disabled, external int
var createdAt string
var lastLogin sql.NullString
if err := rows.Scan(&u.ID, &u.Username, &u.Email, &u.PasswordHash, &isAdmin, &disabled, &external, &createdAt, &lastLogin); err != nil {
return nil, err
}
u.IsAdmin = isAdmin != 0
u.Disabled = disabled != 0
u.ExternalAuth = external != 0
u.CreatedAt, _ = parseTime(createdAt)
if lastLogin.Valid {
if t, err := parseTime(lastLogin.String); err == nil {
u.LastLoginAt = sql.NullTime{Time: t, Valid: true}
}
}
out = append(out, &u)
}
return out, rows.Err()
}
func SetUserPassword(db *sql.DB, userID, passwordHash string) error {
_, err := db.Exec(`UPDATE users SET password_hash = ? WHERE id = ?`, passwordHash, userID)
return err
}
func SetUserDisabled(db *sql.DB, userID string, disabled bool) error {
_, err := db.Exec(`UPDATE users SET disabled = ? WHERE id = ?`, boolInt(disabled), userID)
return err
}
func SetUserAdmin(db *sql.DB, userID string, isAdmin bool) error {
_, err := db.Exec(`UPDATE users SET is_admin = ? WHERE id = ?`, boolInt(isAdmin), userID)
return err
}
func DeleteUser(db *sql.DB, userID string) error {
_, err := db.Exec(`DELETE FROM users WHERE id = ?`, userID)
return err
}
func TouchLastLogin(db *sql.DB, userID string) error {
_, err := db.Exec(`UPDATE users SET last_login_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, userID)
return err
}
func boolInt(b bool) int {
if b {
return 1
}
return 0
}
func nullStringVal(s sql.NullString) any {
if !s.Valid {
return nil
}
return s.String
}
func parseTime(s string) (time.Time, error) {
return time.Parse("2006-01-02T15:04:05.999999999Z", s)
}