create-next-pro-cli 0.1.26 → 0.1.28

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 (107) hide show
  1. package/README.md +281 -291
  2. package/create-next-pro-completion.sh +8 -38
  3. package/create-next-pro-completion.zsh +13 -0
  4. package/dist/bin.bun.js +2265 -794
  5. package/dist/bin.node.js +2425 -805
  6. package/dist/bin.node.js.map +1 -1
  7. package/package.json +50 -27
  8. package/templates/Projects/default/.env.example +17 -0
  9. package/templates/Projects/default/.github/workflows/quality.yml +24 -0
  10. package/templates/Projects/default/.gitignore.template +47 -0
  11. package/templates/Projects/default/.prettierignore +3 -0
  12. package/templates/Projects/default/README.md +52 -108
  13. package/templates/Projects/default/bun.lock +1152 -0
  14. package/templates/Projects/default/eslint.config.mjs +27 -11
  15. package/templates/Projects/default/messages/en/_global_ui.json +23 -10
  16. package/templates/Projects/default/messages/en.ts +17 -2
  17. package/templates/Projects/default/messages/fr/_global_ui.json +23 -10
  18. package/templates/Projects/default/messages/fr.ts +17 -2
  19. package/templates/Projects/default/next.config.ts +43 -3
  20. package/templates/Projects/default/package.json +42 -24
  21. package/templates/Projects/default/playwright.config.ts +26 -0
  22. package/templates/Projects/default/pnpm-workspace.yaml +5 -0
  23. package/templates/Projects/default/public/{cnp-logo.svg → logo.svg} +1 -1
  24. package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +8 -1
  25. package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +8 -0
  26. package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +8 -0
  27. package/templates/Projects/default/src/app/[locale]/(user)/dashboard/error.tsx +25 -0
  28. package/templates/Projects/default/src/app/[locale]/(user)/dashboard/loading.tsx +5 -0
  29. package/templates/Projects/default/src/app/[locale]/(user)/{Dashboard → dashboard}/page.tsx +1 -1
  30. package/templates/Projects/default/src/app/[locale]/(user)/layout.tsx +24 -2
  31. package/templates/Projects/default/src/app/[locale]/(user)/settings/loading.tsx +5 -0
  32. package/templates/Projects/default/src/app/[locale]/(user)/{Settings → settings}/page.tsx +1 -2
  33. package/templates/Projects/default/src/app/[locale]/(user)/userInfo/loading.tsx +5 -0
  34. package/templates/Projects/default/src/app/[locale]/(user)/{UserInfo → userInfo}/page.tsx +1 -2
  35. package/templates/Projects/default/src/app/[locale]/layout.tsx +34 -10
  36. package/templates/Projects/default/src/app/[locale]/loading.tsx +0 -9
  37. package/templates/Projects/default/src/app/[locale]/not-found.tsx +6 -15
  38. package/templates/Projects/default/src/app/[locale]/page.tsx +10 -1
  39. package/templates/Projects/default/src/app/api/auth/[...nextauth]/route.ts +8 -60
  40. package/templates/Projects/default/src/app/not-found.tsx +1 -1
  41. package/templates/Projects/default/src/app/sitemap.ts +2 -2
  42. package/templates/Projects/default/src/app/styles/globals.css +166 -113
  43. package/templates/Projects/default/src/auth.ts +20 -0
  44. package/templates/Projects/default/src/config.ts +3 -3
  45. package/templates/Projects/default/src/env.ts +65 -0
  46. package/templates/Projects/default/src/lib/i18n/messages.ts +8 -0
  47. package/templates/Projects/default/src/lib/i18n/request.ts +2 -16
  48. package/templates/Projects/default/src/lib/security/csp.ts +16 -0
  49. package/templates/Projects/default/src/lib/utils.ts +2 -1
  50. package/templates/Projects/default/src/proxy.ts +13 -0
  51. package/templates/Projects/default/src/ui/_global/BackButton.tsx +4 -2
  52. package/templates/Projects/default/src/ui/_global/Button.tsx +3 -8
  53. package/templates/Projects/default/src/ui/_global/GlobalHeader.tsx +10 -28
  54. package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +1 -1
  55. package/templates/Projects/default/src/ui/_global/LocaleSwitcher.tsx +9 -10
  56. package/templates/Projects/default/src/ui/_global/PublicNav.tsx +51 -17
  57. package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +45 -39
  58. package/templates/Projects/default/src/ui/_global/UserNav.tsx +6 -6
  59. package/templates/Projects/default/src/ui/_home/page-ui.tsx +5 -7
  60. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/LogoutButton.tsx +9 -7
  61. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/StatsCard.tsx +4 -4
  62. package/templates/Projects/default/src/ui/{Dashboard → dashboard}/page-ui.tsx +7 -8
  63. package/templates/Projects/default/src/ui/login/page-ui.tsx +36 -0
  64. package/templates/Projects/default/src/ui/register/page-ui.tsx +38 -0
  65. package/templates/Projects/default/src/ui/settings/page-ui.tsx +15 -0
  66. package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +15 -0
  67. package/templates/Projects/default/tailwind.config.ts +81 -1
  68. package/templates/Projects/default/tests/consumer/validate-template.ts +66 -0
  69. package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +106 -0
  70. package/templates/Projects/default/tests/rendering/verify-rendering.ts +56 -0
  71. package/templates/Projects/default/tests/unit/csp.test.ts +19 -0
  72. package/templates/Projects/default/tests/unit/env.test.ts +76 -0
  73. package/templates/Projects/default/tsconfig.json +6 -6
  74. package/templates/Projects/default/example.env +0 -8
  75. package/templates/Projects/default/messages/getMergedMessages.ts +0 -31
  76. package/templates/Projects/default/middleware.ts +0 -11
  77. package/templates/Projects/default/src/app/[locale]/(public)/Login/page.tsx +0 -6
  78. package/templates/Projects/default/src/app/[locale]/(public)/Register/page.tsx +0 -6
  79. package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/error.tsx +0 -38
  80. package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/loading.tsx +0 -10
  81. package/templates/Projects/default/src/app/[locale]/(user)/Settings/loading.tsx +0 -17
  82. package/templates/Projects/default/src/app/[locale]/(user)/UserInfo/loading.tsx +0 -17
  83. package/templates/Projects/default/src/app/api/auth/post-login/route.ts +0 -26
  84. package/templates/Projects/default/src/app/api/hello/route.ts +0 -5
  85. package/templates/Projects/default/src/app/layout.tsx +0 -11
  86. package/templates/Projects/default/src/app/page.tsx +0 -6
  87. package/templates/Projects/default/src/auth.config.ts +0 -0
  88. package/templates/Projects/default/src/lib/auth/disconnect.ts +0 -11
  89. package/templates/Projects/default/src/lib/auth/isConnected.ts +0 -18
  90. package/templates/Projects/default/src/lib/sample/example.ts +0 -3
  91. package/templates/Projects/default/src/lib/sample/index.ts +0 -3
  92. package/templates/Projects/default/src/ui/Login/page-ui.tsx +0 -22
  93. package/templates/Projects/default/src/ui/Register/page-ui.tsx +0 -26
  94. package/templates/Projects/default/src/ui/Settings/page-ui.tsx +0 -17
  95. package/templates/Projects/default/src/ui/UserInfo/page-ui.tsx +0 -17
  96. /package/templates/Projects/default/messages/en/{Dashboard.json → dashboard.json} +0 -0
  97. /package/templates/Projects/default/messages/en/{Login.json → login.json} +0 -0
  98. /package/templates/Projects/default/messages/en/{Register.json → register.json} +0 -0
  99. /package/templates/Projects/default/messages/en/{Settings.json → settings.json} +0 -0
  100. /package/templates/Projects/default/messages/en/{UserInfo.json → userInfo.json} +0 -0
  101. /package/templates/Projects/default/messages/fr/{Dashboard.json → dashboard.json} +0 -0
  102. /package/templates/Projects/default/messages/fr/{Login.json → login.json} +0 -0
  103. /package/templates/Projects/default/messages/fr/{Register.json → register.json} +0 -0
  104. /package/templates/Projects/default/messages/fr/{Settings.json → settings.json} +0 -0
  105. /package/templates/Projects/default/messages/fr/{UserInfo.json → userInfo.json} +0 -0
  106. /package/templates/Projects/default/public/{cnp-logo.png → logo.png} +0 -0
  107. /package/templates/Projects/default/src/ui/{Dashboard → dashboard}/WelcomeCard.tsx +0 -0
