Files
yovision/Sense/server/app/sense/identity/postgres.go
T

133 lines
5.1 KiB
Go

package identity
import (
"context"
"database/sql"
"encoding/json"
"errors"
"time"
)
type PostgresStore struct{ database *sql.DB }
func NewPostgresStore(database *sql.DB) *PostgresStore { return &PostgresStore{database: database} }
func (s *PostgresStore) BootstrapUser(ctx context.Context, user User) error {
tx, err := s.database.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return err
}
defer tx.Rollback()
var count int
if err := tx.QueryRowContext(ctx, `SELECT count(*) FROM sense_identity_users`).Scan(&count); err != nil {
return err
}
if count != 0 {
return ErrBootstrapDone
}
if _, err := tx.ExecContext(ctx, `INSERT INTO sense_identity_users(id, username, display_name, role, enabled, password_hash, created_at, updated_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, user.ID, user.Username, user.DisplayName, user.Role, user.Enabled, user.PasswordHash, user.CreatedAt, user.UpdatedAt); err != nil {
return err
}
return tx.Commit()
}
func (s *PostgresStore) CreateUser(ctx context.Context, user User) error {
_, err := s.database.ExecContext(ctx, `INSERT INTO sense_identity_users(id, username, display_name, role, enabled, password_hash, created_at, updated_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8)`, user.ID, user.Username, user.DisplayName, user.Role, user.Enabled, user.PasswordHash, user.CreatedAt, user.UpdatedAt)
return err
}
func (s *PostgresStore) UpdateUser(ctx context.Context, user User) error {
result, err := s.database.ExecContext(ctx, `UPDATE sense_identity_users SET display_name=$2, role=$3, enabled=$4, password_hash=$5, updated_at=$6 WHERE id=$1`, user.ID, user.DisplayName, user.Role, user.Enabled, user.PasswordHash, user.UpdatedAt)
if err != nil {
return err
}
rows, _ := result.RowsAffected()
if rows == 0 {
return ErrNotFound
}
return nil
}
func (s *PostgresStore) FindUserByUsername(ctx context.Context, username string) (User, error) {
return scanUser(s.database.QueryRowContext(ctx, `SELECT id,username,display_name,role,enabled,password_hash,created_at,updated_at FROM sense_identity_users WHERE lower(username)=lower($1)`, username))
}
func (s *PostgresStore) FindUserByID(ctx context.Context, id string) (User, error) {
return scanUser(s.database.QueryRowContext(ctx, `SELECT id,username,display_name,role,enabled,password_hash,created_at,updated_at FROM sense_identity_users WHERE id=$1`, id))
}
type rowScanner interface{ Scan(...any) error }
func scanUser(row rowScanner) (User, error) {
var user User
err := row.Scan(&user.ID, &user.Username, &user.DisplayName, &user.Role, &user.Enabled, &user.PasswordHash, &user.CreatedAt, &user.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return User{}, ErrNotFound
}
return user, err
}
func (s *PostgresStore) ListUsers(ctx context.Context) ([]User, error) {
rows, err := s.database.QueryContext(ctx, `SELECT id,username,display_name,role,enabled,password_hash,created_at,updated_at FROM sense_identity_users ORDER BY username`)
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
user, err := scanUser(rows)
if err != nil {
return nil, err
}
users = append(users, user)
}
return users, rows.Err()
}
func (s *PostgresStore) CreateSession(ctx context.Context, session Session) error {
_, err := s.database.ExecContext(ctx, `INSERT INTO sense_identity_sessions(id,user_id,expires_at) VALUES($1,$2,$3)`, session.ID, session.UserID, session.ExpiresAt)
return err
}
func (s *PostgresStore) SessionActive(ctx context.Context, id string, now time.Time) (bool, error) {
var active bool
err := s.database.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM sense_identity_sessions WHERE id=$1 AND revoked_at IS NULL AND expires_at>$2)`, id, now).Scan(&active)
return active, err
}
func (s *PostgresStore) RevokeSession(ctx context.Context, id string, now time.Time) error {
_, err := s.database.ExecContext(ctx, `UPDATE sense_identity_sessions SET revoked_at=$2 WHERE id=$1`, id, now)
return err
}
func (s *PostgresStore) AppendAudit(ctx context.Context, entry AuditEntry) error {
metadata, err := json.Marshal(entry.Metadata)
if err != nil {
return err
}
_, err = s.database.ExecContext(ctx, `INSERT INTO sense_identity_audit(id,actor_id,action,target,outcome,metadata,created_at) VALUES($1,NULLIF($2,''),$3,$4,$5,$6,$7)`, entry.ID, entry.ActorID, entry.Action, entry.Target, entry.Outcome, metadata, entry.CreatedAt)
return err
}
func (s *PostgresStore) ListAudit(ctx context.Context, limit int) ([]AuditEntry, error) {
if limit <= 0 || limit > 200 {
limit = 100
}
rows, err := s.database.QueryContext(ctx, `SELECT id,COALESCE(actor_id,''),action,target,outcome,metadata,created_at FROM sense_identity_audit ORDER BY created_at DESC LIMIT $1`, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []AuditEntry
for rows.Next() {
var entry AuditEntry
var metadata []byte
if err := rows.Scan(&entry.ID, &entry.ActorID, &entry.Action, &entry.Target, &entry.Outcome, &metadata, &entry.CreatedAt); err != nil {
return nil, err
}
_ = json.Unmarshal(metadata, &entry.Metadata)
entries = append(entries, entry)
}
return entries, rows.Err()
}