package repository import ( "context" "time" ) // TOTPProfileRecord captures the persisted TOTP state for a member. // // SecretCipher is the AES-GCM blob; BackupCodesHash holds bcrypt hashes of // the plaintext backup codes (single-use). type TOTPProfileRecord struct { Enrolled bool SecretCipher []byte BackupCodesHash []string EnrolledAt int64 } // TOTPProfileRepository persists per-member TOTP secrets and backup codes. // // Implementations must be safe to call concurrently for distinct (tenant, uid) // pairs; concurrent calls for the same member should be linearised by the // caller (the usecase layer) before mutation. type TOTPProfileRepository interface { Get(ctx context.Context, tenantID, uid string) (*TOTPProfileRecord, error) Save(ctx context.Context, tenantID, uid string, rec *TOTPProfileRecord) error Clear(ctx context.Context, tenantID, uid string) error // ConsumeBackupCode removes the supplied backup-code hash atomically and // returns true when removal succeeded. ConsumeBackupCode(ctx context.Context, tenantID, uid, hash string) (bool, error) // ReplaceBackupCodes overwrites the backup-code hashes in one shot. ReplaceBackupCodes(ctx context.Context, tenantID, uid string, hashes []string) error } // TOTPEnrollStore stages a freshly generated secret until the user confirms // the first code. The blob is short-lived (Redis TTL). type TOTPEnrollStore interface { Save(ctx context.Context, tenantID, uid string, cipher []byte, ttl time.Duration) error Get(ctx context.Context, tenantID, uid string) ([]byte, error) Delete(ctx context.Context, tenantID, uid string) error } // TOTPReplayStore prevents replay of a successful TOTP code within its // allowed verification window. type TOTPReplayStore interface { // MarkUsed returns true when the key was newly recorded (i.e. the code // had not been used yet); false means it was already consumed. MarkUsed(ctx context.Context, tenantID, uid string, timestep uint64, ttl time.Duration) (bool, error) }