@shipfox/api-auth 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +116 -0
  5. package/LICENSE +21 -0
  6. package/README.md +336 -0
  7. package/dist/config.d.ts +22 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +95 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/core/auth.d.ts +108 -0
  12. package/dist/core/auth.d.ts.map +1 -0
  13. package/dist/core/auth.js +476 -0
  14. package/dist/core/auth.js.map +1 -0
  15. package/dist/core/entities/email-verification.d.ts +9 -0
  16. package/dist/core/entities/email-verification.d.ts.map +1 -0
  17. package/dist/core/entities/email-verification.js +3 -0
  18. package/dist/core/entities/email-verification.js.map +1 -0
  19. package/dist/core/entities/password-reset.d.ts +9 -0
  20. package/dist/core/entities/password-reset.d.ts.map +1 -0
  21. package/dist/core/entities/password-reset.js +3 -0
  22. package/dist/core/entities/password-reset.js.map +1 -0
  23. package/dist/core/entities/refresh-token.d.ts +12 -0
  24. package/dist/core/entities/refresh-token.d.ts.map +1 -0
  25. package/dist/core/entities/refresh-token.js +3 -0
  26. package/dist/core/entities/refresh-token.js.map +1 -0
  27. package/dist/core/entities/user.d.ts +12 -0
  28. package/dist/core/entities/user.d.ts.map +1 -0
  29. package/dist/core/entities/user.js +3 -0
  30. package/dist/core/entities/user.js.map +1 -0
  31. package/dist/core/errors.d.ts +54 -0
  32. package/dist/core/errors.d.ts.map +1 -0
  33. package/dist/core/errors.js +106 -0
  34. package/dist/core/errors.js.map +1 -0
  35. package/dist/core/job-lease-token.d.ts +12 -0
  36. package/dist/core/job-lease-token.d.ts.map +1 -0
  37. package/dist/core/job-lease-token.js +58 -0
  38. package/dist/core/job-lease-token.js.map +1 -0
  39. package/dist/core/jwt.d.ts +37 -0
  40. package/dist/core/jwt.d.ts.map +1 -0
  41. package/dist/core/jwt.js +46 -0
  42. package/dist/core/jwt.js.map +1 -0
  43. package/dist/core/password.d.ts +10 -0
  44. package/dist/core/password.d.ts.map +1 -0
  45. package/dist/core/password.js +13 -0
  46. package/dist/core/password.js.map +1 -0
  47. package/dist/core/rate-limit.d.ts +34 -0
  48. package/dist/core/rate-limit.d.ts.map +1 -0
  49. package/dist/core/rate-limit.js +76 -0
  50. package/dist/core/rate-limit.js.map +1 -0
  51. package/dist/core/runner-session-token.d.ts +6 -0
  52. package/dist/core/runner-session-token.d.ts.map +1 -0
  53. package/dist/core/runner-session-token.js +37 -0
  54. package/dist/core/runner-session-token.js.map +1 -0
  55. package/dist/db/db.d.ts +1723 -0
  56. package/dist/db/db.d.ts.map +1 -0
  57. package/dist/db/db.js +28 -0
  58. package/dist/db/db.js.map +1 -0
  59. package/dist/db/email-verifications.d.ts +39 -0
  60. package/dist/db/email-verifications.d.ts.map +1 -0
  61. package/dist/db/email-verifications.js +95 -0
  62. package/dist/db/email-verifications.js.map +1 -0
  63. package/dist/db/migrations.d.ts +2 -0
  64. package/dist/db/migrations.d.ts.map +1 -0
  65. package/dist/db/migrations.js +5 -0
  66. package/dist/db/migrations.js.map +1 -0
  67. package/dist/db/password-resets.d.ts +23 -0
  68. package/dist/db/password-resets.d.ts.map +1 -0
  69. package/dist/db/password-resets.js +37 -0
  70. package/dist/db/password-resets.js.map +1 -0
  71. package/dist/db/rate-limits.d.ts +18 -0
  72. package/dist/db/rate-limits.d.ts.map +1 -0
  73. package/dist/db/rate-limits.js +44 -0
  74. package/dist/db/rate-limits.js.map +1 -0
  75. package/dist/db/refresh-tokens.d.ts +44 -0
  76. package/dist/db/refresh-tokens.d.ts.map +1 -0
  77. package/dist/db/refresh-tokens.js +71 -0
  78. package/dist/db/refresh-tokens.js.map +1 -0
  79. package/dist/db/schema/common.d.ts +2 -0
  80. package/dist/db/schema/common.d.ts.map +1 -0
  81. package/dist/db/schema/common.js +4 -0
  82. package/dist/db/schema/common.js.map +1 -0
  83. package/dist/db/schema/email-verifications.d.ts +114 -0
  84. package/dist/db/schema/email-verifications.d.ts.map +1 -0
  85. package/dist/db/schema/email-verifications.js +35 -0
  86. package/dist/db/schema/email-verifications.js.map +1 -0
  87. package/dist/db/schema/outbox.d.ts +195 -0
  88. package/dist/db/schema/outbox.d.ts.map +1 -0
  89. package/dist/db/schema/outbox.js +5 -0
  90. package/dist/db/schema/outbox.js.map +1 -0
  91. package/dist/db/schema/password-resets.d.ts +114 -0
  92. package/dist/db/schema/password-resets.d.ts.map +1 -0
  93. package/dist/db/schema/password-resets.js +35 -0
  94. package/dist/db/schema/password-resets.js.map +1 -0
  95. package/dist/db/schema/rate-limits.d.ts +145 -0
  96. package/dist/db/schema/rate-limits.d.ts.map +1 -0
  97. package/dist/db/schema/rate-limits.js +25 -0
  98. package/dist/db/schema/rate-limits.js.map +1 -0
  99. package/dist/db/schema/refresh-tokens.d.ts +165 -0
  100. package/dist/db/schema/refresh-tokens.d.ts.map +1 -0
  101. package/dist/db/schema/refresh-tokens.js +47 -0
  102. package/dist/db/schema/refresh-tokens.js.map +1 -0
  103. package/dist/db/schema/users.d.ts +149 -0
  104. package/dist/db/schema/users.d.ts.map +1 -0
  105. package/dist/db/schema/users.js +40 -0
  106. package/dist/db/schema/users.js.map +1 -0
  107. package/dist/db/users.d.ts +28 -0
  108. package/dist/db/users.d.ts.map +1 -0
  109. package/dist/db/users.js +83 -0
  110. package/dist/db/users.js.map +1 -0
  111. package/dist/index.d.ts +14 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +52 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/metrics/index.d.ts +2 -0
  116. package/dist/metrics/index.d.ts.map +1 -0
  117. package/dist/metrics/index.js +3 -0
  118. package/dist/metrics/index.js.map +1 -0
  119. package/dist/metrics/instance.d.ts +16 -0
  120. package/dist/metrics/instance.d.ts.map +1 -0
  121. package/dist/metrics/instance.js +52 -0
  122. package/dist/metrics/instance.js.map +1 -0
  123. package/dist/presentation/auth/bearer-token-auth.d.ts +15 -0
  124. package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -0
  125. package/dist/presentation/auth/bearer-token-auth.js +31 -0
  126. package/dist/presentation/auth/bearer-token-auth.js.map +1 -0
  127. package/dist/presentation/auth/jwt-auth.d.ts +10 -0
  128. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -0
  129. package/dist/presentation/auth/jwt-auth.js +31 -0
  130. package/dist/presentation/auth/jwt-auth.js.map +1 -0
  131. package/dist/presentation/auth/lease-token-auth.d.ts +15 -0
  132. package/dist/presentation/auth/lease-token-auth.d.ts.map +1 -0
  133. package/dist/presentation/auth/lease-token-auth.js +27 -0
  134. package/dist/presentation/auth/lease-token-auth.js.map +1 -0
  135. package/dist/presentation/auth/refresh-cookie.d.ts +7 -0
  136. package/dist/presentation/auth/refresh-cookie.d.ts.map +1 -0
  137. package/dist/presentation/auth/refresh-cookie.js +29 -0
  138. package/dist/presentation/auth/refresh-cookie.js.map +1 -0
  139. package/dist/presentation/auth/runner-session-auth.d.ts +3 -0
  140. package/dist/presentation/auth/runner-session-auth.d.ts.map +1 -0
  141. package/dist/presentation/auth/runner-session-auth.js +16 -0
  142. package/dist/presentation/auth/runner-session-auth.js.map +1 -0
  143. package/dist/presentation/dto/user.d.ts +8 -0
  144. package/dist/presentation/dto/user.d.ts.map +1 -0
  145. package/dist/presentation/dto/user.js +19 -0
  146. package/dist/presentation/dto/user.js.map +1 -0
  147. package/dist/presentation/e2eRoutes/create-session.d.ts +2 -0
  148. package/dist/presentation/e2eRoutes/create-session.d.ts.map +1 -0
  149. package/dist/presentation/e2eRoutes/create-session.js +49 -0
  150. package/dist/presentation/e2eRoutes/create-session.js.map +1 -0
  151. package/dist/presentation/e2eRoutes/create-user.d.ts +2 -0
  152. package/dist/presentation/e2eRoutes/create-user.d.ts.map +1 -0
  153. package/dist/presentation/e2eRoutes/create-user.js +42 -0
  154. package/dist/presentation/e2eRoutes/create-user.js.map +1 -0
  155. package/dist/presentation/e2eRoutes/index.d.ts +3 -0
  156. package/dist/presentation/e2eRoutes/index.d.ts.map +1 -0
  157. package/dist/presentation/e2eRoutes/index.js +15 -0
  158. package/dist/presentation/e2eRoutes/index.js.map +1 -0
  159. package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts +2 -0
  160. package/dist/presentation/routes/email-verification/verify-email-confirm.d.ts.map +1 -0
  161. package/dist/presentation/routes/email-verification/verify-email-confirm.js +35 -0
  162. package/dist/presentation/routes/email-verification/verify-email-confirm.js.map +1 -0
  163. package/dist/presentation/routes/email-verification/verify-email-resend.d.ts +2 -0
  164. package/dist/presentation/routes/email-verification/verify-email-resend.d.ts.map +1 -0
  165. package/dist/presentation/routes/email-verification/verify-email-resend.js +27 -0
  166. package/dist/presentation/routes/email-verification/verify-email-resend.js.map +1 -0
  167. package/dist/presentation/routes/index.d.ts +3 -0
  168. package/dist/presentation/routes/index.d.ts.map +1 -0
  169. package/dist/presentation/routes/index.js +31 -0
  170. package/dist/presentation/routes/index.js.map +1 -0
  171. package/dist/presentation/routes/password/change-password.d.ts +2 -0
  172. package/dist/presentation/routes/password/change-password.d.ts.map +1 -0
  173. package/dist/presentation/routes/password/change-password.js +50 -0
  174. package/dist/presentation/routes/password/change-password.js.map +1 -0
  175. package/dist/presentation/routes/password/password-reset-confirm.d.ts +2 -0
  176. package/dist/presentation/routes/password/password-reset-confirm.d.ts.map +1 -0
  177. package/dist/presentation/routes/password/password-reset-confirm.js +36 -0
  178. package/dist/presentation/routes/password/password-reset-confirm.js.map +1 -0
  179. package/dist/presentation/routes/password/password-reset-request.d.ts +2 -0
  180. package/dist/presentation/routes/password/password-reset-request.d.ts.map +1 -0
  181. package/dist/presentation/routes/password/password-reset-request.js +26 -0
  182. package/dist/presentation/routes/password/password-reset-request.js.map +1 -0
  183. package/dist/presentation/routes/rate-limit.d.ts +10 -0
  184. package/dist/presentation/routes/rate-limit.d.ts.map +1 -0
  185. package/dist/presentation/routes/rate-limit.js +101 -0
  186. package/dist/presentation/routes/rate-limit.js.map +1 -0
  187. package/dist/presentation/routes/registration/signup.d.ts +2 -0
  188. package/dist/presentation/routes/registration/signup.d.ts.map +1 -0
  189. package/dist/presentation/routes/registration/signup.js +82 -0
  190. package/dist/presentation/routes/registration/signup.js.map +1 -0
  191. package/dist/presentation/routes/session/login.d.ts +2 -0
  192. package/dist/presentation/routes/session/login.d.ts.map +1 -0
  193. package/dist/presentation/routes/session/login.js +48 -0
  194. package/dist/presentation/routes/session/login.js.map +1 -0
  195. package/dist/presentation/routes/session/logout.d.ts +2 -0
  196. package/dist/presentation/routes/session/logout.d.ts.map +1 -0
  197. package/dist/presentation/routes/session/logout.js +25 -0
  198. package/dist/presentation/routes/session/logout.js.map +1 -0
  199. package/dist/presentation/routes/session/me.d.ts +2 -0
  200. package/dist/presentation/routes/session/me.d.ts.map +1 -0
  201. package/dist/presentation/routes/session/me.js +42 -0
  202. package/dist/presentation/routes/session/me.js.map +1 -0
  203. package/dist/presentation/routes/session/refresh.d.ts +2 -0
  204. package/dist/presentation/routes/session/refresh.d.ts.map +1 -0
  205. package/dist/presentation/routes/session/refresh.js +49 -0
  206. package/dist/presentation/routes/session/refresh.js.map +1 -0
  207. package/dist/presentation/subscribers/index.d.ts +3 -0
  208. package/dist/presentation/subscribers/index.d.ts.map +1 -0
  209. package/dist/presentation/subscribers/index.js +4 -0
  210. package/dist/presentation/subscribers/index.js.map +1 -0
  211. package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts +3 -0
  212. package/dist/presentation/subscribers/on-email-verification-send-requested.d.ts.map +1 -0
  213. package/dist/presentation/subscribers/on-email-verification-send-requested.js +13 -0
  214. package/dist/presentation/subscribers/on-email-verification-send-requested.js.map +1 -0
  215. package/dist/presentation/subscribers/on-password-reset-send-requested.d.ts +3 -0
  216. package/dist/presentation/subscribers/on-password-reset-send-requested.d.ts.map +1 -0
  217. package/dist/presentation/subscribers/on-password-reset-send-requested.js +14 -0
  218. package/dist/presentation/subscribers/on-password-reset-send-requested.js.map +1 -0
  219. package/dist/tsconfig.test.tsbuildinfo +1 -0
  220. package/drizzle/0000_initial.sql +81 -0
  221. package/drizzle/meta/0000_snapshot.json +643 -0
  222. package/drizzle/meta/_journal.json +13 -0
  223. package/drizzle.config.ts +7 -0
  224. package/package.json +88 -0
  225. package/src/config.ts +88 -0
  226. package/src/core/auth.test.ts +675 -0
  227. package/src/core/auth.ts +560 -0
  228. package/src/core/entities/email-verification.ts +8 -0
  229. package/src/core/entities/password-reset.ts +8 -0
  230. package/src/core/entities/refresh-token.ts +11 -0
  231. package/src/core/entities/user.ts +12 -0
  232. package/src/core/errors.ts +123 -0
  233. package/src/core/job-lease-token.test.ts +201 -0
  234. package/src/core/job-lease-token.ts +83 -0
  235. package/src/core/jwt.test.ts +180 -0
  236. package/src/core/jwt.ts +66 -0
  237. package/src/core/password.test.ts +28 -0
  238. package/src/core/password.ts +21 -0
  239. package/src/core/rate-limit.test.ts +462 -0
  240. package/src/core/rate-limit.ts +130 -0
  241. package/src/core/runner-session-token.test.ts +126 -0
  242. package/src/core/runner-session-token.ts +49 -0
  243. package/src/db/db.ts +28 -0
  244. package/src/db/email-verifications.test.ts +61 -0
  245. package/src/db/email-verifications.ts +160 -0
  246. package/src/db/migrations.ts +4 -0
  247. package/src/db/password-resets.test.ts +61 -0
  248. package/src/db/password-resets.ts +69 -0
  249. package/src/db/rate-limits.ts +69 -0
  250. package/src/db/refresh-tokens.test.ts +164 -0
  251. package/src/db/refresh-tokens.ts +149 -0
  252. package/src/db/schema/common.ts +3 -0
  253. package/src/db/schema/email-verifications.ts +37 -0
  254. package/src/db/schema/outbox.ts +4 -0
  255. package/src/db/schema/password-resets.ts +37 -0
  256. package/src/db/schema/rate-limits.ts +27 -0
  257. package/src/db/schema/refresh-tokens.ts +43 -0
  258. package/src/db/schema/users.ts +37 -0
  259. package/src/db/users.test.ts +75 -0
  260. package/src/db/users.ts +121 -0
  261. package/src/index.test.ts +39 -0
  262. package/src/index.ts +69 -0
  263. package/src/metrics/index.ts +13 -0
  264. package/src/metrics/instance.test.ts +69 -0
  265. package/src/metrics/instance.ts +77 -0
  266. package/src/presentation/auth/bearer-token-auth.test.ts +99 -0
  267. package/src/presentation/auth/bearer-token-auth.ts +51 -0
  268. package/src/presentation/auth/jwt-auth.test.ts +136 -0
  269. package/src/presentation/auth/jwt-auth.ts +39 -0
  270. package/src/presentation/auth/lease-token-auth.test.ts +56 -0
  271. package/src/presentation/auth/lease-token-auth.ts +25 -0
  272. package/src/presentation/auth/refresh-cookie.test.ts +39 -0
  273. package/src/presentation/auth/refresh-cookie.ts +34 -0
  274. package/src/presentation/auth/runner-session-auth.test.ts +54 -0
  275. package/src/presentation/auth/runner-session-auth.ts +13 -0
  276. package/src/presentation/dto/user.ts +21 -0
  277. package/src/presentation/e2eRoutes/create-session.ts +39 -0
  278. package/src/presentation/e2eRoutes/create-user.ts +38 -0
  279. package/src/presentation/e2eRoutes/index.test.ts +102 -0
  280. package/src/presentation/e2eRoutes/index.ts +10 -0
  281. package/src/presentation/routes/email-verification/verify-email-confirm.test.ts +66 -0
  282. package/src/presentation/routes/email-verification/verify-email-confirm.ts +37 -0
  283. package/src/presentation/routes/email-verification/verify-email-resend.test.ts +100 -0
  284. package/src/presentation/routes/email-verification/verify-email-resend.ts +26 -0
  285. package/src/presentation/routes/index.ts +29 -0
  286. package/src/presentation/routes/password/change-password.test.ts +106 -0
  287. package/src/presentation/routes/password/change-password.ts +45 -0
  288. package/src/presentation/routes/password/password-reset-confirm.test.ts +63 -0
  289. package/src/presentation/routes/password/password-reset-confirm.ts +35 -0
  290. package/src/presentation/routes/password/password-reset-request.test.ts +56 -0
  291. package/src/presentation/routes/password/password-reset-request.ts +25 -0
  292. package/src/presentation/routes/rate-limit.test.ts +341 -0
  293. package/src/presentation/routes/rate-limit.ts +119 -0
  294. package/src/presentation/routes/registration/signup.test.ts +284 -0
  295. package/src/presentation/routes/registration/signup.ts +81 -0
  296. package/src/presentation/routes/session/login.test.ts +77 -0
  297. package/src/presentation/routes/session/login.ts +50 -0
  298. package/src/presentation/routes/session/logout.test.ts +55 -0
  299. package/src/presentation/routes/session/logout.ts +23 -0
  300. package/src/presentation/routes/session/me.test.ts +56 -0
  301. package/src/presentation/routes/session/me.ts +35 -0
  302. package/src/presentation/routes/session/refresh.test.ts +89 -0
  303. package/src/presentation/routes/session/refresh.ts +54 -0
  304. package/src/presentation/subscribers/email-send-requested.test.ts +77 -0
  305. package/src/presentation/subscribers/index.ts +2 -0
  306. package/src/presentation/subscribers/on-email-verification-send-requested.ts +10 -0
  307. package/src/presentation/subscribers/on-password-reset-send-requested.ts +13 -0
  308. package/test/env.ts +10 -0
  309. package/test/factories/index.ts +1 -0
  310. package/test/factories/user.ts +36 -0
  311. package/test/globalSetup.ts +18 -0
  312. package/test/index.ts +1 -0
  313. package/test/routes.ts +275 -0
  314. package/test/setup.ts +17 -0
  315. package/tsconfig.build.json +9 -0
  316. package/tsconfig.build.tsbuildinfo +1 -0
  317. package/tsconfig.json +4 -0
  318. package/tsconfig.test.json +8 -0
  319. package/vitest.config.ts +11 -0
