93 lines
2.7 KiB
Go
93 lines
2.7 KiB
Go
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
|
|
}
|