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.
@@ -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
+ }