@nextsparkjs/core 0.1.0-beta.195 → 0.1.0-beta.197

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 (283) hide show
  1. package/README.md +2 -0
  2. package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
  3. package/dist/components/auth/forms/SignupForm.js +55 -45
  4. package/dist/components/billing/InvoicesPagination.js +2 -2
  5. package/dist/components/dashboard/block-editor/config-panel.d.ts.map +1 -1
  6. package/dist/components/dashboard/block-editor/config-panel.js +3 -2
  7. package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
  8. package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
  9. package/dist/components/dashboard/navigation/DynamicNavigation.js +2 -2
  10. package/dist/components/settings/layouts/SettingsSidebar.d.ts.map +1 -1
  11. package/dist/components/settings/layouts/SettingsSidebar.js +0 -1
  12. package/dist/components/teams/CreateTeamButton.js +2 -2
  13. package/dist/components/teams/CreateTeamDialog.js +1 -1
  14. package/dist/components/teams/InviteMemberButton.js +3 -3
  15. package/dist/components/teams/InviteMemberDialog.js +1 -1
  16. package/dist/components/teams/TeamPendingInvitations.js +2 -2
  17. package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
  18. package/dist/components/users/tables/UsersTable.js +3 -4
  19. package/dist/hooks/useAuthReadiness.d.ts +1 -0
  20. package/dist/hooks/useAuthReadiness.d.ts.map +1 -1
  21. package/dist/hooks/useAuthReadiness.js +3 -1
  22. package/dist/lib/actions/entity.actions.js +2 -2
  23. package/dist/lib/actions/team.actions.js +2 -2
  24. package/dist/lib/actions/user.actions.js +4 -4
  25. package/dist/lib/api/api-error.d.ts +2 -0
  26. package/dist/lib/api/api-error.d.ts.map +1 -1
  27. package/dist/lib/api/api-error.js +3 -1
  28. package/dist/lib/api/auth/dual-auth.d.ts +2 -1
  29. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  30. package/dist/lib/api/auth/dual-auth.js +25 -6
  31. package/dist/lib/api/auth/scope-policy.d.ts +6 -3
  32. package/dist/lib/api/auth/scope-policy.d.ts.map +1 -1
  33. package/dist/lib/api/auth.d.ts.map +1 -1
  34. package/dist/lib/api/auth.js +7 -0
  35. package/dist/lib/api/cors-response.d.ts +3 -2
  36. package/dist/lib/api/cors-response.d.ts.map +1 -1
  37. package/dist/lib/api/cors-response.js +8 -6
  38. package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
  39. package/dist/lib/api/entity/generic-handler.js +66 -15
  40. package/dist/lib/api/entity-handler.js +2 -2
  41. package/dist/lib/api/helpers.d.ts.map +1 -1
  42. package/dist/lib/api/helpers.js +9 -24
  43. package/dist/lib/api/permission-middleware.d.ts.map +1 -1
  44. package/dist/lib/api/permission-middleware.js +0 -2
  45. package/dist/lib/api/post-categories.d.ts +29 -0
  46. package/dist/lib/api/post-categories.d.ts.map +1 -0
  47. package/dist/lib/api/post-categories.js +33 -0
  48. package/dist/lib/api/rate-limit.d.ts +8 -0
  49. package/dist/lib/api/rate-limit.d.ts.map +1 -1
  50. package/dist/lib/api/rate-limit.js +34 -3
  51. package/dist/lib/api/request-origin.d.ts.map +1 -1
  52. package/dist/lib/api/request-origin.js +1 -1
  53. package/dist/lib/auth/auth-methods.d.ts +5 -2
  54. package/dist/lib/auth/auth-methods.d.ts.map +1 -1
  55. package/dist/lib/auth/auth-methods.js +4 -2
  56. package/dist/lib/auth/authorization-session.d.ts +14 -0
  57. package/dist/lib/auth/authorization-session.d.ts.map +1 -0
  58. package/dist/lib/auth/authorization-session.js +10 -0
  59. package/dist/lib/auth/request-session.d.ts +5 -0
  60. package/dist/lib/auth/request-session.d.ts.map +1 -1
  61. package/dist/lib/auth/request-session.js +3 -0
  62. package/dist/lib/auth/runtime-readiness.d.ts +7 -4
  63. package/dist/lib/auth/runtime-readiness.d.ts.map +1 -1
  64. package/dist/lib/auth/runtime-readiness.js +9 -6
  65. package/dist/lib/auth/sample-accounts.d.ts +18 -0
  66. package/dist/lib/auth/sample-accounts.d.ts.map +1 -0
  67. package/dist/lib/auth/sample-accounts.js +25 -0
  68. package/dist/lib/auth/suspension.d.ts +22 -0
  69. package/dist/lib/auth/suspension.d.ts.map +1 -0
  70. package/dist/lib/auth/suspension.js +43 -0
  71. package/dist/lib/auth.d.ts.map +1 -1
  72. package/dist/lib/auth.js +20 -17
  73. package/dist/lib/cache/public-entity-cache.d.ts +21 -0
  74. package/dist/lib/cache/public-entity-cache.d.ts.map +1 -0
  75. package/dist/lib/cache/public-entity-cache.js +25 -0
  76. package/dist/lib/config/app.config.d.ts.map +1 -1
  77. package/dist/lib/config/app.config.js +6 -7
  78. package/dist/lib/config/types.d.ts +8 -6
  79. package/dist/lib/config/types.d.ts.map +1 -1
  80. package/dist/lib/db-pool.d.ts +31 -0
  81. package/dist/lib/db-pool.d.ts.map +1 -0
  82. package/dist/lib/db-pool.js +75 -0
  83. package/dist/lib/db.d.ts +1 -1
  84. package/dist/lib/db.d.ts.map +1 -1
  85. package/dist/lib/db.js +11 -14
  86. package/dist/lib/entities/api-generator.js +5 -5
  87. package/dist/lib/entities/entity-hooks.d.ts.map +1 -1
  88. package/dist/lib/entities/entity-hooks.js +4 -0
  89. package/dist/lib/mcp/errors.d.ts +2 -0
  90. package/dist/lib/mcp/errors.d.ts.map +1 -1
  91. package/dist/lib/mcp/errors.js +3 -1
  92. package/dist/lib/mcp/index.d.ts +1 -1
  93. package/dist/lib/mcp/index.d.ts.map +1 -1
  94. package/dist/lib/mcp/index.js +2 -1
  95. package/dist/lib/oauth/token-refresh.d.ts.map +1 -1
  96. package/dist/lib/oauth/token-refresh.js +3 -6
  97. package/dist/lib/permissions/check.d.ts +9 -0
  98. package/dist/lib/permissions/check.d.ts.map +1 -1
  99. package/dist/lib/permissions/check.js +11 -1
  100. package/dist/lib/services/generic-entity.service.d.ts.map +1 -1
  101. package/dist/lib/services/generic-entity.service.js +41 -7
  102. package/dist/lib/services/transactional-meta.service.d.ts.map +1 -1
  103. package/dist/lib/services/transactional-meta.service.js +7 -8
  104. package/dist/lib/teams/actions.d.ts.map +1 -1
  105. package/dist/lib/teams/actions.js +0 -1
  106. package/dist/lib/validation.d.ts +5 -0
  107. package/dist/lib/validation.d.ts.map +1 -1
  108. package/dist/lib/validation.js +2 -0
  109. package/dist/messages/de/billing.json +12 -1
  110. package/dist/messages/de/common.json +2 -0
  111. package/dist/messages/de/docs.json +1 -0
  112. package/dist/messages/de/features.json +16 -4
  113. package/dist/messages/de/home.json +5 -0
  114. package/dist/messages/de/index.d.ts +68 -25
  115. package/dist/messages/de/index.d.ts.map +1 -1
  116. package/dist/messages/de/index.js +2 -0
  117. package/dist/messages/de/index.ts +2 -0
  118. package/dist/messages/de/settings.json +1 -0
  119. package/dist/messages/de/teams.json +3 -18
  120. package/dist/messages/en/billing.json +1 -1
  121. package/dist/messages/en/common.json +2 -0
  122. package/dist/messages/en/docs.json +1 -0
  123. package/dist/messages/en/features.json +16 -4
  124. package/dist/messages/en/home.json +5 -0
  125. package/dist/messages/en/index.d.ts +58 -25
  126. package/dist/messages/en/index.d.ts.map +1 -1
  127. package/dist/messages/en/index.js +2 -0
  128. package/dist/messages/en/index.ts +2 -0
  129. package/dist/messages/en/settings.json +1 -0
  130. package/dist/messages/en/teams.json +3 -18
  131. package/dist/messages/es/billing.json +1 -1
  132. package/dist/messages/es/common.json +2 -0
  133. package/dist/messages/es/docs.json +1 -0
  134. package/dist/messages/es/features.json +16 -4
  135. package/dist/messages/es/home.json +5 -0
  136. package/dist/messages/es/index.d.ts +41 -8
  137. package/dist/messages/es/index.d.ts.map +1 -1
  138. package/dist/messages/es/index.js +2 -0
  139. package/dist/messages/es/index.ts +2 -0
  140. package/dist/messages/es/settings.json +1 -0
  141. package/dist/messages/es/teams.json +3 -0
  142. package/dist/messages/fr/billing.json +12 -1
  143. package/dist/messages/fr/common.json +2 -0
  144. package/dist/messages/fr/docs.json +1 -0
  145. package/dist/messages/fr/features.json +16 -4
  146. package/dist/messages/fr/home.json +5 -0
  147. package/dist/messages/fr/index.d.ts +68 -25
  148. package/dist/messages/fr/index.d.ts.map +1 -1
  149. package/dist/messages/fr/index.js +2 -0
  150. package/dist/messages/fr/index.ts +2 -0
  151. package/dist/messages/fr/settings.json +1 -0
  152. package/dist/messages/fr/teams.json +3 -18
  153. package/dist/messages/it/billing.json +12 -1
  154. package/dist/messages/it/common.json +2 -0
  155. package/dist/messages/it/docs.json +1 -0
  156. package/dist/messages/it/features.json +16 -4
  157. package/dist/messages/it/home.json +5 -0
  158. package/dist/messages/it/index.d.ts +68 -25
  159. package/dist/messages/it/index.d.ts.map +1 -1
  160. package/dist/messages/it/index.js +2 -0
  161. package/dist/messages/it/index.ts +2 -0
  162. package/dist/messages/it/settings.json +1 -0
  163. package/dist/messages/it/teams.json +3 -18
  164. package/dist/messages/pt/billing.json +12 -1
  165. package/dist/messages/pt/common.json +2 -0
  166. package/dist/messages/pt/docs.json +1 -0
  167. package/dist/messages/pt/features.json +16 -4
  168. package/dist/messages/pt/home.json +5 -0
  169. package/dist/messages/pt/index.d.ts +68 -25
  170. package/dist/messages/pt/index.d.ts.map +1 -1
  171. package/dist/messages/pt/index.js +2 -0
  172. package/dist/messages/pt/index.ts +2 -0
  173. package/dist/messages/pt/settings.json +1 -0
  174. package/dist/messages/pt/teams.json +3 -18
  175. package/dist/migrations/029_sample_accounts_outside_development.sql +55 -0
  176. package/dist/migrations/030_taxonomies_team_writes.sql +29 -0
  177. package/dist/migrations/031_users_role_suspended.sql +41 -0
  178. package/dist/migrations/090_sample_data.sql +10 -0
  179. package/dist/routes/(auth)/signup/page.cc.d.ts +3 -1
  180. package/dist/routes/(auth)/signup/page.cc.d.ts.map +1 -1
  181. package/dist/routes/(auth)/signup/page.cc.js +2 -2
  182. package/dist/routes/(auth)/signup/page.d.ts +3 -1
  183. package/dist/routes/(auth)/signup/page.d.ts.map +1 -1
  184. package/dist/routes/(auth)/signup/page.js +4 -3
  185. package/dist/routes/_internal/area-access.js +2 -2
  186. package/dist/routes/_internal/entity-permission-layout.d.ts.map +1 -1
  187. package/dist/routes/_internal/entity-permission-layout.js +0 -7
  188. package/dist/routes/api/devtools/config/entities/route.js +2 -2
  189. package/dist/routes/api/devtools/config/theme/route.js +2 -2
  190. package/dist/routes/api/devtools/tests/[...path]/route.js +2 -2
  191. package/dist/routes/api/devtools/tests/route.js +2 -2
  192. package/dist/routes/api/mcp/route.d.ts.map +1 -1
  193. package/dist/routes/api/mcp/route.js +7 -1
  194. package/dist/routes/api/superadmin/subscriptions/route.js +2 -2
  195. package/dist/routes/api/superadmin/teams/[teamId]/route.js +2 -2
  196. package/dist/routes/api/superadmin/teams/route.js +2 -2
  197. package/dist/routes/api/superadmin/users/[userId]/route.d.ts +5 -3
  198. package/dist/routes/api/superadmin/users/[userId]/route.d.ts.map +1 -1
  199. package/dist/routes/api/superadmin/users/[userId]/route.js +46 -23
  200. package/dist/routes/api/superadmin/users/route.js +2 -2
  201. package/dist/routes/api/user/delete-account/route.d.ts.map +1 -1
  202. package/dist/routes/api/user/delete-account/route.js +2 -3
  203. package/dist/routes/api/user/plan-flags/route.d.ts.map +1 -1
  204. package/dist/routes/api/user/plan-flags/route.js +5 -5
  205. package/dist/routes/api/user/profile/route.js +3 -3
  206. package/dist/routes/api/v1/auth/signup-with-invite/route.d.ts.map +1 -1
  207. package/dist/routes/api/v1/auth/signup-with-invite/route.js +80 -44
  208. package/dist/routes/api/v1/media/upload/route.d.ts +2 -1
  209. package/dist/routes/api/v1/media/upload/route.d.ts.map +1 -1
  210. package/dist/routes/api/v1/media/upload/route.js +10 -2
  211. package/dist/routes/api/v1/post-categories/[id]/route.d.ts.map +1 -1
  212. package/dist/routes/api/v1/post-categories/[id]/route.js +39 -34
  213. package/dist/routes/api/v1/post-categories/route.d.ts.map +1 -1
  214. package/dist/routes/api/v1/post-categories/route.js +18 -14
  215. package/dist/routes/api/v1/team-invitations/route.d.ts.map +1 -1
  216. package/dist/routes/api/v1/team-invitations/route.js +12 -5
  217. package/dist/routes/api/v1/teams/[teamId]/members/route.d.ts.map +1 -1
  218. package/dist/routes/api/v1/teams/[teamId]/members/route.js +12 -10
  219. package/dist/routes/api/v1/teams/switch/route.d.ts.map +1 -1
  220. package/dist/routes/api/v1/teams/switch/route.js +4 -1
  221. package/dist/routes/dashboard/settings/password/page.d.ts.map +1 -1
  222. package/dist/routes/dashboard/settings/password/page.js +10 -12
  223. package/dist/routes/devtools/config/page.js +1 -1
  224. package/dist/routes/devtools/page.js +1 -1
  225. package/dist/routes/devtools/scheduled-actions/page.js +1 -1
  226. package/dist/routes/devtools/tests/[[...path]]/page.js +1 -1
  227. package/dist/routes/superadmin/users/page.js +2 -2
  228. package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
  229. package/dist/templates/ci-workflows/github/cypress-smoke.yml +2 -1
  230. package/dist/templates/next.config.mjs +11 -31
  231. package/dist/templates/projects/blog/package.json +1 -1
  232. package/dist/templates/projects/crm/package.json +1 -1
  233. package/dist/templates/projects/productivity/package.json +1 -1
  234. package/dist/templates/projects/starter/README.md +2 -0
  235. package/dist/templates/projects/starter/config/app.config.ts +8 -0
  236. package/dist/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
  237. package/dist/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
  238. package/dist/templates/proxy.ts +18 -8
  239. package/dist/templates/public/uploads/README.md +5 -94
  240. package/migrations/029_sample_accounts_outside_development.sql +55 -0
  241. package/migrations/030_taxonomies_team_writes.sql +29 -0
  242. package/migrations/031_users_role_suspended.sql +41 -0
  243. package/migrations/090_sample_data.sql +10 -0
  244. package/package.json +3 -3
  245. package/scripts/build/registry/__tests__/build-output.test.mjs +1 -1
  246. package/scripts/build/registry/__tests__/project-mode-root-first.test.mjs +6 -6
  247. package/scripts/build/registry/__tests__/write-places.test.mjs +1 -1
  248. package/scripts/build/registry/contracts/__tests__/contracts-wire.test.mjs +1 -1
  249. package/scripts/build/registry/contracts/__tests__/contracts.test.mjs +1 -1
  250. package/scripts/build/registry/host/README.md +2 -2
  251. package/scripts/build/registry/host/__tests__/facade-emitter.test.mjs +14 -13
  252. package/scripts/build/registry/host/__tests__/host-mode.test.mjs +1 -1
  253. package/scripts/build/registry/host/__tests__/host-project-composition.test.mjs +1 -1
  254. package/scripts/build/registry/host/__tests__/host-project.test.mjs +2 -2
  255. package/scripts/build/registry/host/__tests__/host-proxy-areas.test.mjs +99 -1
  256. package/scripts/build/registry/host/__tests__/host-robustness.test.mjs +1 -1
  257. package/scripts/build/registry/host/facade-emitter.mjs +1 -1
  258. package/scripts/build/registry/host/next-route-exports.json +2 -2
  259. package/scripts/build/registry/host/proxy-areas.mjs +92 -2
  260. package/scripts/db/run-migrations.mjs +30 -10
  261. package/scripts/db/sample-data.mjs +44 -0
  262. package/scripts/db/verify-theme-migrations.mjs +7 -1
  263. package/templates/ci-workflows/github/cypress-regression.yml +1 -1
  264. package/templates/ci-workflows/github/cypress-smoke.yml +2 -1
  265. package/templates/next.config.mjs +11 -31
  266. package/templates/projects/blog/package.json +1 -1
  267. package/templates/projects/crm/package.json +1 -1
  268. package/templates/projects/productivity/package.json +1 -1
  269. package/templates/projects/starter/README.md +2 -0
  270. package/templates/projects/starter/config/app.config.ts +8 -0
  271. package/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
  272. package/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
  273. package/templates/proxy.ts +18 -8
  274. package/templates/public/uploads/README.md +5 -94
  275. package/tests/jest/__mocks__/@nextsparkjs/registries/api-presets-registry.ts +2 -0
  276. package/tests/jest/__mocks__/@nextsparkjs/registries/mcp-registry.ts +2 -0
  277. package/tests/jest/__mocks__/@nextsparkjs/registries/permissions-registry.ts +5 -4
  278. package/dist/templates/projects/starter/public/brand/.gitkeep +0 -0
  279. package/dist/templates/public/uploads/.gitignore +0 -6
  280. package/dist/templates/public/uploads/temp/.gitkeep +0 -1
  281. package/templates/projects/starter/public/brand/.gitkeep +0 -0
  282. package/templates/public/uploads/.gitignore +0 -6
  283. package/templates/public/uploads/temp/.gitkeep +0 -1
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
- import { authenticateRequest, createAuthFailureResponse } from "@nextsparkjs/core/lib/api/auth/dual-auth";
2
+ import { categoryReadTeam, categoryWriter } from "@nextsparkjs/core/lib/api/post-categories";
3
3
  import { query as dbQuery } from "@nextsparkjs/core/lib/db";
