@mdguggenbichler/slugbase-core 0.0.1

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 (381) hide show
  1. package/backend/dist/app-factory.d.ts +17 -0
  2. package/backend/dist/app-factory.d.ts.map +1 -0
  3. package/backend/dist/app-factory.js +106 -0
  4. package/backend/dist/app-factory.js.map +1 -0
  5. package/backend/dist/auth/authorization.d.ts +25 -0
  6. package/backend/dist/auth/authorization.d.ts.map +1 -0
  7. package/backend/dist/auth/authorization.js +100 -0
  8. package/backend/dist/auth/authorization.js.map +1 -0
  9. package/backend/dist/auth/jwt.d.ts +5 -0
  10. package/backend/dist/auth/jwt.d.ts.map +1 -0
  11. package/backend/dist/auth/jwt.js +34 -0
  12. package/backend/dist/auth/jwt.js.map +1 -0
  13. package/backend/dist/auth/oidc.d.ts +4 -0
  14. package/backend/dist/auth/oidc.d.ts.map +1 -0
  15. package/backend/dist/auth/oidc.js +201 -0
  16. package/backend/dist/auth/oidc.js.map +1 -0
  17. package/backend/dist/config/cloud-providers.d.ts +18 -0
  18. package/backend/dist/config/cloud-providers.d.ts.map +1 -0
  19. package/backend/dist/config/cloud-providers.js +60 -0
  20. package/backend/dist/config/cloud-providers.js.map +1 -0
  21. package/backend/dist/config/cookies.d.ts +17 -0
  22. package/backend/dist/config/cookies.d.ts.map +1 -0
  23. package/backend/dist/config/cookies.js +26 -0
  24. package/backend/dist/config/cookies.js.map +1 -0
  25. package/backend/dist/config/mode.d.ts +7 -0
  26. package/backend/dist/config/mode.d.ts.map +1 -0
  27. package/backend/dist/config/mode.js +7 -0
  28. package/backend/dist/config/mode.js.map +1 -0
  29. package/backend/dist/config/swagger.d.ts +2 -0
  30. package/backend/dist/config/swagger.d.ts.map +1 -0
  31. package/backend/dist/config/swagger.js +76 -0
  32. package/backend/dist/config/swagger.js.map +1 -0
  33. package/backend/dist/config.d.ts +29 -0
  34. package/backend/dist/config.d.ts.map +1 -0
  35. package/backend/dist/config.js +41 -0
  36. package/backend/dist/config.js.map +1 -0
  37. package/backend/dist/db/index.d.ts +14 -0
  38. package/backend/dist/db/index.d.ts.map +1 -0
  39. package/backend/dist/db/index.js +114 -0
  40. package/backend/dist/db/index.js.map +1 -0
  41. package/backend/dist/db/migrate-slug-nullable.d.ts +10 -0
  42. package/backend/dist/db/migrate-slug-nullable.d.ts.map +1 -0
  43. package/backend/dist/db/migrate-slug-nullable.js +87 -0
  44. package/backend/dist/db/migrate-slug-nullable.js.map +1 -0
  45. package/backend/dist/db/migrations/001_migrate_slug_nullable.d.ts +10 -0
  46. package/backend/dist/db/migrations/001_migrate_slug_nullable.d.ts.map +1 -0
  47. package/backend/dist/db/migrations/001_migrate_slug_nullable.js +103 -0
  48. package/backend/dist/db/migrations/001_migrate_slug_nullable.js.map +1 -0
  49. package/backend/dist/db/migrations/002_add_oidc_custom_endpoints.d.ts +11 -0
  50. package/backend/dist/db/migrations/002_add_oidc_custom_endpoints.d.ts.map +1 -0
  51. package/backend/dist/db/migrations/002_add_oidc_custom_endpoints.js +92 -0
  52. package/backend/dist/db/migrations/002_add_oidc_custom_endpoints.js.map +1 -0
  53. package/backend/dist/db/migrations/003_add_bookmark_features.d.ts +5 -0
  54. package/backend/dist/db/migrations/003_add_bookmark_features.d.ts.map +1 -0
  55. package/backend/dist/db/migrations/003_add_bookmark_features.js +98 -0
  56. package/backend/dist/db/migrations/003_add_bookmark_features.js.map +1 -0
  57. package/backend/dist/db/migrations/004_make_slug_globally_unique.d.ts +12 -0
  58. package/backend/dist/db/migrations/004_make_slug_globally_unique.d.ts.map +1 -0
  59. package/backend/dist/db/migrations/004_make_slug_globally_unique.js +152 -0
  60. package/backend/dist/db/migrations/004_make_slug_globally_unique.js.map +1 -0
  61. package/backend/dist/db/migrations/005_add_email_verification.d.ts +5 -0
  62. package/backend/dist/db/migrations/005_add_email_verification.d.ts.map +1 -0
  63. package/backend/dist/db/migrations/005_add_email_verification.js +102 -0
  64. package/backend/dist/db/migrations/005_add_email_verification.js.map +1 -0
  65. package/backend/dist/db/migrations/006_refresh_tokens.d.ts +9 -0
  66. package/backend/dist/db/migrations/006_refresh_tokens.d.ts.map +1 -0
  67. package/backend/dist/db/migrations/006_refresh_tokens.js +61 -0
  68. package/backend/dist/db/migrations/006_refresh_tokens.js.map +1 -0
  69. package/backend/dist/db/migrations/007_password_reset_token_hash.d.ts +10 -0
  70. package/backend/dist/db/migrations/007_password_reset_token_hash.d.ts.map +1 -0
  71. package/backend/dist/db/migrations/007_password_reset_token_hash.js +40 -0
  72. package/backend/dist/db/migrations/007_password_reset_token_hash.js.map +1 -0
  73. package/backend/dist/db/migrations/008_slug_preferences.d.ts +8 -0
  74. package/backend/dist/db/migrations/008_slug_preferences.d.ts.map +1 -0
  75. package/backend/dist/db/migrations/008_slug_preferences.js +24 -0
  76. package/backend/dist/db/migrations/008_slug_preferences.js.map +1 -0
  77. package/backend/dist/db/migrations/009_signup_email_verified.d.ts +5 -0
  78. package/backend/dist/db/migrations/009_signup_email_verified.d.ts.map +1 -0
  79. package/backend/dist/db/migrations/009_signup_email_verified.js +79 -0
  80. package/backend/dist/db/migrations/009_signup_email_verified.js.map +1 -0
  81. package/backend/dist/db/migrations/010_organizations.d.ts +5 -0
  82. package/backend/dist/db/migrations/010_organizations.d.ts.map +1 -0
  83. package/backend/dist/db/migrations/010_organizations.js +113 -0
  84. package/backend/dist/db/migrations/010_organizations.js.map +1 -0
  85. package/backend/dist/db/migrations/011_org_scoped_teams.d.ts +11 -0
  86. package/backend/dist/db/migrations/011_org_scoped_teams.d.ts.map +1 -0
  87. package/backend/dist/db/migrations/011_org_scoped_teams.js +59 -0
  88. package/backend/dist/db/migrations/011_org_scoped_teams.js.map +1 -0
  89. package/backend/dist/db/migrations/012_org_invitations_token_hash.d.ts +10 -0
  90. package/backend/dist/db/migrations/012_org_invitations_token_hash.d.ts.map +1 -0
  91. package/backend/dist/db/migrations/012_org_invitations_token_hash.js +40 -0
  92. package/backend/dist/db/migrations/012_org_invitations_token_hash.js.map +1 -0
  93. package/backend/dist/db/migrations/013_signup_verification_token_hash.d.ts +10 -0
  94. package/backend/dist/db/migrations/013_signup_verification_token_hash.d.ts.map +1 -0
  95. package/backend/dist/db/migrations/013_signup_verification_token_hash.js +39 -0
  96. package/backend/dist/db/migrations/013_signup_verification_token_hash.js.map +1 -0
  97. package/backend/dist/db/migrations/014_stats_indexes.d.ts +8 -0
  98. package/backend/dist/db/migrations/014_stats_indexes.d.ts.map +1 -0
  99. package/backend/dist/db/migrations/014_stats_indexes.js +19 -0
  100. package/backend/dist/db/migrations/014_stats_indexes.js.map +1 -0
  101. package/backend/dist/db/migrations/015_api_tokens.d.ts +9 -0
  102. package/backend/dist/db/migrations/015_api_tokens.d.ts.map +1 -0
  103. package/backend/dist/db/migrations/015_api_tokens.js +48 -0
  104. package/backend/dist/db/migrations/015_api_tokens.js.map +1 -0
  105. package/backend/dist/db/migrations/016_free_plan_grace_ends_at.d.ts +9 -0
  106. package/backend/dist/db/migrations/016_free_plan_grace_ends_at.d.ts.map +1 -0
  107. package/backend/dist/db/migrations/016_free_plan_grace_ends_at.js +50 -0
  108. package/backend/dist/db/migrations/016_free_plan_grace_ends_at.js.map +1 -0
  109. package/backend/dist/db/migrations/017_ai_suggestions.d.ts +11 -0
  110. package/backend/dist/db/migrations/017_ai_suggestions.d.ts.map +1 -0
  111. package/backend/dist/db/migrations/017_ai_suggestions.js +78 -0
  112. package/backend/dist/db/migrations/017_ai_suggestions.js.map +1 -0
  113. package/backend/dist/db/migrations/018_ai_cache_output_language.d.ts +10 -0
  114. package/backend/dist/db/migrations/018_ai_cache_output_language.d.ts.map +1 -0
  115. package/backend/dist/db/migrations/018_ai_cache_output_language.js +89 -0
  116. package/backend/dist/db/migrations/018_ai_cache_output_language.js.map +1 -0
  117. package/backend/dist/db/migrations/019_ai_suggestion_usage.d.ts +10 -0
  118. package/backend/dist/db/migrations/019_ai_suggestion_usage.d.ts.map +1 -0
  119. package/backend/dist/db/migrations/019_ai_suggestion_usage.js +47 -0
  120. package/backend/dist/db/migrations/019_ai_suggestion_usage.js.map +1 -0
  121. package/backend/dist/db/migrations/020_tenant_scope.d.ts +5 -0
  122. package/backend/dist/db/migrations/020_tenant_scope.d.ts.map +1 -0
  123. package/backend/dist/db/migrations/020_tenant_scope.js +105 -0
  124. package/backend/dist/db/migrations/020_tenant_scope.js.map +1 -0
  125. package/backend/dist/db/migrations/_legacy_cloud_010_organizations.d.ts +5 -0
  126. package/backend/dist/db/migrations/_legacy_cloud_010_organizations.d.ts.map +1 -0
  127. package/backend/dist/db/migrations/_legacy_cloud_010_organizations.js +113 -0
  128. package/backend/dist/db/migrations/_legacy_cloud_010_organizations.js.map +1 -0
  129. package/backend/dist/db/migrations/_legacy_cloud_011_org_scoped_teams.d.ts +11 -0
  130. package/backend/dist/db/migrations/_legacy_cloud_011_org_scoped_teams.d.ts.map +1 -0
  131. package/backend/dist/db/migrations/_legacy_cloud_011_org_scoped_teams.js +59 -0
  132. package/backend/dist/db/migrations/_legacy_cloud_011_org_scoped_teams.js.map +1 -0
  133. package/backend/dist/db/migrations/_legacy_cloud_012_org_invitations_token_hash.d.ts +10 -0
  134. package/backend/dist/db/migrations/_legacy_cloud_012_org_invitations_token_hash.d.ts.map +1 -0
  135. package/backend/dist/db/migrations/_legacy_cloud_012_org_invitations_token_hash.js +40 -0
  136. package/backend/dist/db/migrations/_legacy_cloud_012_org_invitations_token_hash.js.map +1 -0
  137. package/backend/dist/db/migrations/_legacy_cloud_016_free_plan_grace_ends_at.d.ts +9 -0
  138. package/backend/dist/db/migrations/_legacy_cloud_016_free_plan_grace_ends_at.d.ts.map +1 -0
  139. package/backend/dist/db/migrations/_legacy_cloud_016_free_plan_grace_ends_at.js +50 -0
  140. package/backend/dist/db/migrations/_legacy_cloud_016_free_plan_grace_ends_at.js.map +1 -0
  141. package/backend/dist/db/migrations/_legacy_cloud_017_ai_suggestions.d.ts +11 -0
  142. package/backend/dist/db/migrations/_legacy_cloud_017_ai_suggestions.d.ts.map +1 -0
  143. package/backend/dist/db/migrations/_legacy_cloud_017_ai_suggestions.js +78 -0
  144. package/backend/dist/db/migrations/_legacy_cloud_017_ai_suggestions.js.map +1 -0
  145. package/backend/dist/db/migrations/index.d.ts +22 -0
  146. package/backend/dist/db/migrations/index.d.ts.map +1 -0
  147. package/backend/dist/db/migrations/index.js +194 -0
  148. package/backend/dist/db/migrations/index.js.map +1 -0
  149. package/backend/dist/db/pool.d.ts +13 -0
  150. package/backend/dist/db/pool.d.ts.map +1 -0
  151. package/backend/dist/db/pool.js +41 -0
  152. package/backend/dist/db/pool.js.map +1 -0
  153. package/backend/dist/db/seed-data.d.ts +66 -0
  154. package/backend/dist/db/seed-data.d.ts.map +1 -0
  155. package/backend/dist/db/seed-data.js +394 -0
  156. package/backend/dist/db/seed-data.js.map +1 -0
  157. package/backend/dist/db/seed.d.ts +19 -0
  158. package/backend/dist/db/seed.d.ts.map +1 -0
  159. package/backend/dist/db/seed.js +406 -0
  160. package/backend/dist/db/seed.js.map +1 -0
  161. package/backend/dist/index.d.ts +3 -0
  162. package/backend/dist/index.d.ts.map +1 -0
  163. package/backend/dist/index.js +64 -0
  164. package/backend/dist/index.js.map +1 -0
  165. package/backend/dist/instrument.d.ts +2 -0
  166. package/backend/dist/instrument.d.ts.map +1 -0
  167. package/backend/dist/instrument.js +16 -0
  168. package/backend/dist/instrument.js.map +1 -0
  169. package/backend/dist/load-env.d.ts +2 -0
  170. package/backend/dist/load-env.d.ts.map +1 -0
  171. package/backend/dist/load-env.js +33 -0
  172. package/backend/dist/load-env.js.map +1 -0
  173. package/backend/dist/middleware/auth.d.ts +23 -0
  174. package/backend/dist/middleware/auth.d.ts.map +1 -0
  175. package/backend/dist/middleware/auth.js +69 -0
  176. package/backend/dist/middleware/auth.js.map +1 -0
  177. package/backend/dist/middleware/error-handler.d.ts +11 -0
  178. package/backend/dist/middleware/error-handler.d.ts.map +1 -0
  179. package/backend/dist/middleware/error-handler.js +40 -0
  180. package/backend/dist/middleware/error-handler.js.map +1 -0
  181. package/backend/dist/middleware/security.d.ts +26 -0
  182. package/backend/dist/middleware/security.d.ts.map +1 -0
  183. package/backend/dist/middleware/security.js +162 -0
  184. package/backend/dist/middleware/security.js.map +1 -0
  185. package/backend/dist/middleware/stats-auth.d.ts +7 -0
  186. package/backend/dist/middleware/stats-auth.d.ts.map +1 -0
  187. package/backend/dist/middleware/stats-auth.js +20 -0
  188. package/backend/dist/middleware/stats-auth.js.map +1 -0
  189. package/backend/dist/middleware/tenant.d.ts +3 -0
  190. package/backend/dist/middleware/tenant.d.ts.map +1 -0
  191. package/backend/dist/middleware/tenant.js +13 -0
  192. package/backend/dist/middleware/tenant.js.map +1 -0
  193. package/backend/dist/register-routes.d.ts +12 -0
  194. package/backend/dist/register-routes.d.ts.map +1 -0
  195. package/backend/dist/register-routes.js +59 -0
  196. package/backend/dist/register-routes.js.map +1 -0
  197. package/backend/dist/routes/admin/demo-reset.d.ts +8 -0
  198. package/backend/dist/routes/admin/demo-reset.d.ts.map +1 -0
  199. package/backend/dist/routes/admin/demo-reset.js +66 -0
  200. package/backend/dist/routes/admin/demo-reset.js.map +1 -0
  201. package/backend/dist/routes/admin/settings.d.ts +3 -0
  202. package/backend/dist/routes/admin/settings.d.ts.map +1 -0
  203. package/backend/dist/routes/admin/settings.js +452 -0
  204. package/backend/dist/routes/admin/settings.js.map +1 -0
  205. package/backend/dist/routes/admin/stats.d.ts +7 -0
  206. package/backend/dist/routes/admin/stats.d.ts.map +1 -0
  207. package/backend/dist/routes/admin/stats.js +66 -0
  208. package/backend/dist/routes/admin/stats.js.map +1 -0
  209. package/backend/dist/routes/admin/teams.d.ts +3 -0
  210. package/backend/dist/routes/admin/teams.d.ts.map +1 -0
  211. package/backend/dist/routes/admin/teams.js +509 -0
  212. package/backend/dist/routes/admin/teams.js.map +1 -0
  213. package/backend/dist/routes/admin/users.d.ts +3 -0
  214. package/backend/dist/routes/admin/users.d.ts.map +1 -0
  215. package/backend/dist/routes/admin/users.js +525 -0
  216. package/backend/dist/routes/admin/users.js.map +1 -0
  217. package/backend/dist/routes/auth.d.ts +3 -0
  218. package/backend/dist/routes/auth.d.ts.map +1 -0
  219. package/backend/dist/routes/auth.js +992 -0
  220. package/backend/dist/routes/auth.js.map +1 -0
  221. package/backend/dist/routes/billing.d.ts +8 -0
  222. package/backend/dist/routes/billing.d.ts.map +1 -0
  223. package/backend/dist/routes/billing.js +481 -0
  224. package/backend/dist/routes/billing.js.map +1 -0
  225. package/backend/dist/routes/bookmarks.d.ts +3 -0
  226. package/backend/dist/routes/bookmarks.d.ts.map +1 -0
  227. package/backend/dist/routes/bookmarks.js +1593 -0
  228. package/backend/dist/routes/bookmarks.js.map +1 -0
  229. package/backend/dist/routes/config.d.ts +7 -0
  230. package/backend/dist/routes/config.d.ts.map +1 -0
  231. package/backend/dist/routes/config.js +52 -0
  232. package/backend/dist/routes/config.js.map +1 -0
  233. package/backend/dist/routes/contact.d.ts +9 -0
  234. package/backend/dist/routes/contact.d.ts.map +1 -0
  235. package/backend/dist/routes/contact.js +99 -0
  236. package/backend/dist/routes/contact.js.map +1 -0
  237. package/backend/dist/routes/csrf.d.ts +3 -0
  238. package/backend/dist/routes/csrf.d.ts.map +1 -0
  239. package/backend/dist/routes/csrf.js +39 -0
  240. package/backend/dist/routes/csrf.js.map +1 -0
  241. package/backend/dist/routes/dashboard.d.ts +3 -0
  242. package/backend/dist/routes/dashboard.d.ts.map +1 -0
  243. package/backend/dist/routes/dashboard.js +212 -0
  244. package/backend/dist/routes/dashboard.js.map +1 -0
  245. package/backend/dist/routes/email-verification.d.ts +3 -0
  246. package/backend/dist/routes/email-verification.d.ts.map +1 -0
  247. package/backend/dist/routes/email-verification.js +124 -0
  248. package/backend/dist/routes/email-verification.js.map +1 -0
  249. package/backend/dist/routes/folders.d.ts +3 -0
  250. package/backend/dist/routes/folders.d.ts.map +1 -0
  251. package/backend/dist/routes/folders.js +524 -0
  252. package/backend/dist/routes/folders.js.map +1 -0
  253. package/backend/dist/routes/go-helpers.d.ts +18 -0
  254. package/backend/dist/routes/go-helpers.d.ts.map +1 -0
  255. package/backend/dist/routes/go-helpers.js +64 -0
  256. package/backend/dist/routes/go-helpers.js.map +1 -0
  257. package/backend/dist/routes/go.d.ts +23 -0
  258. package/backend/dist/routes/go.d.ts.map +1 -0
  259. package/backend/dist/routes/go.js +361 -0
  260. package/backend/dist/routes/go.js.map +1 -0
  261. package/backend/dist/routes/health.d.ts +6 -0
  262. package/backend/dist/routes/health.d.ts.map +1 -0
  263. package/backend/dist/routes/health.js +79 -0
  264. package/backend/dist/routes/health.js.map +1 -0
  265. package/backend/dist/routes/invitations.d.ts +3 -0
  266. package/backend/dist/routes/invitations.d.ts.map +1 -0
  267. package/backend/dist/routes/invitations.js +172 -0
  268. package/backend/dist/routes/invitations.js.map +1 -0
  269. package/backend/dist/routes/oidc-providers.d.ts +3 -0
  270. package/backend/dist/routes/oidc-providers.d.ts.map +1 -0
  271. package/backend/dist/routes/oidc-providers.js +495 -0
  272. package/backend/dist/routes/oidc-providers.js.map +1 -0
  273. package/backend/dist/routes/organizations.d.ts +3 -0
  274. package/backend/dist/routes/organizations.d.ts.map +1 -0
  275. package/backend/dist/routes/organizations.js +538 -0
  276. package/backend/dist/routes/organizations.js.map +1 -0
  277. package/backend/dist/routes/password-reset.d.ts +3 -0
  278. package/backend/dist/routes/password-reset.d.ts.map +1 -0
  279. package/backend/dist/routes/password-reset.js +212 -0
  280. package/backend/dist/routes/password-reset.js.map +1 -0
  281. package/backend/dist/routes/redirect.d.ts +3 -0
  282. package/backend/dist/routes/redirect.d.ts.map +1 -0
  283. package/backend/dist/routes/redirect.js +124 -0
  284. package/backend/dist/routes/redirect.js.map +1 -0
  285. package/backend/dist/routes/tags.d.ts +3 -0
  286. package/backend/dist/routes/tags.d.ts.map +1 -0
  287. package/backend/dist/routes/tags.js +302 -0
  288. package/backend/dist/routes/tags.js.map +1 -0
  289. package/backend/dist/routes/teams.d.ts +3 -0
  290. package/backend/dist/routes/teams.d.ts.map +1 -0
  291. package/backend/dist/routes/teams.js +60 -0
  292. package/backend/dist/routes/teams.js.map +1 -0
  293. package/backend/dist/routes/tokens.d.ts +3 -0
  294. package/backend/dist/routes/tokens.d.ts.map +1 -0
  295. package/backend/dist/routes/tokens.js +157 -0
  296. package/backend/dist/routes/tokens.js.map +1 -0
  297. package/backend/dist/routes/users.d.ts +3 -0
  298. package/backend/dist/routes/users.d.ts.map +1 -0
  299. package/backend/dist/routes/users.js +199 -0
  300. package/backend/dist/routes/users.js.map +1 -0
  301. package/backend/dist/services/ai-suggestions.d.ts +29 -0
  302. package/backend/dist/services/ai-suggestions.d.ts.map +1 -0
  303. package/backend/dist/services/ai-suggestions.js +163 -0
  304. package/backend/dist/services/ai-suggestions.js.map +1 -0
  305. package/backend/dist/services/api-tokens.d.ts +66 -0
  306. package/backend/dist/services/api-tokens.d.ts.map +1 -0
  307. package/backend/dist/services/api-tokens.js +129 -0
  308. package/backend/dist/services/api-tokens.js.map +1 -0
  309. package/backend/dist/services/fetch-page-metadata.d.ts +15 -0
  310. package/backend/dist/services/fetch-page-metadata.d.ts.map +1 -0
  311. package/backend/dist/services/fetch-page-metadata.js +205 -0
  312. package/backend/dist/services/fetch-page-metadata.js.map +1 -0
  313. package/backend/dist/services/stats.d.ts +73 -0
  314. package/backend/dist/services/stats.d.ts.map +1 -0
  315. package/backend/dist/services/stats.js +145 -0
  316. package/backend/dist/services/stats.js.map +1 -0
  317. package/backend/dist/types/oidc-provider.d.ts +17 -0
  318. package/backend/dist/types/oidc-provider.d.ts.map +1 -0
  319. package/backend/dist/types/oidc-provider.js +2 -0
  320. package/backend/dist/types/oidc-provider.js.map +1 -0
  321. package/backend/dist/types.d.ts +86 -0
  322. package/backend/dist/types.d.ts.map +1 -0
  323. package/backend/dist/types.js +2 -0
  324. package/backend/dist/types.js.map +1 -0
  325. package/backend/dist/utils/ai-feature.d.ts +23 -0
  326. package/backend/dist/utils/ai-feature.d.ts.map +1 -0
  327. package/backend/dist/utils/ai-feature.js +62 -0
  328. package/backend/dist/utils/ai-feature.js.map +1 -0
  329. package/backend/dist/utils/email.d.ts +40 -0
  330. package/backend/dist/utils/email.d.ts.map +1 -0
  331. package/backend/dist/utils/email.js +456 -0
  332. package/backend/dist/utils/email.js.map +1 -0
  333. package/backend/dist/utils/encryption.d.ts +18 -0
  334. package/backend/dist/utils/encryption.d.ts.map +1 -0
  335. package/backend/dist/utils/encryption.js +95 -0
  336. package/backend/dist/utils/encryption.js.map +1 -0
  337. package/backend/dist/utils/env-validation.d.ts +6 -0
  338. package/backend/dist/utils/env-validation.d.ts.map +1 -0
  339. package/backend/dist/utils/env-validation.js +51 -0
  340. package/backend/dist/utils/env-validation.js.map +1 -0
  341. package/backend/dist/utils/jwt.d.ts +20 -0
  342. package/backend/dist/utils/jwt.d.ts.map +1 -0
  343. package/backend/dist/utils/jwt.js +48 -0
  344. package/backend/dist/utils/jwt.js.map +1 -0
  345. package/backend/dist/utils/org-cleanup.d.ts +6 -0
  346. package/backend/dist/utils/org-cleanup.d.ts.map +1 -0
  347. package/backend/dist/utils/org-cleanup.js +37 -0
  348. package/backend/dist/utils/org-cleanup.js.map +1 -0
  349. package/backend/dist/utils/organizations.d.ts +12 -0
  350. package/backend/dist/utils/organizations.d.ts.map +1 -0
  351. package/backend/dist/utils/organizations.js +24 -0
  352. package/backend/dist/utils/organizations.js.map +1 -0
  353. package/backend/dist/utils/plan-errors.d.ts +18 -0
  354. package/backend/dist/utils/plan-errors.d.ts.map +1 -0
  355. package/backend/dist/utils/plan-errors.js +21 -0
  356. package/backend/dist/utils/plan-errors.js.map +1 -0
  357. package/backend/dist/utils/refresh-token.d.ts +31 -0
  358. package/backend/dist/utils/refresh-token.d.ts.map +1 -0
  359. package/backend/dist/utils/refresh-token.js +63 -0
  360. package/backend/dist/utils/refresh-token.js.map +1 -0
  361. package/backend/dist/utils/session-store.d.ts +46 -0
  362. package/backend/dist/utils/session-store.d.ts.map +1 -0
  363. package/backend/dist/utils/session-store.js +222 -0
  364. package/backend/dist/utils/session-store.js.map +1 -0
  365. package/backend/dist/utils/tenant.d.ts +5 -0
  366. package/backend/dist/utils/tenant.d.ts.map +1 -0
  367. package/backend/dist/utils/tenant.js +12 -0
  368. package/backend/dist/utils/tenant.js.map +1 -0
  369. package/backend/dist/utils/user-key.d.ts +24 -0
  370. package/backend/dist/utils/user-key.d.ts.map +1 -0
  371. package/backend/dist/utils/user-key.js +116 -0
  372. package/backend/dist/utils/user-key.js.map +1 -0
  373. package/backend/dist/utils/validation.d.ts +91 -0
  374. package/backend/dist/utils/validation.d.ts.map +1 -0
  375. package/backend/dist/utils/validation.js +337 -0
  376. package/backend/dist/utils/validation.js.map +1 -0
  377. package/backend/index.js +15 -0
  378. package/frontend/index.js +5 -0
  379. package/frontend/index.tsx +7 -0
  380. package/package.json +16 -0
  381. package/types/index.js +4 -0
