a76ec74338
C++ ImGui kanban for steering LLM agents. Six panels (Board, Calendar, Dashboard, Agent runs, Worktrees, DoD inspector) wired to registry functions http_request, kpi_card, sparkline, agent_runs_timeline, dod_evidence_panel. Backend Go on :8403 (independent operations.db from kanban_web).
130 lines
3.4 KiB
Go
130 lines
3.4 KiB
Go
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
|
|
}
|