4
4
  import * as z from "zod";
5
5
  import { withRateLimitTier } from "@nextsparkjs/core/lib/api/rate-limit";
@@ -15,15 +15,33 @@ const updateCategorySchema = z.object({
15
15
  isDefault: z.boolean().optional(),
16
16
  isActive: z.boolean().optional()
17
17
  });
18
+ async function findCategory(id, writer) {
19
+ const found = await dbQuery(
20
+ `SELECT "teamId" FROM taxonomies WHERE id = $1 AND type = 'post_category' AND ("teamId" IS NULL OR "teamId" = $2)`,
21
+ [id, writer.teamId]
22
+ );
23
+ if (found.rows.length === 0) {
24
+ return { response: NextResponse.json({ success: false, error: "Category not found" }, { status: 404 }) };
25
+ }
26
+ if (found.rows[0].teamId === null && !writer.superadmin) {
27
+ return { response: NextResponse.json(
28
+ { success: false, error: "Only a superadmin can change a category shared by every team", code: "PERMISSION_DENIED" },
29
+ { status: 403 }
30
+ ) };
31
+ }
32
+ return found.rows[0];
33
+ }
18
34
  export const GET = withRateLimitTier(async (request, { params }) => {
19
35
  try {
20
36
  const { id } = await params;
37
+ const scope = await categoryReadTeam(request);
38
+ if ("response" in scope) return scope.response;
21
39
  const result = await dbQuery(
22
40
  `SELECT id, name, slug, description, icon, color, "parentId", "order", "isDefault", "isActive",
23
41
  "createdAt", "updatedAt"
24
42
  FROM taxonomies
25
- WHERE id = $1 AND type = 'post_category' AND "deletedAt" IS NULL`,
26
- [id]
43
+ WHERE id = $1 AND type = 'post_category' AND "deletedAt" IS NULL AND ("teamId" IS NULL OR "teamId" = $2)`,
44
+ [id, scope.teamId]
27
45
  );
28
46
  if (result.rows.length === 0) {
29
47
  return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
@@ -39,24 +57,17 @@ export const GET = withRateLimitTier(async (request, { params }) => {
39
57
  }, "read");
40
58
  export const PUT = withRateLimitTier(async (request, { params }) => {
41
59
  try {
42
- const authResult = await authenticateRequest(request, { requiredScope: "posts:write" });
43
- if (!authResult.success || !authResult.user) {
44
- return createAuthFailureResponse(authResult);
45
- }
60
+ const writer = await categoryWriter(request, "update");
61
+ if ("response" in writer) return writer.response;
46
62
  const body = await request.json();
47
63
  const data = updateCategorySchema.parse(body);
48
64
  const { id } = await params;
49
- const typeCheck = await dbQuery(
50
- "SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category'",
51
- [id]
52
- );
53
- if (typeCheck.rows.length === 0) {
54
- return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
55
- }
65
+ const row = await findCategory(id, writer);
66
+ if ("response" in row) return row.response;
56
67
  if (data.slug) {
57
68
  const slugCheck = await dbQuery(
58
- "SELECT id FROM taxonomies WHERE type = 'post_category' AND slug = $1 AND id != $2",
59
- [data.slug, id]
69
+ `SELECT id FROM taxonomies WHERE type = 'post_category' AND slug = $1 AND id != $2 AND "teamId" IS NOT DISTINCT FROM $3`,
70
+ [data.slug, id, row.teamId]
60
71
  );
61
72
  if (slugCheck.rows.length > 0) {
62
73
  return NextResponse.json(
@@ -67,8 +78,8 @@ export const PUT = withRateLimitTier(async (request, { params }) => {
67
78
  }
68
79
  if (data.parentId) {
69
80
  const parentCheck = await dbQuery(
70
- "SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category'",
71
- [data.parentId]
81
+ `SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category' AND ("teamId" IS NULL OR "teamId" IS NOT DISTINCT FROM $2)`,
82
+ [data.parentId, row.teamId]
72
83
  );
73
84
  if (parentCheck.rows.length === 0) {
74
85
  return NextResponse.json({
@@ -132,7 +143,8 @@ export const PUT = withRateLimitTier(async (request, { params }) => {
132
143
  return NextResponse.json({ success: true, data: current.rows[0] });
133
144
  }
134
145
  values.push(id);
135
- const query = `UPDATE taxonomies SET ${updates.join(", ")} WHERE id = $${paramIndex} RETURNING *`;
146
+ values.push(row.teamId);
147
+ const query = `UPDATE taxonomies SET ${updates.join(", ")} WHERE id = $${paramIndex} AND "teamId" IS NOT DISTINCT FROM $${paramIndex + 1} RETURNING *`;
136
148
  const result = await dbQuery(query, values);
137
149
  if (result.rows.length === 0) {
138
150
  return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
@@ -152,18 +164,11 @@ export const PUT = withRateLimitTier(async (request, { params }) => {
152
164
  }, "write");
153
165
  export const DELETE = withRateLimitTier(async (request, { params }) => {
154
166
  try {
155
- const authResult = await authenticateRequest(request, { requiredScope: "posts:delete" });
156
- if (!authResult.success || !authResult.user) {
157
- return createAuthFailureResponse(authResult);
158
- }
167
+ const writer = await categoryWriter(request, "delete");
168
+ if ("response" in writer) return writer.response;
159
169
  const { id } = await params;
160
- const typeCheck = await dbQuery(
161
- "SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category'",
162
- [id]
163
- );
164
- if (typeCheck.rows.length === 0) {
165
- return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
166
- }
170
+ const row = await findCategory(id, writer);
171
+ if ("response" in row) return row.response;
167
172
  let usageCount = 0;
168
173
  try {
169
174
  const usageCheck = await dbQuery(
@@ -177,8 +182,8 @@ export const DELETE = withRateLimitTier(async (request, { params }) => {
177
182
  }
178
183
  if (usageCount > 0) {
179
184
  await dbQuery(
180
- 'UPDATE taxonomies SET "deletedAt" = now() WHERE id = $1',
181
- [id]
185
+ 'UPDATE taxonomies SET "deletedAt" = now() WHERE id = $1 AND "teamId" IS NOT DISTINCT FROM $2',
186
+ [id, row.teamId]
182
187
  );
183
188
  return NextResponse.json({
184
189
  success: true,
@@ -186,8 +191,8 @@ export const DELETE = withRateLimitTier(async (request, { params }) => {
186
191
  });
187
192
  } else {
188
193
  const result = await dbQuery(
189
- "DELETE FROM taxonomies WHERE id = $1 RETURNING id",
190
- [id]
194
+ 'DELETE FROM taxonomies WHERE id = $1 AND "teamId" IS NOT DISTINCT FROM $2 RETURNING id',
195
+ [id, row.teamId]
191
196
  );
192
197
  if (result.rows.length === 0) {
193
198
  return NextResponse.json({ success: false, error: "Category not found" }, { status: 404 });
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/routes/api/v1/post-categories/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAKvD,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AAyBvE,eAAO,MAAM,GAAG,iDAkBN,CAAC;AAGX,eAAO,MAAM,IAAI,iDAsEN,CAAC;AAEZ,eAAO,MAAM,OAAO,sBAAgB,CAAA"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/routes/api/v1/post-categories/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAKvD,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AA0BvE,eAAO,MAAM,GAAG,iDAuBN,CAAC;AAGX,eAAO,MAAM,IAAI,iDAqEN,CAAC;AAEZ,eAAO,MAAM,OAAO,sBAAgB,CAAA"}
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
- import { authenticateRequest, createAuthFailureResponse } from "@nextsparkjs/core/lib/api/auth/dual-auth";
2
+ import { categoryReadTeam, categoryWriter } from "@nextsparkjs/core/lib/api/post-categories";
3
3
  import { query as dbQuery } from "@nextsparkjs/core/lib/db";
4
4
  import * as z from "zod";
5
5
  import { withRateLimitTier } from "@nextsparkjs/core/lib/api/rate-limit";
@@ -17,14 +17,18 @@ const createCategorySchema = z.object({
17
17
  function generateSlug(name) {
18
18
  return name.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
19
19
  }
20
- export const GET = withRateLimitTier(async () => {
20
+ export const GET = withRateLimitTier(async (request) => {
21
21
  try {
22
+ const scope = await categoryReadTeam(request);
23
+ if ("response" in scope) return scope.response;
22
24
  const result = await dbQuery(
23
25
  `SELECT id, name, slug, description, icon, color, "parentId", "order", "isDefault", "isActive",
24
26
  "createdAt", "updatedAt"
25
27
  FROM taxonomies
26
28
  WHERE type = 'post_category' AND "deletedAt" IS NULL AND "isActive" = true
27
- ORDER BY "order" ASC, name ASC`
29
+ AND ("teamId" IS NULL OR "teamId" = $1)
30
+ ORDER BY "order" ASC, name ASC`,
31
+ [scope.teamId]
28
32
  );
29
33
  return NextResponse.json({
30
34
  success: true,
@@ -37,16 +41,15 @@ export const GET = withRateLimitTier(async () => {
37
41
  }, "read");
38
42
  export const POST = withRateLimitTier(async (request) => {
39
43
  try {
40
- const authResult = await authenticateRequest(request, { requiredScope: "posts:write" });
41
- if (!authResult.success || !authResult.user) {
42
- return createAuthFailureResponse(authResult);
43
- }
44
+ const writer = await categoryWriter(request, "create");
45
+ if ("response" in writer) return writer.response;
46
+ const teamId = writer.teamId;
44
47
  const body = await request.json();
45
48
  const data = createCategorySchema.parse(body);
46
49
  const slug = data.slug || generateSlug(data.name);
47
50
  const existing = await dbQuery(
48
- "SELECT id FROM taxonomies WHERE type = 'post_category' AND slug = $1",
49
- [slug]
51
+ `SELECT id FROM taxonomies WHERE type = 'post_category' AND slug = $1 AND "teamId" = $2`,
52
+ [slug, teamId]
50
53
  );
51
54
  if (existing.rows.length > 0) {
52
55
  return NextResponse.json({
@@ -57,8 +60,8 @@ export const POST = withRateLimitTier(async (request) => {
57
60
  }
58
61
  if (data.parentId) {
59
62
  const parentCheck = await dbQuery(
60
- "SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category'",
61
- [data.parentId]
63
+ `SELECT id FROM taxonomies WHERE id = $1 AND type = 'post_category' AND ("teamId" IS NULL OR "teamId" = $2)`,
64
+ [data.parentId, teamId]
62
65
  );
63
66
  if (parentCheck.rows.length === 0) {
64
67
  return NextResponse.json({
@@ -69,8 +72,8 @@ export const POST = withRateLimitTier(async (request) => {
69
72
  }
70
73
  const result = await dbQuery(
71
74
  `INSERT INTO taxonomies (
72
- type, slug, name, description, icon, color, "parentId", "order", "isDefault", "userId"
73
- ) VALUES ('post_category', $1, $2, $3, $4, $5, $6, $7, $8, $9)
75
+ type, slug, name, description, icon, color, "parentId", "order", "isDefault", "userId", "teamId"
76
+ ) VALUES ('post_category', $1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
74
77
  RETURNING *`,
75
78
  [
76
79
  slug,
@@ -81,7 +84,8 @@ export const POST = withRateLimitTier(async (request) => {
81
84
  data.parentId || null,
82
85
  data.order,
83
86
  data.isDefault,
84
- authResult.user.id
87
+ writer.userId,
88
+ teamId
85
89
  ]
86
90
  );
87
91
  return NextResponse.json({ success: true, data: result.rows[0] }, { status: 201 });
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/routes/api/v1/team-invitations/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAgBvD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,kCAEjD;AAGD,eAAO,MAAM,GAAG,iDA2FL,CAAC"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/routes/api/v1/team-invitations/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAgBvD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,kCAEjD;AAGD,eAAO,MAAM,GAAG,iDAkGL,CAAC"}
@@ -25,11 +25,13 @@ export const GET = withRateLimitTier(withApiLogging(async (req) => {
25
25
  }
26
26
  const userEmail = authResult.user.email;
27
27
  const { searchParams } = new URL(req.url);
28
- const queryParams = {
29
- page: searchParams.get("page"),
30
- limit: searchParams.get("limit"),
31
- status: searchParams.get("status")
32
- };
28
+ const queryParams = Object.fromEntries(
29
+ Object.entries({
30
+ page: searchParams.get("page"),
31
+ limit: searchParams.get("limit"),
32
+ status: searchParams.get("status")
33
+ }).filter(([, value]) => value !== null && value !== "")
34
+ );
33
35
  const validatedQuery = invitationListQuerySchema.parse(queryParams);
34
36
  const { page, limit, status } = validatedQuery;
35
37
  const offset = (page - 1) * limit;
@@ -75,6 +77,11 @@ export const GET = withRateLimitTier(withApiLogging(async (req) => {
75
77
  const response = createApiResponse(invitations, paginationMeta);
76
78
  return addCorsHeaders(response, req);
77
79
  } catch (error) {
80
+ if (error instanceof Error && error.name === "ZodError") {
81
+ const zodError = error;
82
+ const response2 = createApiError("Validation error", 400, zodError.issues, "VALIDATION_ERROR");
83
+ return addCorsHeaders(response2, req);
84
+ }
78
85
  console.error("Error fetching invitations:", error);
79
86
  const response = createApiError("Internal server error", 500);
80
87
  return addCorsHeaders(response, req);
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/routes/api/v1/teams/[teamId]/members/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAwBvD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,kCAEjD;AAGD,eAAO,MAAM,GAAG;YACiC,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;2BA2HlE,CAAA;AAGV,eAAO,MAAM,IAAI;YACgC,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;2BAgMjE,CAAA"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/routes/api/v1/teams/[teamId]/members/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAwBvD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,kCAEjD;AAGD,eAAO,MAAM,GAAG;YACiC,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;2BA2HlE,CAAA;AAGV,eAAO,MAAM,IAAI;YACgC,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;2BAkMjE,CAAA"}
@@ -237,18 +237,20 @@ export const POST = withRateLimitTier(withApiLogging(
237
237
  subject: emailContent.subject,
238
238
  html: emailContent.html
239
239
  });
240
- console.log("\n" + "\u{1F3AB}".repeat(30));
241
- console.log("\u{1F4E8} TEAM INVITATION CREATED");
242
- console.log("\u{1F3AB}".repeat(30));
243
- console.log(`\u{1F4E7} To: ${validatedData.email}`);
244
- console.log(`\u{1F464} Invited by: ${inviterName}`);
245
- console.log(`\u{1F3E2} Team: ${team.name}`);
246
- console.log(`\u{1F451} Role: ${validatedData.role}`);
247
- console.log(`\u{1F517} Accept URL: ${acceptUrl}`);
248
- console.log("\u{1F3AB}".repeat(30) + "\n");
240
+ if (process.env.NODE_ENV === "development") {
241
+ console.log("\n" + "\u{1F3AB}".repeat(30));
242
+ console.log("\u{1F4E8} TEAM INVITATION CREATED");
243
+ console.log("\u{1F3AB}".repeat(30));
244
+ console.log(`\u{1F4E7} To: ${validatedData.email}`);
245
+ console.log(`\u{1F464} Invited by: ${inviterName}`);
246
+ console.log(`\u{1F3E2} Team: ${team.name}`);
247
+ console.log(`\u{1F451} Role: ${validatedData.role}`);
248
+ console.log(`\u{1F517} Accept URL: ${acceptUrl}`);
249
+ console.log("\u{1F3AB}".repeat(30) + "\n");
250
+ }
249
251
  } catch (emailError) {
250
252
  console.error("Failed to send invitation email:", emailError);
251
- console.log(`\u26A0\uFE0F Email failed but invitation created. Accept URL: ${acceptUrl}`);
253
+ if (process.env.NODE_ENV === "development") console.log(`\u26A0\uFE0F Email failed but invitation created. Accept URL: ${acceptUrl}`);
252
254
  }
253
255
  const response = createApiResponse(invitation, { created: true }, 201);
254
256
  return addCorsHeaders(response, req);
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../src/routes/api/v1/teams/switch/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAuBvD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,kCAEjD;AAGD,eAAO,MAAM,IAAI,iDA6EL,CAAA"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../src/routes/api/v1/teams/switch/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAuBvD,wBAAsB,OAAO,CAAC,OAAO,EAAE,WAAW,kCAEjD;AAGD,eAAO,MAAM,IAAI,iDAkFL,CAAA"}
@@ -38,7 +38,10 @@ export const POST = withRateLimitTier(withApiLogging(async (req) => {
38
38
  );
39
39
  return addCorsHeaders(response, req);
40
40
  }
41
- const body = await req.json();
41
+ const body = await req.json().catch(() => void 0);
42
+ if (body === void 0) {
43
+ return addCorsHeaders(createApiError("Request body must be valid JSON", 400, null, "INVALID_JSON"), req);
44
+ }
42
45
  const validatedData = switchTeamSchema.parse(body);
43
46
  try {
44
47
  await TeamService.switchActive(authResult.user.id, validatedData.teamId);
@@ -1 +1 @@
1
- {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../../../src/routes/dashboard/settings/password/page.tsx"],"names":[],"mappings":"AAwBA,iBAAS,kBAAkB,4CAmR1B;AAID,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"page.d.ts","sourceRoot":"","sources":["../../../../../src/routes/dashboard/settings/password/page.tsx"],"names":[],"mappings":"AAwBA,iBAAS,kBAAkB,4CAsR1B;AAID,eAAe,kBAAkB,CAAA"}
@@ -9,7 +9,7 @@ import {
9
9
  CheckCircle,
10
10
  Loader2
11
11
  } from "lucide-react";
12
- import { useState, useCallback } from "react";
12
+ import { useState, useCallback, useEffect } from "react";
13
13
  import { useForm } from "react-hook-form";
14
14
  import { useRouter } from "next/navigation";
15
15
  import { Button } from "@nextsparkjs/core/components/ui/button";
@@ -25,6 +25,7 @@ function UpdatePasswordPage() {
25
25
  const { hasPassword, isLoading: profileLoading } = useUserProfile();
26
26
  const router = useRouter();
27
27
  const t = useTranslations("settings");
28
+ const tCommon = useTranslations("common");
28
29
  const [loading, setLoading] = useState(false);
29
30
  const [error, setError] = useState(null);
30
31
  const [success, setSuccess] = useState(false);
@@ -58,7 +59,7 @@ function UpdatePasswordPage() {
58
59
  if (!result.success) {
59
60
  const errorMsg = result.error || t("password.messages.updateError");
60
61
  setError(errorMsg);
61
- setStatusMessage(`${t("common.status.error")}: ${errorMsg}`);
62
+ setStatusMessage(`${tCommon("status.error")}: ${errorMsg}`);
62
63
  } else {
63
64
  setSuccess(true);
64
65
  setStatusMessage(t("password.messages.updateSuccess"));
@@ -66,25 +67,22 @@ function UpdatePasswordPage() {
66
67
  } catch {
67
68
  const errorMsg = t("password.messages.unexpectedError");
68
69
  setError(errorMsg);
69
- setStatusMessage(`${t("common.status.error")}: ${errorMsg}`);
70
+ setStatusMessage(`${tCommon("status.error")}: ${errorMsg}`);
70
71
  } finally {
71
72
  setLoading(false);
72
73
  }
73
- }, [changePassword, t]);
74
- if (!user) {
75
- router.push("/login");
76
- return null;
77
- }
78
- if (!profileLoading && !hasPassword) {
79
- router.push("/dashboard/settings/profile");
80
- return null;
81
- }
74
+ }, [changePassword, t, tCommon]);
75
+ const mustRedirectToProfile = !!user && !profileLoading && !hasPassword;
76
+ useEffect(() => {
77
+ if (mustRedirectToProfile) router.push("/dashboard/settings/profile");
78
+ }, [mustRedirectToProfile, router]);
82
79
  if (profileLoading) {
83
80
  return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center py-12", children: [
84
81
  /* @__PURE__ */ jsx(Loader2, { className: "h-8 w-8 animate-spin text-muted-foreground" }),
85
82
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("password.loading.verifying") })
86
83
  ] });
87
84
  }
85
+ if (!user || mustRedirectToProfile) return null;
88
86
  if (success) {
89
87
  return /* @__PURE__ */ jsxs(Fragment, { children: [
90
88
  /* @__PURE__ */ jsx(
@@ -4,7 +4,7 @@ import { ConfigViewer } from "@nextsparkjs/core/components/devtools/ConfigViewer
4
4
  import { Settings } from "lucide-react";
5
5
  import { getTranslations } from "next-intl/server";
6
6
  async function DevConfigPageContent() {
7
- const t = await getTranslations("dev.config");
7
+ const t = await getTranslations("devtools.config");
8
8
  return /* @__PURE__ */ jsxs("div", { className: "space-y-6", "data-cy": "devtools-config-page", children: [
9
9
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
10
10
  /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center w-12 h-12 bg-violet-100 dark:bg-violet-950 rounded-lg", children: /* @__PURE__ */ jsx(Settings, { className: "h-6 w-6 text-violet-600 dark:text-violet-400" }) }),
@@ -5,7 +5,7 @@ import Link from "next/link";
5
5
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@nextsparkjs/core/components/ui/card";
6
6
  import { getTranslations } from "next-intl/server";
7
7
  async function DevHomePageContent() {
8
- const t = await getTranslations("dev");
8
+ const t = await getTranslations("devtools");
9
9
  const tCommon = await getTranslations("common");
10
10
  const sections = [
11
11
  {
@@ -26,7 +26,7 @@ async function fetchScheduledActions(filters, page, limit) {
26
26
  return response.json();
27
27
  }
28
28
  export default function ScheduledActionsPage() {
29
- const t = useTranslations("dev.scheduledActions");
29
+ const t = useTranslations("devtools.scheduledActions");
30
30
  const [filters, setFilters] = useState({});
31
31
  const [page, setPage] = useState(1);
32
32
  const limit = 20;
@@ -5,7 +5,7 @@ import { FileText } from "lucide-react";
5
5
  import { getTranslations } from "next-intl/server";
6
6
  async function DevTestsPageContent({ params }) {
7
7
  const { path } = await params;
8
- const t = await getTranslations("dev.tests");
8
+ const t = await getTranslations("devtools.tests");
9
9
  const initialPath = (path == null ? void 0 : path.join("/")) || null;
10
10
  return /* @__PURE__ */ jsxs(
11
11
  "div",
@@ -39,8 +39,8 @@ import { sel } from "@nextsparkjs/core/lib/selectors";
39
39
  import { withBasePath } from "@nextsparkjs/core/lib/base-path";
40
40
  const roleOptions = [
41
41
  { value: "member", label: "Member" },
42
- { value: "colaborator", label: "Colaborator" },
43
- { value: "admin", label: "Admin" }
42
+ { value: "developer", label: "Developer" },
43
+ { value: "suspended", label: "Suspended" }
44
44
  ];
45
45
  const statusOptions = [
46
46
  { value: "verified", label: "Verified" },
@@ -66,7 +66,7 @@ jobs:
66
66
  - name: Seed test data
67
67
  env:
68
68
  DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
69
- run: pnpm db:seed || echo "No seed script found, skipping..."
69
+ run: pnpm db:seed
70
70
 
71
71
  - name: Build application
72
72
  run: pnpm build
@@ -59,7 +59,8 @@ jobs:
59
59
  - name: Setup database
60
60
  env:
61
61
  DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
62
- run: pnpm db:migrate
62
+ # a throwaway test database: migrations plus the sample users the tests sign in as
63
+ run: pnpm db:seed
63
64
 
64
65
  - name: Build application
65
66
  run: pnpm build
@@ -109,42 +109,21 @@ const nextConfig = {
109
109
  formats: ['image/avif', 'image/webp'],
110
110
  deviceSizes: [640, 750, 828, 1080, 1200, 1920],
111
111
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
112
+ // Hosts the server may fetch and resize through /_next/image.
113
+ // Keep this list to the exact hosts you use.
112
114
  remotePatterns: [
115
+ // Google profile pictures shown for accounts that sign in with Google.
113
116
  {
114
117
  protocol: 'https',
115
118
  hostname: 'lh3.googleusercontent.com',
116
119
  pathname: '/**',
117
120
  },
118
- {
119
- protocol: 'https',
120
- hostname: '*.public.blob.vercel-storage.com',
121
- pathname: '/**',
122
- },
123
- {
124
- protocol: 'https',
125
- hostname: 'images.unsplash.com',
126
- pathname: '/**',
127
- },
128
- {
129
- protocol: 'https',
130
- hostname: 'upload.wikimedia.org',
131
- pathname: '/**',
132
- },
133
- {
134
- protocol: 'https',
135
- hostname: 'i.pravatar.cc',
136
- pathname: '/**',
137
- },
138
- {
139
- protocol: 'https',
140
- hostname: '*.supabase.co',
141
- pathname: '/**',
142
- },
143
- {
144
- protocol: 'https',
145
- hostname: '*.cloudinary.com',
146
- pathname: '/**',
147
- },
121
+ // Examples: add the exact host of your own store, e.g. your Vercel Blob store
122
+ // (`<store-id>.public.blob.vercel-storage.com`) once uploads use it.
123
+ // { protocol: 'https', hostname: '<store-id>.public.blob.vercel-storage.com', pathname: '/**' },
124
+ // { protocol: 'https', hostname: '<project-ref>.supabase.co', pathname: '/**' },
125
+ // { protocol: 'https', hostname: 'res.cloudinary.com', pathname: '/<cloud-name>/**' },
126
+ // { protocol: 'https', hostname: 'images.unsplash.com', pathname: '/**' },
148
127
  ],
149
128
  },
150
129
  // Next.js sets TURBOPACK before it loads this file whenever Turbopack builds
@@ -158,7 +137,8 @@ const nextConfig = {
158
137
  async headers() {
159
138
  const isProduction = process.env.NODE_ENV === 'production';
160
139
 
161
- // Allowed image domains (must match remotePatterns above)
140
+ // Image hosts the browser may load directly (CSP img-src). The server only fetches the hosts in
141
+ // images.remotePatterns above; add a host there too when next/image should serve it.
162
142
  // NOTE: Wildcard patterns (*.public.blob.vercel-storage.com, *.supabase.co, *.cloudinary.com)
163
143
  // allow images from any account on these services for development flexibility.
164
144
  // For production with stricter security, consider restricting to specific account subdomains.
@@ -14,7 +14,7 @@
14
14
  "@nextsparkjs/core": ">=0.1.0-0",
15
15
  "@nextsparkjs/testing": ">=0.1.0-0",
16
16
  "lucide-react": "^0.539.0",
17
- "next": "~16.3.6",
17
+ "next": "~16.3.8",
18
18
  "next-intl": "^4.0.0",
19
19
  "react": "^19.2.0",
20
20
  "react-dom": "^19.2.0",
@@ -13,7 +13,7 @@
13
13
  "@nextsparkjs/core": ">=0.1.0-0",
14
14
  "@nextsparkjs/testing": ">=0.1.0-0",
15
15
  "lucide-react": "^0.539.0",
16
- "next": "~16.3.6",
16
+ "next": "~16.3.8",
17
17
  "next-intl": "^4.0.0",
18
18
  "react": "^19.2.0",
19
19
  "react-dom": "^19.2.0",
@@ -17,7 +17,7 @@
17
17
  "@nextsparkjs/core": ">=0.1.0-0",
18
18
  "@nextsparkjs/testing": ">=0.1.0-0",
19
19
  "lucide-react": "^0.539.0",
20
- "next": "~16.3.6",
20
+ "next": "~16.3.8",
21
21
  "react": "^19.2.0",
22
22
  "react-dom": "^19.2.0",
23
23
  "zod": "^4.0.0"
@@ -21,6 +21,8 @@ cp .env.example .env.local
21
21
 
22
22
  # Run migrations
23
23
  pnpm db:migrate
24
+ # Local development only: also load the sample users (DevKeyring, Cypress)
25
+ pnpm db:seed
24
26
 
25
27
  # Start development
26
28
  pnpm dev
@@ -126,6 +126,14 @@ export const APP_CONFIG_OVERRIDES = {
126
126
  // methods: ['email-password', 'google'],
127
127
  // methods: ['email-otp', 'email-password', 'google'],
128
128
 
129
+ /**
130
+ * Password endpoints (sign-in/email, sign-up/email, reset). Left undeclared
131
+ * they follow `methods` in production (off under the passwordless preset) and
132
+ * stay on in development (dev keyring, `pnpm db:seed` users, Cypress).
133
+ * Uncomment to keep password logins in production (API clients, test users):
134
+ */
135
+ // emailAndPassword: { enabled: true },
136
+
129
137
  /**
130
138
  * Session duration / renewal in seconds (defaults: 7 days, renewed daily).
131
139
  * Long-lived sessions for an installed PWA, for example:
@@ -17,6 +17,8 @@ Welcome to the Starter theme for NextSpark. This minimal theme provides everythi
17
17
  3. Run migrations: `pnpm db:migrate`
18
18
  4. Start development: `pnpm dev`
19
19
 
20
+ Sample users (DevKeyring, Cypress) are for local development only: `pnpm db:seed` loads them; `pnpm db:migrate` alone does not, and neither applies them when `NODE_ENV` is `production`.
21
+
20
22
  ## Project Structure
21
23
 
22
24
  ```
@@ -0,0 +1,14 @@
1
+ import { NextIntlClientProvider } from 'next-intl'
2
+ import { getMessages } from 'next-intl/server'
3
+ import { selectMessages } from '@nextsparkjs/core/lib/i18n/client-messages'
4
+
5
+ // TasksChart and RecentActivity are client components that read the `analytics`
6
+ // namespace, which the dashboard group does not carry to the browser by default.
7
+ export default async function AnalyticsLayout({ children }: { children: React.ReactNode }) {
8
+ const messages = await getMessages()
9
+ return (
10
+ <NextIntlClientProvider messages={selectMessages(messages, 'dashboard', ['analytics'])}>
11
+ {children}
12
+ </NextIntlClientProvider>
13
+ )
14
+ }