Foundation: config, schema/migrations, auth, stable IDs, metadata probing
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user