fix auth issues

This commit is contained in:
Fredrik Jensen 2026-02-08 15:34:16 +01:00
parent 654b243ab8
commit 254d2bc5d5
5 changed files with 109 additions and 4 deletions

View File

@ -14,7 +14,8 @@
"Bash(npx tsc:*)",
"Bash(pnpm db:generate:*)",
"Bash(DATABASE_URL=\"postgresql://fedjens@localhost:5432/eyrun_test\" JWT_SECRET=\"test-secret-that-is-at-least-32-characters-long\" pnpm db:migrate:*)",
"Bash(ls:*)"
"Bash(ls:*)",
"Bash(curl:*)"
]
}
}

View File

@ -33,6 +33,7 @@
"@types/node": "^25.2.1",
"@vitest/coverage-v8": "^4.0.18",
"drizzle-kit": "^0.31.8",
"pino-pretty": "^13.1.3",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"

92
pnpm-lock.yaml generated
View File

@ -48,6 +48,9 @@ importers:
drizzle-kit:
specifier: ^0.31.8
version: 0.31.8
pino-pretty:
specifier: ^13.1.3
version: 13.1.3
tsx:
specifier: ^4.21.0
version: 4.21.0
@ -801,10 +804,16 @@ packages:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
engines: {node: '>=18'}
colorette@2.0.20:
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
dateformat@4.6.3:
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@ -942,6 +951,9 @@ packages:
resolution: {integrity: sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==}
engines: {node: '>=8.10.0'}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@ -976,6 +988,9 @@ packages:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
fast-copy@4.0.2:
resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==}
fast-decode-uri-component@1.0.1:
resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
@ -988,6 +1003,9 @@ packages:
fast-querystring@1.1.2:
resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
fast-sha256@1.3.0:
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
@ -1032,6 +1050,9 @@ packages:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
@ -1066,6 +1087,10 @@ packages:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
joycon@3.1.1:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
@ -1141,6 +1166,9 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@ -1160,6 +1188,9 @@ packages:
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
engines: {node: '>=14.0.0'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
@ -1182,6 +1213,10 @@ packages:
pino-abstract-transport@3.0.0:
resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==}
pino-pretty@13.1.3:
resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==}
hasBin: true
pino-std-serializers@7.1.0:
resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==}
@ -1203,6 +1238,9 @@ packages:
process-warning@5.0.0:
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
@ -1303,6 +1341,10 @@ packages:
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
strip-json-comments@5.0.3:
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
engines: {node: '>=14.16'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -1436,6 +1478,9 @@ packages:
engines: {node: '>=8'}
hasBin: true
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
yaml@2.8.2:
resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==}
engines: {node: '>= 14.6'}
@ -1936,8 +1981,12 @@ snapshots:
chai@6.2.2: {}
colorette@2.0.20: {}
cookie@1.1.1: {}
dateformat@4.6.3: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
@ -1985,6 +2034,10 @@ snapshots:
encoding-japanese@2.2.0: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
entities@4.5.0: {}
es-module-lexer@1.7.0: {}
@ -2085,6 +2138,8 @@ snapshots:
expect-type@1.3.0: {}
fast-copy@4.0.2: {}
fast-decode-uri-component@1.0.1: {}
fast-deep-equal@3.1.3: {}
@ -2102,6 +2157,8 @@ snapshots:
dependencies:
fast-decode-uri-component: 1.0.1
fast-safe-stringify@2.1.1: {}
fast-sha256@1.3.0: {}
fast-uri@3.1.0: {}
@ -2151,6 +2208,8 @@ snapshots:
he@1.2.0: {}
help-me@5.0.0: {}
html-escaper@2.0.2: {}
html-to-text@9.0.5:
@ -2191,6 +2250,8 @@ snapshots:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
joycon@3.1.1: {}
js-tokens@10.0.0: {}
json-schema-ref-resolver@3.0.0:
@ -2295,6 +2356,8 @@ snapshots:
dependencies:
semver: 7.7.4
minimist@1.2.8: {}
ms@2.1.3: {}
nanoid@3.3.11: {}
@ -2305,6 +2368,10 @@ snapshots:
on-exit-leak-free@2.1.2: {}
once@1.4.0:
dependencies:
wrappy: 1.0.2
openapi-types@12.1.3: {}
parseley@0.12.1:
@ -2324,6 +2391,22 @@ snapshots:
dependencies:
split2: 4.2.0
pino-pretty@13.1.3:
dependencies:
colorette: 2.0.20
dateformat: 4.6.3
fast-copy: 4.0.2
fast-safe-stringify: 2.1.1
help-me: 5.0.0
joycon: 3.1.1
minimist: 1.2.8
on-exit-leak-free: 2.1.2
pino-abstract-transport: 3.0.0
pump: 3.0.3
secure-json-parse: 4.1.0
sonic-boom: 4.2.0
strip-json-comments: 5.0.3
pino-std-serializers@7.1.0: {}
pino@10.3.0:
@ -2352,6 +2435,11 @@ snapshots:
process-warning@5.0.0: {}
pump@3.0.3:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
punycode.js@2.3.1: {}
quick-format-unescaped@4.0.4: {}
@ -2450,6 +2538,8 @@ snapshots:
std-env@3.10.0: {}
strip-json-comments@5.0.3: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@ -2549,6 +2639,8 @@ snapshots:
siginfo: 2.0.0
stackback: 0.0.2
wrappy@1.0.2: {}
yaml@2.8.2: {}
zod@4.3.6: {}

View File

@ -10,7 +10,12 @@ import { registerRoutes } from "./routes/index.js";
export function buildApp() {
const app = Fastify({
logger: true,
logger: {
transport:
process.env.NODE_ENV !== "production"
? { target: "pino-pretty", options: { translateTime: "HH:MM:ss", ignore: "pid,hostname" } }
: undefined,
},
});
app.addHook("onClose", async () => {

View File

@ -7,9 +7,9 @@ import { otpCodes } from "../db/schema.js";
const resend = new Resend(config.RESEND_API_KEY);
const OTP_MAX_ATTEMPTS = 5;
const OTP_MAX_ATTEMPTS = 10;
const OTP_TTL_MINUTES = 10;
const OTP_RATE_LIMIT_PER_HOUR = 3;
const OTP_RATE_LIMIT_PER_HOUR = 10;
/** Rate-limit, generate, store, and send an OTP for the given email. */
export async function requestOtp(email: string): Promise<void> {
@ -26,6 +26,12 @@ export async function requestOtp(email: string): Promise<void> {
const code = crypto.randomInt(100_000, 999_999).toString();
const expiresAt = new Date(Date.now() + OTP_TTL_MINUTES * 60 * 1000);
// Invalidate any previous unused codes for this email
await db
.update(otpCodes)
.set({ used: true })
.where(and(eq(otpCodes.email, email), eq(otpCodes.used, false)));
await db.insert(otpCodes).values({ email, code, expiresAt });
await sendOtp(email, code);
}