Files
enkly-ai/services/iam/internal/infrastructure/postgres/user_repo.go
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

129 lines
4.3 KiB
Go

// Package postgres implements IAM's domain repository interfaces against
// the iam database (see services/iam/migrations/0001_init.sql). This is
// a one-for-one transplant of the monolith's internal/db/users.go, split
// across files by aggregate instead of living in one package shared
// with every other table in the old schema.
package postgres
import (
"context"
"database/sql"
"errors"
"time"
"enkly/pkg/pgkit"
"enkly/services/iam/internal/domain"
)
const userColumns = `id, email, password_hash, email_verified, created_at`
func scanUser(row interface{ Scan(...any) error }) (*domain.User, error) {
var u domain.User
if err := row.Scan(&u.ID, &u.Email, &u.PasswordHash, &u.EmailVerified, &u.CreatedAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrNotFound
}
return nil, err
}
return &u, nil
}
// UserRepo implements domain.UserRepository.
type UserRepo struct {
DB *pgkit.DB
}
func NewUserRepo(db *pgkit.DB) *UserRepo { return &UserRepo{DB: db} }
// Create inserts a new, unverified account. email must already be
// normalized (trimmed + lower-cased) by the application layer;
// passwordHash is the output of authsupport.HashPassword.
func (r *UserRepo) Create(ctx context.Context, email, passwordHash string) (*domain.User, error) {
u, err := scanUser(r.DB.QueryRowContext(ctx,
`INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING `+userColumns,
email, passwordHash))
if err != nil {
if pgkit.IsUniqueViolation(err) {
return nil, domain.ErrEmailTaken
}
return nil, err
}
return u, nil
}
func (r *UserRepo) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
return scanUser(r.DB.QueryRowContext(ctx,
`SELECT `+userColumns+` FROM users WHERE email = $1`, email))
}
func (r *UserRepo) GetByID(ctx context.Context, id int64) (*domain.User, error) {
return scanUser(r.DB.QueryRowContext(ctx,
`SELECT `+userColumns+` FROM users WHERE id = $1`, id))
}
func (r *UserRepo) MarkEmailVerified(ctx context.Context, userID int64) error {
_, err := r.DB.ExecContext(ctx, `UPDATE users SET email_verified = true WHERE id = $1`, userID)
return err
}
func (r *UserRepo) UpdatePassword(ctx context.Context, userID int64, newPasswordHash string) error {
_, err := r.DB.ExecContext(ctx, `UPDATE users SET password_hash = $2 WHERE id = $1`, userID, newPasswordHash)
return err
}
// ---------------------------------------------------------------------
// Sessions
// ---------------------------------------------------------------------
// SessionRepo implements domain.SessionRepository.
type SessionRepo struct {
DB *pgkit.DB
}
func NewSessionRepo(db *pgkit.DB) *SessionRepo { return &SessionRepo{DB: db} }
func (r *SessionRepo) Create(ctx context.Context, userID int64, tokenHash string, expiresAt time.Time) error {
_, err := r.DB.ExecContext(ctx,
`INSERT INTO sessions (user_id, token_hash, expires_at) VALUES ($1, $2, $3)`,
userID, tokenHash, expiresAt)
return err
}
func (r *SessionRepo) GetUserByTokenHash(ctx context.Context, tokenHash string) (*domain.User, error) {
return scanUser(r.DB.QueryRowContext(ctx, `
SELECT u.id, u.email, u.password_hash, u.email_verified, u.created_at
FROM sessions s
JOIN users u ON u.id = s.user_id
WHERE s.token_hash = $1 AND s.expires_at > now()`, tokenHash))
}
func (r *SessionRepo) Delete(ctx context.Context, tokenHash string) error {
_, err := r.DB.ExecContext(ctx, `DELETE FROM sessions WHERE token_hash = $1`, tokenHash)
return err
}
func (r *SessionRepo) DeleteAllForUser(ctx context.Context, userID int64) error {
_, err := r.DB.ExecContext(ctx, `DELETE FROM sessions WHERE user_id = $1`, userID)
return err
}
// CleanupExpired deletes expired sessions and used/expired verification
// and password-reset tokens, mirroring internal/db.DB.CleanupExpired —
// run periodically by cmd/server's cleanup loop (see main.go).
func CleanupExpired(ctx context.Context, db *pgkit.DB) (int64, error) {
var total int64
for _, q := range []string{
`DELETE FROM sessions WHERE expires_at < now()`,
`DELETE FROM email_verification_codes WHERE expires_at < now() OR used_at < now() - interval '1 day'`,
`DELETE FROM password_reset_tokens WHERE expires_at < now() OR used_at < now() - interval '1 day'`,
} {
res, err := db.ExecContext(ctx, q)
if err != nil {
return total, err
}
n, _ := res.RowsAffected()
total += n
}
return total, nil
}