@allbluecn/web-app 0.11.0 → 0.11.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 (101) hide show
  1. package/package.json +6 -3
  2. package/public/favicon-dark.svg +1 -0
  3. package/public/favicon.ico +0 -0
  4. package/public/favicon.svg +1 -0
  5. package/public/logo/allblue-logo-dark.png +0 -0
  6. package/public/logo/allblue-logo-light.png +0 -0
  7. package/src/__tests__/integration/login-email-acceptance.test.ts +202 -0
  8. package/src/__tests__/integration/login-email-p2-acceptance.test.ts +142 -0
  9. package/src/components/auth/icon-input.tsx +9 -1
  10. package/src/components/auth/password-input.tsx +9 -1
  11. package/src/components/changelog/changelog-page.tsx +44 -1
  12. package/src/components/forms/app-field.tsx +3 -1
  13. package/src/components/knowledge/knowledge-toolbar.tsx +17 -21
  14. package/src/components/layout/sidebar-03/app-sidebar.tsx +4 -15
  15. package/src/components/notifications/settings-notifications.tsx +41 -1
  16. package/src/components/settings/personal-team/account/settings-profile.tsx +68 -1
  17. package/src/lib/__tests__/edition-desktop.test.ts +23 -0
  18. package/src/lib/app-version.ts +1 -1
  19. package/src/lib/auth/auth.config.ts +123 -99
  20. package/src/lib/changelog/sdk-versions.ts +4 -4
  21. package/src/lib/edition-desktop.ts +22 -0
  22. package/src/lib/session.ts +2 -2
  23. package/src/routeTree.gen.ts +114 -0
  24. package/src/routes/__root.tsx +20 -3
  25. package/src/routes/__tests__/-auth-schemas.test.ts +62 -0
  26. package/src/routes/__tests__/-edition-routing.test.tsx +14 -1
  27. package/src/routes/__tests__/route-tree-structure.test.ts +1 -0
  28. package/src/routes/_login/changelog/confirm/route.lazy.tsx +72 -0
  29. package/src/routes/_login/changelog/confirm/route.tsx +28 -0
  30. package/src/routes/_login/email-unsubscribe/route.lazy.tsx +76 -0
  31. package/src/routes/_login/email-unsubscribe/route.tsx +28 -0
  32. package/src/routes/_login/forgot-password/route.lazy.tsx +2 -1
  33. package/src/routes/_login/login/route.lazy.tsx +4 -2
  34. package/src/routes/_login/register/route.lazy.tsx +28 -17
  35. package/src/routes/_login/register/route.tsx +8 -1
  36. package/src/routes/_login/reset-password/route.lazy.tsx +4 -2
  37. package/src/routes/_login/schemas/-auth.ts +30 -7
  38. package/src/routes/_login/verify-email/route.lazy.tsx +82 -0
  39. package/src/routes/_login/verify-email/route.tsx +28 -0
  40. package/src/routes/_nav/route.tsx +5 -0
  41. package/src/routes/_public/index/route.tsx +2 -1
  42. package/src/routes/api/auth/desktop-callback.ts +2 -2
  43. package/src/routes/api/billing-webhook.ts +4 -0
  44. package/src/routes/api/cron/changelog-release.ts +100 -0
  45. package/src/routes/api/cron/subscription-reminders.ts +110 -0
  46. package/src/routes/api.auth.$.ts +3 -5
  47. package/src/server/__tests__/auth-registration.test.ts +60 -0
  48. package/src/server/auth-registration.ts +40 -0
  49. package/src/server/billing-email.ts +39 -0
  50. package/src/server/email/__tests__/deliverability.test.ts +68 -0
  51. package/src/server/email/__tests__/index.test.ts +32 -0
  52. package/src/server/email/__tests__/outbox.test.tsx +129 -0
  53. package/src/server/email/__tests__/product-templates.test.tsx +106 -0
  54. package/src/server/email/__tests__/registry.test.tsx +50 -0
  55. package/src/server/email/deliverability.ts +57 -0
  56. package/src/server/email/drivers/console.ts +11 -0
  57. package/src/server/email/drivers/resend.ts +31 -0
  58. package/src/server/email/drivers/smtp.ts +27 -0
  59. package/src/server/email/index.ts +56 -0
  60. package/src/server/email/outbox.ts +141 -0
  61. package/src/server/email/preferences.ts +20 -0
  62. package/src/server/email/registry.ts +38 -0
  63. package/src/server/email/sender.ts +34 -17
  64. package/src/server/email/templates/_base/button.tsx +12 -0
  65. package/src/server/email/templates/_base/email-layout.tsx +117 -0
  66. package/src/server/email/templates/_base/theme-fonts.tsx +43 -0
  67. package/src/server/email/templates/_base/theme.ts +22 -0
  68. package/src/server/email/templates/auth/reset-password.tsx +58 -0
  69. package/src/server/email/templates/auth/verify-email.tsx +59 -0
  70. package/src/server/email/templates/auth/welcome.tsx +49 -0
  71. package/src/server/email/templates/billing/payment-receipt.tsx +51 -0
  72. package/src/server/email/templates/billing/subscription-canceled.tsx +51 -0
  73. package/src/server/email/templates/billing/subscription-ending.tsx +51 -0
  74. package/src/server/email/templates/notification/mirror.tsx +50 -0
  75. package/src/server/email/templates/product/changelog-confirm.tsx +53 -0
  76. package/src/server/email/templates/product/changelog-release.tsx +65 -0
  77. package/src/server/email/templates/team/apply-notify.tsx +53 -0
  78. package/src/server/email/templates/team/approved.tsx +53 -0
  79. package/src/server/email/templates/team/invite.tsx +57 -0
  80. package/src/server/middlewares/security-headers.ts +11 -2
  81. package/src/server/notifications/__tests__/email-mirror.test.ts +193 -0
  82. package/src/server/notifications/__tests__/verify-reminder.test.ts +94 -0
  83. package/src/server/notifications/notification-service.ts +71 -1
  84. package/src/server/notifications/verify-reminder.ts +55 -0
  85. package/src/server/oauth-config.ts +59 -0
  86. package/src/server/serverFns/__tests__/auth-server-schemas.test.ts +168 -0
  87. package/src/server/serverFns/__tests__/email-verification.test.ts +149 -0
  88. package/src/server/serverFns/__tests__/team-invitation.test.ts +2 -2
  89. package/src/server/serverFns/auth/email-verification.ts +132 -0
  90. package/src/server/serverFns/auth/forgot-password.ts +22 -2
  91. package/src/server/serverFns/auth/providers.ts +4 -3
  92. package/src/server/serverFns/auth/register.ts +63 -19
  93. package/src/server/serverFns/auth/registration-mode.ts +23 -0
  94. package/src/server/serverFns/auth/reset-password.ts +1 -1
  95. package/src/server/serverFns/auth/{invitation.ts → verify-reminder.ts} +9 -26
  96. package/src/server/serverFns/changelog/__tests__/subscribe.test.ts +119 -0
  97. package/src/server/serverFns/changelog/subscribe.ts +89 -0
  98. package/src/server/serverFns/email-unsubscribe.ts +66 -0
  99. package/src/server/serverFns/notifications.ts +10 -0
  100. package/src/server/serverFns/team.ts +49 -11
  101. package/vite.shared.ts +87 -3
@@ -18,10 +18,12 @@ import { Route as DocsRouteImport } from './routes/docs'
18
18
  import { Route as ShareRouteRouteImport } from './routes/share/route'
19
19
  import { Route as WorkspaceRouteImport } from './routes/workspace'
20
20
  import { Route as _desktopOnboardingRouteRouteImport } from './routes/__desktop/onboarding/route'
21
+ import { Route as LoginEmailUnsubscribeRouteRouteImport } from './routes/_login/email-unsubscribe/route'
21
22
  import { Route as LoginForgotPasswordRouteRouteImport } from './routes/_login/forgot-password/route'
22
23
  import { Route as LoginLoginRouteRouteImport } from './routes/_login/login/route'
23
24
  import { Route as LoginRegisterRouteRouteImport } from './routes/_login/register/route'
24
25
  import { Route as LoginResetPasswordRouteRouteImport } from './routes/_login/reset-password/route'
26
+ import { Route as LoginVerifyEmailRouteRouteImport } from './routes/_login/verify-email/route'
25
27
  import { Route as NavAcceptanceCriteriaRouteRouteImport } from './routes/_nav/acceptance-criteria/route'
26
28
  import { Route as NavAgentsRouteRouteImport } from './routes/_nav/agents/route'
27
29
  import { Route as NavAppsRouteRouteImport } from './routes/_nav/apps/route'
@@ -54,6 +56,7 @@ import { Route as ServerAiStreamRouteImport } from './routes/server.ai-stream'
54
56
  import { Route as ServerWsRouteImport } from './routes/server.ws'
55
57
  import { Route as ShareVersionIdRouteRouteImport } from './routes/share/$versionId/route'
56
58
  import { Route as LangDocsSplatRouteRouteImport } from './routes/$lang/docs/$/route'
59
+ import { Route as LoginChangelogConfirmRouteRouteImport } from './routes/_login/changelog/confirm/route'
57
60
  import { Route as NavAppsAnyhubRouteRouteImport } from './routes/_nav/apps/anyhub/route'
58
61
  import { Route as NavChatThreadIdRouteImport } from './routes/_nav/chat/$threadId'
59
62
  import { Route as NavDesignIndexRouteImport } from './routes/_nav/design/index'
@@ -88,6 +91,8 @@ import { Route as NavWorkspacesWorkspaceIdRouteRouteImport } from './routes/_nav
88
91
  import { Route as ApiAttachmentsUploadRouteImport } from './routes/api/attachments/upload'
89
92
  import { Route as ApiAuthSplatRouteImport } from './routes/api.auth.$'
90
93
  import { Route as ApiAuthDesktopCallbackRouteImport } from './routes/api/auth/desktop-callback'
94
+ import { Route as ApiCronChangelogReleaseRouteImport } from './routes/api/cron/changelog-release'
95
+ import { Route as ApiCronSubscriptionRemindersRouteImport } from './routes/api/cron/subscription-reminders'
91
96
  import { Route as ApiDesignUploadRouteImport } from './routes/api/design/upload'
92
97
  import { Route as ApiKnowledgeSyncGithubRouteImport } from './routes/api/knowledge/sync-github'
93
98
  import { Route as ApiLicenseActivateRouteRouteImport } from './routes/api/license/activate/route'
@@ -187,6 +192,14 @@ const _desktopOnboardingRouteRoute = _desktopOnboardingRouteRouteImport
187
192
  .lazy(() =>
188
193
  import('./routes/__desktop/onboarding/route.lazy').then((d) => d.Route),
189
194
  )
195
+ const LoginEmailUnsubscribeRouteRoute =
196
+ LoginEmailUnsubscribeRouteRouteImport.update({
197
+ id: '/email-unsubscribe',
198
+ path: '/email-unsubscribe',
199
+ getParentRoute: () => LoginRouteRoute,
200
+ } as any).lazy(() =>
201
+ import('./routes/_login/email-unsubscribe/route.lazy').then((d) => d.Route),
202
+ )
190
203
  const LoginForgotPasswordRouteRoute =
