package approval import ( "context" "database/sql" "errors" "fmt" "time" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/db" ) const ( insertRememberApproval = `INSERT INTO stored_approvals (email, client_id, expiry) VALUES (?, ?, ?)` getApproval = `SELECT expiry FROM stored_approvalts WHERE email = ? AND client_id = ?` dbTimeout = 30 * time.Second approvalExpiration = 30 * 24 * time.Hour ) type Claim struct { Email string ClientID string } type ApprovalService interface { Remember(ctx context.Context, claim Claim) error IsRemembered(ctx context.Context, claim Claim) (bool, error) } type concreteApprovalService struct { conf *config.AppConfig } func (cas *concreteApprovalService) Remember(ctx context.Context, claim Claim) error { db, err := db.Connect(cas.conf) if err != nil { return fmt.Errorf("failed to connect to db: %w", err) } queryCtx, cancel := context.WithTimeout(ctx, dbTimeout) defer cancel() tx, err := db.BeginTx(queryCtx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } expiry := time.Now().UTC().Add(approvalExpiration) _, err = tx.Exec(insertRememberApproval, claim.Email, claim.ClientID, expiry) if err != nil { return fmt.Errorf("failed to insert approval in DB: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } func (cas *concreteApprovalService) IsRemembered(ctx context.Context, claim Claim) (bool, error) { db, err := db.Connect(cas.conf) if err != nil { return false, fmt.Errorf("failed to connect to db: %w", err) } row := db.QueryRow(getApproval, claim.Email, claim.ClientID) var expiry time.Time if err := row.Scan(&expiry); err != nil { if errors.Is(err, sql.ErrNoRows) { return false, nil } return false, fmt.Errorf("failed to run query: %w", err) } return time.Now().UTC().Before(expiry), nil } func New(conf *config.AppConfig) ApprovalService { return &concreteApprovalService{conf: conf} }