Skip to content

Commit b6013c4

Browse files
committed
codes endpoints
1 parent 2858deb commit b6013c4

12 files changed

Lines changed: 1031 additions & 0 deletions

File tree

docs/swagger.yaml

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,216 @@ paths:
15641564
application/json:
15651565
schema:
15661566
$ref: '#/components/schemas/ErrorResponse'
1567+
/api/auth/2fa/setup:
1568+
post:
1569+
summary: Initiate TOTP 2FA setup
1570+
description: |
1571+
Generates a TOTP secret for the authenticated user and returns an `otpauth://` URI
1572+
for QR-code rendering. 2FA is NOT yet active — the user must call
1573+
`POST /api/auth/2fa/verify` with a valid code to enable it.
1574+
tags:
1575+
- Auth
1576+
security:
1577+
- bearerAuth: []
1578+
responses:
1579+
'200':
1580+
description: 2FA setup initiated
1581+
content:
1582+
application/json:
1583+
schema:
1584+
type: object
1585+
properties:
1586+
success:
1587+
type: boolean
1588+
message:
1589+
type: string
1590+
data:
1591+
type: object
1592+
properties:
1593+
otpauthUrl:
1594+
type: string
1595+
description: otpauth:// URI to encode as QR code
1596+
secret:
1597+
type: string
1598+
description: Raw TOTP secret for manual entry
1599+
'401':
1600+
description: Missing or invalid auth token
1601+
content:
1602+
application/json:
1603+
schema:
1604+
$ref: '#/components/schemas/ErrorResponse'
1605+
'409':
1606+
description: 2FA is already enabled
1607+
content:
1608+
application/json:
1609+
schema:
1610+
$ref: '#/components/schemas/ErrorResponse'
1611+
/api/auth/2fa/verify:
1612+
post:
1613+
summary: Verify first TOTP code and enable 2FA
1614+
description: |
1615+
Validates the first TOTP code after setup. On success, enables 2FA and returns
1616+
10 single-use backup codes (plaintext, shown exactly once — store them safely).
1617+
tags:
1618+
- Auth
1619+
security:
1620+
- bearerAuth: []
1621+
requestBody:
1622+
required: true
1623+
content:
1624+
application/json:
1625+
schema:
1626+
type: object
1627+
required:
1628+
- code
1629+
properties:
1630+
code:
1631+
type: string
1632+
pattern: '^\d{6}$'
1633+
description: 6-digit TOTP code from the authenticator app
1634+
responses:
1635+
'200':
1636+
description: 2FA enabled successfully
1637+
content:
1638+
application/json:
1639+
schema:
1640+
type: object
1641+
properties:
1642+
success:
1643+
type: boolean
1644+
message:
1645+
type: string
1646+
data:
1647+
type: object
1648+
properties:
1649+
backupCodes:
1650+
type: array
1651+
items:
1652+
type: string
1653+
description: 10 single-use backup codes (shown once only)
1654+
'400':
1655+
description: Invalid or wrong TOTP code, or setup not initiated
1656+
content:
1657+
application/json:
1658+
schema:
1659+
$ref: '#/components/schemas/ErrorResponse'
1660+
'401':
1661+
description: Missing or invalid auth token
1662+
content:
1663+
application/json:
1664+
schema:
1665+
$ref: '#/components/schemas/ErrorResponse'
1666+
'409':
1667+
description: 2FA already enabled
1668+
content:
1669+
application/json:
1670+
schema:
1671+
$ref: '#/components/schemas/ErrorResponse'
1672+
'429':
1673+
description: Too many verification attempts (rate-limited)
1674+
content:
1675+
application/json:
1676+
schema:
1677+
type: object
1678+
properties:
1679+
success:
1680+
type: boolean
1681+
message:
1682+
type: string
1683+
retryAfter:
1684+
type: integer
1685+
data:
1686+
nullable: true
1687+
/api/auth/2fa:
1688+
delete:
1689+
summary: Disable 2FA
1690+
description: |
1691+
Disables 2FA on the account after verifying the user's current password.
1692+
Clears the TOTP secret and all backup codes.
1693+
tags:
1694+
- Auth
1695+
security:
1696+
- bearerAuth: []
1697+
requestBody:
1698+
required: true
1699+
content:
1700+
application/json:
1701+
schema:
1702+
type: object
1703+
required:
1704+
- password
1705+
properties:
1706+
password:
1707+
type: string
1708+
description: Current account password
1709+
responses:
1710+
'200':
1711+
description: 2FA disabled successfully
1712+
content:
1713+
application/json:
1714+
schema:
1715+
type: object
1716+
properties:
1717+
success:
1718+
type: boolean
1719+
message:
1720+
type: string
1721+
data:
1722+
nullable: true
1723+
'400':
1724+
description: 2FA is not enabled on this account
1725+
content:
1726+
application/json:
1727+
schema:
1728+
$ref: '#/components/schemas/ErrorResponse'
1729+
'401':
1730+
description: Incorrect password or missing/invalid auth token
1731+
content:
1732+
application/json:
1733+
schema:
1734+
$ref: '#/components/schemas/ErrorResponse'
1735+
/api/auth/2fa/backup-codes/regenerate:
1736+
post:
1737+
summary: Regenerate backup codes
1738+
description: |
1739+
Invalidates all existing backup codes and issues 10 new single-use codes.
1740+
Requires 2FA to be enabled.
1741+
tags:
1742+
- Auth
1743+
security:
1744+
- bearerAuth: []
1745+
responses:
1746+
'200':
1747+
description: New backup codes generated
1748+
content:
1749+
application/json:
1750+
schema:
1751+
type: object
1752+
properties:
1753+
success:
1754+
type: boolean
1755+
message:
1756+
type: string
1757+
data:
1758+
type: object
1759+
properties:
1760+
backupCodes:
1761+
type: array
1762+
items:
1763+
type: string
1764+
description: 10 new single-use backup codes (shown once only)
1765+
'400':
1766+
description: 2FA is not enabled on this account
1767+
content:
1768+
application/json:
1769+
schema:
1770+
$ref: '#/components/schemas/ErrorResponse'
1771+
'401':
1772+
description: Missing or invalid auth token
1773+
content:
1774+
application/json:
1775+
schema:
1776+
$ref: '#/components/schemas/ErrorResponse'
15671777
/api/auth/api-keys/{organizationId}:
15681778
get:
15691779
summary: List API keys for an organization

