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() }