package store import ( "context" "errors" "time" "github.com/jackc/pgx/v5" "github.com/szabadpalya/api/internal/store/dbgen" ) type User struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` IsAdmin bool `json:"isAdmin"` } // UpsertUser finds or creates the account behind an email address. Requesting // a magic link is what creates an account; there is no separate signup. func (s *Store) UpsertUser(ctx context.Context, email string) (*User, error) { r, err := s.q.UpsertUser(ctx, dbgen.UpsertUserParams{ID: NewID(), Email: email}) if err != nil { return nil, err } return &User{ID: r.ID, Email: r.Email, Name: r.Name, IsAdmin: r.IsAdmin}, nil } // CreateVerification stores a hashed magic-link token. The plaintext never // reaches the database, so a dump cannot be replayed into a login. func (s *Store) CreateVerification(ctx context.Context, identifier, hashed string, expiresAt time.Time) error { return s.q.CreateVerification(ctx, dbgen.CreateVerificationParams{ ID: NewID(), Identifier: identifier, Value: hashed, ExpiresAt: expiresAt, }) } // ConsumeVerification redeems a token exactly once. The single UPDATE is what // makes it single-use: a second request finds consumed_at already set and gets // no rows back, even if both arrive at the same instant. func (s *Store) ConsumeVerification(ctx context.Context, hashed string) (string, error) { identifier, err := s.q.ConsumeVerification(ctx, hashed) if errors.Is(err, pgx.ErrNoRows) { return "", ErrNotFound } return identifier, err } func (s *Store) CreateSession(ctx context.Context, userID, hashed string, expiresAt time.Time, ip, ua string) error { return s.q.CreateSession(ctx, dbgen.CreateSessionParams{ ID: NewID(), Token: hashed, ExpiresAt: expiresAt, UserID: userID, IpAddress: nullable(ip), UserAgent: nullable(ua), }) } func (s *Store) UserBySessionToken(ctx context.Context, hashed string) (*User, error) { r, err := s.q.UserBySessionToken(ctx, hashed) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } return &User{ID: r.ID, Email: r.Email, Name: r.Name, IsAdmin: r.IsAdmin}, nil } func (s *Store) DeleteSession(ctx context.Context, hashed string) error { return s.q.DeleteSession(ctx, hashed) } // PurgeExpiredAuth keeps the two token tables from growing without bound. // Expired rows are already refused on read, so this is housekeeping only. func (s *Store) PurgeExpiredAuth(ctx context.Context) error { if err := s.q.PurgeExpiredSessions(ctx); err != nil { return err } return s.q.PurgeExpiredVerifications(ctx) } func nullable(s string) *string { if s == "" { return nil } return &s }