133 lines
5.1 KiB
Go
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()
|
|
}
|