Implement owner setup and browser authentication sessions
This commit is contained in:
parent
077cf7601a
commit
715423ab8e
21 changed files with 2185 additions and 14 deletions
214
internal/adapters/postgres/authentication.go
Normal file
214
internal/adapters/postgres/authentication.go
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
dbgen "drive.local/drivev2/internal/adapters/postgres/generated"
|
||||
"drive.local/drivev2/internal/application"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) WithAuthTransaction(ctx context.Context, operation func(application.AuthTransaction) error) (returnErr error) {
|
||||
transaction, err := s.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.Serializable})
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin authentication transaction: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := transaction.Rollback(ctx); err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
returnErr = errors.Join(returnErr, fmt.Errorf("rollback authentication transaction: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
transactionStore := &Store{queries: s.queries.WithTx(transaction)}
|
||||
if err := operation(transactionStore); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := transaction.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("commit authentication transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateOwner(ctx context.Context, record application.CreateOwnerRecord) (string, error) {
|
||||
id, err := s.queries.CreateOwner(ctx, dbgen.CreateOwnerParams{
|
||||
Username: record.Username, UsernameKey: record.UsernameKey, DisplayName: record.DisplayName, PasswordHash: record.PasswordHash,
|
||||
})
|
||||
if ownerAlreadyExists(err) {
|
||||
return "", application.ErrSetupAlreadyComplete
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create owner: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateOwnerRoot(ctx context.Context, ownerID string, createdAt time.Time) (string, error) {
|
||||
ownerUUID, err := parseUUID(ownerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id, err := s.queries.CreateOwnerRootNode(ctx, dbgen.CreateOwnerRootNodeParams{
|
||||
OwnerID: ownerUUID, CreatedAt: timestamp(createdAt),
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create owner root node: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetOwnerRoot(ctx context.Context, ownerID, rootNodeID string, updatedAt time.Time) error {
|
||||
ownerUUID, err := parseUUID(ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootUUID, err := parseUUID(rootNodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.queries.SetOwnerRootNode(ctx, dbgen.SetOwnerRootNodeParams{
|
||||
RootNodeID: rootUUID, UpdatedAt: timestamp(updatedAt), OwnerID: ownerUUID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("set owner root node: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateRecoveryCode(ctx context.Context, ownerID string, codeHash []byte) error {
|
||||
ownerUUID, err := parseUUID(ownerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.queries.CreateRecoveryCode(ctx, dbgen.CreateRecoveryCodeParams{OwnerID: ownerUUID, CodeHash: codeHash}); err != nil {
|
||||
return fmt.Errorf("create recovery code: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateBrowserSession(ctx context.Context, record application.CreateSessionRecord) (string, error) {
|
||||
ownerUUID, err := parseUUID(record.OwnerID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id, err := s.queries.CreateBrowserSession(ctx, dbgen.CreateBrowserSessionParams{
|
||||
OwnerID: ownerUUID, TokenHash: record.TokenHash, CsrfTokenHash: record.CSRFTokenHash,
|
||||
RemoteAddress: record.RemoteAddress, UserAgent: record.UserAgent,
|
||||
AuthenticatedAt: timestamp(record.AuthenticatedAt), ExpiresAt: timestamp(record.ExpiresAt),
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create browser session: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateInitialRootChange(ctx context.Context, record application.InitialRootChangeRecord) error {
|
||||
ownerUUID, err := parseUUID(record.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootUUID, err := parseUUID(record.RootNodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.queries.CreateInitialRootChange(ctx, dbgen.CreateInitialRootChangeParams{
|
||||
OwnerID: ownerUUID, RootNodeID: rootUUID, ResultingState: record.ResultingState,
|
||||
MutationOrigin: record.MutationOrigin, CommittedAt: timestamp(record.CommittedAt),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("create initial root change: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) CreateAuthenticationAudit(ctx context.Context, record application.AuthenticationAuditRecord) error {
|
||||
ownerUUID, err := parseUUID(record.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resourceUUID, err := parseUUID(record.ResourceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resourceType := record.ResourceType
|
||||
if err := s.queries.CreateAuthenticationAuditEvent(ctx, dbgen.CreateAuthenticationAuditEventParams{
|
||||
OwnerID: ownerUUID, Actor: record.Actor, Action: record.Action, ResourceType: &resourceType,
|
||||
ResourceID: resourceUUID, Outcome: record.Outcome, Details: record.Details, OccurredAt: timestamp(record.OccurredAt),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("create authentication audit event: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) GetOwnerAuthenticationRecord(ctx context.Context) (application.OwnerAuthenticationRecord, error) {
|
||||
record, err := s.queries.GetOwnerAuthenticationRecord(ctx)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return application.OwnerAuthenticationRecord{}, application.ErrOwnerNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return application.OwnerAuthenticationRecord{}, fmt.Errorf("get owner authentication record: %w", err)
|
||||
}
|
||||
return application.OwnerAuthenticationRecord{
|
||||
ID: record.ID, Username: record.Username, UsernameKey: record.UsernameKey,
|
||||
DisplayName: record.DisplayName, PasswordHash: record.PasswordHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetActiveBrowserSession(ctx context.Context, tokenHash []byte, now time.Time) (application.BrowserSession, error) {
|
||||
record, err := s.queries.GetActiveBrowserSession(ctx, dbgen.GetActiveBrowserSessionParams{TokenHash: tokenHash, NowAt: timestamp(now)})
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return application.BrowserSession{}, application.ErrSessionNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return application.BrowserSession{}, fmt.Errorf("get active browser session: %w", err)
|
||||
}
|
||||
return application.BrowserSession{
|
||||
ID: record.SessionID, OwnerID: record.OwnerID, Username: record.Username, DisplayName: record.DisplayName,
|
||||
CSRFTokenHash: record.CsrfTokenHash, AuthenticatedAt: record.AuthenticatedAt.Time,
|
||||
LastSeenAt: record.LastSeenAt.Time, ExpiresAt: record.ExpiresAt.Time,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) TouchBrowserSession(ctx context.Context, sessionID string, seenAt time.Time) error {
|
||||
sessionUUID, err := parseUUID(sessionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.queries.TouchBrowserSession(ctx, dbgen.TouchBrowserSessionParams{SeenAt: timestamp(seenAt), SessionID: sessionUUID}); err != nil {
|
||||
return fmt.Errorf("touch browser session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) RevokeBrowserSession(ctx context.Context, sessionID string, revokedAt time.Time) (bool, error) {
|
||||
sessionUUID, err := parseUUID(sessionID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
rows, err := s.queries.RevokeBrowserSession(ctx, dbgen.RevokeBrowserSessionParams{RevokedAt: timestamp(revokedAt), SessionID: sessionUUID})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("revoke browser session: %w", err)
|
||||
}
|
||||
return rows == 1, nil
|
||||
}
|
||||
|
||||
func parseUUID(value string) (pgtype.UUID, error) {
|
||||
var parsed pgtype.UUID
|
||||
if err := parsed.Scan(value); err != nil {
|
||||
return pgtype.UUID{}, fmt.Errorf("parse UUID: %w", err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func timestamp(value time.Time) pgtype.Timestamptz {
|
||||
return pgtype.Timestamptz{Time: value, Valid: true}
|
||||
}
|
||||
|
||||
func ownerAlreadyExists(err error) bool {
|
||||
var postgresError *pgconn.PgError
|
||||
return errors.As(err, &postgresError) && postgresError.Code == pgerrcode.UniqueViolation &&
|
||||
(postgresError.ConstraintName == "users_single_owner" || postgresError.ConstraintName == "users_username_key_unique")
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue