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

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 (168) hide show
  1. package/dist/components/billing/InvoicesPagination.js +2 -2
  2. package/dist/components/dashboard/navigation/DynamicNavigation.js +2 -2
  3. package/dist/components/settings/layouts/SettingsSidebar.d.ts.map +1 -1
  4. package/dist/components/settings/layouts/SettingsSidebar.js +0 -1
  5. package/dist/components/teams/CreateTeamButton.js +2 -2
  6. package/dist/components/teams/CreateTeamDialog.js +1 -1
  7. package/dist/components/teams/InviteMemberButton.js +3 -3
  8. package/dist/components/teams/InviteMemberDialog.js +1 -1
  9. package/dist/components/teams/TeamPendingInvitations.js +2 -2
  10. package/dist/lib/api/api-error.d.ts +2 -0
  11. package/dist/lib/api/api-error.d.ts.map +1 -1
  12. package/dist/lib/api/api-error.js +3 -1
  13. package/dist/lib/api/auth/dual-auth.d.ts +2 -1
  14. package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
  15. package/dist/lib/api/auth/dual-auth.js +22 -4
  16. package/dist/lib/api/auth/scope-policy.d.ts +6 -3
  17. package/dist/lib/api/auth/scope-policy.d.ts.map +1 -1
  18. package/dist/lib/api/cors-response.d.ts +3 -2
  19. package/dist/lib/api/cors-response.d.ts.map +1 -1
  20. package/dist/lib/api/cors-response.js +8 -6
  21. package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
  22. package/dist/lib/api/entity/generic-handler.js +0 -3
  23. package/dist/lib/api/helpers.d.ts.map +1 -1
  24. package/dist/lib/api/helpers.js +5 -22
  25. package/dist/lib/api/permission-middleware.d.ts.map +1 -1
  26. package/dist/lib/api/permission-middleware.js +0 -2
  27. package/dist/lib/api/rate-limit.d.ts +8 -0
  28. package/dist/lib/api/rate-limit.d.ts.map +1 -1
  29. package/dist/lib/api/rate-limit.js +34 -3
  30. package/dist/lib/api/request-origin.d.ts.map +1 -1
  31. package/dist/lib/api/request-origin.js +1 -1
  32. package/dist/lib/auth/runtime-readiness.d.ts.map +1 -1
  33. package/dist/lib/auth/runtime-readiness.js +2 -0
  34. package/dist/lib/auth/sample-accounts.d.ts +17 -0
  35. package/dist/lib/auth/sample-accounts.d.ts.map +1 -0
  36. package/dist/lib/auth/sample-accounts.js +22 -0
  37. package/dist/lib/auth.d.ts.map +1 -1
  38. package/dist/lib/auth.js +6 -3
  39. package/dist/lib/cache/public-entity-cache.d.ts +21 -0
  40. package/dist/lib/cache/public-entity-cache.d.ts.map +1 -0
  41. package/dist/lib/cache/public-entity-cache.js +25 -0
  42. package/dist/lib/entities/entity-hooks.d.ts.map +1 -1
  43. package/dist/lib/entities/entity-hooks.js +4 -0
  44. package/dist/lib/mcp/errors.d.ts +2 -0
  45. package/dist/lib/mcp/errors.d.ts.map +1 -1
  46. package/dist/lib/mcp/errors.js +3 -1
  47. package/dist/lib/mcp/index.d.ts +1 -1
  48. package/dist/lib/mcp/index.d.ts.map +1 -1
  49. package/dist/lib/mcp/index.js +2 -1
  50. package/dist/lib/services/generic-entity.service.d.ts.map +1 -1
  51. package/dist/lib/services/generic-entity.service.js +2 -0
  52. package/dist/lib/teams/actions.d.ts.map +1 -1
  53. package/dist/lib/teams/actions.js +0 -1
  54. package/dist/messages/de/billing.json +12 -1
  55. package/dist/messages/de/common.json +2 -0
  56. package/dist/messages/de/docs.json +1 -0
  57. package/dist/messages/de/features.json +16 -4
  58. package/dist/messages/de/home.json +5 -0
  59. package/dist/messages/de/index.d.ts +68 -25
  60. package/dist/messages/de/index.d.ts.map +1 -1
  61. package/dist/messages/de/index.js +2 -0
  62. package/dist/messages/de/index.ts +2 -0
  63. package/dist/messages/de/settings.json +1 -0
  64. package/dist/messages/de/teams.json +3 -18
  65. package/dist/messages/en/billing.json +1 -1
  66. package/dist/messages/en/common.json +2 -0
  67. package/dist/messages/en/docs.json +1 -0
  68. package/dist/messages/en/features.json +16 -4
  69. package/dist/messages/en/home.json +5 -0
  70. package/dist/messages/en/index.d.ts +58 -25
  71. package/dist/messages/en/index.d.ts.map +1 -1
  72. package/dist/messages/en/index.js +2 -0
  73. package/dist/messages/en/index.ts +2 -0
  74. package/dist/messages/en/settings.json +1 -0
  75. package/dist/messages/en/teams.json +3 -18
  76. package/dist/messages/es/billing.json +1 -1
  77. package/dist/messages/es/common.json +2 -0
  78. package/dist/messages/es/docs.json +1 -0
  79. package/dist/messages/es/features.json +16 -4
  80. package/dist/messages/es/home.json +5 -0
  81. package/dist/messages/es/index.d.ts +41 -8
  82. package/dist/messages/es/index.d.ts.map +1 -1
  83. package/dist/messages/es/index.js +2 -0
  84. package/dist/messages/es/index.ts +2 -0
  85. package/dist/messages/es/settings.json +1 -0
  86. package/dist/messages/es/teams.json +3 -0
  87. package/dist/messages/fr/billing.json +12 -1
  88. package/dist/messages/fr/common.json +2 -0
  89. package/dist/messages/fr/docs.json +1 -0
  90. package/dist/messages/fr/features.json +16 -4
  91. package/dist/messages/fr/home.json +5 -0
  92. package/dist/messages/fr/index.d.ts +68 -25
  93. package/dist/messages/fr/index.d.ts.map +1 -1
  94. package/dist/messages/fr/index.js +2 -0
  95. package/dist/messages/fr/index.ts +2 -0
  96. package/dist/messages/fr/settings.json +1 -0
  97. package/dist/messages/fr/teams.json +3 -18
  98. package/dist/messages/it/billing.json +12 -1
  99. package/dist/messages/it/common.json +2 -0
  100. package/dist/messages/it/docs.json +1 -0
  101. package/dist/messages/it/features.json +16 -4
  102. package/dist/messages/it/home.json +5 -0
  103. package/dist/messages/it/index.d.ts +68 -25
  104. package/dist/messages/it/index.d.ts.map +1 -1
  105. package/dist/messages/it/index.js +2 -0
  106. package/dist/messages/it/index.ts +2 -0
  107. package/dist/messages/it/settings.json +1 -0
  108. package/dist/messages/it/teams.json +3 -18
  109. package/dist/messages/pt/billing.json +12 -1
  110. package/dist/messages/pt/common.json +2 -0
  111. package/dist/messages/pt/docs.json +1 -0
  112. package/dist/messages/pt/features.json +16 -4
  113. package/dist/messages/pt/home.json +5 -0
  114. package/dist/messages/pt/index.d.ts +68 -25
  115. package/dist/messages/pt/index.d.ts.map +1 -1
  116. package/dist/messages/pt/index.js +2 -0
  117. package/dist/messages/pt/index.ts +2 -0
  118. package/dist/messages/pt/settings.json +1 -0
  119. package/dist/messages/pt/teams.json +3 -18
  120. package/dist/migrations/029_sample_accounts_outside_development.sql +55 -0
  121. package/dist/migrations/090_sample_data.sql +10 -0
  122. package/dist/routes/_internal/entity-permission-layout.d.ts.map +1 -1
  123. package/dist/routes/_internal/entity-permission-layout.js +0 -7
  124. package/dist/routes/api/mcp/route.d.ts.map +1 -1
  125. package/dist/routes/api/mcp/route.js +7 -1
  126. package/dist/routes/api/v1/media/upload/route.d.ts +2 -1
  127. package/dist/routes/api/v1/media/upload/route.d.ts.map +1 -1
  128. package/dist/routes/api/v1/media/upload/route.js +10 -2
  129. package/dist/routes/api/v1/teams/[teamId]/members/route.d.ts.map +1 -1
  130. package/dist/routes/api/v1/teams/[teamId]/members/route.js +12 -10
  131. package/dist/routes/api/v1/teams/switch/route.d.ts.map +1 -1
  132. package/dist/routes/api/v1/teams/switch/route.js +4 -1
  133. package/dist/routes/dashboard/settings/password/page.d.ts.map +1 -1
  134. package/dist/routes/dashboard/settings/password/page.js +10 -12
  135. package/dist/routes/devtools/config/page.js +1 -1
  136. package/dist/routes/devtools/page.js +1 -1
  137. package/dist/routes/devtools/scheduled-actions/page.js +1 -1
  138. package/dist/routes/devtools/tests/[[...path]]/page.js +1 -1
  139. package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
  140. package/dist/templates/ci-workflows/github/cypress-smoke.yml +2 -1
  141. package/dist/templates/projects/starter/README.md +2 -0
  142. package/dist/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
  143. package/dist/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
  144. package/dist/templates/proxy.ts +15 -8
  145. package/dist/templates/public/uploads/README.md +5 -94
  146. package/migrations/029_sample_accounts_outside_development.sql +55 -0
  147. package/migrations/090_sample_data.sql +10 -0
  148. package/package.json +2 -2
  149. package/scripts/build/registry/host/__tests__/host-proxy-areas.test.mjs +72 -1
  150. package/scripts/build/registry/host/proxy-areas.mjs +65 -2
  151. package/scripts/db/run-migrations.mjs +30 -10
  152. package/scripts/db/sample-data.mjs +44 -0
  153. package/scripts/db/verify-theme-migrations.mjs +6 -0
  154. package/templates/ci-workflows/github/cypress-regression.yml +1 -1
  155. package/templates/ci-workflows/github/cypress-smoke.yml +2 -1
  156. package/templates/projects/starter/README.md +2 -0
  157. package/templates/projects/starter/docs/public/01-overview/01-introduction.md +2 -0
  158. package/templates/projects/starter/templates/dashboard/analytics/layout.tsx +14 -0
  159. package/templates/proxy.ts +15 -8
  160. package/templates/public/uploads/README.md +5 -94
  161. package/tests/jest/__mocks__/@nextsparkjs/registries/api-presets-registry.ts +2 -0
  162. package/tests/jest/__mocks__/@nextsparkjs/registries/mcp-registry.ts +2 -0
  163. package/dist/templates/projects/starter/public/brand/.gitkeep +0 -0
  164. package/dist/templates/public/uploads/.gitignore +0 -6
  165. package/dist/templates/public/uploads/temp/.gitkeep +0 -1
  166. package/templates/projects/starter/public/brand/.gitkeep +0 -0
  167. package/templates/public/uploads/.gitignore +0 -6
  168. package/templates/public/uploads/temp/.gitkeep +0 -1