package-lock.json

Lines changed: 70 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"morgan": "^1.10.1",
4040
"multer": "^2.0.2",
4141
"nodemailer": "^9.0.3",
42+
"otplib": "12.0.1",
4243
"pino": "^9.14.0",
4344
"rate-limit-redis": "4.2.0",
4445
"socket.io": "^4.8.3",

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export function buildApp() {
6565
app.use('/api/auth/signup', strictLimiter);
6666
app.use('/api/auth/forgot-password', otpLimiter);
6767
app.use('/api/auth/reset-password', otpLimiter);
68+
app.use('/api/auth/2fa/verify', otpLimiter);
69+
app.use('/api/auth/2fa/backup-codes/regenerate', strictLimiter);
6870

6971
app.use('/api/health', healthRouter);
7072
app.use('/api/auth', authRouter);

src/modules/auth/auth.routes.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
ResetPasswordBodySchema,
1212
RefreshBodySchema,
1313
RegisterCompanyBodySchema,
14+
Verify2faBodySchema,
15+
Disable2faBodySchema,
1416
} from './auth.validation.js';
1517
import {
1618
signupController,
@@ -31,6 +33,12 @@ import {
3133
CreateApiKeyBodySchema,
3234
OrganizationIdParamSchema,
3335
} from './apiKey.validation.js';
36+
import {
37+
setup2faController,
38+
verify2faController,
39+
disable2faController,
40+
regenerateBackupCodesController,
41+
} from './twoFactor.controller.js';
3442

3543
export const authRouter = Router();
3644

@@ -91,3 +99,31 @@ authRouter.delete(
9199
validateRequest({ params: ApiKeyIdParamSchema }),
92100
asyncHandler(revokeApiKeyController)
93101
);
102+
103+
// ── TOTP 2FA routes (all require JWT auth) ────────────────────────────────────
104+
105+
// Initiate 2FA setup: generates TOTP secret + otpauth URI
106+
authRouter.post('/2fa/setup', asyncHandler(requireAuth), asyncHandler(setup2faController));
107+
108+
// Verify first TOTP code, enable 2FA, return backup codes (rate-limited in app.ts)
109+
authRouter.post(
110+
'/2fa/verify',
111+
asyncHandler(requireAuth),
112+
validateRequest({ body: Verify2faBodySchema }),
113+
asyncHandler(verify2faController)
114+
);
115+
116+
// Disable 2FA — requires current password confirmation
117+
authRouter.delete(
118+
'/2fa',
119+
asyncHandler(requireAuth),
120+
validateRequest({ body: Disable2faBodySchema }),
121+
asyncHandler(disable2faController)
122+
);
123+
124+
// Regenerate backup codes — invalidates old ones
125+
authRouter.post(
126+
'/2fa/backup-codes/regenerate',
127+
asyncHandler(requireAuth),
128+
asyncHandler(regenerateBackupCodesController)
129+
);

0 commit comments

Comments
 (0)