create-solostack 1.2.3 → 1.3.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/package.json +1 -1
- package/src/generators/pro/admin.js +344 -0
- package/src/generators/pro/api-keys.js +464 -0
- package/src/generators/pro/database-full.js +233 -0
- package/src/generators/pro/emails.js +248 -0
- package/src/generators/pro/oauth.js +217 -0
- package/src/generators/pro/stripe-advanced.js +521 -0
- package/src/index.js +95 -3
- package/src/utils/license.js +83 -0
- package/src/utils/logger.js +33 -0
- package/src/utils/packages.js +14 -0
- package/src/utils/pro-api.js +71 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email Templates Generator - Pro Feature
|
|
3
|
+
* Generates email verification, password reset, and transactional emails
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, ensureDir } from '../../utils/files.js';
|
|
8
|
+
|
|
9
|
+
export async function generateAdvancedEmails(projectPath) {
|
|
10
|
+
await ensureDir(path.join(projectPath, 'src/lib/email-templates'));
|
|
11
|
+
await ensureDir(path.join(projectPath, 'src/app/api/auth/verify-email'));
|
|
12
|
+
await ensureDir(path.join(projectPath, 'src/app/api/auth/reset-password'));
|
|
13
|
+
await ensureDir(path.join(projectPath, 'src/app/api/auth/request-reset'));
|
|
14
|
+
|
|
15
|
+
// Generate email sending utility
|
|
16
|
+
const sendEmail = `import { Resend } from 'resend';
|
|
17
|
+
|
|
18
|
+
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
19
|
+
|
|
20
|
+
interface SendEmailOptions {
|
|
21
|
+
to: string;
|
|
22
|
+
subject: string;
|
|
23
|
+
html: string;
|
|
24
|
+
from?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function sendEmail({ to, subject, html, from }: SendEmailOptions) {
|
|
28
|
+
const fromEmail = from || process.env.FROM_EMAIL || 'onboarding@resend.dev';
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const { data, error } = await resend.emails.send({
|
|
32
|
+
from: fromEmail,
|
|
33
|
+
to,
|
|
34
|
+
subject,
|
|
35
|
+
html,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (error) {
|
|
39
|
+
console.error('Failed to send email:', error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return data;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Email sending error:', error);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a verification token
|
|
52
|
+
*/
|
|
53
|
+
export function generateToken(length = 32): string {
|
|
54
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
55
|
+
let token = '';
|
|
56
|
+
for (let i = 0; i < length; i++) {
|
|
57
|
+
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
58
|
+
}
|
|
59
|
+
return token;
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
await writeFile(path.join(projectPath, 'src/lib/send-email.ts'), sendEmail);
|
|
64
|
+
|
|
65
|
+
// Generate verification email template
|
|
66
|
+
const verifyEmailTemplate = `interface VerifyEmailProps {
|
|
67
|
+
name: string;
|
|
68
|
+
verifyUrl: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function VerifyEmailTemplate({ name, verifyUrl }: VerifyEmailProps) {
|
|
72
|
+
return \`
|
|
73
|
+
<!DOCTYPE html>
|
|
74
|
+
<html>
|
|
75
|
+
<head>
|
|
76
|
+
<meta charset="utf-8">
|
|
77
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
78
|
+
<title>Verify your email</title>
|
|
79
|
+
</head>
|
|
80
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f4f4f5; padding: 40px 20px;">
|
|
81
|
+
<div style="max-width: 480px; margin: 0 auto; background: white; border-radius: 12px; padding: 40px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
|
82
|
+
<h1 style="font-size: 24px; font-weight: 600; color: #18181b; margin: 0 0 16px;">
|
|
83
|
+
Verify your email
|
|
84
|
+
</h1>
|
|
85
|
+
<p style="font-size: 16px; color: #52525b; line-height: 1.6; margin: 0 0 24px;">
|
|
86
|
+
Hi \${name || 'there'},
|
|
87
|
+
</p>
|
|
88
|
+
<p style="font-size: 16px; color: #52525b; line-height: 1.6; margin: 0 0 24px;">
|
|
89
|
+
Thanks for signing up! Please verify your email address by clicking the button below.
|
|
90
|
+
</p>
|
|
91
|
+
<a href="\${verifyUrl}" style="display: inline-block; background: #4f46e5; color: white; font-size: 16px; font-weight: 500; text-decoration: none; padding: 12px 32px; border-radius: 8px;">
|
|
92
|
+
Verify Email
|
|
93
|
+
</a>
|
|
94
|
+
<p style="font-size: 14px; color: #71717a; margin: 24px 0 0;">
|
|
95
|
+
If you didn't create this account, you can safely ignore this email.
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
</body>
|
|
99
|
+
</html>
|
|
100
|
+
\`;
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
await writeFile(
|
|
105
|
+
path.join(projectPath, 'src/lib/email-templates/verify-email.ts'),
|
|
106
|
+
verifyEmailTemplate
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// Generate password reset email template
|
|
110
|
+
const resetPasswordTemplate = `interface ResetPasswordProps {
|
|
111
|
+
name: string;
|
|
112
|
+
resetUrl: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function ResetPasswordTemplate({ name, resetUrl }: ResetPasswordProps) {
|
|
116
|
+
return \`
|
|
117
|
+
<!DOCTYPE html>
|
|
118
|
+
<html>
|
|
119
|
+
<head>
|
|
120
|
+
<meta charset="utf-8">
|
|
121
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
122
|
+
<title>Reset your password</title>
|
|
123
|
+
</head>
|
|
124
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f4f4f5; padding: 40px 20px;">
|
|
125
|
+
<div style="max-width: 480px; margin: 0 auto; background: white; border-radius: 12px; padding: 40px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
|
126
|
+
<h1 style="font-size: 24px; font-weight: 600; color: #18181b; margin: 0 0 16px;">
|
|
127
|
+
Reset your password
|
|
128
|
+
</h1>
|
|
129
|
+
<p style="font-size: 16px; color: #52525b; line-height: 1.6; margin: 0 0 24px;">
|
|
130
|
+
Hi \${name || 'there'},
|
|
131
|
+
</p>
|
|
132
|
+
<p style="font-size: 16px; color: #52525b; line-height: 1.6; margin: 0 0 24px;">
|
|
133
|
+
We received a request to reset your password. Click the button below to choose a new password.
|
|
134
|
+
</p>
|
|
135
|
+
<a href="\${resetUrl}" style="display: inline-block; background: #4f46e5; color: white; font-size: 16px; font-weight: 500; text-decoration: none; padding: 12px 32px; border-radius: 8px;">
|
|
136
|
+
Reset Password
|
|
137
|
+
</a>
|
|
138
|
+
<p style="font-size: 14px; color: #71717a; margin: 24px 0 0;">
|
|
139
|
+
This link will expire in 1 hour. If you didn't request a password reset, you can safely ignore this email.
|
|
140
|
+
</p>
|
|
141
|
+
</div>
|
|
142
|
+
</body>
|
|
143
|
+
</html>
|
|
144
|
+
\`;
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
await writeFile(
|
|
149
|
+
path.join(projectPath, 'src/lib/email-templates/reset-password.ts'),
|
|
150
|
+
resetPasswordTemplate
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Generate payment success email template
|
|
154
|
+
const paymentSuccessTemplate = `interface PaymentSuccessProps {
|
|
155
|
+
name: string;
|
|
156
|
+
amount: string;
|
|
157
|
+
planName: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function PaymentSuccessTemplate({ name, amount, planName }: PaymentSuccessProps) {
|
|
161
|
+
return \`
|
|
162
|
+
<!DOCTYPE html>
|
|
163
|
+
<html>
|
|
164
|
+
<head>
|
|
165
|
+
<meta charset="utf-8">
|
|
166
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
167
|
+
<title>Payment Successful</title>
|
|
168
|
+
</head>
|
|
169
|
+
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f4f4f5; padding: 40px 20px;">
|
|
170
|
+
<div style="max-width: 480px; margin: 0 auto; background: white; border-radius: 12px; padding: 40px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
|
171
|
+
<div style="text-align: center; margin-bottom: 24px;">
|
|
172
|
+
<div style="width: 48px; height: 48px; background: #22c55e; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;">
|
|
173
|
+
<span style="color: white; font-size: 24px;">✓</span>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
<h1 style="font-size: 24px; font-weight: 600; color: #18181b; margin: 0 0 16px; text-align: center;">
|
|
177
|
+
Payment Successful!
|
|
178
|
+
</h1>
|
|
179
|
+
<p style="font-size: 16px; color: #52525b; line-height: 1.6; margin: 0 0 24px; text-align: center;">
|
|
180
|
+
Hi \${name}, your payment of <strong>\${amount}</strong> for the <strong>\${planName}</strong> plan has been processed successfully.
|
|
181
|
+
</p>
|
|
182
|
+
<div style="background: #f4f4f5; border-radius: 8px; padding: 16px; margin-bottom: 24px;">
|
|
183
|
+
<p style="font-size: 14px; color: #71717a; margin: 0;">
|
|
184
|
+
Your subscription is now active. Enjoy all the premium features!
|
|
185
|
+
</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
|
190
|
+
\`;
|
|
191
|
+
}
|
|
192
|
+
`;
|
|
193
|
+
|
|
194
|
+
await writeFile(
|
|
195
|
+
path.join(projectPath, 'src/lib/email-templates/payment-success.ts'),
|
|
196
|
+
paymentSuccessTemplate
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Generate verify email API route
|
|
200
|
+
const verifyEmailRoute = `import { NextRequest, NextResponse } from 'next/server';
|
|
201
|
+
import { db } from '@/lib/db';
|
|
202
|
+
|
|
203
|
+
export async function GET(req: NextRequest) {
|
|
204
|
+
const token = req.nextUrl.searchParams.get('token');
|
|
205
|
+
|
|
206
|
+
if (!token) {
|
|
207
|
+
return NextResponse.redirect(new URL('/login?error=InvalidToken', req.url));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const verificationToken = await db.verificationToken.findFirst({
|
|
212
|
+
where: {
|
|
213
|
+
token,
|
|
214
|
+
expires: { gt: new Date() },
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!verificationToken) {
|
|
219
|
+
return NextResponse.redirect(new URL('/login?error=TokenExpired', req.url));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await db.user.update({
|
|
223
|
+
where: { email: verificationToken.identifier },
|
|
224
|
+
data: { emailVerified: new Date() },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await db.verificationToken.delete({
|
|
228
|
+
where: {
|
|
229
|
+
identifier_token: {
|
|
230
|
+
identifier: verificationToken.identifier,
|
|
231
|
+
token,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
return NextResponse.redirect(new URL('/login?verified=true', req.url));
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Verification error:', error);
|
|
239
|
+
return NextResponse.redirect(new URL('/login?error=VerificationFailed', req.url));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
`;
|
|
243
|
+
|
|
244
|
+
await writeFile(
|
|
245
|
+
path.join(projectPath, 'src/app/api/auth/verify-email/route.ts'),
|
|
246
|
+
verifyEmailRoute
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Generator - Pro Feature
|
|
3
|
+
* Generates Google and GitHub OAuth configuration for NextAuth.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, ensureDir } from '../../utils/files.js';
|
|
8
|
+
|
|
9
|
+
export async function generateOAuth(projectPath) {
|
|
10
|
+
// Create components directory
|
|
11
|
+
await ensureDir(path.join(projectPath, 'src/components/auth'));
|
|
12
|
+
|
|
13
|
+
// Generate OAuth Buttons Component
|
|
14
|
+
const oauthButtons = `'use client';
|
|
15
|
+
|
|
16
|
+
import { signIn } from 'next-auth/react';
|
|
17
|
+
|
|
18
|
+
interface OAuthButtonsProps {
|
|
19
|
+
callbackUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function OAuthButtons({ callbackUrl = '/dashboard' }: OAuthButtonsProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="space-y-3">
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
onClick={() => signIn('google', { callbackUrl })}
|
|
28
|
+
className="w-full flex items-center justify-center gap-3 rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
29
|
+
>
|
|
30
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24">
|
|
31
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
32
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
33
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
34
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
35
|
+
</svg>
|
|
36
|
+
Continue with Google
|
|
37
|
+
</button>
|
|
38
|
+
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={() => signIn('github', { callbackUrl })}
|
|
42
|
+
className="w-full flex items-center justify-center gap-3 rounded-lg border border-gray-300 bg-gray-900 px-4 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
|
|
43
|
+
>
|
|
44
|
+
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24">
|
|
45
|
+
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd"/>
|
|
46
|
+
</svg>
|
|
47
|
+
Continue with GitHub
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
await writeFile(
|
|
55
|
+
path.join(projectPath, 'src/components/auth/oauth-buttons.tsx'),
|
|
56
|
+
oauthButtons
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Generate auth providers configuration
|
|
60
|
+
const authProviders = `/**
|
|
61
|
+
* OAuth Provider Configurations
|
|
62
|
+
* Add your OAuth credentials in .env
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
import GoogleProvider from 'next-auth/providers/google';
|
|
66
|
+
import GitHubProvider from 'next-auth/providers/github';
|
|
67
|
+
|
|
68
|
+
export const oauthProviders = [
|
|
69
|
+
GoogleProvider({
|
|
70
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
71
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
72
|
+
}),
|
|
73
|
+
GitHubProvider({
|
|
74
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
75
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
|
76
|
+
}),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if OAuth providers are configured
|
|
81
|
+
*/
|
|
82
|
+
export function getConfiguredProviders() {
|
|
83
|
+
const providers = [];
|
|
84
|
+
|
|
85
|
+
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
|
86
|
+
providers.push('google');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
|
|
90
|
+
providers.push('github');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return providers;
|
|
94
|
+
}
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
await writeFile(
|
|
98
|
+
path.join(projectPath, 'src/lib/auth-providers.ts'),
|
|
99
|
+
authProviders
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Generate updated auth route with OAuth
|
|
103
|
+
const authRoute = `import NextAuth from 'next-auth';
|
|
104
|
+
import { authConfig } from '@/lib/auth.config';
|
|
105
|
+
|
|
106
|
+
const handler = NextAuth(authConfig);
|
|
107
|
+
|
|
108
|
+
export { handler as GET, handler as POST };
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
await writeFile(
|
|
112
|
+
path.join(projectPath, 'src/app/api/auth/[...nextauth]/route.ts'),
|
|
113
|
+
authRoute
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Generate updated auth.config.ts with OAuth providers
|
|
117
|
+
const authConfig = `import type { NextAuthConfig } from 'next-auth';
|
|
118
|
+
import CredentialsProvider from 'next-auth/providers/credentials';
|
|
119
|
+
import GoogleProvider from 'next-auth/providers/google';
|
|
120
|
+
import GitHubProvider from 'next-auth/providers/github';
|
|
121
|
+
import { PrismaAdapter } from '@auth/prisma-adapter';
|
|
122
|
+
import { db } from '@/lib/db';
|
|
123
|
+
import bcrypt from 'bcryptjs';
|
|
124
|
+
|
|
125
|
+
export const authConfig: NextAuthConfig = {
|
|
126
|
+
adapter: PrismaAdapter(db),
|
|
127
|
+
providers: [
|
|
128
|
+
// OAuth Providers
|
|
129
|
+
GoogleProvider({
|
|
130
|
+
clientId: process.env.GOOGLE_CLIENT_ID ?? '',
|
|
131
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
|
|
132
|
+
}),
|
|
133
|
+
GitHubProvider({
|
|
134
|
+
clientId: process.env.GITHUB_CLIENT_ID ?? '',
|
|
135
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET ?? '',
|
|
136
|
+
}),
|
|
137
|
+
// Email/Password Provider
|
|
138
|
+
CredentialsProvider({
|
|
139
|
+
name: 'credentials',
|
|
140
|
+
credentials: {
|
|
141
|
+
email: { label: 'Email', type: 'email' },
|
|
142
|
+
password: { label: 'Password', type: 'password' },
|
|
143
|
+
},
|
|
144
|
+
async authorize(credentials) {
|
|
145
|
+
if (!credentials?.email || !credentials?.password) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const user = await db.user.findUnique({
|
|
150
|
+
where: { email: credentials.email as string },
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!user || !user.password) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const isValid = await bcrypt.compare(
|
|
158
|
+
credentials.password as string,
|
|
159
|
+
user.password
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (!isValid) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
id: user.id,
|
|
168
|
+
email: user.email,
|
|
169
|
+
name: user.name,
|
|
170
|
+
role: user.role,
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
],
|
|
175
|
+
session: {
|
|
176
|
+
strategy: 'jwt',
|
|
177
|
+
},
|
|
178
|
+
callbacks: {
|
|
179
|
+
async jwt({ token, user }) {
|
|
180
|
+
if (user) {
|
|
181
|
+
token.id = user.id;
|
|
182
|
+
token.role = (user as any).role;
|
|
183
|
+
}
|
|
184
|
+
return token;
|
|
185
|
+
},
|
|
186
|
+
async session({ session, token }) {
|
|
187
|
+
if (session.user) {
|
|
188
|
+
session.user.id = token.id as string;
|
|
189
|
+
session.user.role = token.role as string;
|
|
190
|
+
}
|
|
191
|
+
return session;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
pages: {
|
|
195
|
+
signIn: '/login',
|
|
196
|
+
error: '/login',
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
await writeFile(
|
|
202
|
+
path.join(projectPath, 'src/lib/auth.config.ts'),
|
|
203
|
+
authConfig
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Generate auth.ts that exports auth() function for use in Pro pages
|
|
207
|
+
const authTs = `import NextAuth from 'next-auth';
|
|
208
|
+
import { authConfig } from './auth.config';
|
|
209
|
+
|
|
210
|
+
export const { handlers, auth, signIn, signOut } = NextAuth(authConfig);
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
await writeFile(
|
|
214
|
+
path.join(projectPath, 'src/lib/auth.ts'),
|
|
215
|
+
authTs
|
|
216
|
+
);
|
|
217
|
+
}
|