@@ -0,0 +1,13 @@
1
+ import {AUTH_RUNNER_SESSION, setRunnerSessionContext} from '@shipfox/api-auth-context';
2
+ import type {AuthMethod} from '@shipfox/node-fastify';
3
+ import {verifyRunnerSessionToken} from '#core/runner-session-token.js';
4
+ import {createBearerTokenAuthMethod} from './bearer-token-auth.js';
5
+
6
+ export function createRunnerSessionAuthMethod(): AuthMethod {
7
+ return createBearerTokenAuthMethod({
8
+ name: AUTH_RUNNER_SESSION,
9
+ verifyToken: verifyRunnerSessionToken,
10
+ invalidTokenError: {message: 'Invalid or expired runner session token', code: 'unauthorized'},
11
+ setContext: setRunnerSessionContext,
12
+ });
13
+ }
@@ -0,0 +1,21 @@
1
+ import type {LoginResponseDto, UserDto} from '@shipfox/api-auth-dto';
2
+ import type {User} from '#core/entities/user.js';
3
+
4
+ export function toUserDto(user: User): UserDto {
5
+ return {
6
+ id: user.id,
7
+ email: user.email,
8
+ name: user.name,
9
+ email_verified_at: user.emailVerifiedAt ? user.emailVerifiedAt.toISOString() : null,
10
+ status: user.status,
11
+ created_at: user.createdAt.toISOString(),
12
+ updated_at: user.updatedAt.toISOString(),
13
+ };
14
+ }
15
+
16
+ export function toAuthSessionDto(result: {token: string; user: User}): LoginResponseDto {
17
+ return {
18
+ token: result.token,
19
+ user: toUserDto(result.user),
20
+ };
21
+ }
@@ -0,0 +1,39 @@
1
+ import {e2eCreateSessionBodySchema, e2eCreateSessionResponseSchema} from '@shipfox/api-auth-dto';
2
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
3
+ import {createSessionForUser} from '#core/auth.js';
4
+ import {EmailNotVerifiedError, InvalidCredentialsError, UserNotFoundError} from '#core/errors.js';
5
+ import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
6
+ import {toAuthSessionDto} from '#presentation/dto/user.js';
7
+
8
+ export const createE2eSessionRoute = defineRoute({
9
+ method: 'POST',
10
+ path: '/sessions',
11
+ description: 'Create an authenticated browser session for E2E tests.',
12
+ schema: {
13
+ body: e2eCreateSessionBodySchema,
14
+ response: {
15
+ 200: e2eCreateSessionResponseSchema,
16
+ },
17
+ },
18
+ errorHandler: (error) => {
19
+ if (error instanceof UserNotFoundError) {
20
+ throw new ClientError('User not found', 'user-not-found', {status: 404});
21
+ }
22
+ if (error instanceof EmailNotVerifiedError) {
23
+ throw new ClientError('Email not verified', 'email-not-verified', {status: 403});
24
+ }
25
+ if (error instanceof InvalidCredentialsError) {
26
+ throw new ClientError('User is not active', 'user-inactive', {status: 409});
27
+ }
28
+ throw error;
29
+ },
30
+ handler: async (request, reply) => {
31
+ const result = await createSessionForUser({
32
+ ...(request.body.user_id ? {userId: request.body.user_id} : {}),
33
+ ...(request.body.email ? {email: request.body.email} : {}),
34
+ });
35
+
36
+ setRefreshTokenCookie(reply, result.refreshToken);
37
+ return toAuthSessionDto(result);
38
+ },
39
+ });
@@ -0,0 +1,38 @@
1
+ import {e2eCreateUserBodySchema, e2eCreateUserResponseSchema} from '@shipfox/api-auth-dto';
2
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
3
+ import {createUser} from '#core/auth.js';
4
+ import {EmailTakenError} from '#core/errors.js';
5
+ import {toUserDto} from '#presentation/dto/user.js';
6
+
7
+ export const createE2eUserRoute = defineRoute({
8
+ method: 'POST',
9
+ path: '/users',
10
+ description: 'Create a user for E2E tests.',
11
+ schema: {
12
+ body: e2eCreateUserBodySchema,
13
+ response: {
14
+ 201: e2eCreateUserResponseSchema,
15
+ },
16
+ },
17
+ errorHandler: (error) => {
18
+ if (error instanceof EmailTakenError) {
19
+ throw new ClientError('Email already registered', 'email-taken', {status: 409});
20
+ }
21
+ throw error;
22
+ },
23
+ handler: async (request, reply) => {
24
+ const user = await createUser({
25
+ email: request.body.email,
26
+ password: request.body.password,
27
+ verified: request.body.verified,
28
+ ...(request.body.name ? {name: request.body.name} : {}),
29
+ });
30
+
31
+ reply.code(201);
32
+ return {
33
+ user: toUserDto(user),
34
+ email: user.email,
35
+ password: request.body.password,
36
+ };
37
+ },
38
+ });
@@ -0,0 +1,102 @@
1
+ import {createApp} from '@shipfox/node-fastify';
2
+ import type {FastifyInstance} from 'fastify';
3
+ import {authE2eRoutes} from './index.js';
4
+
5
+ vi.mock('@shipfox/api-workspaces', () => ({
6
+ listMembershipsByUser: vi.fn(() => Promise.resolve([])),
7
+ }));
8
+
9
+ describe('auth E2E routes', () => {
10
+ let app: FastifyInstance;
11
+
12
+ beforeAll(async () => {
13
+ app = await createApp({
14
+ routes: [authE2eRoutes],
15
+ swagger: false,
16
+ });
17
+ });
18
+
19
+ afterAll(async () => {
20
+ await app.close();
21
+ });
22
+
23
+ test('creates a verified user from explicit credentials', async () => {
24
+ const password = 'correct horse battery staple';
25
+ const res = await app.inject({
26
+ method: 'POST',
27
+ url: '/auth/users',
28
+ payload: {email: `user-${crypto.randomUUID()}@example.test`, password, verified: true},
29
+ });
30
+
31
+ expect(res.statusCode).toBe(201);
32
+ expect(res.json().email).toBe(res.json().user.email);
33
+ expect(res.json().password).toBe(password);
34
+ expect(res.json().user.email_verified_at).not.toBeNull();
35
+ });
36
+
37
+ test('rejects missing credentials', async () => {
38
+ const res = await app.inject({
39
+ method: 'POST',
40
+ url: '/auth/users',
41
+ payload: {},
42
+ });
43
+
44
+ expect(res.statusCode).toBe(400);
45
+ });
46
+
47
+ test('transforms duplicate email into 409', async () => {
48
+ const email = `duplicate-${crypto.randomUUID()}@example.test`;
49
+ await app.inject({
50
+ method: 'POST',
51
+ url: '/auth/users',
52
+ payload: {email, password: 'correct horse battery staple', verified: true},
53
+ });
54
+
55
+ const res = await app.inject({
56
+ method: 'POST',
57
+ url: '/auth/users',
58
+ payload: {email, password: 'correct horse battery staple', verified: true},
59
+ });
60
+
61
+ expect(res.statusCode).toBe(409);
62
+ expect(res.json().code).toBe('email-taken');
63
+ });
64
+
65
+ test('creates a browser session for an E2E-created user', async () => {
66
+ const userRes = await app.inject({
67
+ method: 'POST',
68
+ url: '/auth/users',
69
+ payload: {
70
+ email: `session-${crypto.randomUUID()}@example.test`,
71
+ password: 'correct horse battery staple',
72
+ verified: true,
73
+ },
74
+ });
75
+
76
+ const res = await app.inject({
77
+ method: 'POST',
78
+ url: '/auth/sessions',
79
+ payload: {user_id: userRes.json().user.id},
80
+ });
81
+
82
+ expect(res.statusCode).toBe(200);
83
+ expect(res.json().user.id).toBe(userRes.json().user.id);
84
+ expect(res.json().token).toBeDefined();
85
+ expect(res.headers['set-cookie']).toContain('shipfox_refresh_token=');
86
+ });
87
+
88
+ test('creates an unverified user', async () => {
89
+ const res = await app.inject({
90
+ method: 'POST',
91
+ url: '/auth/users',
92
+ payload: {
93
+ email: `unverified-${crypto.randomUUID()}@example.test`,
94
+ password: 'correct horse battery staple',
95
+ verified: false,
96
+ },
97
+ });
98
+
99
+ expect(res.statusCode).toBe(201);
100
+ expect(res.json().user.email_verified_at).toBeNull();
101
+ });
102
+ });
@@ -0,0 +1,10 @@
1
+ import cookie from '@fastify/cookie';
2
+ import type {RouteGroup} from '@shipfox/node-fastify';
3
+ import {createE2eSessionRoute} from './create-session.js';
4
+ import {createE2eUserRoute} from './create-user.js';
5
+
6
+ export const authE2eRoutes: RouteGroup = {
7
+ prefix: '/auth',
8
+ plugins: [cookie],
9
+ routes: [createE2eUserRoute, createE2eSessionRoute],
10
+ };
@@ -0,0 +1,66 @@
1
+ import {AUTH_EMAIL_VERIFICATION_SEND_REQUESTED} from '@shipfox/api-auth-dto';
2
+ import type {FastifyInstance} from 'fastify';
3
+ import {
4
+ cookieHeader,
5
+ createAuthTestApp,
6
+ extractToken,
7
+ getSetCookie,
8
+ latestEmailLinkTo,
9
+ resetCapturedMail,
10
+ signup,
11
+ uniqueEmail,
12
+ } from '#test/routes.js';
13
+
14
+ describe('POST /auth/verify-email/confirm', () => {
15
+ let app: FastifyInstance;
16
+
17
+ beforeAll(async () => {
18
+ app = await createAuthTestApp();
19
+ });
20
+
21
+ beforeEach(() => {
22
+ resetCapturedMail();
23
+ });
24
+
25
+ afterAll(async () => {
26
+ await app.close();
27
+ });
28
+
29
+ test('verifies a user email with a valid token and starts a session', async () => {
30
+ const email = uniqueEmail('verify-confirm');
31
+ const password = 'correct horse battery staple';
32
+ await signup(app, {email, password});
33
+ const token = extractToken(
34
+ await latestEmailLinkTo(email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED),
35
+ );
36
+
37
+ const confirm = await app.inject({
38
+ method: 'POST',
39
+ url: '/auth/verify-email/confirm',
40
+ payload: {token},
41
+ });
42
+ const refreshCookie = getSetCookie(confirm);
43
+ const refresh = await app.inject({
44
+ method: 'POST',
45
+ url: '/auth/refresh',
46
+ headers: {cookie: cookieHeader(refreshCookie)},
47
+ });
48
+
49
+ expect(confirm.statusCode).toBe(200);
50
+ expect(confirm.json().token).toEqual(expect.any(String));
51
+ expect(confirm.json().user.email).toBe(email);
52
+ expect(refreshCookie).toContain('shipfox_refresh_token=');
53
+ expect(refresh.statusCode).toBe(200);
54
+ });
55
+
56
+ test('transforms invalid token into 410', async () => {
57
+ const res = await app.inject({
58
+ method: 'POST',
59
+ url: '/auth/verify-email/confirm',
60
+ payload: {token: 'bad-token'},
61
+ });
62
+
63
+ expect(res.statusCode).toBe(410);
64
+ expect(res.json().code).toBe('token-invalid');
65
+ });
66
+ });
@@ -0,0 +1,37 @@
1
+ import {
2
+ verifyEmailConfirmBodySchema,
3
+ verifyEmailConfirmResponseSchema,
4
+ } from '@shipfox/api-auth-dto';
5
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
6
+ import {confirmEmailVerification} from '#core/auth.js';
7
+ import {TokenInvalidError} from '#core/errors.js';
8
+ import {setRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
9
+ import {toAuthSessionDto} from '#presentation/dto/user.js';
10
+
11
+ export const verifyEmailConfirmRoute = defineRoute({
12
+ method: 'POST',
13
+ path: '/verify-email/confirm',
14
+ description: 'Verify a user email address with the link sent by email.',
15
+ schema: {
16
+ body: verifyEmailConfirmBodySchema,
17
+ response: {
18
+ 200: verifyEmailConfirmResponseSchema,
19
+ },
20
+ },
21
+ errorHandler: (error) => {
22
+ if (error instanceof TokenInvalidError) {
23
+ throw new ClientError('Verification token is invalid or expired', 'token-invalid', {
24
+ status: 410,
25
+ });
26
+ }
27
+ throw error;
28
+ },
29
+ handler: async (request, reply) => {
30
+ const {token} = request.body;
31
+
32
+ const result = await confirmEmailVerification({token});
33
+ setRefreshTokenCookie(reply, result.refreshToken);
34
+
35
+ return toAuthSessionDto(result);
36
+ },
37
+ });
@@ -0,0 +1,100 @@
1
+ import {
2
+ AUTH_EMAIL_VERIFICATION_SEND_REQUESTED,
3
+ EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS,
4
+ } from '@shipfox/api-auth-dto';
5
+ import {eq} from 'drizzle-orm';
6
+ import type {FastifyInstance} from 'fastify';
7
+ import {db} from '#db/db.js';
8
+ import {emailVerifications} from '#db/schema/email-verifications.js';
9
+ import {
10
+ createAuthTestApp,
11
+ outboxEventsTo,
12
+ resetCapturedMail,
13
+ signup,
14
+ uniqueEmail,
15
+ verifyEmail,
16
+ } from '#test/routes.js';
17
+
18
+ describe('POST /auth/verify-email/resend', () => {
19
+ let app: FastifyInstance;
20
+
21
+ beforeAll(async () => {
22
+ app = await createAuthTestApp();
23
+ });
24
+
25
+ beforeEach(() => {
26
+ resetCapturedMail();
27
+ });
28
+
29
+ afterAll(async () => {
30
+ await app.close();
31
+ });
32
+
33
+ test('returns 200 without sending during cooldown for an unverified account', async () => {
34
+ const email = uniqueEmail('verify-resend');
35
+ await signup(app, {email, password: 'correct horse battery staple'});
36
+
37
+ const res = await app.inject({
38
+ method: 'POST',
39
+ url: '/auth/verify-email/resend',
40
+ payload: {email: email.toUpperCase()},
41
+ });
42
+
43
+ expect(res.statusCode).toBe(200);
44
+ expect(res.json().next_resend_available_at).toEqual(expect.any(String));
45
+ expect(await outboxEventsTo(email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(1);
46
+ });
47
+
48
+ test('returns 200 and sends a new token after cooldown', async () => {
49
+ const email = uniqueEmail('verify-resend-ready');
50
+ const signupRes = await signup(app, {email, password: 'correct horse battery staple'});
51
+ const staleCreatedAt = new Date(
52
+ Date.now() - (EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS + 1) * 1000,
53
+ );
54
+ await db()
55
+ .update(emailVerifications)
56
+ .set({createdAt: staleCreatedAt})
57
+ .where(eq(emailVerifications.userId, signupRes.json().user.id));
58
+
59
+ const res = await app.inject({
60
+ method: 'POST',
61
+ url: '/auth/verify-email/resend',
62
+ payload: {email},
63
+ });
64
+
65
+ expect(res.statusCode).toBe(200);
66
+ expect(res.json().next_resend_available_at).toEqual(expect.any(String));
67
+ expect(await outboxEventsTo(email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(2);
68
+ });
69
+
70
+ test('returns 200 without sending mail for a verified account', async () => {
71
+ const email = uniqueEmail('verify-resend-verified');
72
+ await signup(app, {email, password: 'correct horse battery staple'});
73
+ await verifyEmail(app, email);
74
+ resetCapturedMail();
75
+
76
+ const res = await app.inject({
77
+ method: 'POST',
78
+ url: '/auth/verify-email/resend',
79
+ payload: {email},
80
+ });
81
+
82
+ expect(res.statusCode).toBe(200);
83
+ expect(res.json().next_resend_available_at).toEqual(expect.any(String));
84
+ expect(await outboxEventsTo(email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(1);
85
+ });
86
+
87
+ test('returns the same response shape for a missing account', async () => {
88
+ const email = uniqueEmail('verify-resend-missing');
89
+
90
+ const res = await app.inject({
91
+ method: 'POST',
92
+ url: '/auth/verify-email/resend',
93
+ payload: {email},
94
+ });
95
+
96
+ expect(res.statusCode).toBe(200);
97
+ expect(res.json()).toEqual({next_resend_available_at: expect.any(String)});
98
+ expect(await outboxEventsTo(email, AUTH_EMAIL_VERIFICATION_SEND_REQUESTED)).toHaveLength(0);
99
+ });
100
+ });
@@ -0,0 +1,26 @@
1
+ import {verifyEmailResendBodySchema, verifyEmailResendResponseSchema} from '@shipfox/api-auth-dto';
2
+ import {defineRoute} from '@shipfox/node-fastify';
3
+ import {resendEmailVerification} from '#core/auth.js';
4
+ import {createAuthRateLimitPreHandler} from '#presentation/routes/rate-limit.js';
5
+
6
+ export const verifyEmailResendRoute = defineRoute({
7
+ method: 'POST',
8
+ path: '/verify-email/resend',
9
+ description: 'Send a new email verification link if the account can be verified.',
10
+ schema: {
11
+ body: verifyEmailResendBodySchema,
12
+ response: {
13
+ 200: verifyEmailResendResponseSchema,
14
+ },
15
+ },
16
+ preHandler: createAuthRateLimitPreHandler('email-send'),
17
+ handler: async (request, reply) => {
18
+ const {email} = request.body;
19
+
20
+ const result = await resendEmailVerification({email});
21
+
22
+ reply.code(200).send({
23
+ next_resend_available_at: result.nextResendAvailableAt.toISOString(),
24
+ });
25
+ },
26
+ });
@@ -0,0 +1,29 @@
1
+ import type {RouteGroup} from '@shipfox/node-fastify';
2
+ import {authCookiePlugin} from '#presentation/auth/refresh-cookie.js';
3
+ import {verifyEmailConfirmRoute} from './email-verification/verify-email-confirm.js';
4
+ import {verifyEmailResendRoute} from './email-verification/verify-email-resend.js';
5
+ import {changePasswordRoute} from './password/change-password.js';
6
+ import {passwordResetConfirmRoute} from './password/password-reset-confirm.js';
7
+ import {passwordResetRequestRoute} from './password/password-reset-request.js';
8
+ import {signupRoute} from './registration/signup.js';
9
+ import {loginRoute} from './session/login.js';
10
+ import {logoutRoute} from './session/logout.js';
11
+ import {meRoute} from './session/me.js';
12
+ import {refreshRoute} from './session/refresh.js';
13
+
14
+ export const authRoutes: RouteGroup = {
15
+ prefix: '/auth',
16
+ plugins: [authCookiePlugin],
17
+ routes: [
18
+ signupRoute,
19
+ verifyEmailConfirmRoute,
20
+ verifyEmailResendRoute,
21
+ loginRoute,
22
+ refreshRoute,
23
+ logoutRoute,
24
+ meRoute,
25
+ changePasswordRoute,
26
+ passwordResetRequestRoute,
27
+ passwordResetConfirmRoute,
28
+ ],
29
+ };
@@ -0,0 +1,106 @@
1
+ import type {FastifyInstance} from 'fastify';
2
+ import {signUserToken} from '#core/jwt.js';
3
+ import {verifyPassword} from '#core/password.js';
4
+ import {findUserByEmail} from '#db/users.js';
5
+ import {
6
+ cookieHeader,
7
+ createAuthTestApp,
8
+ createVerifiedSession,
9
+ ROUTE_TEST_SECRET,
10
+ resetCapturedMail,
11
+ signupVerifyLogin,
12
+ } from '#test/routes.js';
13
+
14
+ describe('POST /auth/change-password', () => {
15
+ let app: FastifyInstance;
16
+
17
+ beforeAll(async () => {
18
+ app = await createAuthTestApp();
19
+ });
20
+
21
+ beforeEach(() => {
22
+ resetCapturedMail();
23
+ });
24
+
25
+ afterAll(async () => {
26
+ await app.close();
27
+ });
28
+
29
+ test('updates credentials and rejects the old password', async () => {
30
+ const account = await createVerifiedSession('change-password');
31
+
32
+ const change = await app.inject({
33
+ method: 'POST',
34
+ url: '/auth/change-password',
35
+ headers: {
36
+ authorization: `Bearer ${account.token}`,
37
+ cookie: cookieHeader(account.refreshCookie),
38
+ },
39
+ payload: {
40
+ current_password: account.password,
41
+ new_password: 'new password is long',
42
+ },
43
+ });
44
+ const updatedUser = await findUserByEmail({email: account.email});
45
+ const refresh = await app.inject({
46
+ method: 'POST',
47
+ url: '/auth/refresh',
48
+ headers: {cookie: cookieHeader(account.refreshCookie)},
49
+ payload: {},
50
+ });
51
+ const oldPasswordValid = await verifyPassword({
52
+ password: account.password,
53
+ hash: updatedUser?.hashedPassword ?? '',
54
+ });
55
+ const newPasswordValid = await verifyPassword({
56
+ password: 'new password is long',
57
+ hash: updatedUser?.hashedPassword ?? '',
58
+ });
59
+
60
+ expect(change.statusCode).toBe(204);
61
+ expect(updatedUser).toBeDefined();
62
+ expect(oldPasswordValid).toBe(false);
63
+ expect(newPasswordValid).toBe(true);
64
+ expect(refresh.statusCode).toBe(200);
65
+ });
66
+
67
+ test('transforms invalid current password into 401', async () => {
68
+ const account = await signupVerifyLogin(app, 'change-password-bad');
69
+
70
+ const res = await app.inject({
71
+ method: 'POST',
72
+ url: '/auth/change-password',
73
+ headers: {authorization: `Bearer ${account.token}`},
74
+ payload: {
75
+ current_password: 'not the current password',
76
+ new_password: 'new password is long',
77
+ },
78
+ });
79
+
80
+ expect(res.statusCode).toBe(401);
81
+ expect(res.json().code).toBe('invalid-credentials');
82
+ });
83
+
84
+ test('transforms a token for a missing user into 404', async () => {
85
+ const token = await signUserToken({
86
+ userId: crypto.randomUUID(),
87
+ email: 'missing@example.com',
88
+ memberships: [],
89
+ secret: ROUTE_TEST_SECRET,
90
+ expiresIn: '15m',
91
+ });
92
+
93
+ const res = await app.inject({
94
+ method: 'POST',
95
+ url: '/auth/change-password',
96
+ headers: {authorization: `Bearer ${token}`},
97
+ payload: {
98
+ current_password: 'old password',
99
+ new_password: 'new password is long',
100
+ },
101
+ });
102
+
103
+ expect(res.statusCode).toBe(404);
104
+ expect(res.json().code).toBe('not-found');
105
+ });
106
+ });
@@ -0,0 +1,45 @@
1
+ import {AUTH_USER} from '@shipfox/api-auth-context';
2
+ import {changePasswordBodySchema} from '@shipfox/api-auth-dto';
3
+ import {ClientError, defineRoute} from '@shipfox/node-fastify';
4
+ import {z} from 'zod';
5
+ import {changePassword} from '#core/auth.js';
6
+ import {InvalidCredentialsError, UserNotFoundError} from '#core/errors.js';
7
+ import {getClientContext} from '#presentation/auth/jwt-auth.js';
8
+ import {getRefreshTokenCookie} from '#presentation/auth/refresh-cookie.js';
9
+
10
+ export const changePasswordRoute = defineRoute({
11
+ method: 'POST',
12
+ path: '/change-password',
13
+ description: 'Change the signed-in user password.',
14
+ auth: AUTH_USER,
15
+ schema: {
16
+ body: changePasswordBodySchema,
17
+ response: {
18
+ 204: z.void(),
19
+ },
20
+ },
21
+ errorHandler: (error) => {
22
+ if (error instanceof UserNotFoundError) {
23
+ throw new ClientError('User not found', 'not-found', {status: 404});
24
+ }
25
+ if (error instanceof InvalidCredentialsError) {
26
+ throw new ClientError('Invalid credentials', 'invalid-credentials', {status: 401});
27
+ }
28
+ throw error;
29
+ },
30
+ handler: async (request, reply) => {
31
+ const client = getClientContext(request);
32
+ if (!client) {
33
+ throw new ClientError('Authentication required', 'unauthorized', {status: 401});
34
+ }
35
+
36
+ await changePassword({
37
+ userId: client.userId,
38
+ currentPassword: request.body.current_password,
39
+ newPassword: request.body.new_password,
40
+ refreshToken: getRefreshTokenCookie(request),
41
+ });
42
+
43
+ reply.code(204);
44
+ },
45
+ });