@@ -1 +1,81 @@
1
- // tailwind.config.ts
1
+ import type { Config } from "tailwindcss";
2
+ import tailwindcssAnimate from "tailwindcss-animate";
3
+
4
+ const config = {
5
+ darkMode: "class",
6
+ content: [
7
+ "./src/ui/**/*.{ts,tsx}",
8
+ "./src/app/**/*.{ts,tsx}",
9
+ "./ui/**/*.{ts,tsx}",
10
+ "./app/**/*.{ts,tsx}",
11
+ "./src/**/*.{ts,tsx}",
12
+ ],
13
+ theme: {
14
+ container: {
15
+ center: true,
16
+ padding: "2rem",
17
+ screens: {
18
+ "2xl": "1400px",
19
+ },
20
+ },
21
+ extend: {
22
+ colors: {
23
+ border: "rgb(var(--border))",
24
+ input: "rgb(var(--input))",
25
+ ring: "rgb(var(--ring))",
26
+ background: "rgb(var(--background))",
27
+ foreground: "rgb(var(--foreground))",
28
+ primary: {
29
+ DEFAULT: "rgb(var(--primary))",
30
+ foreground: "rgb(var(--primary-foreground))",
31
+ },
32
+ secondary: {
33
+ DEFAULT: "rgb(var(--secondary))",
34
+ foreground: "rgb(var(--secondary-foreground))",
35
+ },
36
+ destructive: {
37
+ DEFAULT: "rgb(var(--destructive))",
38
+ foreground: "rgb(var(--destructive-foreground))",
39
+ },
40
+ muted: {
41
+ DEFAULT: "rgb(var(--muted))",
42
+ foreground: "rgb(var(--muted-foreground))",
43
+ },
44
+ accent: {
45
+ DEFAULT: "rgb(var(--accent))",
46
+ foreground: "rgb(var(--accent-foreground))",
47
+ },
48
+ popover: {
49
+ DEFAULT: "rgb(var(--popover))",
50
+ foreground: "rgb(var(--popover-foreground))",
51
+ },
52
+ card: {
53
+ DEFAULT: "rgb(var(--card))",
54
+ foreground: "rgb(var(--card-foreground))",
55
+ },
56
+ },
57
+ borderRadius: {
58
+ lg: "var(--radius)",
59
+ md: "calc(var(--radius) - 2px)",
60
+ sm: "calc(var(--radius) - 4px)",
61
+ },
62
+ keyframes: {
63
+ "accordion-down": {
64
+ from: { height: 0 },
65
+ to: { height: "var(--radix-accordion-content-height)" },
66
+ },
67
+ "accordion-up": {
68
+ from: { height: "var(--radix-accordion-content-height)" },
69
+ to: { height: 0 },
70
+ },
71
+ },
72
+ animation: {
73
+ "accordion-down": "accordion-down 0.2s ease-out",
74
+ "accordion-up": "accordion-up 0.2s ease-out",
75
+ },
76
+ },
77
+ },
78
+ plugins: [tailwindcssAnimate],
79
+ } satisfies Config;
80
+
81
+ export default config;
@@ -0,0 +1,66 @@
1
+ import { spawn } from "node:child_process";
2
+ import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+
6
+ const root = process.cwd();
7
+ const target = await mkdtemp(path.join(tmpdir(), "create-next-pro-template-"));
8
+ const manager = process.env.CNP_PACKAGE_MANAGER ?? "bun";
9
+ const entries = [
10
+ ".env.example",
11
+ ".gitignore",
12
+ ".prettierignore",
13
+ "README.md",
14
+ "bun.lock",
15
+ "eslint.config.mjs",
16
+ "messages",
17
+ "next-env.d.ts",
18
+ "next.config.ts",
19
+ "package.json",
20
+ "playwright.config.ts",
21
+ "pnpm-workspace.yaml",
22
+ "postcss.config.mjs",
23
+ "public",
24
+ "src",
25
+ "tailwind.config.ts",
26
+ "tests/unit",
27
+ "tests/rendering",
28
+ "tsconfig.json",
29
+ ];
30
+
31
+ function run(command: string, args: string[]) {
32
+ return new Promise<void>((resolve, reject) => {
33
+ const child = spawn(command, args, {
34
+ cwd: target,
35
+ env: { ...process.env, AUTH_DISABLED: "true", CI: "true" },
36
+ stdio: "inherit",
37
+ });
38
+ child.on("error", reject);
39
+ child.on("exit", (code) =>
40
+ code === 0
41
+ ? resolve()
42
+ : reject(new Error(`${command} ${args.join(" ")} failed`)),
43
+ );
44
+ });
45
+ }
46
+
47
+ try {
48
+ for (const entry of entries) {
49
+ await cp(path.join(root, entry), path.join(target, entry), {
50
+ recursive: true,
51
+ });
52
+ }
53
+ if (manager !== "bun") {
54
+ const packagePath = path.join(target, "package.json");
55
+ const manifest = JSON.parse(await readFile(packagePath, "utf8"));
56
+ delete manifest.packageManager;
57
+ await writeFile(packagePath, `${JSON.stringify(manifest, null, 2)}\n`);
58
+ }
59
+ await run(
60
+ manager,
61
+ manager === "bun" ? ["install", "--frozen-lockfile"] : ["install"],
62
+ );
63
+ await run(manager, ["run", "check"]);
64
+ } finally {
65
+ await rm(target, { recursive: true, force: true });
66
+ }
@@ -0,0 +1,106 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ const captureByProject: Record<string, string> = {
4
+ desktop: "artifacts/captures/phase-2-template-integration-desktop.png",
5
+ mobile: "artifacts/captures/phase-2-template-integration-mobile.png",
6
+ };
7
+
8
+ test("public template routes render in both locales", async ({
9
+ page,
10
+ }, testInfo) => {
11
+ await page.goto("/");
12
+ await expect(page).toHaveURL(/\/(en|fr)$/);
13
+ await expect(page.getByRole("banner")).toBeVisible();
14
+
15
+ await page.goto("/en");
16
+ await expect(page.getByRole("banner")).toBeVisible();
17
+ const homeHeading = page
18
+ .getByRole("main")
19
+ .getByRole("heading", { name: "Home" });
20
+ await expect(homeHeading).toBeVisible();
21
+ await page.getByLabel("Toggle theme").click();
22
+ await expect(page.locator("html")).toHaveClass(/dark/);
23
+ await expect(homeHeading).toHaveCSS("color", "rgb(255, 255, 255)");
24
+ await page.getByLabel("Toggle theme").click();
25
+ await expect(page.locator("html")).toHaveClass(/light/);
26
+
27
+ await page.goto("/fr/login");
28
+ await expect(page.getByRole("heading", { name: /connexion/i })).toBeVisible();
29
+ await expect(page.getByLabel("Choisir la langue")).toHaveValue("fr");
30
+ await expect(page.locator("html")).toHaveClass(/light/);
31
+ await page.getByLabel("Changer de thème").click();
32
+ await expect(page.locator("html")).toHaveClass(/dark/);
33
+ await page.getByLabel("Changer de thème").click();
34
+ await expect(page.locator("html")).toHaveClass(/light/);
35
+
36
+ const capturePath = captureByProject[testInfo.project.name];
37
+ if (capturePath) {
38
+ await page.goto("/fr/login");
39
+ await page.mouse.move(0, 0);
40
+ await page.screenshot({ path: capturePath, fullPage: true });
41
+ }
42
+ });
43
+
44
+ test("anonymous users are redirected away from private pages", async ({
45
+ page,
46
+ }) => {
47
+ await page.goto("/en/dashboard");
48
+ await expect(page).toHaveURL(/\/en\/login/);
49
+ });
50
+
51
+ test("security headers and disabled auth are explicit", async ({ request }) => {
52
+ const pageResponse = await request.get("/en");
53
+ const csp = pageResponse.headers()["content-security-policy"];
54
+ expect(csp).toContain("default-src 'self'");
55
+ expect(csp).toContain("script-src 'self' 'unsafe-inline'");
56
+ expect(csp).not.toContain("'unsafe-eval'");
57
+ expect(csp).not.toContain("'nonce-");
58
+ expect(pageResponse.headers()["x-content-type-options"]).toBe("nosniff");
59
+
60
+ const authResponse = await request.get("/api/auth/providers");
61
+ expect(authResponse.status()).toBe(503);
62
+ await expect(authResponse.json()).resolves.toEqual({
63
+ error: "Authentication is not configured for this deployment.",
64
+ });
65
+ });
66
+
67
+ test("theme is initialized before hydration without a React warning", async ({
68
+ page,
69
+ }) => {
70
+ const hydrationWarnings: string[] = [];
71
+ page.on("console", (message) => {
72
+ if (
73
+ message.type() === "warning" &&
74
+ (message.text().includes("hydrated") ||
75
+ message.text().includes("Encountered a script tag"))
76
+ ) {
77
+ hydrationWarnings.push(message.text());
78
+ }
79
+ });
80
+
81
+ await page.addInitScript(() => localStorage.setItem("theme", "dark"));
82
+ await page.goto("/en", { waitUntil: "domcontentloaded" });
83
+ await expect(page.locator("#theme-initializer")).toHaveCount(1);
84
+ await expect(page.locator("html")).toHaveClass(/dark/);
85
+ expect(hydrationWarnings).toEqual([]);
86
+ });
87
+
88
+ test("theme falls back to the system preference", async ({ page }) => {
89
+ await page.emulateMedia({ colorScheme: "dark" });
90
+ await page.goto("/en", { waitUntil: "domcontentloaded" });
91
+
92
+ await expect(page.locator("html")).toHaveClass(/dark/);
93
+ });
94
+
95
+ test("mobile navigation supports keyboard dismissal", async ({
96
+ page,
97
+ }, testInfo) => {
98
+ test.skip(testInfo.project.name !== "mobile");
99
+ await page.goto("/en");
100
+ const menuButton = page.getByRole("button", { name: "Menu" });
101
+ await menuButton.click();
102
+ await expect(page.getByRole("dialog")).toBeVisible();
103
+ await page.keyboard.press("Escape");
104
+ await expect(page.getByRole("dialog")).toBeHidden();
105
+ await expect(menuButton).toBeFocused();
106
+ });
@@ -0,0 +1,56 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ type PrerenderManifest = {
5
+ routes: Record<string, unknown>;
6
+ };
7
+
8
+ const manifestPath = join(process.cwd(), ".next", "prerender-manifest.json");
9
+ const manifest = JSON.parse(
10
+ await readFile(manifestPath, "utf8"),
11
+ ) as PrerenderManifest;
12
+ const prerenderedRoutes = new Set(Object.keys(manifest.routes));
13
+ const expectedPublicRoutes = [
14
+ "/en",
15
+ "/fr",
16
+ "/en/login",
17
+ "/fr/login",
18
+ "/en/register",
19
+ "/fr/register",
20
+ "/sitemap.xml",
21
+ ];
22
+ const privateRoutes = [
23
+ "/en/dashboard",
24
+ "/fr/dashboard",
25
+ "/en/settings",
26
+ "/fr/settings",
27
+ "/en/userInfo",
28
+ "/fr/userInfo",
29
+ "/api/auth/[...nextauth]",
30
+ ];
31
+
32
+ const missingPublicRoutes = expectedPublicRoutes.filter(
33
+ (route) => !prerenderedRoutes.has(route),
34
+ );
35
+ const leakedPrivateRoutes = privateRoutes.filter((route) =>
36
+ prerenderedRoutes.has(route),
37
+ );
38
+
39
+ if (missingPublicRoutes.length > 0 || leakedPrivateRoutes.length > 0) {
40
+ throw new Error(
41
+ [
42
+ missingPublicRoutes.length > 0
43
+ ? `Public routes not prerendered: ${missingPublicRoutes.join(", ")}`
44
+ : null,
45
+ leakedPrivateRoutes.length > 0
46
+ ? `Private routes unexpectedly prerendered: ${leakedPrivateRoutes.join(", ")}`
47
+ : null,
48
+ ]
49
+ .filter(Boolean)
50
+ .join("\n"),
51
+ );
52
+ }
53
+
54
+ console.log(
55
+ `Rendering verified: ${expectedPublicRoutes.length} public routes prerendered; private routes remain runtime-only.`,
56
+ );
@@ -0,0 +1,19 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createContentSecurityPolicy } from "../../src/lib/security/csp";
3
+
4
+ describe("Content Security Policy", () => {
5
+ test("allows the stable inline scripts required by static Next.js pages", () => {
6
+ const csp = createContentSecurityPolicy(false);
7
+
8
+ expect(csp).toContain("script-src 'self' 'unsafe-inline'");
9
+ expect(csp).not.toContain("'unsafe-eval'");
10
+ expect(csp).not.toContain("'nonce-");
11
+ });
12
+
13
+ test("allows eval only for the Next.js development runtime", () => {
14
+ const csp = createContentSecurityPolicy(true);
15
+
16
+ expect(csp).toContain("script-src 'self' 'unsafe-inline' 'unsafe-eval'");
17
+ expect(csp).not.toContain("'nonce-");
18
+ });
19
+ });
@@ -0,0 +1,76 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ import { getAuthEnv, getPublicUrl } from "../../src/env";
3
+
4
+ const envKeys = [
5
+ "AUTH_SECRET",
6
+ "NEXTAUTH_SECRET",
7
+ "AUTH_GOOGLE_ID",
8
+ "GOOGLE_CLIENT_ID",
9
+ "AUTH_GOOGLE_SECRET",
10
+ "AUTH_DISABLED",
11
+ "GOOGLE_CLIENT_SECRET",
12
+ "NEXT_PUBLIC_APP_URL",
13
+ "PROJECT_PRODUCTION_URL",
14
+ "PORT",
15
+ ] as const;
16
+
17
+ const originalEnv = new Map<string, string | undefined>();
18
+
19
+ beforeEach(() => {
20
+ for (const key of envKeys) {
21
+ originalEnv.set(key, process.env[key]);
22
+ delete process.env[key];
23
+ }
24
+ });
25
+
26
+ afterEach(() => {
27
+ for (const key of envKeys) {
28
+ const value = originalEnv.get(key);
29
+ if (value === undefined) {
30
+ delete process.env[key];
31
+ } else {
32
+ process.env[key] = value;
33
+ }
34
+ }
35
+ originalEnv.clear();
36
+ });
37
+
38
+ describe("environment helpers", () => {
39
+ test("disables authentication when all auth values are missing", () => {
40
+ expect(getAuthEnv()).toBeNull();
41
+ });
42
+
43
+ test("rejects partial authentication configuration", () => {
44
+ process.env.AUTH_SECRET = "secret";
45
+ expect(() => getAuthEnv()).toThrow("Incomplete Auth.js configuration");
46
+ });
47
+
48
+ test("uses Auth.js env aliases for generated templates", () => {
49
+ process.env.NEXTAUTH_SECRET = "secret";
50
+ process.env.GOOGLE_CLIENT_ID = "google-id";
51
+ process.env.GOOGLE_CLIENT_SECRET = "google-secret";
52
+
53
+ expect(getAuthEnv()).toEqual({
54
+ secret: "secret",
55
+ googleClientId: "google-id",
56
+ googleClientSecret: "google-secret",
57
+ });
58
+ });
59
+
60
+ test("builds a local public URL when no deployment URL is configured", () => {
61
+ process.env.PORT = "3100";
62
+
63
+ expect(getPublicUrl()).toBe("http://localhost:3100");
64
+ });
65
+
66
+ test("normalizes host-only public URLs to https", () => {
67
+ process.env.PROJECT_PRODUCTION_URL = "example.com";
68
+
69
+ expect(getPublicUrl()).toBe("https://example.com");
70
+ });
71
+
72
+ test("rejects invalid public URLs", () => {
73
+ process.env.NEXT_PUBLIC_APP_URL = "https://[invalid";
74
+ expect(() => getPublicUrl()).toThrow();
75
+ });
76
+ });
@@ -2,7 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2017",
4
4
  "lib": ["dom", "dom.iterable", "esnext"],
5
- "allowJs": true,
5
+ "allowJs": false,
6
6
  "skipLibCheck": true,
7
7
  "strict": true,
8
8
  "noEmit": true,
@@ -11,8 +11,9 @@
11
11
  "moduleResolution": "bundler",
12
12
  "resolveJsonModule": true,
13
13
  "isolatedModules": true,
14
- "jsx": "preserve",
14
+ "jsx": "react-jsx",
15
15
  "incremental": true,
16
+ "types": ["node", "vitest/globals"],
16
17
  "plugins": [
17
18
  {
18
19
  "name": "next"
@@ -26,9 +27,8 @@
26
27
  "next-env.d.ts",
27
28
  "**/*.ts",
28
29
  "**/*.tsx",
29
- ".next/types/**/*.ts"
30
- /* "src/app/not-found.tsx.save",
31
- "src/app/layout.tsx.save" */
30
+ ".next/types/**/*.ts",
31
+ ".next/dev/types/**/*.ts"
32
32
  ],
33
- "exclude": ["node_modules"]
33
+ "exclude": ["node_modules", ".next/dev"]
34
34
  }
@@ -1,8 +0,0 @@
1
- PORT=3000
2
- PROJECT_PRODUCTION_URL=your-production-url.com
3
- NEXTAUTH_URL=http://localhost:3000/api/auth
4
- NEXTAUTH_SECRET=ONGGG2GIf5Mc4LpWv69WR8J+WazM8BmGa8hSex1dEvg=# generate a new secret using `openssl rand -base64 32`
5
- APP_JWT_SECRET=3bsnnr4PqTaXBvDMstnSjhd2FdYbi1CgSjXMF9s446c=# generate a new secret using `openssl rand -base64 32`
6
-
7
- GOOGLE_CLIENT_ID=# your Google Client ID , you can get it from https://console.cloud.google.com/apis/credentials
8
- GOOGLE_CLIENT_SECRET=# your Google Client Secret , you can get it from https://console.cloud.google.com/apis/credentials
@@ -1,31 +0,0 @@
1
- // lib/i18n/getMergedMessages.ts
2
- import { readdir, readFile } from "fs/promises";
3
- import { join, basename } from "path";
4
-
5
- export default async function getMergedMessages(currentModuleFilename: string) {
6
- const locale = basename(currentModuleFilename, ".ts");
7
- const basePath = join(process.cwd(), "messages", locale);
8
- const merged: Record<string, any> = {};
9
-
10
- try {
11
- const files = await readdir(basePath);
12
- for (const file of files) {
13
- if (file.endsWith(".json")) {
14
- try {
15
- const content = await readFile(join(basePath, file), "utf8");
16
- const key = basename(file, ".json");
17
- merged[key] = JSON.parse(content);
18
- } catch (err) {
19
- console.error(
20
- `❌ Failed to read ${file} for locale '${locale}':`,
21
- err
22
- );
23
- }
24
- }
25
- }
26
- } catch (err) {
27
- console.error(`❌ Failed to read directory '${basePath}':`, err);
28
- }
29
-
30
- return merged;
31
- }
@@ -1,11 +0,0 @@
1
- import createMiddleware from "next-intl/middleware";
2
- import { routing } from "@/lib/i18n/routing";
3
-
4
- export default createMiddleware(routing);
5
-
6
- export const config = {
7
- // Match all pathnames except for
8
- // - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
9
- // - … the ones containing a dot (e.g. `favicon.ico`)
10
- matcher: "/((?!api|trpc|_next|_vercel|.*\\..*).*)",
11
- };
@@ -1,6 +0,0 @@
1
- // src/app/[locale]/(public)/Login/page.tsx
2
- import LoginPageUI from "@/ui/Login/page-ui";
3
-
4
- export default function LoginPage() {
5
- return <LoginPageUI />;
6
- }
@@ -1,6 +0,0 @@
1
- // src/app/[locale]/(public)/Register/page.tsx
2
- import RegisterPageUI from "@/ui/Register/page-ui";
3
-
4
- export default function RegisterPage() {
5
- return <RegisterPageUI />;
6
- }
@@ -1,38 +0,0 @@
1
- // app/[locale]/error.tsx
2
- "use client"; // Error boundaries must be Client Components
3
-
4
- import { useEffect } from "react";
5
-
6
- export default function Error({
7
- error,
8
- reset,
9
- }: {
10
- error: Error & { digest?: string };
11
- reset: () => void;
12
- }) {
13
- useEffect(() => {
14
- // Log the error to an error reporting service
15
- console.error(error);
16
- }, [error]);
17
-
18
- return (
19
- <div>
20
- <h2>Something went wrong!</h2>
21
- <button
22
- onClick={
23
- // Attempt to recover by trying to re-render the segment
24
- () => reset()
25
- }
26
- >
27
- Try again
28
- </button>
29
- </div>
30
- );
31
- }
32
- // This page is used to handle errors in the application
33
- // You can customize this page to display a user-friendly error message
34
- // or redirect to a different page if needed
35
- // This is part of the Next.js app directory structure and will be automatically used by Next.js
36
- // when an error occurs in the application
37
- // Make sure to handle errors gracefully to improve user experience
38
- // You can also log errors or send them to an error tracking service
@@ -1,10 +0,0 @@
1
- // app/[locale]/dashboard/loading.tsx
2
- "use client";
3
-
4
- export default function Loading() {
5
- return (
6
- <div className="flex flex-col flex-1 items-center justify-center">
7
- <p className="text-gray-500">Loading dashboard...</p>
8
- </div>
9
- );
10
- }
@@ -1,17 +0,0 @@
1
- // app/[locale]/dashboard/loading.tsx
2
- "use client";
3
-
4
- export default function Loading() {
5
- return (
6
- <div className="flex flex-col flex-1 items-center justify-center">
7
- <p className="text-gray-500">Loading user info...</p>
8
- </div>
9
- );
10
- }
11
- // This component is used to show a loading state while the dashboard is being prepared
12
- // You can customize it with a spinner, skeletons, or any loading UI you prefer
13
- // It will be displayed when the dashboard is loading, for example during data fetching or component rendering
14
- // This is useful for providing feedback to users that something is happening in the background
15
- // You can also use this to implement a more complex loading state with animations or placeholders
16
- // Make sure to keep it lightweight to avoid blocking the main thread
17
- // This component is part of the Next.js app directory structure and will be automatically used by Next.js
@@ -1,17 +0,0 @@
1
- // app/[locale]/dashboard/loading.tsx
2
- "use client";
3
-
4
- export default function Loading() {
5
- return (
6
- <div className="flex flex-col flex-1 items-center justify-center">
7
- <p className="text-gray-500">Loading user info...</p>
8
- </div>
9
- );
10
- }
11
- // This component is used to show a loading state while the dashboard is being prepared
12
- // You can customize it with a spinner, skeletons, or any loading UI you prefer
13
- // It will be displayed when the dashboard is loading, for example during data fetching or component rendering
14
- // This is useful for providing feedback to users that something is happening in the background
15
- // You can also use this to implement a more complex loading state with animations or placeholders
16
- // Make sure to keep it lightweight to avoid blocking the main thread
17
- // This component is part of the Next.js app directory structure and will be automatically used by Next.js
@@ -1,26 +0,0 @@
1
- import { getToken } from "next-auth/jwt";
2
- import { sign } from "jsonwebtoken";
3
- import { NextResponse, NextRequest } from "next/server";
4
- import { redirect } from "@/lib/i18n/navigation";
5
-
6
- export async function GET(req: NextRequest) {
7
- const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
8
- if (token) {
9
- const appJwt = sign(
10
- { id: token.id, email: token.email, name: token.name },
11
- process.env.APP_JWT_SECRET!,
12
- { expiresIn: "7d" }
13
- );
14
- const res = NextResponse.redirect(req.nextUrl.origin + "/");
15
- res.cookies.set("access_token", appJwt, {
16
- httpOnly: true,
17
- secure: process.env.NODE_ENV === "production",
18
- sameSite: "strict",
19
- path: "/",
20
- maxAge: 60 * 60 * 24 * 7,
21
- });
22
- return res;
23
- }
24
- // Si pas de token, redirige vers login
25
- return redirect({ href: "/login", locale: "en" });
26
- }
@@ -1,5 +0,0 @@
1
- import { NextResponse } from "next/server";
2
-
3
- export async function GET() {
4
- return NextResponse.json({ message: "Hello" });
5
- }
@@ -1,11 +0,0 @@
1
- import { ReactNode } from "react";
2
-
3
- type Props = {
4
- children: ReactNode;
5
- };
6
-
7
- // Since we have a `not-found.tsx` page on the root, a layout file
8
- // is required, even if it's just passing children through.
9
- export default function RootLayout({ children }: Props) {
10
- return children;
11
- }
@@ -1,6 +0,0 @@
1
- import { redirect } from "next/navigation";
2
-
3
- // This page only renders when the app is built statically (output: 'export')
4
- export default function RootPage() {
5
- redirect("/en");
6
- }
File without changes
@@ -1,11 +0,0 @@
1
- // src/lib/auth/disconnect.ts
2
- "use server";
3
- import { cookies } from "next/headers";
4
-
5
- /**
6
- * Supprime le cookie access_token pour déconnecter l'utilisateur côté serveur.
7
- */
8
- export async function disconnect() {
9
- const cookieStore = await cookies();
10
- cookieStore.delete("access_token");
11
- }