@@ -0,0 +1,992 @@
1
+ import { Router } from 'express';
2
+ import passport from 'passport';
3
+ import bcrypt from 'bcryptjs';
4
+ import { query, queryOne, execute, isInitialized } from '../db/index.js';
5
+ import { v4 as uuidv4 } from 'uuid';
6
+ import { generateToken } from '../utils/jwt.js';
7
+ import { requireAuth } from '../middleware/auth.js';
8
+ import { authRateLimiter, refreshRateLimiter, strictRateLimiter } from '../middleware/security.js';
9
+ import { validateEmail, normalizeEmail, validatePassword, validateLength, sanitizeString } from '../utils/validation.js';
10
+ import { generateUserKey } from '../utils/user-key.js';
11
+ import { getAuthCookieOptions, getClearAuthCookieOptions } from '../config/cookies.js';
12
+ import { sendSignupVerificationEmail } from '../utils/email.js';
13
+ import crypto from 'crypto';
14
+ import { getDefaultTenantId } from '../utils/tenant.js';
15
+ const router = Router();
16
+ const DB_TYPE = process.env.DB_TYPE || 'sqlite';
17
+ function hashToken(token) {
18
+ return crypto.createHash('sha256').update(token).digest('hex');
19
+ }
20
+ /** Convert ? placeholders to $1, $2 for PostgreSQL */
21
+ function toPg(sqlStr) {
22
+ let n = 0;
23
+ return sqlStr.replace(/\?/g, () => `$${++n}`);
24
+ }
25
+ function sql(sqlStr, params) {
26
+ return DB_TYPE === 'postgresql' ? [toPg(sqlStr), params] : [sqlStr, params];
27
+ }
28
+ /**
29
+ * Find signup verification token by submitted token (hash-first, then legacy plaintext).
30
+ * Returns row with id, user_id, expires_at, used; for status/resend also need email (join users).
31
+ * Migrates legacy rows to token_hash on use.
32
+ */
33
+ async function findSignupTokenByToken(submittedToken) {
34
+ const tokenHash = hashToken(submittedToken);
35
+ const [qHash, pHash] = sql('SELECT id, user_id, expires_at, used FROM signup_verification_tokens WHERE token_hash = ?', [tokenHash]);
36
+ let row = await queryOne(qHash, pHash);
37
+ if (row)
38
+ return row;
39
+ // Legacy: token stored in plaintext (token_hash IS NULL)
40
+ const [qLegacy, pLegacy] = sql('SELECT id, user_id, expires_at, used FROM signup_verification_tokens WHERE token = ? AND token_hash IS NULL', [submittedToken]);
41
+ row = await queryOne(qLegacy, pLegacy);
42
+ if (!row)
43
+ return null;
44
+ const r = row;
45
+ // Migrate legacy row to token_hash
46
+ const [qUp, pUp] = sql('UPDATE signup_verification_tokens SET token_hash = ?, token = ? WHERE id = ?', [tokenHash, 'h:' + r.id, r.id]);
47
+ await execute(qUp, pUp);
48
+ return row;
49
+ }
50
+ /**
51
+ * Find signup verification token with user email (for status/resend endpoints).
52
+ */
53
+ async function findSignupTokenWithEmailByToken(submittedToken) {
54
+ const tokenHash = hashToken(submittedToken);
55
+ const [qHash, pHash] = sql(`SELECT svt.id, svt.user_id, svt.expires_at, svt.used, u.email
56
+ FROM signup_verification_tokens svt
57
+ JOIN users u ON u.id = svt.user_id
58
+ WHERE svt.token_hash = ?`, [tokenHash]);
59
+ let row = await queryOne(qHash, pHash);
60
+ if (row)
61
+ return row;
62
+ // Legacy: token stored in plaintext (token_hash IS NULL)
63
+ const [qLegacy, pLegacy] = sql(`SELECT svt.id, svt.user_id, svt.expires_at, svt.used, u.email
64
+ FROM signup_verification_tokens svt
65
+ JOIN users u ON u.id = svt.user_id
66
+ WHERE svt.token = ? AND svt.token_hash IS NULL`, [submittedToken]);
67
+ row = await queryOne(qLegacy, pLegacy);
68
+ if (!row)
69
+ return null;
70
+ const r = row;
71
+ // Migrate legacy row to token_hash
72
+ const [qUp, pUp] = sql('UPDATE signup_verification_tokens SET token_hash = ?, token = ? WHERE id = ?', [tokenHash, 'h:' + r.id, r.id]);
73
+ await execute(qUp, pUp);
74
+ return row;
75
+ }
76
+ /** Set auth cookie for self-hosted JWT auth. */
77
+ function setAuthCookies(res, options) {
78
+ const accessMaxAgeMs = 7 * 24 * 60 * 60 * 1000;
79
+ res.cookie('token', options.accessToken, { ...getAuthCookieOptions(accessMaxAgeMs), maxAge: accessMaxAgeMs });
80
+ }
81
+ /**
82
+ * @swagger
83
+ * /api/auth/providers:
84
+ * get:
85
+ * summary: Get available OIDC providers
86
+ * description: Returns a list of all configured OIDC providers (public endpoint, no authentication required)
87
+ * tags: [Authentication]
88
+ * responses:
89
+ * 200:
90
+ * description: List of OIDC providers
91
+ * content:
92
+ * application/json:
93
+ * schema:
94
+ * type: array
95
+ * items:
96
+ * type: object
97
+ * properties:
98
+ * id:
99
+ * type: string
100
+ * example: "123e4567-e89b-12d3-a456-426614174000"
101
+ * provider_key:
102
+ * type: string
103
+ * example: "google"
104
+ * issuer_url:
105
+ * type: string
106
+ * example: "https://accounts.google.com"
107
+ * 500:
108
+ * description: Server error
109
+ */
110
+ router.get('/providers', async (req, res) => {
111
+ try {
112
+ const baseUrl = process.env.BASE_URL || 'http://localhost:5000';
113
+ const providers = await query('SELECT id, provider_key, issuer_url FROM oidc_providers WHERE tenant_id = ?', [getDefaultTenantId()]);
114
+ const providersList = Array.isArray(providers) ? providers : (providers ? [providers] : []);
115
+ const providersWithCallback = providersList.map((p) => ({
116
+ ...p,
117
+ callback_url: `${baseUrl}/api/auth/${p.provider_key}/callback`,
118
+ }));
119
+ res.json(providersWithCallback);
120
+ }
121
+ catch (error) {
122
+ res.status(500).json({ error: error.message });
123
+ }
124
+ });
125
+ /**
126
+ * @swagger
127
+ * /api/auth/me:
128
+ * get:
129
+ * summary: Get current user information
130
+ * description: Returns the authenticated user's profile information
131
+ * tags: [Authentication]
132
+ * security:
133
+ * - cookieAuth: []
134
+ * - bearerAuth: []
135
+ * responses:
136
+ * 200:
137
+ * description: Current user information
138
+ * content:
139
+ * application/json:
140
+ * schema:
141
+ * type: object
142
+ * properties:
143
+ * id:
144
+ * type: string
145
+ * example: "123e4567-e89b-12d3-a456-426614174000"
146
+ * email:
147
+ * type: string
148
+ * example: "user@example.com"
149
+ * name:
150
+ * type: string
151
+ * example: "John Doe"
152
+ * user_key:
153
+ * type: string
154
+ * example: "abc12345"
155
+ * is_admin:
156
+ * type: boolean
157
+ * example: false
158
+ * language:
159
+ * type: string
160
+ * example: "en"
161
+ * theme:
162
+ * type: string
163
+ * example: "auto"
164
+ * 401:
165
+ * description: Unauthorized
166
+ */
167
+ router.get('/me', requireAuth(), async (req, res) => {
168
+ const authReq = req;
169
+ const user = authReq.user;
170
+ const userRow = await queryOne('SELECT id, email, name, user_key, is_admin, language, theme, ai_suggestions_enabled FROM users WHERE id = ?', [user.id]);
171
+ const u = userRow;
172
+ const payload = {
173
+ id: user.id,
174
+ email: u?.email ?? user.email,
175
+ name: u?.name ?? user.name,
176
+ user_key: user.user_key,
177
+ is_admin: user.is_admin,
178
+ language: u?.language || user.language || 'en',
179
+ theme: u?.theme || user.theme || 'auto',
180
+ ai_suggestions_enabled: u?.ai_suggestions_enabled !== 0 && u?.ai_suggestions_enabled !== false,
181
+ };
182
+ res.json(payload);
183
+ });
184
+ /**
185
+ * @swagger
186
+ * /api/auth/login:
187
+ * post:
188
+ * summary: Login with email and password
189
+ * description: Authenticate a user with email and password. Returns user information and sets an httpOnly JWT cookie.
190
+ * tags: [Authentication]
191
+ * requestBody:
192
+ * required: true
193
+ * content:
194
+ * application/json:
195
+ * schema:
196
+ * type: object
197
+ * required:
198
+ * - email
199
+ * - password
200
+ * properties:
201
+ * email:
202
+ * type: string
203
+ * format: email
204
+ * example: "user@example.com"
205
+ * password:
206
+ * type: string
207
+ * format: password
208
+ * example: "securepassword123"
209
+ * responses:
210
+ * 200:
211
+ * description: Login successful
212
+ * content:
213
+ * application/json:
214
+ * schema:
215
+ * type: object
216
+ * properties:
217
+ * id:
218
+ * type: string
219
+ * example: "123e4567-e89b-12d3-a456-426614174000"
220
+ * email:
221
+ * type: string
222
+ * example: "user@example.com"
223
+ * name:
224
+ * type: string
225
+ * example: "John Doe"
226
+ * user_key:
227
+ * type: string
228
+ * example: "abc12345"
229
+ * is_admin:
230
+ * type: boolean
231
+ * example: false
232
+ * language:
233
+ * type: string
234
+ * example: "en"
235
+ * theme:
236
+ * type: string
237
+ * example: "auto"
238
+ * headers:
239
+ * Set-Cookie:
240
+ * description: JWT token in httpOnly cookie
241
+ * schema:
242
+ * type: string
243
+ * example: "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Strict"
244
+ * 400:
245
+ * description: Missing email or password
246
+ * 401:
247
+ * description: Invalid credentials or account has no password set
248
+ */
249
+ // Local authentication (email/password)
250
+ router.post('/login', authRateLimiter, async (req, res, next) => {
251
+ try {
252
+ const { email, password } = req.body;
253
+ if (!email || !password) {
254
+ return res.status(400).json({ error: 'Email and password are required' });
255
+ }
256
+ // Validate and normalize email
257
+ const emailValidation = validateEmail(email);
258
+ if (!emailValidation.valid) {
259
+ return res.status(400).json({ error: emailValidation.error });
260
+ }
261
+ const normalizedEmail = normalizeEmail(email);
262
+ // Find user by email (use normalized email)
263
+ const user = await queryOne('SELECT * FROM users WHERE email = ?', [normalizedEmail]);
264
+ if (!user) {
265
+ return res.status(401).json({ error: 'Invalid email or password' });
266
+ }
267
+ // Check if user has a password set
268
+ if (!user.password_hash) {
269
+ return res.status(401).json({ error: 'This account does not have a password set. Please use OIDC login.' });
270
+ }
271
+ // Verify password
272
+ const isValid = await bcrypt.compare(password, user.password_hash);
273
+ if (!isValid) {
274
+ return res.status(401).json({ error: 'Invalid email or password' });
275
+ }
276
+ // Require email verification for password users (CLOUD signup flow)
277
+ const emailVerified = user.email_verified;
278
+ if (emailVerified === false || emailVerified === 0) {
279
+ return res.status(403).json({ error: 'Please verify your email', code: 'EMAIL_NOT_VERIFIED' });
280
+ }
281
+ const userPayload = {
282
+ id: user.id,
283
+ email: user.email,
284
+ name: user.name,
285
+ user_key: user.user_key,
286
+ is_admin: user.is_admin,
287
+ };
288
+ const token = generateToken(userPayload);
289
+ setAuthCookies(res, { accessToken: token });
290
+ const payload = {
291
+ id: user.id,
292
+ email: user.email,
293
+ name: user.name,
294
+ user_key: user.user_key,
295
+ is_admin: user.is_admin,
296
+ language: user.language,
297
+ theme: user.theme,
298
+ };
299
+ res.json(payload);
300
+ }
301
+ catch (error) {
302
+ res.status(500).json({ error: error.message });
303
+ }
304
+ });
305
+ /**
306
+ * @swagger
307
+ * /api/auth/logout:
308
+ * post:
309
+ * summary: Logout current user
310
+ * description: Clears the authentication cookie
311
+ * tags: [Authentication]
312
+ * responses:
313
+ * 200:
314
+ * description: Logout successful
315
+ * content:
316
+ * application/json:
317
+ * schema:
318
+ * type: object
319
+ * properties:
320
+ * message:
321
+ * type: string
322
+ * example: "Logged out"
323
+ */
324
+ router.post('/logout', async (req, res) => {
325
+ const clearOpts = getClearAuthCookieOptions();
326
+ res.clearCookie('token', clearOpts);
327
+ res.json({ message: 'Logged out' });
328
+ });
329
+ /**
330
+ * POST /auth/register — CLOUD only. Create account with email verification.
331
+ * Returns 404 when not CLOUD so SELFHOSTED never exposes public registration.
332
+ */
333
+ router.post('/register', authRateLimiter, async (req, res) => {
334
+ const registrationsEnabled = process.env.REGISTRATIONS_ENABLED !== 'false';
335
+ if (!registrationsEnabled) {
336
+ return res.status(403).json({ error: 'Registrations are disabled' });
337
+ }
338
+ try {
339
+ const { email, name, password } = req.body;
340
+ if (!email || !name || !password) {
341
+ return res.status(400).json({ error: 'Email, name, and password are required' });
342
+ }
343
+ const emailValidation = validateEmail(email);
344
+ if (!emailValidation.valid) {
345
+ return res.status(400).json({ error: emailValidation.error });
346
+ }
347
+ const normalizedEmail = normalizeEmail(email);
348
+ const nameValidation = validateLength(name, 'Name', 1, 255);
349
+ if (!nameValidation.valid) {
350
+ return res.status(400).json({ error: nameValidation.error });
351
+ }
352
+ const sanitizedName = sanitizeString(name);
353
+ const passwordValidation = validatePassword(password);
354
+ if (!passwordValidation.valid) {
355
+ return res.status(400).json({ error: passwordValidation.error });
356
+ }
357
+ const existingUser = await queryOne('SELECT id FROM users WHERE email = ?', [normalizedEmail]);
358
+ if (existingUser) {
359
+ return res.status(400).json({ error: 'Email already registered' });
360
+ }
361
+ const isFirstUser = !(await isInitialized());
362
+ const passwordHash = await bcrypt.hash(password, 10);
363
+ const userId = uuidv4();
364
+ let userKey = await generateUserKey();
365
+ const DB_TYPE = process.env.DB_TYPE || 'sqlite';
366
+ let retries = 0;
367
+ const maxRetries = 3;
368
+ while (retries < maxRetries) {
369
+ try {
370
+ if (DB_TYPE === 'postgresql') {
371
+ await execute(`INSERT INTO users (id, email, name, user_key, password_hash, is_admin, email_verified)
372
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [userId, normalizedEmail, sanitizedName, userKey, passwordHash, isFirstUser, false]);
373
+ }
374
+ else {
375
+ await execute(`INSERT INTO users (id, email, name, user_key, password_hash, is_admin, email_verified)
376
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, [userId, normalizedEmail, sanitizedName, userKey, passwordHash, isFirstUser, 0]);
377
+ }
378
+ break;
379
+ }
380
+ catch (error) {
381
+ if (error.message && (error.message.includes('UNIQUE constraint') || error.message.includes('duplicate')) && error.message.includes('user_key')) {
382
+ retries++;
383
+ if (retries >= maxRetries) {
384
+ return res.status(500).json({ error: 'Failed to complete registration. Please try again.' });
385
+ }
386
+ userKey = await generateUserKey();
387
+ continue;
388
+ }
389
+ throw error;
390
+ }
391
+ }
392
+ const tokenId = uuidv4();
393
+ const token = crypto.randomBytes(32).toString('hex');
394
+ const tokenHash = hashToken(token);
395
+ const tokenPlaceholder = 'h:' + tokenId;
396
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
397
+ const expiresAtStr = expiresAt.toISOString();
398
+ if (DB_TYPE === 'postgresql') {
399
+ await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
400
+ VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, userId, tokenPlaceholder, tokenHash, expiresAtStr, false]);
401
+ }
402
+ else {
403
+ await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
404
+ VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, userId, tokenPlaceholder, tokenHash, expiresAtStr, 0]);
405
+ }
406
+ const frontendUrl = (process.env.FRONTEND_URL || 'http://localhost:3000').replace(/\/$/, '');
407
+ const verificationUrl = `${frontendUrl}/verify-email?token=${encodeURIComponent(token)}`;
408
+ await sendSignupVerificationEmail(normalizedEmail, verificationUrl);
409
+ return res.status(201).json({ message: 'Check your email to verify your account' });
410
+ }
411
+ catch (error) {
412
+ console.error('Register error:', error);
413
+ return res.status(500).json({ error: error.message || 'Registration failed' });
414
+ }
415
+ });
416
+ /**
417
+ * POST /auth/verify-signup — CLOUD only. Verify signup token and set email_verified.
418
+ */
419
+ router.post('/verify-signup', authRateLimiter, async (req, res) => {
420
+ try {
421
+ const { token } = req.body;
422
+ if (!token || typeof token !== 'string') {
423
+ return res.status(400).json({ error: 'Token is required' });
424
+ }
425
+ const row = await findSignupTokenByToken(token.trim());
426
+ if (!row) {
427
+ return res.status(400).json({ error: 'Invalid or expired verification link' });
428
+ }
429
+ const r = row;
430
+ if (r.used === true || r.used === 1) {
431
+ return res.status(400).json({ error: 'This verification link has already been used' });
432
+ }
433
+ const expiresAt = new Date(r.expires_at);
434
+ if (expiresAt.getTime() < Date.now()) {
435
+ return res.status(400).json({ error: 'This verification link has expired' });
436
+ }
437
+ if (DB_TYPE === 'postgresql') {
438
+ await execute('UPDATE users SET email_verified = TRUE WHERE id = ?', [r.user_id]);
439
+ await execute('UPDATE signup_verification_tokens SET used = TRUE WHERE id = ?', [r.id]);
440
+ }
441
+ else {
442
+ await execute('UPDATE users SET email_verified = 1 WHERE id = ?', [r.user_id]);
443
+ await execute('UPDATE signup_verification_tokens SET used = 1 WHERE id = ?', [r.id]);
444
+ }
445
+ return res.json({ message: 'Email verified. You can log in.' });
446
+ }
447
+ catch (error) {
448
+ console.error('Verify-signup error:', error);
449
+ return res.status(500).json({ error: error.message || 'Verification failed' });
450
+ }
451
+ });
452
+ /**
453
+ * GET /auth/signup-verification/status — CLOUD only. Get status of signup verification token.
454
+ */
455
+ router.get('/signup-verification/status', authRateLimiter, async (req, res) => {
456
+ try {
457
+ const token = req.query.token?.trim();
458
+ if (!token) {
459
+ return res.status(400).json({ status: 'invalid' });
460
+ }
461
+ const row = await findSignupTokenWithEmailByToken(token);
462
+ if (!row) {
463
+ return res.json({ status: 'invalid' });
464
+ }
465
+ const r = row;
466
+ if (r.used === true || r.used === 1) {
467
+ return res.json({ status: 'used' });
468
+ }
469
+ const expiresAt = new Date(r.expires_at);
470
+ if (expiresAt.getTime() < Date.now()) {
471
+ return res.json({ status: 'expired', email: r.email });
472
+ }
473
+ return res.json({ status: 'valid', email: r.email });
474
+ }
475
+ catch (error) {
476
+ console.error('Signup verification status error:', error);
477
+ return res.status(500).json({ status: 'invalid' });
478
+ }
479
+ });
480
+ /**
481
+ * POST /auth/resend-signup-verification — CLOUD only. Resend verification email, optionally with updated email.
482
+ */
483
+ router.post('/resend-signup-verification', authRateLimiter, async (req, res) => {
484
+ try {
485
+ const { token, newEmail } = req.body;
486
+ if (!token || typeof token !== 'string') {
487
+ return res.status(400).json({ error: 'Token is required' });
488
+ }
489
+ const row = await findSignupTokenWithEmailByToken(token.trim());
490
+ if (!row) {
491
+ return res.status(400).json({ error: 'Invalid verification token' });
492
+ }
493
+ const r = row;
494
+ if (r.used === true || r.used === 1) {
495
+ return res.status(400).json({ error: 'This verification link has already been used' });
496
+ }
497
+ let targetEmail = r.email;
498
+ if (newEmail && typeof newEmail === 'string' && newEmail.trim()) {
499
+ const emailValidation = validateEmail(newEmail.trim());
500
+ if (!emailValidation.valid) {
501
+ return res.status(400).json({ error: emailValidation.error });
502
+ }
503
+ const normalizedNew = normalizeEmail(newEmail.trim());
504
+ if (normalizedNew !== targetEmail) {
505
+ const existing = await queryOne('SELECT id FROM users WHERE email = ?', [normalizedNew]);
506
+ if (existing) {
507
+ return res.status(400).json({ error: 'Email already registered' });
508
+ }
509
+ targetEmail = normalizedNew;
510
+ await execute('UPDATE users SET email = ? WHERE id = ?', [targetEmail, r.user_id]);
511
+ }
512
+ }
513
+ await execute('DELETE FROM signup_verification_tokens WHERE user_id = ?', [r.user_id]);
514
+ const tokenId = uuidv4();
515
+ const newToken = crypto.randomBytes(32).toString('hex');
516
+ const newTokenHash = hashToken(newToken);
517
+ const newTokenPlaceholder = 'h:' + tokenId;
518
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
519
+ const expiresAtStr = expiresAt.toISOString();
520
+ if (DB_TYPE === 'postgresql') {
521
+ await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
522
+ VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, r.user_id, newTokenPlaceholder, newTokenHash, expiresAtStr, false]);
523
+ }
524
+ else {
525
+ await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
526
+ VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, r.user_id, newTokenPlaceholder, newTokenHash, expiresAtStr, 0]);
527
+ }
528
+ const frontendUrl = (process.env.FRONTEND_URL || 'http://localhost:3000').replace(/\/$/, '');
529
+ const verificationUrl = `${frontendUrl}/verify-email?token=${encodeURIComponent(newToken)}`;
530
+ await sendSignupVerificationEmail(targetEmail, verificationUrl);
531
+ return res.json({ message: 'Verification email sent' });
532
+ }
533
+ catch (error) {
534
+ console.error('Resend signup verification error:', error);
535
+ return res.status(500).json({ error: error.message || 'Failed to resend verification' });
536
+ }
537
+ });
538
+ /**
539
+ * POST /auth/request-signup-resend — CLOUD only. Request resend of verification email by email (no token).
540
+ */
541
+ router.post('/request-signup-resend', authRateLimiter, async (req, res) => {
542
+ try {
543
+ const { email } = req.body;
544
+ if (!email || typeof email !== 'string') {
545
+ return res.status(400).json({ error: 'Email is required' });
546
+ }
547
+ const emailValidation = validateEmail(email.trim());
548
+ if (!emailValidation.valid) {
549
+ return res.status(400).json({ error: emailValidation.error });
550
+ }
551
+ const normalizedEmail = normalizeEmail(email.trim());
552
+ const user = await queryOne('SELECT id FROM users WHERE email = ? AND (email_verified = FALSE OR email_verified = 0)', [normalizedEmail]);
553
+ if (user) {
554
+ const u = user;
555
+ await execute('DELETE FROM signup_verification_tokens WHERE user_id = ?', [u.id]);
556
+ const tokenId = uuidv4();
557
+ const newToken = crypto.randomBytes(32).toString('hex');
558
+ const newTokenHash = hashToken(newToken);
559
+ const newTokenPlaceholder = 'h:' + tokenId;
560
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
561
+ const expiresAtStr = expiresAt.toISOString();
562
+ if (DB_TYPE === 'postgresql') {
563
+ await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
564
+ VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, u.id, newTokenPlaceholder, newTokenHash, expiresAtStr, false]);
565
+ }
566
+ else {
567
+ await execute(`INSERT INTO signup_verification_tokens (id, user_id, token, token_hash, expires_at, used)
568
+ VALUES (?, ?, ?, ?, ?, ?)`, [tokenId, u.id, newTokenPlaceholder, newTokenHash, expiresAtStr, 0]);
569
+ }
570
+ const frontendUrl = (process.env.FRONTEND_URL || 'http://localhost:3000').replace(/\/$/, '');
571
+ const verificationUrl = `${frontendUrl}/verify-email?token=${encodeURIComponent(newToken)}`;
572
+ await sendSignupVerificationEmail(normalizedEmail, verificationUrl);
573
+ }
574
+ return res.json({ message: 'If an unverified account exists with that email, a new verification link has been sent.' });
575
+ }
576
+ catch (error) {
577
+ console.error('Request signup resend error:', error);
578
+ return res.status(500).json({ error: error.message || 'Failed to request resend' });
579
+ }
580
+ });
581
+ /**
582
+ * @swagger
583
+ * /api/auth/refresh:
584
+ * post:
585
+ * summary: Refresh access token (CLOUD mode)
586
+ * description: Exchange refresh token cookie for new access + refresh tokens. CLOUD only.
587
+ * tags: [Authentication]
588
+ * responses:
589
+ * 200:
590
+ * description: New tokens set via cookies; returns user payload
591
+ * 401:
592
+ * description: Invalid or missing refresh token
593
+ */
594
+ router.post('/refresh', refreshRateLimiter, async (req, res) => {
595
+ return res.status(404).json({ error: 'Not found' });
596
+ });
597
+ /**
598
+ * @swagger
599
+ * /api/auth/{provider}:
600
+ * get:
601
+ * summary: Initiate OIDC login
602
+ * description: Redirects to the OIDC provider's authentication page. This is a redirect endpoint, not a JSON API.
603
+ * tags: [Authentication]
604
+ * parameters:
605
+ * - in: path
606
+ * name: provider
607
+ * required: true
608
+ * schema:
609
+ * type: string
610
+ * description: OIDC provider key (e.g., "google", "github")
611
+ * example: "google"
612
+ * responses:
613
+ * 302:
614
+ * description: Redirect to OIDC provider
615
+ * 404:
616
+ * description: Provider not found
617
+ */
618
+ // OIDC login route
619
+ // Note: OIDC requires sessions for the OAuth flow, so we don't use session: false here
620
+ router.get('/:provider', async (req, res, next) => {
621
+ const { provider } = req.params;
622
+ const strategies = passport._strategies || {};
623
+ if (!strategies[provider]) {
624
+ return res.status(404).json({ error: 'Not found' });
625
+ }
626
+ passport.authenticate(provider)(req, res, next);
627
+ });
628
+ /**
629
+ * @swagger
630
+ * /api/auth/{provider}/callback:
631
+ * get:
632
+ * summary: OIDC callback endpoint
633
+ * description: Handles the OIDC provider callback after authentication. This is a redirect endpoint used by the OIDC flow.
634
+ * tags: [Authentication]
635
+ * parameters:
636
+ * - in: path
637
+ * name: provider
638
+ * required: true
639
+ * schema:
640
+ * type: string
641
+ * description: OIDC provider key
642
+ * example: "google"
643
+ * - in: query
644
+ * name: code
645
+ * schema:
646
+ * type: string
647
+ * description: Authorization code from OIDC provider
648
+ * - in: query
649
+ * name: state
650
+ * schema:
651
+ * type: string
652
+ * description: State parameter for CSRF protection
653
+ * responses:
654
+ * 302:
655
+ * description: Redirect to frontend (success) or login page (error)
656
+ */
657
+ // OIDC callback route
658
+ // Note: OIDC requires sessions for the OAuth flow, but we convert to JWT after authentication
659
+ router.get('/:provider/callback', (req, res, next) => {
660
+ const { provider } = req.params;
661
+ const strategies = passport._strategies || {};
662
+ if (!strategies[provider]) {
663
+ return res.status(404).json({ error: 'Not found' });
664
+ }
665
+ passport.authenticate(provider, async (err, user, info) => {
666
+ // Handle "ID token not present" error - some providers don't return ID tokens
667
+ // and passport-openidconnect fails before it can use userInfo endpoint
668
+ if (err && err.message === 'ID token not present in token response') {
669
+ try {
670
+ // Get provider configuration from database.
671
+ let configuredIssuer;
672
+ let configuredUserinfoUrl;
673
+ const providerConfig = await queryOne('SELECT issuer_url, userinfo_url FROM oidc_providers WHERE provider_key = ? AND tenant_id = ?', [provider, getDefaultTenantId()]);
674
+ if (!providerConfig)
675
+ throw new Error('Provider configuration not found');
676
+ configuredIssuer = providerConfig.issuer_url;
677
+ configuredUserinfoUrl = providerConfig.userinfo_url || `${configuredIssuer}/userinfo`;
678
+ // Get the access token from the session (stored by passport during OAuth flow)
679
+ // passport-openidconnect stores it under a key like 'openidconnect:issuer'
680
+ // Use the configured issuer, not user input
681
+ const sessionKey = `openidconnect:${configuredIssuer}`;
682
+ const oauthState = req.session?.[sessionKey];
683
+ if (!oauthState) {
684
+ // Try to find any openidconnect key in session that matches the configured issuer
685
+ const allKeys = Object.keys(req.session || {});
686
+ const oidcKeys = allKeys.filter((k) => k.startsWith('openidconnect:'));
687
+ // Find a key that matches our configured issuer
688
+ const matchingKey = oidcKeys.find((k) => k === sessionKey);
689
+ if (matchingKey) {
690
+ const matchingState = req.session?.[matchingKey];
691
+ if (matchingState?.token_response?.access_token) {
692
+ const accessToken = matchingState.token_response.access_token;
693
+ // Use the configured userinfo URL (safe, from database)
694
+ const userInfoResponse = await fetch(configuredUserinfoUrl, {
695
+ headers: {
696
+ 'Authorization': `Bearer ${accessToken}`,
697
+ 'Accept': 'application/json',
698
+ },
699
+ });
700
+ if (!userInfoResponse.ok) {
701
+ const errorText = await userInfoResponse.text();
702
+ throw new Error(`UserInfo request failed: ${userInfoResponse.status} ${userInfoResponse.statusText} - ${errorText}`);
703
+ }
704
+ const userInfo = await userInfoResponse.json();
705
+ // Get the verify function from the strategy
706
+ const strategy = passport._strategies[provider];
707
+ if (!strategy || !strategy._verify) {
708
+ throw new Error('Verify function not found for provider');
709
+ }
710
+ // Create a mock profile from userInfo
711
+ const profile = {
712
+ id: userInfo.sub || userInfo.id,
713
+ displayName: userInfo.name || userInfo.preferred_username,
714
+ name: userInfo.name,
715
+ emails: userInfo.email ? [{ value: userInfo.email }] : [],
716
+ email: userInfo.email,
717
+ };
718
+ // Call verify function with userInfo data
719
+ strategy._verify(configuredIssuer, profile, {}, // context
720
+ null, // idToken (not available in this flow)
721
+ accessToken, matchingState.token_response.refresh_token, {}, // params
722
+ async (verifyErr, verifiedUser) => {
723
+ if (verifyErr || !verifiedUser) {
724
+ console.error(`[OIDC] Verify function error:`, verifyErr);
725
+ return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/login?error=auth_failed`);
726
+ }
727
+ user = verifiedUser;
728
+ await handleSuccess();
729
+ });
730
+ return; // Exit early, handleSuccess already ran
731
+ }
732
+ }
733
+ throw new Error('No OAuth state found in session');
734
+ }
735
+ const accessToken = oauthState.token_response?.access_token;
736
+ if (!accessToken) {
737
+ throw new Error('No access token found in token response');
738
+ }
739
+ // Use the configured userinfo URL (safe, from database, not user input)
740
+ const userInfoResponse = await fetch(configuredUserinfoUrl, {
741
+ headers: {
742
+ 'Authorization': `Bearer ${accessToken}`,
743
+ 'Accept': 'application/json',
744
+ },
745
+ });
746
+ if (!userInfoResponse.ok) {
747
+ const errorText = await userInfoResponse.text();
748
+ throw new Error(`UserInfo request failed: ${userInfoResponse.status} ${userInfoResponse.statusText} - ${errorText}`);
749
+ }
750
+ const userInfo = await userInfoResponse.json();
751
+ // Get the verify function from the strategy
752
+ const strategy = passport._strategies[provider];
753
+ if (!strategy || !strategy._verify) {
754
+ throw new Error('Verify function not found for provider');
755
+ }
756
+ // Create a mock profile from userInfo
757
+ const profile = {
758
+ id: userInfo.sub || userInfo.id,
759
+ displayName: userInfo.name || userInfo.preferred_username,
760
+ name: userInfo.name,
761
+ emails: userInfo.email ? [{ value: userInfo.email }] : [],
762
+ email: userInfo.email,
763
+ };
764
+ // Call verify function with userInfo data
765
+ // Updated signature: (iss, profile, context, idToken, accessToken, refreshToken, params, cb)
766
+ strategy._verify(configuredIssuer, profile, {}, // context
767
+ null, // idToken (not available in this flow)
768
+ accessToken, oauthState.token_response?.refresh_token, {}, // params
769
+ async (verifyErr, verifiedUser) => {
770
+ if (verifyErr || !verifiedUser) {
771
+ console.error(`[OIDC] Verify function error:`, verifyErr);
772
+ return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/login?error=auth_failed`);
773
+ }
774
+ user = verifiedUser;
775
+ await handleSuccess();
776
+ });
777
+ return; // Exit early, handleSuccess already ran
778
+ }
779
+ catch (manualFetchError) {
780
+ console.error(`[OIDC] Manual userInfo fetch failed:`, {
781
+ message: manualFetchError.message,
782
+ stack: manualFetchError.stack,
783
+ });
784
+ return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/login?error=auth_failed`);
785
+ }
786
+ }
787
+ if (err || !user) {
788
+ // Check for specific error types
789
+ let errorParam = 'auth_failed';
790
+ if (err) {
791
+ // Sanitize provider for logging to prevent log injection
792
+ const safeProvider = String(provider || 'unknown').replace(/[^\w-]/g, '');
793
+ console.error('[OIDC] Authentication error', {
794
+ provider: safeProvider,
795
+ message: err.message,
796
+ stack: err.stack,
797
+ name: err.name,
798
+ });
799
+ if (err.message === 'AUTO_CREATE_DISABLED') {
800
+ errorParam = 'auto_create_disabled';
801
+ }
802
+ }
803
+ else if (!user) {
804
+ // Sanitize provider for logging to prevent log injection
805
+ const safeProvider = String(provider || 'unknown').replace(/[^\w-]/g, '');
806
+ console.error('[OIDC] No user returned', {
807
+ provider: safeProvider,
808
+ info: info,
809
+ });
810
+ }
811
+ return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:3000'}/login?error=${errorParam}`);
812
+ }
813
+ async function handleSuccess() {
814
+ const userPayload = { id: user.id, email: user.email, name: user.name, user_key: user.user_key, is_admin: user.is_admin };
815
+ const token = generateToken(userPayload);
816
+ setAuthCookies(res, { accessToken: token });
817
+ const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:3000';
818
+ const redirectUrl = frontendUrl;
819
+ req.session?.destroy((sessionErr) => {
820
+ if (sessionErr)
821
+ console.error('Error destroying session:', sessionErr);
822
+ res.redirect(redirectUrl);
823
+ });
824
+ }
825
+ // If we got here normally (not from manual fetch), handle success
826
+ if (user) {
827
+ await handleSuccess();
828
+ }
829
+ })(req, res, next);
830
+ });
831
+ /**
832
+ * @swagger
833
+ * /api/auth/setup:
834
+ * post:
835
+ * summary: Initial system setup
836
+ * description: Creates the first admin user. Only accessible when the system is not yet initialized.
837
+ * tags: [Authentication]
838
+ * requestBody:
839
+ * required: true
840
+ * content:
841
+ * application/json:
842
+ * schema:
843
+ * type: object
844
+ * required:
845
+ * - email
846
+ * - name
847
+ * - password
848
+ * properties:
849
+ * email:
850
+ * type: string
851
+ * format: email
852
+ * example: "admin@example.com"
853
+ * name:
854
+ * type: string
855
+ * example: "Admin User"
856
+ * password:
857
+ * type: string
858
+ * format: password
859
+ * minLength: 8
860
+ * example: "securepassword123"
861
+ * responses:
862
+ * 200:
863
+ * description: Setup completed successfully
864
+ * content:
865
+ * application/json:
866
+ * schema:
867
+ * type: object
868
+ * properties:
869
+ * message:
870
+ * type: string
871
+ * example: "Setup completed successfully. You can now log in."
872
+ * 400:
873
+ * description: Invalid input or user already exists
874
+ * 403:
875
+ * description: System already initialized
876
+ */
877
+ // Setup route - only accessible when system is not initialized. SELFHOSTED only.
878
+ router.post('/setup', strictRateLimiter, async (req, res) => {
879
+ try {
880
+ const initialized = await isInitialized();
881
+ if (initialized) {
882
+ return res.status(403).json({ error: 'System already initialized' });
883
+ }
884
+ const { email, name, password } = req.body;
885
+ if (!email || !name || !password) {
886
+ return res.status(400).json({ error: 'Email, name, and password are required' });
887
+ }
888
+ // Validate and normalize email
889
+ const emailValidation = validateEmail(email);
890
+ if (!emailValidation.valid) {
891
+ return res.status(400).json({ error: emailValidation.error });
892
+ }
893
+ const normalizedEmail = normalizeEmail(email);
894
+ // Validate name length
895
+ const nameValidation = validateLength(name, 'Name', 1, 255);
896
+ if (!nameValidation.valid) {
897
+ return res.status(400).json({ error: nameValidation.error });
898
+ }
899
+ const sanitizedName = sanitizeString(name);
900
+ // Validate password complexity
901
+ const passwordValidation = validatePassword(password);
902
+ if (!passwordValidation.valid) {
903
+ return res.status(400).json({ error: passwordValidation.error });
904
+ }
905
+ // Check if email already exists (use normalized email)
906
+ const existingUser = await queryOne('SELECT id FROM users WHERE email = ?', [normalizedEmail]);
907
+ if (existingUser) {
908
+ return res.status(400).json({ error: 'User with this email already exists' });
909
+ }
910
+ // Hash password
911
+ const passwordHash = await bcrypt.hash(password, 10);
912
+ // Create first admin user
913
+ const userId = uuidv4();
914
+ let userKey = await generateUserKey();
915
+ // Retry logic for user_key collisions (should be extremely rare)
916
+ let retries = 0;
917
+ const maxRetries = 3;
918
+ while (retries < maxRetries) {
919
+ try {
920
+ await execute(`INSERT INTO users (id, email, name, user_key, password_hash, is_admin)
921
+ VALUES (?, ?, ?, ?, ?, ?)`, [userId, normalizedEmail, sanitizedName, userKey, passwordHash, true] // First user is always admin
922
+ );
923
+ break; // Success, exit retry loop
924
+ }
925
+ catch (error) {
926
+ // If user_key collision, generate new key and retry
927
+ if (error.message && (error.message.includes('UNIQUE constraint') || error.message.includes('duplicate'))
928
+ && error.message.includes('user_key')) {
929
+ retries++;
930
+ if (retries >= maxRetries) {
931
+ return res.status(500).json({ error: 'Failed to complete setup. Please try again.' });
932
+ }
933
+ userKey = await generateUserKey();
934
+ continue; // Retry with new key
935
+ }
936
+ // For other errors (like email duplicate), throw to outer catch
937
+ throw error;
938
+ }
939
+ }
940
+ // Automatically log in the user after successful setup
941
+ const userPayload = { id: userId, email: normalizedEmail, name: sanitizedName, user_key: userKey, is_admin: true };
942
+ setAuthCookies(res, { accessToken: generateToken(userPayload) });
943
+ // Return user data (same format as login endpoint)
944
+ res.json({
945
+ id: userId,
946
+ email: normalizedEmail,
947
+ name: sanitizedName,
948
+ user_key: userKey,
949
+ is_admin: true,
950
+ language: 'en', // Default language
951
+ theme: 'auto', // Default theme
952
+ });
953
+ }
954
+ catch (error) {
955
+ // Handle unique constraint violations
956
+ if (error.message && (error.message.includes('UNIQUE constraint') || error.message.includes('duplicate'))) {
957
+ return res.status(400).json({ error: 'User with this email already exists' });
958
+ }
959
+ res.status(500).json({ error: error.message });
960
+ }
961
+ });
962
+ /**
963
+ * @swagger
964
+ * /api/auth/setup/status:
965
+ * get:
966
+ * summary: Check system initialization status
967
+ * description: Returns whether the system has been initialized (has at least one user)
968
+ * tags: [Authentication]
969
+ * responses:
970
+ * 200:
971
+ * description: System initialization status
972
+ * content:
973
+ * application/json:
974
+ * schema:
975
+ * type: object
976
+ * properties:
977
+ * initialized:
978
+ * type: boolean
979
+ * example: false
980
+ * description: true if system has been initialized, false otherwise
981
+ */
982
+ router.get('/setup/status', async (req, res) => {
983
+ try {
984
+ const initialized = await isInitialized();
985
+ res.json({ initialized });
986
+ }
987
+ catch (error) {
988
+ res.status(500).json({ error: error.message });
989
+ }
990
+ });
991
+ export default router;
992
+ //# sourceMappingURL=auth.js.map