package email import ( "context" "fmt" "time" "github.com/jackc/pgx/v5/pgtype" "github.com/memohai/memoh/internal/db" "github.com/memohai/memoh/internal/db/sqlc" ) // OAuthToken holds a stored OAuth2 token for an email provider. type OAuthToken struct { ProviderID string `json:"provider_id"` EmailAddress string `json:"email_address"` AccessToken string `json:"access_token"` //nolint:gosec // encrypted at rest, needed for token refresh. RefreshToken string `json:"refresh_token"` //nolint:gosec // encrypted at rest, needed for token refresh. ExpiresAt time.Time `json:"expires_at"` Scope string `json:"scope"` } // OAuthTokenStore persists and retrieves OAuth tokens for email providers. type OAuthTokenStore interface { Get(ctx context.Context, providerID string) (*OAuthToken, error) Save(ctx context.Context, t OAuthToken) error SetPendingState(ctx context.Context, providerID, state string) error GetByState(ctx context.Context, state string) (*OAuthToken, error) Delete(ctx context.Context, providerID string) error } // DBOAuthTokenStore is the DB-backed implementation of OAuthTokenStore. type DBOAuthTokenStore struct { queries *sqlc.Queries } func NewDBOAuthTokenStore(queries *sqlc.Queries) *DBOAuthTokenStore { return &DBOAuthTokenStore{queries: queries} } func (s *DBOAuthTokenStore) Get(ctx context.Context, providerID string) (*OAuthToken, error) { pgID, err := db.ParseUUID(providerID) if err != nil { return nil, err } row, err := s.queries.GetEmailOAuthTokenByProvider(ctx, pgID) if err != nil { return nil, fmt.Errorf("get oauth token: %w", err) } return s.toOAuthToken(row), nil } func (s *DBOAuthTokenStore) Save(ctx context.Context, t OAuthToken) error { pgID, err := db.ParseUUID(t.ProviderID) if err != nil { return err } var expiresAt pgtype.Timestamptz if !t.ExpiresAt.IsZero() { expiresAt = pgtype.Timestamptz{Time: t.ExpiresAt, Valid: true} } _, err = s.queries.UpsertEmailOAuthToken(ctx, sqlc.UpsertEmailOAuthTokenParams{ EmailProviderID: pgID, EmailAddress: t.EmailAddress, AccessToken: t.AccessToken, RefreshToken: t.RefreshToken, ExpiresAt: expiresAt, Scope: t.Scope, State: "", }) return err } func (s *DBOAuthTokenStore) SetPendingState(ctx context.Context, providerID, state string) error { pgID, err := db.ParseUUID(providerID) if err != nil { return err } return s.queries.UpdateEmailOAuthState(ctx, sqlc.UpdateEmailOAuthStateParams{ EmailProviderID: pgID, State: state, }) } func (s *DBOAuthTokenStore) GetByState(ctx context.Context, state string) (*OAuthToken, error) { row, err := s.queries.GetEmailOAuthTokenByState(ctx, state) if err != nil { return nil, fmt.Errorf("get oauth token by state: %w", err) } return s.toOAuthToken(row), nil } func (s *DBOAuthTokenStore) Delete(ctx context.Context, providerID string) error { pgID, err := db.ParseUUID(providerID) if err != nil { return err } return s.queries.DeleteEmailOAuthToken(ctx, pgID) } func (*DBOAuthTokenStore) toOAuthToken(row sqlc.EmailOauthToken) *OAuthToken { t := &OAuthToken{ ProviderID: row.EmailProviderID.String(), EmailAddress: row.EmailAddress, AccessToken: row.AccessToken, RefreshToken: row.RefreshToken, Scope: row.Scope, } if row.ExpiresAt.Valid { t.ExpiresAt = row.ExpiresAt.Time } return t }