191
204
  LoginForgotPasswordRouteRouteImport.update({
192
205
  id: '/forgot-password',
@@ -216,6 +229,13 @@ const LoginResetPasswordRouteRoute = LoginResetPasswordRouteRouteImport.update({
216
229
  } as any).lazy(() =>
217
230
  import('./routes/_login/reset-password/route.lazy').then((d) => d.Route),
218
231
  )
232
+ const LoginVerifyEmailRouteRoute = LoginVerifyEmailRouteRouteImport.update({
233
+ id: '/verify-email',
234
+ path: '/verify-email',
235
+ getParentRoute: () => LoginRouteRoute,
236
+ } as any).lazy(() =>
237
+ import('./routes/_login/verify-email/route.lazy').then((d) => d.Route),
238
+ )
219
239
  const NavAcceptanceCriteriaRouteRoute =
220
240
  NavAcceptanceCriteriaRouteRouteImport.update({
221
241
  id: '/acceptance-criteria',
@@ -395,6 +415,14 @@ const LangDocsSplatRouteRoute = LangDocsSplatRouteRouteImport.update({
395
415
  } as any).lazy(() =>
396
416
  import('./routes/$lang/docs/$/route.lazy').then((d) => d.Route),
397
417
  )
418
+ const LoginChangelogConfirmRouteRoute =
419
+ LoginChangelogConfirmRouteRouteImport.update({
420
+ id: '/changelog/confirm',
421
+ path: '/changelog/confirm',
422
+ getParentRoute: () => LoginRouteRoute,
423
+ } as any).lazy(() =>
424
+ import('./routes/_login/changelog/confirm/route.lazy').then((d) => d.Route),
425
+ )
398
426
  const NavAppsAnyhubRouteRoute = NavAppsAnyhubRouteRouteImport.update({
399
427
  id: '/anyhub',
400
428
  path: '/anyhub',
@@ -615,6 +643,17 @@ const ApiAuthDesktopCallbackRoute = ApiAuthDesktopCallbackRouteImport.update({
615
643
  path: '/api/auth/desktop-callback',
616
644
  getParentRoute: () => rootRouteImport,
617
645
  } as any)
646
+ const ApiCronChangelogReleaseRoute = ApiCronChangelogReleaseRouteImport.update({
647
+ id: '/api/cron/changelog-release',
648
+ path: '/api/cron/changelog-release',
649
+ getParentRoute: () => rootRouteImport,
650
+ } as any)
651
+ const ApiCronSubscriptionRemindersRoute =
652
+ ApiCronSubscriptionRemindersRouteImport.update({
653
+ id: '/api/cron/subscription-reminders',
654
+ path: '/api/cron/subscription-reminders',
655
+ getParentRoute: () => rootRouteImport,
656
+ } as any)
618
657
  const ApiDesignUploadRoute = ApiDesignUploadRouteImport.update({
619
658
  id: '/api/design/upload',
620
659
  path: '/api/design/upload',
@@ -901,10 +940,12 @@ export interface FileRoutesByFullPath {
901
940
  '/docs': typeof DocsRoute
902
941
  '/workspace': typeof WorkspaceRoute
903
942
  '/onboarding': typeof _desktopOnboardingRouteRoute
943
+ '/email-unsubscribe': typeof LoginEmailUnsubscribeRouteRoute
904
944
  '/forgot-password': typeof LoginForgotPasswordRouteRoute
905
945
  '/login': typeof LoginLoginRouteRoute
906
946
  '/register': typeof LoginRegisterRouteRoute
907
947
  '/reset-password': typeof LoginResetPasswordRouteRoute
948
+ '/verify-email': typeof LoginVerifyEmailRouteRoute
908
949
  '/acceptance-criteria': typeof NavAcceptanceCriteriaRouteRoute
909
950
  '/agents': typeof NavAgentsRouteRoute
910
951
  '/apps': typeof NavAppsRouteRouteWithChildren
@@ -936,6 +977,7 @@ export interface FileRoutesByFullPath {
936
977
  '/server/ai-stream': typeof ServerAiStreamRoute
937
978
  '/server/ws': typeof ServerWsRoute
938
979
  '/$lang/docs/$': typeof LangDocsSplatRouteRoute
980
+ '/changelog/confirm': typeof LoginChangelogConfirmRouteRoute
939
981
  '/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
940
982
  '/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
941
983
  '/knowledge/$kbId': typeof NavKnowledgeKbIdRouteRouteWithChildren
@@ -969,6 +1011,8 @@ export interface FileRoutesByFullPath {
969
1011
  '/api/attachments/upload': typeof ApiAttachmentsUploadRoute
970
1012
  '/api/auth/$': typeof ApiAuthSplatRoute
971
1013
  '/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
1014
+ '/api/cron/changelog-release': typeof ApiCronChangelogReleaseRoute
1015
+ '/api/cron/subscription-reminders': typeof ApiCronSubscriptionRemindersRoute
972
1016
  '/api/design/upload': typeof ApiDesignUploadRoute
973
1017
  '/api/knowledge/sync-github': typeof ApiKnowledgeSyncGithubRoute
974
1018
  '/api/notifications/stream': typeof ApiNotificationsStreamRoute
@@ -1013,10 +1057,12 @@ export interface FileRoutesByTo {
1013
1057
  '/docs': typeof DocsRoute
1014
1058
  '/workspace': typeof WorkspaceRoute
1015
1059
  '/onboarding': typeof _desktopOnboardingRouteRoute
1060
+ '/email-unsubscribe': typeof LoginEmailUnsubscribeRouteRoute
1016
1061
  '/forgot-password': typeof LoginForgotPasswordRouteRoute
1017
1062
  '/login': typeof LoginLoginRouteRoute
1018
1063
  '/register': typeof LoginRegisterRouteRoute
1019
1064
  '/reset-password': typeof LoginResetPasswordRouteRoute
1065
+ '/verify-email': typeof LoginVerifyEmailRouteRoute
1020
1066
  '/acceptance-criteria': typeof NavAcceptanceCriteriaRouteRoute
1021
1067
  '/agents': typeof NavAgentsRouteRoute
1022
1068
  '/apps': typeof NavAppsRouteRouteWithChildren
@@ -1042,6 +1088,7 @@ export interface FileRoutesByTo {
1042
1088
  '/server/ai-stream': typeof ServerAiStreamRoute
1043
1089
  '/server/ws': typeof ServerWsRoute
1044
1090
  '/$lang/docs/$': typeof LangDocsSplatRouteRoute
1091
+ '/changelog/confirm': typeof LoginChangelogConfirmRouteRoute
1045
1092
  '/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
1046
1093
  '/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
1047
1094
  '/prd/$id': typeof NavPrdIdRouteRoute
@@ -1067,6 +1114,8 @@ export interface FileRoutesByTo {
1067
1114
  '/api/attachments/upload': typeof ApiAttachmentsUploadRoute
1068
1115
  '/api/auth/$': typeof ApiAuthSplatRoute
1069
1116
  '/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
1117
+ '/api/cron/changelog-release': typeof ApiCronChangelogReleaseRoute
1118
+ '/api/cron/subscription-reminders': typeof ApiCronSubscriptionRemindersRoute
1070
1119
  '/api/design/upload': typeof ApiDesignUploadRoute
1071
1120
  '/api/knowledge/sync-github': typeof ApiKnowledgeSyncGithubRoute
1072
1121
  '/api/notifications/stream': typeof ApiNotificationsStreamRoute
@@ -1115,10 +1164,12 @@ export interface FileRoutesById {
1115
1164
  '/workspace': typeof WorkspaceRoute
1116
1165
  '/_public/': typeof PublicIndexRouteRoute
1117
1166
  '/__desktop/onboarding': typeof _desktopOnboardingRouteRoute
1167
+ '/_login/email-unsubscribe': typeof LoginEmailUnsubscribeRouteRoute
1118
1168
  '/_login/forgot-password': typeof LoginForgotPasswordRouteRoute
1119
1169
  '/_login/login': typeof LoginLoginRouteRoute
1120
1170
  '/_login/register': typeof LoginRegisterRouteRoute
1121
1171
  '/_login/reset-password': typeof LoginResetPasswordRouteRoute
1172
+ '/_login/verify-email': typeof LoginVerifyEmailRouteRoute
1122
1173
  '/_nav/acceptance-criteria': typeof NavAcceptanceCriteriaRouteRoute
1123
1174
  '/_nav/agents': typeof NavAgentsRouteRoute
1124
1175
  '/_nav/apps': typeof NavAppsRouteRouteWithChildren
@@ -1150,6 +1201,7 @@ export interface FileRoutesById {
1150
1201
  '/server/ai-stream': typeof ServerAiStreamRoute
1151
1202
  '/server/ws': typeof ServerWsRoute
1152
1203
  '/$lang/docs/$': typeof LangDocsSplatRouteRoute
1204
+ '/_login/changelog/confirm': typeof LoginChangelogConfirmRouteRoute
1153
1205
  '/_nav/apps/anyhub': typeof NavAppsAnyhubRouteRouteWithChildren
1154
1206
  '/_nav/design/$projectId': typeof NavDesignProjectIdRouteRouteWithChildren
1155
1207
  '/_nav/knowledge/$kbId': typeof NavKnowledgeKbIdRouteRouteWithChildren
@@ -1183,6 +1235,8 @@ export interface FileRoutesById {
1183
1235
  '/api/attachments/upload': typeof ApiAttachmentsUploadRoute
1184
1236
  '/api/auth/$': typeof ApiAuthSplatRoute
1185
1237
  '/api/auth/desktop-callback': typeof ApiAuthDesktopCallbackRoute
1238
+ '/api/cron/changelog-release': typeof ApiCronChangelogReleaseRoute
1239
+ '/api/cron/subscription-reminders': typeof ApiCronSubscriptionRemindersRoute
1186
1240
  '/api/design/upload': typeof ApiDesignUploadRoute
1187
1241
  '/api/knowledge/sync-github': typeof ApiKnowledgeSyncGithubRoute
1188
1242
  '/api/notifications/stream': typeof ApiNotificationsStreamRoute
@@ -1229,10 +1283,12 @@ export interface FileRouteTypes {
1229
1283
  | '/docs'
1230
1284
  | '/workspace'
1231
1285
  | '/onboarding'
1286
+ | '/email-unsubscribe'
1232
1287
  | '/forgot-password'
1233
1288
  | '/login'
1234
1289
  | '/register'
1235
1290
  | '/reset-password'
1291
+ | '/verify-email'
1236
1292
  | '/acceptance-criteria'
1237
1293
  | '/agents'
1238
1294
  | '/apps'
@@ -1264,6 +1320,7 @@ export interface FileRouteTypes {
1264
1320
  | '/server/ai-stream'
1265
1321
  | '/server/ws'
1266
1322
  | '/$lang/docs/$'
1323
+ | '/changelog/confirm'
1267
1324
  | '/apps/anyhub'
1268
1325
  | '/design/$projectId'
1269
1326
  | '/knowledge/$kbId'
@@ -1297,6 +1354,8 @@ export interface FileRouteTypes {
1297
1354
  | '/api/attachments/upload'
1298
1355
  | '/api/auth/$'
1299
1356
  | '/api/auth/desktop-callback'
1357
+ | '/api/cron/changelog-release'
1358
+ | '/api/cron/subscription-reminders'
1300
1359
  | '/api/design/upload'
1301
1360
  | '/api/knowledge/sync-github'
1302
1361
  | '/api/notifications/stream'
@@ -1341,10 +1400,12 @@ export interface FileRouteTypes {
1341
1400
  | '/docs'
1342
1401
  | '/workspace'
1343
1402
  | '/onboarding'
1403
+ | '/email-unsubscribe'
1344
1404
  | '/forgot-password'
1345
1405
  | '/login'
1346
1406
  | '/register'
1347
1407
  | '/reset-password'
1408
+ | '/verify-email'
1348
1409
  | '/acceptance-criteria'
1349
1410
  | '/agents'
1350
1411
  | '/apps'
@@ -1370,6 +1431,7 @@ export interface FileRouteTypes {
1370
1431
  | '/server/ai-stream'
1371
1432
  | '/server/ws'
1372
1433
  | '/$lang/docs/$'
1434
+ | '/changelog/confirm'
1373
1435
  | '/apps/anyhub'
1374
1436
  | '/design/$projectId'
1375
1437
  | '/prd/$id'
@@ -1395,6 +1457,8 @@ export interface FileRouteTypes {
1395
1457
  | '/api/attachments/upload'
1396
1458
  | '/api/auth/$'
1397
1459
  | '/api/auth/desktop-callback'
1460
+ | '/api/cron/changelog-release'
1461
+ | '/api/cron/subscription-reminders'
1398
1462
  | '/api/design/upload'
1399
1463
  | '/api/knowledge/sync-github'
1400
1464
  | '/api/notifications/stream'
@@ -1442,10 +1506,12 @@ export interface FileRouteTypes {
1442
1506
  | '/workspace'
1443
1507
  | '/_public/'
1444
1508
  | '/__desktop/onboarding'
1509
+ | '/_login/email-unsubscribe'
1445
1510
  | '/_login/forgot-password'
1446
1511
  | '/_login/login'
1447
1512
  | '/_login/register'
1448
1513
  | '/_login/reset-password'
1514
+ | '/_login/verify-email'
1449
1515
  | '/_nav/acceptance-criteria'
1450
1516
  | '/_nav/agents'
1451
1517
  | '/_nav/apps'
@@ -1477,6 +1543,7 @@ export interface FileRouteTypes {
1477
1543
  | '/server/ai-stream'
1478
1544
  | '/server/ws'
1479
1545
  | '/$lang/docs/$'
1546
+ | '/_login/changelog/confirm'
1480
1547
  | '/_nav/apps/anyhub'
1481
1548
  | '/_nav/design/$projectId'
1482
1549
  | '/_nav/knowledge/$kbId'
@@ -1510,6 +1577,8 @@ export interface FileRouteTypes {
1510
1577
  | '/api/attachments/upload'
1511
1578
  | '/api/auth/$'
1512
1579
  | '/api/auth/desktop-callback'
1580
+ | '/api/cron/changelog-release'
1581
+ | '/api/cron/subscription-reminders'
1513
1582
  | '/api/design/upload'
1514
1583
  | '/api/knowledge/sync-github'
1515
1584
  | '/api/notifications/stream'
@@ -1571,6 +1640,8 @@ export interface RootRouteChildren {
1571
1640
  ApiAttachmentsUploadRoute: typeof ApiAttachmentsUploadRoute
1572
1641
  ApiAuthSplatRoute: typeof ApiAuthSplatRoute
1573
1642
  ApiAuthDesktopCallbackRoute: typeof ApiAuthDesktopCallbackRoute
1643
+ ApiCronChangelogReleaseRoute: typeof ApiCronChangelogReleaseRoute
1644
+ ApiCronSubscriptionRemindersRoute: typeof ApiCronSubscriptionRemindersRoute
1574
1645
  ApiDesignUploadRoute: typeof ApiDesignUploadRoute
1575
1646
  ApiKnowledgeSyncGithubRoute: typeof ApiKnowledgeSyncGithubRoute
1576
1647
  ApiNotificationsStreamRoute: typeof ApiNotificationsStreamRoute
@@ -1631,6 +1702,13 @@ declare module '@tanstack/react-router' {
1631
1702
  preLoaderRoute: typeof _desktopOnboardingRouteRouteImport
1632
1703
  parentRoute: typeof rootRouteImport
1633
1704
  }
1705
+ '/_login/email-unsubscribe': {
1706
+ id: '/_login/email-unsubscribe'
1707
+ path: '/email-unsubscribe'
1708
+ fullPath: '/email-unsubscribe'
1709
+ preLoaderRoute: typeof LoginEmailUnsubscribeRouteRouteImport
1710
+ parentRoute: typeof LoginRouteRoute
1711
+ }
1634
1712
  '/_login/forgot-password': {
1635
1713
  id: '/_login/forgot-password'
1636
1714
  path: '/forgot-password'
@@ -1659,6 +1737,13 @@ declare module '@tanstack/react-router' {
1659
1737
  preLoaderRoute: typeof LoginResetPasswordRouteRouteImport
1660
1738
  parentRoute: typeof LoginRouteRoute
1661
1739
  }
1740
+ '/_login/verify-email': {
1741
+ id: '/_login/verify-email'
1742
+ path: '/verify-email'
1743
+ fullPath: '/verify-email'
1744
+ preLoaderRoute: typeof LoginVerifyEmailRouteRouteImport
1745
+ parentRoute: typeof LoginRouteRoute
1746
+ }
1662
1747
  '/_nav/acceptance-criteria': {
1663
1748
  id: '/_nav/acceptance-criteria'
1664
1749
  path: '/acceptance-criteria'
@@ -1883,6 +1968,13 @@ declare module '@tanstack/react-router' {
1883
1968
  preLoaderRoute: typeof LangDocsSplatRouteRouteImport
1884
1969
  parentRoute: typeof rootRouteImport
1885
1970
  }
1971
+ '/_login/changelog/confirm': {
1972
+ id: '/_login/changelog/confirm'
1973
+ path: '/changelog/confirm'
1974
+ fullPath: '/changelog/confirm'
1975
+ preLoaderRoute: typeof LoginChangelogConfirmRouteRouteImport
1976
+ parentRoute: typeof LoginRouteRoute
1977
+ }
1886
1978
  '/_nav/apps/anyhub': {
1887
1979
  id: '/_nav/apps/anyhub'
1888
1980
  path: '/anyhub'
@@ -2121,6 +2213,20 @@ declare module '@tanstack/react-router' {
2121
2213
  preLoaderRoute: typeof ApiAuthDesktopCallbackRouteImport
2122
2214
  parentRoute: typeof rootRouteImport
2123
2215
  }
2216
+ '/api/cron/changelog-release': {
2217
+ id: '/api/cron/changelog-release'
2218
+ path: '/api/cron/changelog-release'
2219
+ fullPath: '/api/cron/changelog-release'
2220
+ preLoaderRoute: typeof ApiCronChangelogReleaseRouteImport
2221
+ parentRoute: typeof rootRouteImport
2222
+ }
2223
+ '/api/cron/subscription-reminders': {
2224
+ id: '/api/cron/subscription-reminders'
2225
+ path: '/api/cron/subscription-reminders'
2226
+ fullPath: '/api/cron/subscription-reminders'
2227
+ preLoaderRoute: typeof ApiCronSubscriptionRemindersRouteImport
2228
+ parentRoute: typeof rootRouteImport
2229
+ }
2124
2230
  '/api/design/upload': {
2125
2231
  id: '/api/design/upload'
2126
2232
  path: '/api/design/upload'
@@ -2377,17 +2483,23 @@ declare module '@tanstack/react-router' {
2377
2483
  }
2378
2484
 
2379
2485
  interface LoginRouteRouteChildren {
2486
+ LoginEmailUnsubscribeRouteRoute: typeof LoginEmailUnsubscribeRouteRoute
2380
2487
  LoginForgotPasswordRouteRoute: typeof LoginForgotPasswordRouteRoute
2381
2488
  LoginLoginRouteRoute: typeof LoginLoginRouteRoute
2382
2489
  LoginRegisterRouteRoute: typeof LoginRegisterRouteRoute
2383
2490
  LoginResetPasswordRouteRoute: typeof LoginResetPasswordRouteRoute
2491
+ LoginVerifyEmailRouteRoute: typeof LoginVerifyEmailRouteRoute
2492
+ LoginChangelogConfirmRouteRoute: typeof LoginChangelogConfirmRouteRoute
2384
2493
  }
2385
2494
 
2386
2495
  const LoginRouteRouteChildren: LoginRouteRouteChildren = {
2496
+ LoginEmailUnsubscribeRouteRoute: LoginEmailUnsubscribeRouteRoute,
2387
2497
  LoginForgotPasswordRouteRoute: LoginForgotPasswordRouteRoute,
2388
2498
  LoginLoginRouteRoute: LoginLoginRouteRoute,
2389
2499
  LoginRegisterRouteRoute: LoginRegisterRouteRoute,
2390
2500
  LoginResetPasswordRouteRoute: LoginResetPasswordRouteRoute,
2501
+ LoginVerifyEmailRouteRoute: LoginVerifyEmailRouteRoute,
2502
+ LoginChangelogConfirmRouteRoute: LoginChangelogConfirmRouteRoute,
2391
2503
  }
2392
2504
 
2393
2505
  const LoginRouteRouteWithChildren = LoginRouteRoute._addFileChildren(
@@ -2840,6 +2952,8 @@ const rootRouteChildren: RootRouteChildren = {
2840
2952
  ApiAttachmentsUploadRoute: ApiAttachmentsUploadRoute,
2841
2953
  ApiAuthSplatRoute: ApiAuthSplatRoute,
2842
2954
  ApiAuthDesktopCallbackRoute: ApiAuthDesktopCallbackRoute,
2955
+ ApiCronChangelogReleaseRoute: ApiCronChangelogReleaseRoute,
2956
+ ApiCronSubscriptionRemindersRoute: ApiCronSubscriptionRemindersRoute,
2843
2957
  ApiDesignUploadRoute: ApiDesignUploadRoute,
2844
2958
  ApiKnowledgeSyncGithubRoute: ApiKnowledgeSyncGithubRoute,
2845
2959
  ApiNotificationsStreamRoute: ApiNotificationsStreamRoute,
@@ -16,6 +16,7 @@
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
18
  import { Link, Outlet, createRootRouteWithContext, HeadContent, Scripts, useParams } from '@tanstack/react-router'
19
+ import { useEffect } from 'react'
19
20
  import { HotkeysProvider } from '@tanstack/react-hotkeys'
20
21
  import { ShortcutsPanel } from '@/components/hotkeys/shortcuts-panel'
21
22
  import { Analytics } from '@vercel/analytics/react'
@@ -53,7 +54,8 @@ export const Route = createRootRouteWithContext<RouterContext>()({
53
54
  { name: 'description', content: '专为产品经理打造的协作平台' },
54
55
  ],
55
56
  links: [
56
- { rel: 'icon', href: '/favicon.ico' },
57
+ { rel: 'icon', href: '/favicon.svg', type: 'image/svg+xml' },
58
+ { rel: 'icon', href: '/favicon.ico', sizes: '48x48' },
57
59
  { rel: 'stylesheet', href: '/fonts/fonts.css' },
58
60
  { rel: 'stylesheet', href: globalsCss },
59
61
  ],
@@ -66,6 +68,20 @@ export const Route = createRootRouteWithContext<RouterContext>()({
66
68
  function RootComponent() {
67
69
  const session = Route.useRouteContext({ select: (c) => c.session }) as Session | null
68
70
  const { lang } = useParams({ strict: false }) as { lang?: string | undefined }
71
+
72
+ useEffect(() => {
73
+ const media = window.matchMedia('(prefers-color-scheme: dark)')
74
+ const update = () => {
75
+ const link = document.querySelector<HTMLLinkElement>('link[rel="icon"][type="image/svg+xml"]')
76
+ if (!link) return
77
+ const target = media.matches ? '/favicon-dark.svg' : '/favicon.svg'
78
+ if (link.getAttribute('href') !== target) link.href = target
79
+ }
80
+ update()
81
+ media.addEventListener('change', update)
82
+ return () => media.removeEventListener('change', update)
83
+ }, [])
84
+
69
85
  return (
70
86
  <html lang={getLocale()} suppressHydrationWarning>
71
87
  <head>
@@ -83,8 +99,9 @@ function RootComponent() {
83
99
  </RootProvider>
84
100
  </Providers>
85
101
  <Scripts />
86
- <Analytics />
87
- <SpeedInsights />
102
+ {/* va.vercel-scripts.com 在部分网络环境不可达,dev 下跳过避免控制台噪音 */}
103
+ {import.meta.env.PROD && <Analytics />}
104
+ {import.meta.env.PROD && <SpeedInsights />}
88
105
  </body>
89
106
  </html>
90
107
  )
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { loginSchema, registerSchema, resetPasswordSchema, forgotPasswordSchema } from '../_login/schemas/-auth'
3
+
4
+ describe('registerSchema', () => {
5
+ it('拒绝纯数字密码(缺字母)', () => {
6
+ expect(registerSchema.safeParse({ username: 'alice', email: 'a@b.com', password: '12345678', confirmPassword: '12345678', invitationCode: 'ABC123' }).success).toBe(false)
7
+ })
8
+ it('拒绝纯字母密码(缺数字)', () => {
9
+ expect(registerSchema.safeParse({ username: 'alice', email: 'a@b.com', password: 'abcdefgh', confirmPassword: 'abcdefgh', invitationCode: 'ABC123' }).success).toBe(false)
10
+ })
11
+ it('拒绝非法用户名字符', () => {
12
+ expect(registerSchema.safeParse({ username: '阿里', email: 'a@b.com', password: 'abc12345', confirmPassword: 'abc12345', invitationCode: 'ABC123' }).success).toBe(false)
13
+ })
14
+ it('接受字母+数字密码与合法用户名', () => {
15
+ expect(registerSchema.safeParse({ username: 'alice_01', email: 'a@b.com', password: 'abc12345', confirmPassword: 'abc12345', invitationCode: 'ABC123' }).success).toBe(true)
16
+ })
17
+ it('invitationCode 可空(open 模式前端忽略,服务端校验)', () => {
18
+ expect(registerSchema.safeParse({ username: 'alice', email: 'a@b.com', password: 'abc12345', confirmPassword: 'abc12345', invitationCode: '' }).success).toBe(true)
19
+ })
20
+ it('invitationCode 拒绝非法字符', () => {
21
+ const r = registerSchema.safeParse({ username: 'alice', email: 'a@b.com', password: 'abc12345', confirmPassword: 'abc12345', invitationCode: '!!!' })
22
+ expect(r.success).toBe(false)
23
+ if (!r.success) expect(r.error.issues[0].path).toEqual(['invitationCode'])
24
+ })
25
+ it('invitationCode 拒绝超长(>32 位)', () => {
26
+ expect(registerSchema.safeParse({ username: 'alice', email: 'a@b.com', password: 'abc12345', confirmPassword: 'abc12345', invitationCode: 'a'.repeat(33) }).success).toBe(false)
27
+ })
28
+ it('invitationCode 缺省(未传字段)→ 默认空串通过', () => {
29
+ expect(registerSchema.safeParse({ username: 'alice', email: 'a@b.com', password: 'abc12345', confirmPassword: 'abc12345' }).success).toBe(true)
30
+ })
31
+ })
32
+
33
+ describe('resetPasswordSchema', () => {
34
+ it('拒绝纯数字密码', () => {
35
+ expect(resetPasswordSchema.safeParse({ password: '12345678', confirmPassword: '12345678' }).success).toBe(false)
36
+ })
37
+ it('接受字母+数字密码', () => {
38
+ expect(resetPasswordSchema.safeParse({ password: 'abc12345', confirmPassword: 'abc12345' }).success).toBe(true)
39
+ })
40
+ })
41
+
42
+ describe('邮箱归一化(emailRule transform)', () => {
43
+ it('loginSchema 归一小写并去首尾空格', () => {
44
+ const r = loginSchema.safeParse({ email: ' A@B.COM ', password: 'x', remember: false })
45
+ expect(r.success).toBe(true)
46
+ if (r.success) expect(r.data.email).toBe('a@b.com')
47
+ })
48
+
49
+ it('forgotPasswordSchema 同样归一', () => {
50
+ const r = forgotPasswordSchema.safeParse({ email: 'X@Y.ORG' })
51
+ expect(r.success).toBe(true)
52
+ if (r.success) expect(r.data.email).toBe('x@y.org')
53
+ })
54
+
55
+ it('registerSchema 同样归一', () => {
56
+ const r = registerSchema.safeParse({
57
+ username: 'alice_01', email: ' M@N.IO ', password: 'abc12345', confirmPassword: 'abc12345',
58
+ })
59
+ expect(r.success).toBe(true)
60
+ if (r.success) expect(r.data.email).toBe('m@n.io')
61
+ })
62
+ })
@@ -3,10 +3,11 @@ import React from 'react'
3
3
 
4
4
  // edition.ts 从 @d plugins/modules.gen 读 profileId 并派生 isSaasEdition,
5
5
  // mock 必须在 edition 被 import 之前生效,否则 isSaasEdition 被缓存为 false(oss)。
6
- async function importIndexRoute(profileId: 'oss' | 'saas') {
6
+ async function importIndexRoute(profileId: 'oss' | 'saas', desktop = false) {
7
7
  vi.doMock('@/paraglide/runtime', () => ({ getLocale: () => 'zh', overwriteGetLocale: () => {}, locales: ['zh', 'en'], baseLocale: 'zh' }))
8
8
  vi.resetModules()
9
9
  vi.doMock('@/plugins/modules.gen', () => ({ profileId }))
10
+ vi.doMock('@/lib/edition-desktop', () => ({ isDesktopEdition: () => desktop }))
10
11
  return import('../_public/index/route')
11
12
  }
12
13
 
@@ -55,6 +56,18 @@ describe('_public/index beforeLoad', () => {
55
56
  const { Route } = await importIndexRoute('saas')
56
57
  expect(catchRedirect(() => Route.options.beforeLoad!( {} as never))).toBeNull()
57
58
  }, 20000)
59
+
60
+ it('desktop + SaaS:redirect 到 /dashboard(跳过官网)', async () => {
61
+ const { Route } = await importIndexRoute('saas', true)
62
+ const r = catchRedirect(() => Route.options.beforeLoad!( {} as never))
63
+ expect(r?.to).toBe('/dashboard')
64
+ }, 20000)
65
+
66
+ it('desktop + OSS:redirect 到 /dashboard', async () => {
67
+ const { Route } = await importIndexRoute('oss', true)
68
+ const r = catchRedirect(() => Route.options.beforeLoad!( {} as never))
69
+ expect(r?.to).toBe('/dashboard')
70
+ }, 20000)
58
71
  })
59
72
 
60
73
  describe('_nav beforeLoad', () => {
@@ -17,6 +17,7 @@ const MUST_EXIST_PATHS: string[] = [
17
17
  "'/register'",
18
18
  "'/forgot-password'",
19
19
  "'/reset-password'",
20
+ "'/verify-email'",
20
21
  // 导航区(_nav pathless layout,登录后落地页)
21
22
  "'/dashboard'",
22
23
  "'/workspaces'",
@@ -0,0 +1,72 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute, Link } from '@tanstack/react-router'
19
+ import { RainbowButton } from '@/components/magicui/rainbow-button'
20
+ import { ArrowRight } from 'lucide-react'
21
+ import { AuthLayout, AuthLayoutLeft, AuthLayoutRight } from '@/components/auth/auth-layout'
22
+ import { AuthHeader } from '@/components/auth/auth-header'
23
+ import { AuthBrandPanel } from '@/components/auth/auth-brand-panel'
24
+ import { m } from '@/paraglide/messages.js'
25
+
26
+ export const Route = createLazyFileRoute('/_login/changelog/confirm')({
27
+ component: ChangelogConfirmPage,
28
+ })
29
+
30
+ function ChangelogConfirmPage() {
31
+ const { ok } = Route.useRouteContext()
32
+
33
+ return (
34
+ <AuthLayout>
35
+ <AuthLayoutLeft>
36
+ {ok ? (
37
+ <>
38
+ <AuthHeader
39
+ title={m.common_changelog_confirm_success()}
40
+ subtitle={m.common_changelog_confirm_successDesc()}
41
+ />
42
+ <RainbowButton asChild className="w-full">
43
+ <Link to="/changelog">
44
+ {m.common_changelog_confirm_view()}
45
+ <ArrowRight className="ml-2 h-4 w-4" />
46
+ </Link>
47
+ </RainbowButton>
48
+ </>
49
+ ) : (
50
+ <>
51
+ <AuthHeader
52
+ title={m.common_changelog_confirm_invalid()}
53
+ subtitle={m.common_changelog_confirm_invalidDesc()}
54
+ />
55
+ <RainbowButton asChild className="w-full">
56
+ <Link to="/changelog">
57
+ {m.common_changelog_confirm_resubscribe()}
58
+ <ArrowRight className="ml-2 h-4 w-4" />
59
+ </Link>
60
+ </RainbowButton>
61
+ </>
62
+ )}
63
+ </AuthLayoutLeft>
64
+ <AuthLayoutRight>
65
+ <AuthBrandPanel
66
+ title="AllBlue"
67
+ subtitle={ok ? m.common_changelog_confirm_success() : m.common_changelog_confirm_invalid()}
68
+ />
69
+ </AuthLayoutRight>
70
+ </AuthLayout>
71
+ )
72
+ }
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from '@tanstack/react-router'
19
+ import { confirmChangelogSubscriptionFn } from '@/server/serverFns/changelog/subscribe'
20
+
21
+ export const Route = createFileRoute('/_login/changelog/confirm')({
22
+ validateSearch: (s: { token?: string }) => ({ token: s.token ?? '' }),
23
+ beforeLoad: async ({ search }) => {
24
+ if (!search.token) return { ok: false }
25
+ const r = await confirmChangelogSubscriptionFn({ data: { token: search.token } })
26
+ return { ok: r.ok }
27
+ },
28
+ })