@mars-stack/cli 8.0.3 → 8.0.5
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/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/template/e2e/README.md +2 -0
- package/template/package.json +7 -7
- package/template/playwright.config.ts +7 -10
- package/template/scripts/playwright-web-server.mjs +2 -1
- package/template/scripts/seed.ts +1 -1
- package/template/src/app/(protected)/settings/page.tsx +22 -7
- package/template/src/app/api/csrf/route.test.ts +4 -1
- package/template/src/app/api/csrf/route.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mars-stack/cli",
|
|
3
|
-
"version": "8.0.
|
|
3
|
+
"version": "8.0.5",
|
|
4
4
|
"description": "MARS CLI: scaffold, configure, and maintain SaaS apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/fs-extra": "^11.0.0",
|
|
50
50
|
"@types/js-yaml": "^4.0.9",
|
|
51
|
-
"@types/node": "^25.5.
|
|
51
|
+
"@types/node": "^25.5.2",
|
|
52
52
|
"@types/prompts": "^2.4.0",
|
|
53
53
|
"tsup": "^8.0.0",
|
|
54
|
-
"typescript": "^
|
|
54
|
+
"typescript": "^6.0.2",
|
|
55
55
|
"vitest": "^4.1.2"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/template/e2e/README.md
CHANGED
|
@@ -10,6 +10,8 @@ Browser-level tests for user-visible behaviour. Run: `yarn test:e2e` from the pr
|
|
|
10
10
|
|
|
11
11
|
Monorepo CI runs this on pull requests via the `template-e2e` job in `.github/workflows/ci.yml` (Postgres service + `E2E_USE_CI_SERVER=1` + `scripts/start-e2e-server.mjs`).
|
|
12
12
|
|
|
13
|
+
**Local default web server:** `playwright-web-server.mjs` runs `ensure-db`, then **`npx prisma db seed`**, then `yarn dev` — same seeded-user assumption as CI (`user@example.com` / `Password123!` from `scripts/seed.ts`). You no longer need a separate manual `yarn db:seed` before `yarn test:e2e` on a fresh scaffold.
|
|
14
|
+
|
|
13
15
|
## Kitchen-sink CLI catalog (MARS-041, Option A)
|
|
14
16
|
|
|
15
17
|
Full **`mars generate`** surface is exercised against a **materialized kitchen-sink fixture** (not the bare template tree):
|
package/template/package.json
CHANGED
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"@mars-stack/ui": "*",
|
|
33
33
|
"@prisma/adapter-pg": "^7.6.0",
|
|
34
34
|
"@prisma/client": "^7.6.0",
|
|
35
|
-
"dotenv": "^17.
|
|
36
|
-
"@react-email/components": "^1.0.
|
|
35
|
+
"dotenv": "^17.4.1",
|
|
36
|
+
"@react-email/components": "^1.0.11",
|
|
37
37
|
"@sendgrid/mail": "^8.1.0",
|
|
38
38
|
"@upstash/ratelimit": "^2.0.0",
|
|
39
39
|
"@upstash/redis": "^1.37.0",
|
|
40
40
|
"bcryptjs": "^3.0.3",
|
|
41
41
|
"clsx": "^2.1.1",
|
|
42
42
|
"jose": "^6.2.2",
|
|
43
|
-
"next": "^16.2.
|
|
43
|
+
"next": "^16.2.2",
|
|
44
44
|
"pino": "^9.6.0",
|
|
45
45
|
"pino-pretty": "^13.0.0",
|
|
46
46
|
"react": "^19.0.0",
|
|
@@ -56,17 +56,17 @@
|
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@eslint/eslintrc": "^3.3.5",
|
|
59
|
-
"@playwright/test": "^1.
|
|
59
|
+
"@playwright/test": "^1.59.1",
|
|
60
60
|
"@tailwindcss/postcss": "^4.2.2",
|
|
61
61
|
"@testing-library/jest-dom": "^6.9.1",
|
|
62
62
|
"@testing-library/react": "^16.0.0",
|
|
63
63
|
"@types/bcryptjs": "^3.0.0",
|
|
64
|
-
"@types/node": "^25.5.
|
|
64
|
+
"@types/node": "^25.5.2",
|
|
65
65
|
"@types/react": "^19.0.0",
|
|
66
66
|
"@types/react-dom": "^19.0.0",
|
|
67
67
|
"@vitejs/plugin-react": "^5.1.4",
|
|
68
68
|
"eslint": "^9.0.0",
|
|
69
|
-
"eslint-config-next": "^16.2.
|
|
69
|
+
"eslint-config-next": "^16.2.2",
|
|
70
70
|
"jsdom": "^29.0.1",
|
|
71
71
|
"postcss": "^8.5.0",
|
|
72
72
|
"prettier": "^3.5.0",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"prisma": "^7.6.0",
|
|
75
75
|
"tailwindcss": "^4.0.0",
|
|
76
76
|
"tsx": "^4.0.0",
|
|
77
|
-
"typescript": "^
|
|
77
|
+
"typescript": "^6.0.2",
|
|
78
78
|
"vitest": "^4.1.2"
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -23,16 +23,13 @@ export default defineConfig({
|
|
|
23
23
|
trace: 'on-first-retry',
|
|
24
24
|
screenshot: 'only-on-failure',
|
|
25
25
|
},
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
use: { ...devices['iPhone 14'] },
|
|
34
|
-
},
|
|
35
|
-
],
|
|
26
|
+
// CI (`template-e2e`) installs Chromium only — match that here so dogfood/automation does not fail on WebKit.
|
|
27
|
+
projects: process.env.CI
|
|
28
|
+
? [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }]
|
|
29
|
+
: [
|
|
30
|
+
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
|
31
|
+
{ name: 'mobile', use: { ...devices['iPhone 14'] } },
|
|
32
|
+
],
|
|
36
33
|
webServer:
|
|
37
34
|
process.env.E2E_USE_CI_SERVER === '1'
|
|
38
35
|
? {
|
|
@@ -10,7 +10,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
|
|
11
11
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/** Match CI `start-e2e-server.mjs`: seed so smoke tests find `user@example.com` without a manual `db:seed`. */
|
|
14
|
+
const child = spawn('sh', ['-c', 'node scripts/ensure-db.mjs && npx prisma db seed && yarn dev'], {
|
|
14
15
|
cwd: ROOT,
|
|
15
16
|
stdio: 'inherit',
|
|
16
17
|
shell: false,
|
package/template/scripts/seed.ts
CHANGED
|
@@ -35,8 +35,9 @@ function parseUserAgent(ua: string | null): string {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export default function Settings() {
|
|
38
|
-
const { getCSRFHeaders } = useCSRFContext();
|
|
38
|
+
const { getCSRFHeaders, csrfToken, isLoading: csrfLoading } = useCSRFContext();
|
|
39
39
|
const { user, isLoading: authLoading, updateUser } = useAuth();
|
|
40
|
+
const csrfReady = !csrfLoading && csrfToken !== null;
|
|
40
41
|
|
|
41
42
|
const [name, setName] = useState('');
|
|
42
43
|
const [nameInitialized, setNameInitialized] = useState(false);
|
|
@@ -77,10 +78,12 @@ export default function Settings() {
|
|
|
77
78
|
}
|
|
78
79
|
}, [user, fetchSessions]);
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!nameInitialized && user?.name) {
|
|
83
|
+
setName(user.name);
|
|
84
|
+
setNameInitialized(true);
|
|
85
|
+
}
|
|
86
|
+
}, [nameInitialized, user?.name]);
|
|
84
87
|
|
|
85
88
|
if (authLoading) {
|
|
86
89
|
return (
|
|
@@ -96,6 +99,7 @@ export default function Settings() {
|
|
|
96
99
|
|
|
97
100
|
async function handleNameSubmit(event: FormEvent<HTMLFormElement>) {
|
|
98
101
|
event.preventDefault();
|
|
102
|
+
if (!csrfReady) return;
|
|
99
103
|
setNameStatus(null);
|
|
100
104
|
setNameSaving(true);
|
|
101
105
|
|
|
@@ -124,6 +128,7 @@ export default function Settings() {
|
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
async function handleRevokeSession(sessionId: string) {
|
|
131
|
+
if (!csrfReady) return;
|
|
127
132
|
setRevokingId(sessionId);
|
|
128
133
|
setSessionsStatus(null);
|
|
129
134
|
try {
|
|
@@ -147,6 +152,7 @@ export default function Settings() {
|
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
async function handleRevokeAllSessions() {
|
|
155
|
+
if (!csrfReady) return;
|
|
150
156
|
setRevokingAll(true);
|
|
151
157
|
setSessionsStatus(null);
|
|
152
158
|
try {
|
|
@@ -171,6 +177,7 @@ export default function Settings() {
|
|
|
171
177
|
|
|
172
178
|
async function handlePasswordSubmit(event: FormEvent<HTMLFormElement>) {
|
|
173
179
|
event.preventDefault();
|
|
180
|
+
if (!csrfReady) return;
|
|
174
181
|
setPasswordStatus(null);
|
|
175
182
|
|
|
176
183
|
if (newPassword !== confirmPassword) {
|
|
@@ -236,7 +243,11 @@ export default function Settings() {
|
|
|
236
243
|
</p>
|
|
237
244
|
)}
|
|
238
245
|
|
|
239
|
-
<Button
|
|
246
|
+
<Button
|
|
247
|
+
type="submit"
|
|
248
|
+
loading={nameSaving}
|
|
249
|
+
disabled={!name.trim() || !csrfReady}
|
|
250
|
+
>
|
|
240
251
|
Save Name
|
|
241
252
|
</Button>
|
|
242
253
|
</form>
|
|
@@ -299,7 +310,9 @@ export default function Settings() {
|
|
|
299
310
|
<Button
|
|
300
311
|
type="submit"
|
|
301
312
|
loading={passwordSaving}
|
|
302
|
-
disabled={
|
|
313
|
+
disabled={
|
|
314
|
+
!currentPassword || !newPassword || !confirmPassword || !csrfReady
|
|
315
|
+
}
|
|
303
316
|
>
|
|
304
317
|
Change Password
|
|
305
318
|
</Button>
|
|
@@ -314,6 +327,7 @@ export default function Settings() {
|
|
|
314
327
|
variant="subtle"
|
|
315
328
|
size="sm"
|
|
316
329
|
loading={revokingAll}
|
|
330
|
+
disabled={!csrfReady}
|
|
317
331
|
onClick={handleRevokeAllSessions}
|
|
318
332
|
>
|
|
319
333
|
Revoke All
|
|
@@ -359,6 +373,7 @@ export default function Settings() {
|
|
|
359
373
|
variant="subtle"
|
|
360
374
|
size="sm"
|
|
361
375
|
loading={revokingId === session.id}
|
|
376
|
+
disabled={!csrfReady}
|
|
362
377
|
onClick={() => handleRevokeSession(session.id)}
|
|
363
378
|
>
|
|
364
379
|
Revoke
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
3
|
|
|
4
|
-
const { mockRequireCSRFToken } = vi.hoisted(() => ({
|
|
4
|
+
const { mockRequireCSRFToken, mockGetSession } = vi.hoisted(() => ({
|
|
5
5
|
mockRequireCSRFToken: vi.fn(),
|
|
6
|
+
mockGetSession: vi.fn(),
|
|
6
7
|
}));
|
|
7
8
|
|
|
8
9
|
vi.mock('@/lib/mars', () => ({
|
|
10
|
+
getSession: mockGetSession,
|
|
9
11
|
requireCSRFToken: mockRequireCSRFToken,
|
|
10
12
|
}));
|
|
11
13
|
|
|
@@ -14,6 +16,7 @@ import { GET } from './route';
|
|
|
14
16
|
describe('GET /api/csrf', () => {
|
|
15
17
|
beforeEach(() => {
|
|
16
18
|
vi.clearAllMocks();
|
|
19
|
+
mockGetSession.mockResolvedValueOnce(null);
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
it('returns token and csrfToken with the same value', async () => {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { requireCSRFToken } from '@/lib/mars';
|
|
1
|
+
import { getSession, requireCSRFToken } from '@/lib/mars';
|
|
2
2
|
import { NextResponse } from 'next/server';
|
|
3
3
|
|
|
4
4
|
export async function GET() {
|
|
5
5
|
try {
|
|
6
|
-
const
|
|
6
|
+
const session = await getSession();
|
|
7
|
+
const token = await requireCSRFToken(session?.fingerprint);
|
|
7
8
|
return NextResponse.json({ token, csrfToken: token }, { status: 200 });
|
|
8
9
|
} catch (error) {
|
|
9
10
|
console.error('CSRF token generation error:', error);
|