-
Notifications
You must be signed in to change notification settings - Fork 4
feat: configurable OTP length with 6-digit default support #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
17f978e
d8b18ab
776e77b
9b32c70
c774f26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /** | ||
| * Tests for buildOtpInputProps — derives HTML input attributes from OTP config. | ||
| * | ||
| * Covers: | ||
| * 1. Correct pattern and placeholder for numeric charset | ||
| * 2. Correct pattern and placeholder for alphanumeric charset | ||
| * 3. Pattern and placeholder length match the requested otpLength | ||
| * 4. Numeric pattern rejects letters | ||
| * 5. Alphanumeric pattern accepts both letters and digits | ||
| */ | ||
| import { describe, it, expect } from 'vitest' | ||
| import { buildOtpInputProps } from '../otp-input.js' | ||
|
|
||
| describe('Recovery flow: OTP input props', () => { | ||
| it('numeric charset produces digit-only pattern and zero placeholder', () => { | ||
| const props = buildOtpInputProps(8, 'numeric') | ||
| expect(props.pattern).toBe('[0-9]{8}') | ||
| expect(props.placeholder).toBe('00000000') | ||
| expect(props.inputmode).toBe('numeric') | ||
| expect(props.autocapitalize).toBe('off') | ||
| }) | ||
|
|
||
| it('alphanumeric charset produces alphanumeric pattern and X placeholder', () => { | ||
| const props = buildOtpInputProps(8, 'alphanumeric') | ||
| expect(props.pattern).toBe('[A-Z0-9]{8}') | ||
| expect(props.placeholder).toBe('XXXXXXXX') | ||
| expect(props.inputmode).toBe('text') | ||
| expect(props.autocapitalize).toBe('characters') | ||
| }) | ||
|
|
||
| it('pattern and placeholder length match otpLength', () => { | ||
| const numeric = buildOtpInputProps(6, 'numeric') | ||
| expect(numeric.pattern).toBe('[0-9]{6}') | ||
| expect(numeric.placeholder).toHaveLength(6) | ||
| const numericRe = new RegExp(`^${numeric.pattern}$`) | ||
| expect(numericRe.test('123456')).toBe(true) | ||
| expect(numericRe.test('12345')).toBe(false) // too short | ||
| expect(numericRe.test('1234567')).toBe(false) // too long | ||
|
|
||
| const alpha = buildOtpInputProps(6, 'alphanumeric') | ||
| expect(alpha.pattern).toBe('[A-Z0-9]{6}') | ||
| expect(alpha.placeholder).toHaveLength(6) | ||
| const alphaRe = new RegExp(`^${alpha.pattern}$`) | ||
| expect(alphaRe.test('A1B2C3')).toBe(true) | ||
| expect(alphaRe.test('A1B2C')).toBe(false) // too short | ||
| expect(alphaRe.test('A1B2C3D')).toBe(false) // too long | ||
| }) | ||
|
|
||
| it('numeric pattern does not accept letters', () => { | ||
| const { pattern } = buildOtpInputProps(8, 'numeric') | ||
| const re = new RegExp(`^${pattern}$`) | ||
| expect(re.test('12345678')).toBe(true) | ||
| expect(re.test('1234567A')).toBe(false) | ||
| }) | ||
|
|
||
| it('alphanumeric pattern accepts both letters and digits', () => { | ||
| const { pattern } = buildOtpInputProps(8, 'alphanumeric') | ||
| const re = new RegExp(`^${pattern}$`) | ||
| expect(re.test('A1B2C3D4')).toBe(true) | ||
| expect(re.test('12345678')).toBe(true) | ||
| expect(re.test('ABCDEFGH')).toBe(true) | ||
| expect(re.test('abcdefgh')).toBe(false) // lowercase rejected | ||
| expect(re.test('A1B2C3D')).toBe(false) // one short | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,7 +28,12 @@ export function createAuthService(config: AuthServiceConfig): { | |
|
|
||
| // Mount better-auth BEFORE express.json() so it can parse its own request bodies. | ||
| // All better-auth endpoints live under /api/auth/*. | ||
| const betterAuthInstance = createBetterAuth(ctx.emailSender, ctx.db) | ||
| const betterAuthInstance = createBetterAuth( | ||
| ctx.emailSender, | ||
| ctx.db, | ||
| config.otpLength, | ||
| config.otpCharset, | ||
| ) | ||
| app.all('/api/auth/*', toNodeHandler(betterAuthInstance)) | ||
|
|
||
| // Middleware | ||
|
|
@@ -75,7 +80,7 @@ export function createAuthService(config: AuthServiceConfig): { | |
| app.use(createLoginPageRouter(ctx)) | ||
| app.use(createConsentRouter(ctx)) | ||
| app.use(createRecoveryRouter(ctx, betterAuthInstance)) | ||
| app.use(createAccountLoginRouter(betterAuthInstance)) | ||
| app.use(createAccountLoginRouter(betterAuthInstance, ctx)) | ||
| app.use(createAccountSettingsRouter(ctx, betterAuthInstance)) | ||
| app.use(createCompleteRouter(ctx, betterAuthInstance)) | ||
| app.use(createChooseHandleRouter(ctx, betterAuthInstance)) | ||
|
|
@@ -132,9 +137,35 @@ async function main() { | |
| fromName: process.env.SMTP_FROM_NAME || 'ePDS', | ||
| }, | ||
| dbLocation: process.env.DB_LOCATION || './data/epds.sqlite', | ||
| otpLength: Number(process.env.OTP_LENGTH ?? '8'), | ||
| otpCharset: (process.env.OTP_CHARSET || 'numeric') as | ||
| | 'numeric' | ||
| | 'alphanumeric', | ||
| } | ||
|
|
||
| await runBetterAuthMigrations(config.dbLocation, config.hostname) | ||
| if ( | ||
| isNaN(config.otpLength) || | ||
| config.otpLength < 4 || | ||
| config.otpLength > 12 | ||
| ) { | ||
| throw new Error( | ||
| `Invalid OTP_LENGTH: must be between 4 and 12, got "${process.env.OTP_LENGTH}"`, | ||
| ) | ||
| } | ||
|
|
||
| const validCharsets = ['numeric', 'alphanumeric'] | ||
| if (!validCharsets.includes(config.otpCharset)) { | ||
| throw new Error( | ||
| `Invalid OTP_CHARSET: must be 'numeric' or 'alphanumeric', got "${process.env.OTP_CHARSET}"`, | ||
| ) | ||
| } | ||
|
Comment on lines
+146
to
+161
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general please always look for opportunities to keep functions small and extract logic out into smaller chunks. Keeping everything smaller yields an enormous boost in code quality / maintainability / legibility (even for agents). For example these bits could be refactored as Feel free to adopt https://github.com/aspiers/ai-config/blob/main/.agents/skills/code-refactoring-small/SKILL.md |
||
|
|
||
| await runBetterAuthMigrations( | ||
| config.dbLocation, | ||
| config.hostname, | ||
| config.otpLength, | ||
| config.otpCharset, | ||
| ) | ||
|
|
||
| const { app, ctx } = createAuthService(config) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Kzoeps Please can you check if you need to add these two vars to
scripts/setup.sh?