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.
- package/README.md +281 -291
- package/create-next-pro-completion.sh +8 -38
- package/create-next-pro-completion.zsh +13 -0
- package/dist/bin.bun.js +2265 -794
- package/dist/bin.node.js +2425 -805
- package/dist/bin.node.js.map +1 -1
- package/package.json +50 -27
- package/templates/Projects/default/.env.example +17 -0
- package/templates/Projects/default/.github/workflows/quality.yml +24 -0
- package/templates/Projects/default/.gitignore.template +47 -0
- package/templates/Projects/default/.prettierignore +3 -0
- package/templates/Projects/default/README.md +52 -108
- package/templates/Projects/default/bun.lock +1152 -0
- package/templates/Projects/default/eslint.config.mjs +27 -11
- package/templates/Projects/default/messages/en/_global_ui.json +23 -10
- package/templates/Projects/default/messages/en.ts +17 -2
- package/templates/Projects/default/messages/fr/_global_ui.json +23 -10
- package/templates/Projects/default/messages/fr.ts +17 -2
- package/templates/Projects/default/next.config.ts +43 -3
- package/templates/Projects/default/package.json +42 -24
- package/templates/Projects/default/playwright.config.ts +26 -0
- package/templates/Projects/default/pnpm-workspace.yaml +5 -0
- package/templates/Projects/default/public/{cnp-logo.svg → logo.svg} +1 -1
- package/templates/Projects/default/src/app/[locale]/(public)/layout.tsx +8 -1
- package/templates/Projects/default/src/app/[locale]/(public)/login/page.tsx +8 -0
- package/templates/Projects/default/src/app/[locale]/(public)/register/page.tsx +8 -0
- package/templates/Projects/default/src/app/[locale]/(user)/dashboard/error.tsx +25 -0
- package/templates/Projects/default/src/app/[locale]/(user)/dashboard/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{Dashboard → dashboard}/page.tsx +1 -1
- package/templates/Projects/default/src/app/[locale]/(user)/layout.tsx +24 -2
- package/templates/Projects/default/src/app/[locale]/(user)/settings/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{Settings → settings}/page.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/(user)/userInfo/loading.tsx +5 -0
- package/templates/Projects/default/src/app/[locale]/(user)/{UserInfo → userInfo}/page.tsx +1 -2
- package/templates/Projects/default/src/app/[locale]/layout.tsx +34 -10
- package/templates/Projects/default/src/app/[locale]/loading.tsx +0 -9
- package/templates/Projects/default/src/app/[locale]/not-found.tsx +6 -15
- package/templates/Projects/default/src/app/[locale]/page.tsx +10 -1
- package/templates/Projects/default/src/app/api/auth/[...nextauth]/route.ts +8 -60
- package/templates/Projects/default/src/app/not-found.tsx +1 -1
- package/templates/Projects/default/src/app/sitemap.ts +2 -2
- package/templates/Projects/default/src/app/styles/globals.css +166 -113
- package/templates/Projects/default/src/auth.ts +20 -0
- package/templates/Projects/default/src/config.ts +3 -3
- package/templates/Projects/default/src/env.ts +65 -0
- package/templates/Projects/default/src/lib/i18n/messages.ts +8 -0
- package/templates/Projects/default/src/lib/i18n/request.ts +2 -16
- package/templates/Projects/default/src/lib/security/csp.ts +16 -0
- package/templates/Projects/default/src/lib/utils.ts +2 -1
- package/templates/Projects/default/src/proxy.ts +13 -0
- package/templates/Projects/default/src/ui/_global/BackButton.tsx +4 -2
- package/templates/Projects/default/src/ui/_global/Button.tsx +3 -8
- package/templates/Projects/default/src/ui/_global/GlobalHeader.tsx +10 -28
- package/templates/Projects/default/src/ui/_global/GlobalMain.tsx +1 -1
- package/templates/Projects/default/src/ui/_global/LocaleSwitcher.tsx +9 -10
- package/templates/Projects/default/src/ui/_global/PublicNav.tsx +51 -17
- package/templates/Projects/default/src/ui/_global/ThemeToggle.tsx +45 -39
- package/templates/Projects/default/src/ui/_global/UserNav.tsx +6 -6
- package/templates/Projects/default/src/ui/_home/page-ui.tsx +5 -7
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/LogoutButton.tsx +9 -7
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/StatsCard.tsx +4 -4
- package/templates/Projects/default/src/ui/{Dashboard → dashboard}/page-ui.tsx +7 -8
- package/templates/Projects/default/src/ui/login/page-ui.tsx +36 -0
- package/templates/Projects/default/src/ui/register/page-ui.tsx +38 -0
- package/templates/Projects/default/src/ui/settings/page-ui.tsx +15 -0
- package/templates/Projects/default/src/ui/userInfo/page-ui.tsx +15 -0
- package/templates/Projects/default/tailwind.config.ts +81 -1
- package/templates/Projects/default/tests/consumer/validate-template.ts +66 -0
- package/templates/Projects/default/tests/e2e/template-remediation.playwright.ts +106 -0
- package/templates/Projects/default/tests/rendering/verify-rendering.ts +56 -0
- package/templates/Projects/default/tests/unit/csp.test.ts +19 -0
- package/templates/Projects/default/tests/unit/env.test.ts +76 -0
- package/templates/Projects/default/tsconfig.json +6 -6
- package/templates/Projects/default/example.env +0 -8
- package/templates/Projects/default/messages/getMergedMessages.ts +0 -31
- package/templates/Projects/default/middleware.ts +0 -11
- package/templates/Projects/default/src/app/[locale]/(public)/Login/page.tsx +0 -6
- package/templates/Projects/default/src/app/[locale]/(public)/Register/page.tsx +0 -6
- package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/error.tsx +0 -38
- package/templates/Projects/default/src/app/[locale]/(user)/Dashboard/loading.tsx +0 -10
- package/templates/Projects/default/src/app/[locale]/(user)/Settings/loading.tsx +0 -17
- package/templates/Projects/default/src/app/[locale]/(user)/UserInfo/loading.tsx +0 -17
- package/templates/Projects/default/src/app/api/auth/post-login/route.ts +0 -26
- package/templates/Projects/default/src/app/api/hello/route.ts +0 -5
- package/templates/Projects/default/src/app/layout.tsx +0 -11
- package/templates/Projects/default/src/app/page.tsx +0 -6
- package/templates/Projects/default/src/auth.config.ts +0 -0
- package/templates/Projects/default/src/lib/auth/disconnect.ts +0 -11
- package/templates/Projects/default/src/lib/auth/isConnected.ts +0 -18
- package/templates/Projects/default/src/lib/sample/example.ts +0 -3
- package/templates/Projects/default/src/lib/sample/index.ts +0 -3
- package/templates/Projects/default/src/ui/Login/page-ui.tsx +0 -22
- package/templates/Projects/default/src/ui/Register/page-ui.tsx +0 -26
- package/templates/Projects/default/src/ui/Settings/page-ui.tsx +0 -17
- package/templates/Projects/default/src/ui/UserInfo/page-ui.tsx +0 -17
- /package/templates/Projects/default/messages/en/{Dashboard.json → dashboard.json} +0 -0
- /package/templates/Projects/default/messages/en/{Login.json → login.json} +0 -0
- /package/templates/Projects/default/messages/en/{Register.json → register.json} +0 -0
- /package/templates/Projects/default/messages/en/{Settings.json → settings.json} +0 -0
- /package/templates/Projects/default/messages/en/{UserInfo.json → userInfo.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Dashboard.json → dashboard.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Login.json → login.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Register.json → register.json} +0 -0
- /package/templates/Projects/default/messages/fr/{Settings.json → settings.json} +0 -0
- /package/templates/Projects/default/messages/fr/{UserInfo.json → userInfo.json} +0 -0
- /package/templates/Projects/default/public/{cnp-logo.png → logo.png} +0 -0
- /package/templates/Projects/default/src/ui/{Dashboard → dashboard}/WelcomeCard.tsx +0 -0
|
@@ -1 +1,81 @@
|
|
|
1
|
-
|
|
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":
|
|
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": "
|
|
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
|
-
|
|
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,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,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,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
|
-
}
|
|
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
|
-
}
|