@mars-stack/cli 1.0.2 → 2.0.1
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 +539 -355
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/template/package.json +4 -4
- package/template/src/app/(auth)/forgotten-password/page.tsx +18 -0
- package/template/src/app/(auth)/register/page.tsx +3 -0
- package/template/src/app/(auth)/verify/page.tsx +19 -0
- package/template/src/app/(protected)/layout.tsx +18 -0
- package/template/src/app/api/auth/forgot/route.ts +7 -1
- package/template/src/app/api/auth/signup/route.ts +9 -0
- package/template/src/config/app.config.ts +0 -8
- package/template/src/config/routes.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mars-stack/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "MARS CLI: scaffold, configure, and maintain SaaS apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@mars-stack/ui": "*",
|
|
41
41
|
"commander": "^14.0.3",
|
|
42
42
|
"fs-extra": "^11.0.0",
|
|
43
|
-
"ora": "^
|
|
43
|
+
"ora": "^9.3.0",
|
|
44
44
|
"picocolors": "^1.0.0",
|
|
45
45
|
"prompts": "^2.4.0"
|
|
46
46
|
},
|
|
@@ -50,6 +50,6 @@
|
|
|
50
50
|
"@types/prompts": "^2.4.0",
|
|
51
51
|
"tsup": "^8.0.0",
|
|
52
52
|
"typescript": "^5.7.0",
|
|
53
|
-
"vitest": "^4.0
|
|
53
|
+
"vitest": "^4.1.0"
|
|
54
54
|
}
|
|
55
55
|
}
|
package/template/package.json
CHANGED
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"@sendgrid/mail": "^8.1.0",
|
|
38
38
|
"@upstash/ratelimit": "^2.0.0",
|
|
39
39
|
"@upstash/redis": "^1.36.4",
|
|
40
|
-
"bcryptjs": "^
|
|
40
|
+
"bcryptjs": "^3.0.3",
|
|
41
41
|
"clsx": "^2.1.1",
|
|
42
42
|
"jose": "^6.2.1",
|
|
43
|
-
"next": "^16.
|
|
43
|
+
"next": "^16.1.6",
|
|
44
44
|
"pino": "^9.6.0",
|
|
45
45
|
"pino-pretty": "^13.0.0",
|
|
46
46
|
"react": "^19.0.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@tailwindcss/postcss": "^4.0.0",
|
|
61
61
|
"@testing-library/jest-dom": "^6.9.1",
|
|
62
62
|
"@testing-library/react": "^16.0.0",
|
|
63
|
-
"@types/bcryptjs": "^
|
|
63
|
+
"@types/bcryptjs": "^3.0.0",
|
|
64
64
|
"@types/node": "^25.4.0",
|
|
65
65
|
"@types/react": "^19.0.0",
|
|
66
66
|
"@types/react-dom": "^19.0.0",
|
|
@@ -75,6 +75,6 @@
|
|
|
75
75
|
"tailwindcss": "^4.0.0",
|
|
76
76
|
"tsx": "^4.0.0",
|
|
77
77
|
"typescript": "^5.7.0",
|
|
78
|
-
"vitest": "^
|
|
78
|
+
"vitest": "^4.1.0"
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -12,6 +12,7 @@ export default function ForgottenPassword() {
|
|
|
12
12
|
const { getCSRFHeaders } = useCSRF();
|
|
13
13
|
const [serverError, setServerError] = useState('');
|
|
14
14
|
const [success, setSuccess] = useState(false);
|
|
15
|
+
const [devLink, setDevLink] = useState<string | null>(null);
|
|
15
16
|
|
|
16
17
|
const form = useZodForm({
|
|
17
18
|
schema: formSchemas.forgotPassword,
|
|
@@ -34,6 +35,10 @@ export default function ForgottenPassword() {
|
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
if (data.devLink) {
|
|
40
|
+
setDevLink(data.devLink);
|
|
41
|
+
}
|
|
37
42
|
setSuccess(true);
|
|
38
43
|
} catch {
|
|
39
44
|
setServerError('Network error. Please try again.');
|
|
@@ -47,6 +52,19 @@ export default function ForgottenPassword() {
|
|
|
47
52
|
<Text.Paragraph>
|
|
48
53
|
If an account exists for that email, we've sent password reset instructions.
|
|
49
54
|
</Text.Paragraph>
|
|
55
|
+
{devLink && (
|
|
56
|
+
<div className="mx-auto mt-4 max-w-sm rounded-md border border-border-warning bg-warning-muted p-3">
|
|
57
|
+
<Text.Paragraph className="text-sm font-medium text-text-warning">
|
|
58
|
+
Dev mode — console email provider
|
|
59
|
+
</Text.Paragraph>
|
|
60
|
+
<a
|
|
61
|
+
href={devLink}
|
|
62
|
+
className="mt-1 inline-block text-sm font-medium text-text-link hover:text-text-link-hover hover:underline"
|
|
63
|
+
>
|
|
64
|
+
Click here to reset your password →
|
|
65
|
+
</a>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
50
68
|
<Link href={routes.signIn} className="mt-6 inline-block text-sm text-text-link hover:text-text-link-hover hover:underline">
|
|
51
69
|
Back to sign in
|
|
52
70
|
</Link>
|
|
@@ -52,6 +52,9 @@ function SignUpForm() {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
sessionStorage.setItem('verify-email', values.email);
|
|
55
|
+
if (data.devLink) {
|
|
56
|
+
sessionStorage.setItem('verify-dev-link', data.devLink);
|
|
57
|
+
}
|
|
55
58
|
router.push('/verify');
|
|
56
59
|
} catch {
|
|
57
60
|
setServerError('Network error. Please try again.');
|
|
@@ -7,6 +7,7 @@ import { Suspense, useEffect, useState } from 'react';
|
|
|
7
7
|
|
|
8
8
|
function VerifyContent() {
|
|
9
9
|
const [email, setEmail] = useState<string | null>(null);
|
|
10
|
+
const [devLink, setDevLink] = useState<string | null>(null);
|
|
10
11
|
|
|
11
12
|
useEffect(() => {
|
|
12
13
|
const stored = sessionStorage.getItem('verify-email');
|
|
@@ -14,6 +15,11 @@ function VerifyContent() {
|
|
|
14
15
|
setEmail(stored);
|
|
15
16
|
sessionStorage.removeItem('verify-email');
|
|
16
17
|
}
|
|
18
|
+
const storedDevLink = sessionStorage.getItem('verify-dev-link');
|
|
19
|
+
if (storedDevLink) {
|
|
20
|
+
setDevLink(storedDevLink);
|
|
21
|
+
sessionStorage.removeItem('verify-dev-link');
|
|
22
|
+
}
|
|
17
23
|
}, []);
|
|
18
24
|
|
|
19
25
|
return (
|
|
@@ -37,6 +43,19 @@ function VerifyContent() {
|
|
|
37
43
|
<Text.Paragraph className="mt-1 text-sm text-text-muted">
|
|
38
44
|
Didn't receive the email? Check your spam folder.
|
|
39
45
|
</Text.Paragraph>
|
|
46
|
+
{devLink && (
|
|
47
|
+
<div className="mt-4 rounded-md border border-border-warning bg-warning-muted p-3">
|
|
48
|
+
<Text.Paragraph className="text-sm font-medium text-text-warning">
|
|
49
|
+
Dev mode — console email provider
|
|
50
|
+
</Text.Paragraph>
|
|
51
|
+
<a
|
|
52
|
+
href={devLink}
|
|
53
|
+
className="mt-1 inline-block text-sm font-medium text-text-link hover:text-text-link-hover hover:underline"
|
|
54
|
+
>
|
|
55
|
+
Click here to verify your email →
|
|
56
|
+
</a>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
40
59
|
<Link
|
|
41
60
|
href={routes.signIn}
|
|
42
61
|
className="mt-8 inline-block text-sm text-text-link hover:text-text-link-hover hover:underline"
|
|
@@ -28,6 +28,13 @@ function NavLink({ href, label, active }: { href: string; label: string; active:
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function AdminLink({ pathname }: { pathname: string }) {
|
|
32
|
+
const { user } = useAuth();
|
|
33
|
+
if (!user || user.role !== 'admin') return null;
|
|
34
|
+
|
|
35
|
+
return <NavLink href={routes.admin} label="Admin" active={pathname === routes.admin} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
function MobileNavLink({
|
|
32
39
|
href,
|
|
33
40
|
label,
|
|
@@ -54,6 +61,15 @@ function MobileNavLink({
|
|
|
54
61
|
);
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
function MobileAdminLink({ pathname, onClick }: { pathname: string; onClick: () => void }) {
|
|
65
|
+
const { user } = useAuth();
|
|
66
|
+
if (!user || user.role !== 'admin') return null;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<MobileNavLink href={routes.admin} label="Admin" active={pathname === routes.admin} onClick={onClick} />
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
function UserMenu() {
|
|
58
74
|
const { user, logout } = useAuth();
|
|
59
75
|
const router = useRouter();
|
|
@@ -212,6 +228,7 @@ export default function ProtectedLayout({ children }: Readonly<{ children: React
|
|
|
212
228
|
{NAV_ITEMS.map((item) => (
|
|
213
229
|
<NavLink key={item.href} href={item.href} label={item.label} active={pathname === item.href} />
|
|
214
230
|
))}
|
|
231
|
+
<AdminLink pathname={pathname} />
|
|
215
232
|
</div>
|
|
216
233
|
</div>
|
|
217
234
|
|
|
@@ -248,6 +265,7 @@ export default function ProtectedLayout({ children }: Readonly<{ children: React
|
|
|
248
265
|
onClick={() => setMobileMenuOpen(false)}
|
|
249
266
|
/>
|
|
250
267
|
))}
|
|
268
|
+
<MobileAdminLink pathname={pathname} onClick={() => setMobileMenuOpen(false)} />
|
|
251
269
|
</div>
|
|
252
270
|
<div className="border-t border-border-default px-4 py-3">
|
|
253
271
|
<UserMenu />
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
|
|
1
3
|
import { prisma } from '@/lib/prisma';
|
|
2
4
|
import { sendEmail, handleApiError, getBaseUrl } from '@/lib/mars';
|
|
3
5
|
import { checkRateLimit, getClientIP, RATE_LIMITS, rateLimitResponse } from '@mars-stack/core/rate-limit';
|
|
@@ -53,8 +55,12 @@ export async function POST(request: Request) {
|
|
|
53
55
|
text,
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
const isConsoleEmail =
|
|
59
|
+
process.env.NODE_ENV !== 'production' &&
|
|
60
|
+
appConfig.services.email.provider === 'console';
|
|
61
|
+
|
|
56
62
|
return NextResponse.json(
|
|
57
|
-
{ message: 'If an account exists, a reset email will be sent' },
|
|
63
|
+
{ message: 'If an account exists, a reset email will be sent', ...(isConsoleEmail && { devLink: resetUrl }) },
|
|
58
64
|
{ status: 200 },
|
|
59
65
|
);
|
|
60
66
|
} catch (error) {
|
|
@@ -76,6 +76,15 @@ export async function POST(request: Request) {
|
|
|
76
76
|
html,
|
|
77
77
|
text,
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
const isConsoleEmail =
|
|
81
|
+
process.env.NODE_ENV !== 'production' &&
|
|
82
|
+
appConfig.services.email.provider === 'console';
|
|
83
|
+
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{ message: 'User created successfully', ...(isConsoleEmail && { devLink: verifyUrl }) },
|
|
86
|
+
{ status: 201 },
|
|
87
|
+
);
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
return NextResponse.json({ message: 'User created successfully' }, { status: 201 });
|
|
@@ -17,15 +17,7 @@ export const appConfig = {
|
|
|
17
17
|
address: '',
|
|
18
18
|
},
|
|
19
19
|
theme: {
|
|
20
|
-
primaryColor: 'blue-600' as string,
|
|
21
|
-
secondaryColor: 'amber-400' as string,
|
|
22
20
|
font: 'Inter' as string,
|
|
23
|
-
designDirection: 'modern-saas' as
|
|
24
|
-
| 'modern-saas'
|
|
25
|
-
| 'minimal'
|
|
26
|
-
| 'enterprise'
|
|
27
|
-
| 'creative'
|
|
28
|
-
| 'dashboard',
|
|
29
21
|
},
|
|
30
22
|
features: {
|
|
31
23
|
auth: true,
|