@@ -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",
@@ -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
@@ -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
@@ -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
+ }
@@ -20,8 +20,8 @@
20
20
  * themselves and use only x-pathname from here, as a routing hint; the
21
21
  * identity headers are still stripped and re-set for project code.
22
22
  */
23
- import { betterFetch } from '@better-fetch/fetch'
24
23
  import { NextRequest, NextResponse } from 'next/server'
24
+ import { auth } from '@nextsparkjs/core/lib/auth'
25
25
  import {
26
26
  hasProjectMiddleware,
27
27
  executeProjectMiddleware,
@@ -181,14 +181,22 @@ function redirectAccessDenied(request: NextRequest): NextResponse {
181
181
  }
182
182
 
183
183
  /**
184
- * The session the request's cookies carry, asked of this app's own auth route,
185
- * which is served under the base path like every other route.
184
+ * The session the request's cookies carry, read in this process the way core's
185
+ * server code reads it. Asking the app's own /api/auth/get-session over HTTP
186
+ * built the URL from the request's origin, which behind a proxy that
187
+ * terminates TLS (X-Forwarded-Proto: https) is https on a port that speaks
188
+ * plain HTTP: the fetch failed and every signed-in user was sent to /login.
189
+ * Nothing here leaves the process, so no forwarded scheme or host can point
190
+ * the check anywhere. `disableRefresh`: this response cannot carry the renewed
191
+ * session cookie, so the rolling renewal is left to the auth route
192
+ * (lib/auth/session-refresh).
186
193
  */
187
- function getSession(request: NextRequest) {
188
- return betterFetch<Session>('/api/auth/get-session', {
189
- baseURL: `${request.nextUrl.origin}${request.nextUrl.basePath}`,
190
- headers: { cookie: request.headers.get('cookie') || '' },
194
+ async function getSession(request: NextRequest): Promise<Session | null> {
195
+ const session = await auth.api.getSession({
196
+ headers: new Headers({ cookie: request.headers.get('cookie') || '' }),
197
+ query: { disableRefresh: true },
191
198
  })
199
+ return session as Session | null
192
200
  }
193
201
 
194
202
  /**
@@ -490,7 +498,6 @@ export async function proxy(request: NextRequest) {
490
498
  const verifiedSession = async (): Promise<Session | null> => {
491
499
  if (!sessionPromise) {
492
500
  sessionPromise = getSession(request)
493
- .then(({ data }) => data)
494
501
  .catch(error => {
495
502
  sessionError = error
496
503
  return null
@@ -1,97 +1,8 @@
1
- # Media Upload System
1
+ # Media uploads
2
2
 
3
- ## Overview
4
- This application uses **Vercel Blob Storage** for all media uploads. Files are stored in the cloud, not on the local filesystem.
3
+ `POST /api/v1/media/upload` stores files in **Vercel Blob** when `BLOB_READ_WRITE_TOKEN` is set. The media library lists the Blob URLs.
5
4
 
6
- > **Note:** This directory (`public/uploads/`) is legacy from a previous filesystem-based implementation and is no longer used.
5
+ - **Development without a token:** files are written to `public/uploads/temp/` (git-ignored) so the library works offline.
6
+ - **Production without a token:** the upload answers `503` with code `STORAGE_NOT_CONFIGURED`. Nothing is written to disk: a file under `public/` is not served until the next restart and does not survive a redeploy.
7
7
 
8
- ## Upload Endpoint
9
- **POST** `/api/v1/media/upload`
10
-
11
- ### Authentication
12
- Requires valid user session (Better Auth)
13
-
14
- ### Request
15
- - **Content-Type**: `multipart/form-data`
16
- - **Field name**: `files` (accepts multiple files)
17
-
18
- ### Supported File Types
19
- - Images: JPEG, PNG, GIF, WebP
20
- - Videos: MP4, MPEG, QuickTime, WebM
21
-
22
- ### File Size Limit
23
- - Maximum: 10MB per file
24
-
25
- ### Response
26
- ```json
27
- {
28
- "message": "Files uploaded successfully",
29
- "urls": [
30
- "https://xxxxx.public.blob.vercel-storage.com/uploads/temp/1234567890_abc123.jpg",
31
- "https://xxxxx.public.blob.vercel-storage.com/uploads/temp/1234567890_def456.png"
32
- ],
33
- "count": 2
34
- }
35
- ```
36
-
37
- ## File Naming Convention
38
- Files are automatically renamed using the pattern:
39
- ```
40
- {timestamp}_{randomString}.{extension}
41
- ```
42
-
43
- Example: `1704067200000_k9x2m4p.jpg`
44
-
45
- ## Storage Location
46
- - **Storage Provider**: Vercel Blob (cloud object storage)
47
- - **Blob Path**: `uploads/temp/{filename}` (organizational prefix, not a filesystem path)
48
- - **Public URL**: `https://xxxxx.public.blob.vercel-storage.com/uploads/temp/{filename}`
49
-
50
- ### How It Works
51
-
52
- **Local Development:**
53
- - Files are uploaded directly to Vercel Blob using `BLOB_READ_WRITE_TOKEN` from `.env`
54
- - No files are saved to your local filesystem
55
- - URLs point to Vercel's CDN
56
-
57
- **Production/Staging:**
58
- - Same behavior - files go directly to Vercel Blob
59
- - Uses environment-specific Blob token
60
- - No filesystem writes (Vercel serverless is read-only)
61
-
62
- ## Usage in AI Content Generation
63
- The generate-content endpoint (`/api/content-buddy/generate-content`) automatically processes images for AI analysis.
64
-
65
- **Supported URL formats:**
66
- - Vercel Blob URLs: `https://xxxxx.public.blob.vercel-storage.com/uploads/temp/image.jpg`
67
- - External URLs: `https://example.com/image.jpg` (also supported)
68
-
69
- ## Configuration
70
- Requires `BLOB_READ_WRITE_TOKEN` environment variable:
71
-
72
- ```env
73
- BLOB_READ_WRITE_TOKEN="vercel_blob_rw_..."
74
- ```
75
-
76
- ## Cleanup
77
- Files are managed through Vercel Blob's retention policies.
78
-
79
- **Manual cleanup** (if needed):
80
- ```typescript
81
- import { del } from '@vercel/blob'
82
-
83
- // Delete specific file
84
- await del(fileUrl)
85
-
86
- // Or list and delete old files
87
- import { list } from '@vercel/blob'
88
- const { blobs } = await list({ prefix: 'uploads/temp/' })
89
- // Filter and delete as needed
90
- ```
91
-
92
- ## Security Notes
93
- - All uploads require authentication
94
- - File type validation prevents malicious uploads
95
- - File size limits prevent DoS attacks
96
- - Random filenames prevent enumeration attacks
97
- - Files stored in secure Vercel Blob infrastructure
8
+ Self-hosting needs a storage provider: set `BLOB_READ_WRITE_TOKEN` (a Vercel Blob token, starts with `vercel_blob_`). See the media library docs.
@@ -0,0 +1,55 @@
1
+ -- Migration: 029_sample_accounts_outside_development.sql
2
+ -- Description: sample accounts keep no usable credentials outside development
3
+ --
4
+ -- Sample data (090_sample_data.sql and the projects' *sample_data* files) is
5
+ -- only applied by an explicit development opt-in (`nextspark db:seed`,
6
+ -- `--sample-data` or NEXTSPARK_SEED_SAMPLE_DATA=1). Databases migrated before
7
+ -- that may still hold the sample accounts. This migration finds them by the
8
+ -- sample password, never by name, so a renamed account is found and an account
9
+ -- whose password was changed is left alone; for each one it:
10
+ -- - removes the sample password (its "credential" account row),
11
+ -- - removes its sessions,
12
+ -- - deactivates its API keys (status 'inactive'; deleting them would delete
13
+ -- their api_audit_log rows),
14
+ -- - sets its global role to 'member' (027's trigger then removes it from the
15
+ -- System Admin Team, as for any user who loses that role).
16
+ -- It also removes the sample sessions and deactivates the sample API key by
17
+ -- their fixed values, whoever owns them now. Nothing else is changed.
18
+ --
19
+ -- It runs once, everywhere, except in a development run that opted into sample
20
+ -- data (the runner sets nextspark.seed_sample_data to 'on' then), where those
21
+ -- accounts are wanted. To keep using one of them deliberately, give it a new
22
+ -- password before upgrading; to restore one afterwards, set its role back and
23
+ -- reset its password.
24
+
25
+ DO $$
26
+ DECLARE
27
+ sample_passwords CONSTANT TEXT[] := ARRAY[
28
+ '22de14d5472248ed0bece911df908b2a:d29576424798ba6845d348a3767c0b0f38a00f2aca461b3b1d34b99a93cab06c86774c6edb183e6d6ec47457649b032a49a7b60a48f6f4f7fbbc4ea40258f19f',
29
+ '3db9e98e2b4d3caca97fdf2783791cbc:34b293de615caf277a237773208858e960ea8aa10f1f5c5c309b632f192cac34d52ceafbd338385616f4929e4b1b6c055b67429c6722ffdb80b01d9bf4764866'
30
+ ];
31
+ sample_session_tokens CONSTANT TEXT[] := ARRAY['tmt_superadmin_session_token_001', 'tmt_developer_session_token_001'];
32
+ sample_key_hashes CONSTANT TEXT[] := ARRAY[
33
+ '205f13ae4d2e18df417e61123bb84030c124fcb5013699046379ae1b5ec04324',
34
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
35
+ ];
36
+ sample_users TEXT[];
37
+ BEGIN
38
+ IF current_setting('nextspark.seed_sample_data', true) = 'on' THEN
39
+ RAISE NOTICE 'Sample accounts: kept (development run with sample data).';
40
+ RETURN;
41
+ END IF;
42
+
43
+ SELECT COALESCE(array_agg(DISTINCT "userId"), '{}') INTO sample_users
44
+ FROM "account"
45
+ WHERE "providerId" = 'credential' AND "password" = ANY (sample_passwords);
46
+
47
+ DELETE FROM "account" WHERE "providerId" = 'credential' AND "password" = ANY (sample_passwords);
48
+ DELETE FROM "session" WHERE "userId" = ANY (sample_users) OR token = ANY (sample_session_tokens);
49
+ UPDATE "api_key" SET status = 'inactive', "updatedAt" = now()
50
+ WHERE status IS DISTINCT FROM 'inactive' AND ("userId" = ANY (sample_users) OR "keyHash" = ANY (sample_key_hashes));
51
+ UPDATE "users" SET role = 'member', "updatedAt" = now()
52
+ WHERE id = ANY (sample_users) AND role <> 'member';
53
+
54
+ RAISE NOTICE 'Sample accounts: % disabled (no password, sessions or active API keys; role member).', cardinality(sample_users);
55
+ END $$;
@@ -7,6 +7,16 @@
7
7
  -- Theme-specific demo data (TMT.dev users, companies, invoices) is in:
8
8
  -- migrations/090_demo_users_teams.sql
9
9
 
10
+ -- Only a development run that asked for sample data applies this file (see
11
+ -- scripts/db/sample-data.mjs); anything else that runs it, such as a tool that
12
+ -- applies every core .sql file in order, fails here instead of creating these accounts.
13
+ DO $$
14
+ BEGIN
15
+ IF current_setting('nextspark.seed_sample_data', true) IS DISTINCT FROM 'on' THEN
16
+ RAISE EXCEPTION 'Sample data is applied only by `pnpm db:seed` (or NEXTSPARK_SEED_SAMPLE_DATA=1) outside production. If you ran that, the session setting it relies on was lost: a transaction-mode pooler (PgBouncer, Supavisor on port 6543) drops it, so point MIGRATE_DATABASE_URL at a direct connection.';
17
+ END IF;
18
+ END $$;
19
+
10
20
  -- ============================================
11
21
  -- STEP 1: CREATE CORE TEST USERS
12
22
  -- ============================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextsparkjs/core",
3
- "version": "0.1.0-beta.195",
3
+ "version": "0.1.0-beta.196",
4
4
  "description": "NextSpark - The complete SaaS framework for Next.js",
5
5
  "license": "MIT",
6
6
  "author": "NextSpark <hello@nextspark.dev>",
@@ -496,7 +496,7 @@
496
496
  "use-intl": "^4.9.1",
497
497
  "uuid": "^13.0.0",
498
498
  "zod": "^4.3.6",
499
- "@nextsparkjs/ui": "0.1.0-beta.195"
499
+ "@nextsparkjs/ui": "0.1.0-beta.196"
500
500
  },
501
501
  "engines": {
502
502
  "node": ">=22.14.0"
@@ -12,7 +12,7 @@ import { mkdtempSync, readFileSync, rmSync, symlinkSync } from 'node:fs'
12
12
  import { tmpdir } from 'node:os'
13
13
  import { join } from 'node:path'
14
14
 
15
- import { PROXY_AREA_WARNING, missingProxyAreas, proxyAreaNotice, proxyAreaNotices } from '../proxy-areas.mjs'
15
+ import { PROXY_AREA_WARNING, PROXY_SESSION_WARNING, missingProxyAreas, withoutComments, proxyAreaNotice, proxyAreaNotices, proxySessionNotice } from '../proxy-areas.mjs'
16
16
  import { predictHost } from '../prepare.mjs'
17
17
  import { PAGE, tempHost, write } from './host-helpers.mjs'
18
18
 
@@ -30,6 +30,20 @@ test('an area counts only when the proxy names its path; a look-alike prefix or
30
30
  assert.deepEqual(missingProxyAreas("const docs = '/superadmin-guide'; const tips = \"/devtools-tips\"").map(area => area.path), ['/superadmin', '/devtools'])
31
31
  })
32
32
 
33
+ test("a proxy that re-exports core's template has the check; a re-export of anything else does not", () => {
34
+ assert.deepEqual(missingProxyAreas("export { proxy } from '../../../packages/core/templates/proxy'\nexport const config = { matcher: [] }"), [])
35
+ assert.deepEqual(missingProxyAreas("export { proxy } from '@nextsparkjs/core/templates/proxy'"), [])
36
+ assert.deepEqual(missingProxyAreas("export { proxy } from './my-proxy'").map(area => area.path), ['/superadmin', '/devtools'])
37
+ assert.deepEqual(missingProxyAreas("export { other } from '@nextsparkjs/core/templates/proxy'").map(area => area.path), ['/superadmin', '/devtools'])
38
+ const both = ['/superadmin', '/devtools']
39
+ assert.deepEqual(missingProxyAreas("export { proxy as coreProxy } from '@nextsparkjs/core/templates/proxy'\nexport function proxy() {}").map(a => a.path), both)
40
+ assert.deepEqual(missingProxyAreas("export { other as proxy } from '@nextsparkjs/core/templates/proxy'").map(a => a.path), both)
41
+ assert.deepEqual(missingProxyAreas("export { proxy } from './vendor/core/templates/proxy'").map(a => a.path), both)
42
+ assert.deepEqual(missingProxyAreas("// export { proxy } from '@nextsparkjs/core/templates/proxy'\nexport function proxy() {}").map(a => a.path), both)
43
+ assert.deepEqual(missingProxyAreas("/*\nexport { proxy } from '@nextsparkjs/core/templates/proxy'\n*/\nexport function proxy() {}").map(a => a.path), both)
44
+ assert.deepEqual(missingProxyAreas("export { config, proxy } from '@nextsparkjs/core/templates/proxy.ts'"), [])
45
+ })
46
+
33
47
  test('the warning names the file, each missing area, its roles and where the check comes from', () => {
34
48
  const notice = proxyAreaNotice('src/proxy.ts', "const PROTECTED = ['/superadmin']")
35
49
  assert.equal(notice.code, PROXY_AREA_WARNING)
@@ -53,6 +67,63 @@ test('the proxy Next loads is the one checked; a project without one is not warn
53
67
  }
54
68
  })
55
69
 
70
+ // S80 (D1): the session check that fetched /api/auth/get-session from the request's origin, as every proxy.ts copied
71
+ // before 0.1.0-beta.196 does. Upgrading core never touches that copy, so prepare says what to change.
72
+ const HTTP_SESSION_CHECK = `function getSession(request: NextRequest) {
73
+ return betterFetch<Session>('/api/auth/get-session', {
74
+ baseURL: \`\${request.nextUrl.origin}\${request.nextUrl.basePath}\`,
75
+ headers: { cookie: request.headers.get('cookie') || '' },
76
+ })
77
+ }
78
+ const PROTECTED = ['/superadmin', '/devtools']
79
+ `
80
+
81
+ test('a proxy that asks its own /api/auth/get-session over HTTP is warned about, with the in-process call to use', () => {
82
+ const notice = proxySessionNotice('src/proxy.ts', HTTP_SESSION_CHECK)
83
+ assert.equal(notice.code, PROXY_SESSION_WARNING)
84
+ assert.match(notice.message, /^src\/proxy\.ts checks the session by fetching \/api\/auth\/get-session/)
85
+ assert.match(notice.message, /X-Forwarded-Proto: https/)
86
+ assert.match(notice.message, /auth\.api\.getSession\(\{ headers: new Headers\(\{ cookie: request\.headers\.get\('cookie'\) \|\| '' \}\), query: \{ disableRefresh: true \} \}\)/)
87
+ assert.equal(proxySessionNotice('src/proxy.ts', readFileSync(join(CORE_ROOT, 'templates/proxy.ts'), 'utf8')), null)
88
+ assert.equal(proxySessionNotice('src/proxy.ts', "// was: betterFetch('/api/auth/get-session')\nexport function proxy() {}"), null)
89
+ assert.equal(proxySessionNotice('src/proxy.ts', "export { proxy } from '@nextsparkjs/core/templates/proxy'"), null)
90
+ })
91
+
92
+ // The S80 review's measured edges: an end-of-line comment, and a '/*' / '*/' pair inside two string literals.
93
+ const COMMENT_EDGES = [
94
+ ['an end-of-line comment after code', "const x = 1 // was '/api/auth/get-session'\nexport function proxy() {}", false],
95
+ ['an end-of-line comment after code, no space', "const x = 1// was '/api/auth/get-session'", false],
96
+ ["a '/*' and a '*/' in two strings around the call", "const a = '/api/*'\nbetterFetch('/api/auth/get-session')\nconst b = 'x*/'", true],
97
+ ["a '//' inside a string before the call", "const u = 'https://example.com'; betterFetch('/api/auth/get-session')", true],
98
+ ['a block comment', "/* betterFetch('/api/auth/get-session') */ export function proxy() {}", false],
99
+ ['a quote escaped inside a string', "const q = 'it\\'s'; betterFetch('/api/auth/get-session')", true],
100
+ ]
101
+
102
+ test('comments are stripped wherever they start, and never inside a string literal', () => {
103
+ for (const [label, source, warns] of COMMENT_EDGES) {
104
+ assert.equal(proxySessionNotice('src/proxy.ts', source) !== null, warns, label)
105
+ }
106
+ assert.equal(withoutComments("a // b\nc"), 'a \nc')
107
+ assert.equal(withoutComments("'//' /* x */ \"/*\""), "'//' \"/*\"")
108
+ // The repository template, old and new, reads the same with comments gone: every regex literal in it survives.
109
+ const template = readFileSync(join(CORE_ROOT, 'templates/proxy.ts'), 'utf8')
110
+ assert.match(withoutComments(template), /\.replace\(\/\\\\\/g, '\/'\)\.replace\(\/\\\/\\\/\+\/g, '\/'\)/)
111
+ })
112
+
113
+ test("the proxy Next loads gets the session warning next to the areas one; core's template gets neither", () => {
114
+ const root = mkdtempSync(join(tmpdir(), 'nextspark-proxy-session-'))
115
+ try {
116
+ write(root, 'src/proxy.ts', HTTP_SESSION_CHECK)
117
+ assert.deepEqual(proxyAreaNotices(root).map(notice => notice.code), [PROXY_SESSION_WARNING])
118
+ write(root, 'src/proxy.ts', `${HTTP_SESSION_CHECK.replace("['/superadmin', '/devtools']", '[]')}`)
119
+ assert.deepEqual(proxyAreaNotices(root).map(notice => notice.code), [PROXY_AREA_WARNING, PROXY_SESSION_WARNING])
120
+ write(root, 'src/proxy.ts', readFileSync(join(CORE_ROOT, 'templates/proxy.ts'), 'utf8'))
121
+ assert.deepEqual(proxyAreaNotices(root), [])
122
+ } finally {
123
+ rmSync(root, { recursive: true, force: true })
124
+ }
125
+ })
126
+
56
127
  test("prepare's prediction carries the warning among its notices", async () => {
57
128
  const host = tempHost({ 'templates/about/page.tsx': PAGE('About') })
58
129
  const project = mkdtempSync(join(tmpdir(), 'nextspark-proxy-areas-project-'))