create-stackkit-app 0.4.4 → 0.4.6
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 +1 -1
- package/dist/index.js +61 -4
- package/dist/lib/code-generator.d.ts +80 -0
- package/dist/lib/code-generator.js +541 -0
- package/dist/lib/create-project.d.ts +20 -1
- package/dist/lib/create-project.js +154 -96
- package/dist/lib/framework-utils.d.ts +22 -0
- package/dist/lib/framework-utils.js +74 -0
- package/dist/lib/utils/logger.d.ts +16 -0
- package/dist/lib/utils/logger.js +59 -0
- package/dist/lib/utils/module-discovery.d.ts +62 -0
- package/dist/lib/utils/module-discovery.js +180 -0
- package/dist/lib/utils/package-utils.js +2 -2
- package/modules/auth/authjs/files/api/auth/[...nextauth]/route.ts +6 -0
- package/modules/auth/authjs/files/lib/auth-client.ts +11 -0
- package/modules/auth/authjs/files/lib/auth.ts +36 -0
- package/modules/auth/authjs/files/schemas/prisma-schema.prisma +45 -0
- package/modules/auth/authjs/module.json +22 -0
- package/modules/auth/better-auth/files/lib/auth-client.ts +7 -0
- package/modules/auth/better-auth/files/lib/auth.ts +77 -1
- package/modules/auth/better-auth/files/lib/email-service.ts +34 -0
- package/modules/auth/better-auth/files/lib/email-templates.ts +89 -0
- package/modules/auth/better-auth/files/schemas/prisma-schema.prisma +7 -7
- package/modules/auth/better-auth/generator.json +83 -0
- package/modules/auth/better-auth/module.json +17 -34
- package/modules/database/mongoose/files/lib/db.ts +63 -0
- package/modules/database/{mongoose-mongodb → mongoose}/files/models/User.ts +0 -5
- package/modules/database/mongoose/generator.json +33 -0
- package/modules/database/mongoose/module.json +15 -0
- package/modules/database/prisma/files/lib/prisma.ts +7 -4
- package/modules/database/prisma/files/prisma/schema.prisma +1 -1
- package/modules/database/prisma/files/prisma.config.ts +2 -2
- package/modules/database/prisma/generator.json +46 -0
- package/modules/database/prisma/module.json +6 -132
- package/package.json +1 -1
- package/templates/express/.env.example +0 -1
- package/templates/express/package.json +4 -4
- package/templates/express/src/app.ts +2 -2
- package/templates/express/src/features/health/health.controller.ts +18 -0
- package/templates/express/src/features/health/health.route.ts +9 -0
- package/templates/express/src/features/health/health.service.ts +6 -0
- package/templates/express/template.json +6 -19
- package/templates/nextjs/lib/env.ts +8 -0
- package/templates/nextjs/package.json +7 -7
- package/templates/nextjs/template.json +5 -1
- package/templates/react-vite/.env.example +1 -2
- package/templates/react-vite/.prettierignore +4 -0
- package/templates/react-vite/.prettierrc +9 -0
- package/templates/react-vite/README.md +22 -0
- package/templates/react-vite/package.json +16 -16
- package/templates/react-vite/src/router.tsx +0 -12
- package/templates/react-vite/template.json +6 -14
- package/templates/react-vite/vite.config.ts +0 -6
- package/dist/lib/utils/config-utils.d.ts +0 -2
- package/dist/lib/utils/config-utils.js +0 -33
- package/dist/lib/utils/file-utils.d.ts +0 -8
- package/dist/lib/utils/file-utils.js +0 -75
- package/dist/lib/utils/module-utils.d.ts +0 -2
- package/dist/lib/utils/module-utils.js +0 -311
- package/modules/auth/clerk/files/express/auth.ts +0 -7
- package/modules/auth/clerk/files/nextjs/auth-provider.tsx +0 -5
- package/modules/auth/clerk/files/nextjs/middleware.ts +0 -9
- package/modules/auth/clerk/files/react/auth-provider.tsx +0 -15
- package/modules/auth/clerk/module.json +0 -115
- package/modules/database/mongoose-mongodb/files/lib/db.ts +0 -78
- package/modules/database/mongoose-mongodb/module.json +0 -70
- package/templates/express/src/features/auth/auth.controller.ts +0 -48
- package/templates/express/src/features/auth/auth.route.ts +0 -10
- package/templates/express/src/features/auth/auth.service.ts +0 -21
- package/templates/react-vite/src/api/services/user.service.ts +0 -18
- package/templates/react-vite/src/pages/UserProfile.tsx +0 -40
- package/templates/react-vite/src/types/user.d.ts +0 -6
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.discoverModules = discoverModules;
|
|
7
|
+
exports.getValidDatabaseOptions = getValidDatabaseOptions;
|
|
8
|
+
exports.getValidAuthOptions = getValidAuthOptions;
|
|
9
|
+
exports.parseDatabaseOption = parseDatabaseOption;
|
|
10
|
+
exports.getCompatibleAuthOptions = getCompatibleAuthOptions;
|
|
11
|
+
exports.getDatabaseChoices = getDatabaseChoices;
|
|
12
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
/**
|
|
15
|
+
* Discover all available modules from the modules directory
|
|
16
|
+
*/
|
|
17
|
+
async function discoverModules(modulesDir) {
|
|
18
|
+
const discovered = {
|
|
19
|
+
frameworks: [],
|
|
20
|
+
databases: [],
|
|
21
|
+
auth: [],
|
|
22
|
+
};
|
|
23
|
+
if (!(await fs_extra_1.default.pathExists(modulesDir))) {
|
|
24
|
+
return discovered;
|
|
25
|
+
}
|
|
26
|
+
// Discover frameworks from templates directory
|
|
27
|
+
const templatesDir = path_1.default.join(modulesDir, '..', 'templates');
|
|
28
|
+
if (await fs_extra_1.default.pathExists(templatesDir)) {
|
|
29
|
+
const frameworkDirs = await fs_extra_1.default.readdir(templatesDir);
|
|
30
|
+
for (const frameworkName of frameworkDirs) {
|
|
31
|
+
const templateJsonPath = path_1.default.join(templatesDir, frameworkName, 'template.json');
|
|
32
|
+
if (await fs_extra_1.default.pathExists(templateJsonPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const templateConfig = await fs_extra_1.default.readJson(templateJsonPath);
|
|
35
|
+
discovered.frameworks.push({
|
|
36
|
+
...templateConfig,
|
|
37
|
+
name: frameworkName,
|
|
38
|
+
category: 'framework',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Silently skip invalid templates
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Discover database modules
|
|
48
|
+
const databaseDir = path_1.default.join(modulesDir, 'database');
|
|
49
|
+
if (await fs_extra_1.default.pathExists(databaseDir)) {
|
|
50
|
+
const dbModules = await fs_extra_1.default.readdir(databaseDir);
|
|
51
|
+
for (const moduleName of dbModules) {
|
|
52
|
+
const modulePath = path_1.default.join(databaseDir, moduleName);
|
|
53
|
+
const moduleJsonPath = path_1.default.join(modulePath, 'module.json');
|
|
54
|
+
if (await fs_extra_1.default.pathExists(moduleJsonPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const metadata = await fs_extra_1.default.readJson(moduleJsonPath);
|
|
57
|
+
discovered.databases.push(metadata);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Silently skip invalid modules
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Discover auth modules
|
|
66
|
+
const authDir = path_1.default.join(modulesDir, 'auth');
|
|
67
|
+
if (await fs_extra_1.default.pathExists(authDir)) {
|
|
68
|
+
const authModules = await fs_extra_1.default.readdir(authDir);
|
|
69
|
+
for (const moduleName of authModules) {
|
|
70
|
+
const modulePath = path_1.default.join(authDir, moduleName);
|
|
71
|
+
const moduleJsonPath = path_1.default.join(modulePath, 'module.json');
|
|
72
|
+
if (await fs_extra_1.default.pathExists(moduleJsonPath)) {
|
|
73
|
+
try {
|
|
74
|
+
const metadata = await fs_extra_1.default.readJson(moduleJsonPath);
|
|
75
|
+
discovered.auth.push(metadata);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Silently skip invalid modules
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return discovered;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get valid database options for CLI
|
|
87
|
+
*/
|
|
88
|
+
function getValidDatabaseOptions(databases) {
|
|
89
|
+
const options = ['none'];
|
|
90
|
+
for (const db of databases) {
|
|
91
|
+
if (db.name === 'prisma') {
|
|
92
|
+
// For Prisma, add provider-specific options
|
|
93
|
+
options.push('prisma-postgresql', 'prisma-mongodb', 'prisma-mysql', 'prisma-sqlite');
|
|
94
|
+
}
|
|
95
|
+
else if (db.name === 'mongoose') {
|
|
96
|
+
options.push('mongoose-mongodb');
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// For other databases, add the name directly
|
|
100
|
+
options.push(db.name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return options;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get valid auth options for CLI
|
|
107
|
+
*/
|
|
108
|
+
function getValidAuthOptions(authModules) {
|
|
109
|
+
const options = ['none'];
|
|
110
|
+
for (const auth of authModules) {
|
|
111
|
+
options.push(auth.name);
|
|
112
|
+
}
|
|
113
|
+
return options;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse database option into database name and provider
|
|
117
|
+
*/
|
|
118
|
+
function parseDatabaseOption(dbOption) {
|
|
119
|
+
if (dbOption === 'none') {
|
|
120
|
+
return { database: 'none' };
|
|
121
|
+
}
|
|
122
|
+
if (dbOption.startsWith('prisma-')) {
|
|
123
|
+
const provider = dbOption.split('-')[1];
|
|
124
|
+
return { database: 'prisma', provider };
|
|
125
|
+
}
|
|
126
|
+
if (dbOption === 'mongoose-mongodb') {
|
|
127
|
+
return { database: 'mongoose' };
|
|
128
|
+
}
|
|
129
|
+
return { database: dbOption };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get compatible auth options for given framework and database
|
|
133
|
+
*/
|
|
134
|
+
function getCompatibleAuthOptions(authModules, framework, database) {
|
|
135
|
+
const compatible = [];
|
|
136
|
+
for (const auth of authModules) {
|
|
137
|
+
// Check if auth supports the framework
|
|
138
|
+
if (auth.supportedFrameworks && !auth.supportedFrameworks.includes(framework)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Special compatibility rules
|
|
142
|
+
if (auth.name === 'authjs' && (database !== 'prisma' || framework !== 'nextjs')) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (auth.name === 'better-auth' && database === 'none' && framework !== 'react-vite') {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
compatible.push({
|
|
149
|
+
name: auth.displayName,
|
|
150
|
+
value: auth.name
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Add "None" at the end
|
|
154
|
+
compatible.push({ name: 'None', value: 'none' });
|
|
155
|
+
return compatible;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get database choices for inquirer prompts
|
|
159
|
+
*/
|
|
160
|
+
function getDatabaseChoices(databases, framework) {
|
|
161
|
+
const choices = [];
|
|
162
|
+
for (const db of databases) {
|
|
163
|
+
// Check framework compatibility
|
|
164
|
+
if (db.supportedFrameworks && !db.supportedFrameworks.includes(framework)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (db.name === 'prisma') {
|
|
168
|
+
choices.push({ name: 'Prisma (PostgreSQL)', value: 'prisma-postgresql' }, { name: 'Prisma (MongoDB)', value: 'prisma-mongodb' }, { name: 'Prisma (MySQL)', value: 'prisma-mysql' }, { name: 'Prisma (SQLite)', value: 'prisma-sqlite' });
|
|
169
|
+
}
|
|
170
|
+
else if (db.name === 'mongoose') {
|
|
171
|
+
choices.push({ name: 'Mongoose (MongoDB)', value: 'mongoose-mongodb' });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
choices.push({ name: db.displayName, value: db.name });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Add "None" at the end
|
|
178
|
+
choices.push({ name: 'None', value: 'none' });
|
|
179
|
+
return choices;
|
|
180
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.installDependencies = installDependencies;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
|
+
const logger_1 = require("./logger");
|
|
5
6
|
async function installDependencies(cwd, packageManager) {
|
|
6
7
|
const commands = {
|
|
7
8
|
npm: "npm install",
|
|
@@ -23,8 +24,7 @@ async function installDependencies(cwd, packageManager) {
|
|
|
23
24
|
const fallbacks = ["pnpm", "npm", "yarn", "bun"];
|
|
24
25
|
const found = fallbacks.find((p) => isAvailable(p));
|
|
25
26
|
if (found) {
|
|
26
|
-
|
|
27
|
-
console.warn(`Selected package manager '${chosen}' was not found. Falling back to '${found}'.`);
|
|
27
|
+
logger_1.logger.warn(`Selected package manager '${chosen}' was not found. Falling back to '${found}'.`);
|
|
28
28
|
chosen = found;
|
|
29
29
|
}
|
|
30
30
|
else {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getServerSession } from "next-auth/next";
|
|
2
|
+
import { authOptions } from "@/lib/auth";
|
|
3
|
+
|
|
4
|
+
export async function getSession() {
|
|
5
|
+
return await getServerSession(authOptions);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function getCurrentUser() {
|
|
9
|
+
const session = await getSession();
|
|
10
|
+
return session?.user;
|
|
11
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { NextAuthOptions } from "next-auth";
|
|
2
|
+
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import GoogleProvider from "next-auth/providers/google";
|
|
5
|
+
|
|
6
|
+
export const authOptions: NextAuthOptions = {
|
|
7
|
+
adapter: PrismaAdapter(prisma),
|
|
8
|
+
providers: [
|
|
9
|
+
GoogleProvider({
|
|
10
|
+
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
11
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
session: {
|
|
15
|
+
strategy: "jwt",
|
|
16
|
+
},
|
|
17
|
+
callbacks: {
|
|
18
|
+
async jwt({ token, user }) {
|
|
19
|
+
if (user) {
|
|
20
|
+
token.id = user.id;
|
|
21
|
+
}
|
|
22
|
+
return token;
|
|
23
|
+
},
|
|
24
|
+
async session({ session, token }) {
|
|
25
|
+
if (token) {
|
|
26
|
+
session.user.id = token.id as string;
|
|
27
|
+
}
|
|
28
|
+
return session;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
pages: {
|
|
32
|
+
signIn: "/auth/signin",
|
|
33
|
+
signOut: "/auth/signout",
|
|
34
|
+
error: "/auth/error",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
|
|
2
|
+
model Account {
|
|
3
|
+
id String @id {{idDefault}}
|
|
4
|
+
userId String {{userIdType}}
|
|
5
|
+
type String
|
|
6
|
+
provider String
|
|
7
|
+
providerAccountId String
|
|
8
|
+
refresh_token String? @db.Text
|
|
9
|
+
access_token String? @db.Text
|
|
10
|
+
expires_at Int?
|
|
11
|
+
token_type String?
|
|
12
|
+
scope String?
|
|
13
|
+
id_token String? @db.Text
|
|
14
|
+
session_state String?
|
|
15
|
+
|
|
16
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
17
|
+
|
|
18
|
+
@@unique([provider, providerAccountId])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
model Session {
|
|
22
|
+
id String @id {{idDefault}}
|
|
23
|
+
sessionToken String @unique
|
|
24
|
+
userId String {{userIdType}}
|
|
25
|
+
expires DateTime
|
|
26
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
model User {
|
|
30
|
+
id String @id {{idDefault}}
|
|
31
|
+
name String?
|
|
32
|
+
email String @unique
|
|
33
|
+
emailVerified DateTime?
|
|
34
|
+
image String?
|
|
35
|
+
accounts Account[]
|
|
36
|
+
sessions Session[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
model VerificationToken {
|
|
40
|
+
identifier String
|
|
41
|
+
token String @unique
|
|
42
|
+
expires DateTime
|
|
43
|
+
|
|
44
|
+
@@unique([identifier, token])
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "authjs",
|
|
3
|
+
"displayName": "Auth.js",
|
|
4
|
+
"description": "Authentication with Auth.js (NextAuth.js v5)",
|
|
5
|
+
"category": "auth",
|
|
6
|
+
"provider": "authjs",
|
|
7
|
+
"supportedFrameworks": ["nextjs"],
|
|
8
|
+
"compatibility": {
|
|
9
|
+
"frameworks": ["nextjs"],
|
|
10
|
+
"databases": ["prisma"],
|
|
11
|
+
"languages": ["typescript", "javascript"],
|
|
12
|
+
"packageManagers": ["npm", "yarn", "pnpm", "bun"]
|
|
13
|
+
},
|
|
14
|
+
"databaseAdapters": {
|
|
15
|
+
"common": {
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@auth/prisma-adapter": "^2.7.1"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,13 +1,89 @@
|
|
|
1
1
|
import { betterAuth } from "better-auth";
|
|
2
|
+
import { sendEmail } from "./email/email-service";
|
|
3
|
+
import { getVerificationEmailTemplate, getPasswordResetEmailTemplate } from "./email/email-templates";
|
|
4
|
+
{{#if database=='prisma'}}
|
|
5
|
+
import { prisma } from "{{framework=='nextjs' ? '@/lib' : '.'}}/prisma";
|
|
6
|
+
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
7
|
+
{{/if}}
|
|
8
|
+
{{#if database=='mongoose'}}
|
|
9
|
+
import { mongoClient, db } from "{{framework=='nextjs' ? '@/lib' : '.'}}/db";
|
|
10
|
+
import { mongodbAdapter } from "better-auth/adapters/mongodb";
|
|
11
|
+
{{/if}}
|
|
2
12
|
|
|
3
|
-
{
|
|
13
|
+
export const auth = betterAuth({
|
|
14
|
+
{{#if database=='prisma'}}
|
|
15
|
+
database: prismaAdapter(prisma),
|
|
16
|
+
{{/if}}
|
|
17
|
+
{{#if database=='mongoose'}}
|
|
18
|
+
database: mongodbAdapter(db),
|
|
19
|
+
{{/if}}
|
|
20
|
+
user: {
|
|
21
|
+
additionalFields: {
|
|
22
|
+
role: {
|
|
23
|
+
type: "string",
|
|
24
|
+
defaultValue: "USER",
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
4
29
|
emailAndPassword: {
|
|
5
30
|
enabled: true,
|
|
31
|
+
requireEmailVerification: {{feature:emailVerification}},
|
|
6
32
|
},
|
|
33
|
+
{{#if features.includes('socialAuth')}}
|
|
7
34
|
socialProviders: {
|
|
8
35
|
google: {
|
|
9
36
|
clientId: process.env.GOOGLE_CLIENT_ID!,
|
|
10
37
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
11
38
|
},
|
|
12
39
|
},
|
|
40
|
+
{{/if}}
|
|
41
|
+
emailVerification: {
|
|
42
|
+
sendVerificationEmail: async ({ user, url }) => {
|
|
43
|
+
const { html, text } = getVerificationEmailTemplate(user, url);
|
|
44
|
+
await sendEmail({
|
|
45
|
+
to: user.email,
|
|
46
|
+
subject: "Verify Your Email Address",
|
|
47
|
+
text,
|
|
48
|
+
html,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
sendOnSignIn: true,
|
|
52
|
+
},
|
|
53
|
+
password: {
|
|
54
|
+
reset: {
|
|
55
|
+
sendResetEmail: async ({ user, url }) => {
|
|
56
|
+
const { html, text } = getPasswordResetEmailTemplate(user, url);
|
|
57
|
+
await sendEmail({
|
|
58
|
+
to: user.email,
|
|
59
|
+
subject: "Reset Your Password",
|
|
60
|
+
text,
|
|
61
|
+
html,
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
// Additional production-ready options
|
|
67
|
+
rateLimit: {
|
|
68
|
+
window: 10, // 10 seconds
|
|
69
|
+
max: 100, // max requests per window
|
|
70
|
+
},
|
|
71
|
+
account: {
|
|
72
|
+
accountLinking: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
trustedProviders: ["google"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
session: {
|
|
78
|
+
cookieCache: {
|
|
79
|
+
enabled: true,
|
|
80
|
+
},
|
|
81
|
+
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
|
82
|
+
updateAge: 60 * 60 * 24, // 1 day
|
|
83
|
+
},
|
|
84
|
+
user: {
|
|
85
|
+
changeEmail: {
|
|
86
|
+
enabled: true,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
13
89
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
|
|
3
|
+
// Create email transporter
|
|
4
|
+
const transporter = nodemailer.createTransporter({
|
|
5
|
+
host: process.env.EMAIL_HOST,
|
|
6
|
+
port: parseInt(process.env.EMAIL_PORT || "587"),
|
|
7
|
+
secure: process.env.EMAIL_PORT === "465",
|
|
8
|
+
auth: {
|
|
9
|
+
user: process.env.EMAIL_USER,
|
|
10
|
+
pass: process.env.EMAIL_PASS,
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Send email function
|
|
15
|
+
export const sendEmail = async ({ to, subject, text, html }: {
|
|
16
|
+
to: string;
|
|
17
|
+
subject: string;
|
|
18
|
+
text?: string;
|
|
19
|
+
html?: string;
|
|
20
|
+
}) => {
|
|
21
|
+
try {
|
|
22
|
+
await transporter.sendMail({
|
|
23
|
+
from: process.env.EMAIL_FROM,
|
|
24
|
+
to,
|
|
25
|
+
subject,
|
|
26
|
+
text,
|
|
27
|
+
html,
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.error("Email sending failed:", error);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export const getVerificationEmailTemplate = (user: { name?: string; email: string }, url: string) => {
|
|
2
|
+
const html = `
|
|
3
|
+
<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>Verify Your Email</title>
|
|
9
|
+
<style>
|
|
10
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #000; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #fff; }
|
|
11
|
+
.header { padding: 20px; text-align: center; border-bottom: 1px solid #000; }
|
|
12
|
+
.content { padding: 20px; }
|
|
13
|
+
.button { display: inline-block; background-color: #fff; color: #000; padding: 10px 20px; text-decoration: none; border: 1px solid #000; border-radius: 5px; margin: 20px 0; }
|
|
14
|
+
.footer { font-size: 12px; color: #000; text-align: center; margin-top: 20px; border-top: 1px solid #000; padding-top: 20px; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div class="header">
|
|
19
|
+
<h1>Verify Your Email Address</h1>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="content">
|
|
22
|
+
<p>Hi ${user.name || user.email},</p>
|
|
23
|
+
<p>Thank you for signing up. Please verify your email address to complete your registration.</p>
|
|
24
|
+
<a href="${url}" class="button">Verify Email</a>
|
|
25
|
+
<p>If the button doesn't work, copy and paste this link: <a href="${url}">${url}</a></p>
|
|
26
|
+
<p>This link expires in 24 hours.</p>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="footer">
|
|
29
|
+
<p>If you didn't create an account, ignore this email.</p>
|
|
30
|
+
</div>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const text = `Hi ${user.name || user.email},
|
|
36
|
+
|
|
37
|
+
Thank you for signing up. Please verify your email address by clicking this link: ${url}
|
|
38
|
+
|
|
39
|
+
This link expires in 24 hours.
|
|
40
|
+
|
|
41
|
+
If you didn't create an account, ignore this email.`;
|
|
42
|
+
|
|
43
|
+
return { html, text };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const getPasswordResetEmailTemplate = (user: { name?: string; email: string }, url: string) => {
|
|
47
|
+
const html = `
|
|
48
|
+
<!DOCTYPE html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="UTF-8">
|
|
52
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
53
|
+
<title>Reset Your Password</title>
|
|
54
|
+
<style>
|
|
55
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #000; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #fff; }
|
|
56
|
+
.header { padding: 20px; text-align: center; border-bottom: 1px solid #000; }
|
|
57
|
+
.content { padding: 20px; }
|
|
58
|
+
.button { display: inline-block; background-color: #fff; color: #000; padding: 10px 20px; text-decoration: none; border: 1px solid #000; border-radius: 5px; margin: 20px 0; }
|
|
59
|
+
.footer { font-size: 12px; color: #000; text-align: center; margin-top: 20px; border-top: 1px solid #000; padding-top: 20px; }
|
|
60
|
+
</style>
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<div class="header">
|
|
64
|
+
<h1>Reset Your Password</h1>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="content">
|
|
67
|
+
<p>Hi ${user.name || user.email},</p>
|
|
68
|
+
<p>You requested a password reset. Click the link below to reset your password.</p>
|
|
69
|
+
<a href="${url}" class="button">Reset Password</a>
|
|
70
|
+
<p>If the button doesn't work, copy and paste this link: <a href="${url}">${url}</a></p>
|
|
71
|
+
<p>This link expires in 1 hour.</p>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="footer">
|
|
74
|
+
<p>If you didn't request this, ignore this email.</p>
|
|
75
|
+
</div>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
const text = `Hi ${user.name || user.email},
|
|
81
|
+
|
|
82
|
+
You requested a password reset. Click this link to reset your password: ${url}
|
|
83
|
+
|
|
84
|
+
This link expires in 1 hour.
|
|
85
|
+
|
|
86
|
+
If you didn't request this, ignore this email.`;
|
|
87
|
+
|
|
88
|
+
return { html, text };
|
|
89
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
2
|
model User {
|
|
3
|
-
id String @id {{
|
|
3
|
+
id String @id {{#if prismaProvider == postgresql}}@default(cuid()){{/if}}{{#if prismaProvider == mysql}}@default(uuid()){{/if}}{{#if prismaProvider == sqlite}}@default(uuid()){{/if}}
|
|
4
4
|
name String
|
|
5
5
|
email String
|
|
6
6
|
emailVerified Boolean @default(false)
|
|
@@ -16,14 +16,14 @@ model User {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
model Session {
|
|
19
|
-
id String @id {{
|
|
19
|
+
id String @id {{#if prismaProvider == postgresql}}@default(cuid()){{/if}}{{#if prismaProvider == mysql}}@default(uuid()){{/if}}{{#if prismaProvider == sqlite}}@default(uuid()){{/if}}
|
|
20
20
|
expiresAt DateTime
|
|
21
21
|
token String @unique
|
|
22
22
|
createdAt DateTime @default(now())
|
|
23
23
|
updatedAt DateTime @updatedAt
|
|
24
24
|
ipAddress String?
|
|
25
25
|
userAgent String?
|
|
26
|
-
userId String {{
|
|
26
|
+
userId String {{#if prismaProvider == postgresql}}{{/if}}{{#if prismaProvider == mysql}}{{/if}}{{#if prismaProvider == sqlite}}{{/if}}
|
|
27
27
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
28
28
|
|
|
29
29
|
@@index([userId])
|
|
@@ -31,10 +31,10 @@ model Session {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
model Account {
|
|
34
|
-
id String @id {{
|
|
34
|
+
id String @id {{#if prismaProvider == postgresql}}@default(cuid()){{/if}}{{#if prismaProvider == mysql}}@default(uuid()){{/if}}{{#if prismaProvider == sqlite}}@default(uuid()){{/if}}
|
|
35
35
|
accountId String
|
|
36
36
|
providerId String
|
|
37
|
-
userId String {{
|
|
37
|
+
userId String {{#if prismaProvider == postgresql}}{{/if}}{{#if prismaProvider == mysql}}{{/if}}{{#if prismaProvider == sqlite}}{{/if}}
|
|
38
38
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
39
39
|
accessToken String?
|
|
40
40
|
refreshToken String?
|
|
@@ -51,7 +51,7 @@ model Account {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
model Verification {
|
|
54
|
-
id String @id {{
|
|
54
|
+
id String @id {{#if prismaProvider == postgresql}}@default(cuid()){{/if}}{{#if prismaProvider == mysql}}@default(uuid()){{/if}}{{#if prismaProvider == sqlite}}@default(uuid()){{/if}}
|
|
55
55
|
identifier String
|
|
56
56
|
value String
|
|
57
57
|
expiresAt DateTime
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "better-auth",
|
|
3
|
+
"type": "auth",
|
|
4
|
+
"priority": 10,
|
|
5
|
+
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"type": "create-file",
|
|
8
|
+
"description": "Create Better Auth configuration file",
|
|
9
|
+
"source": "lib/auth.ts",
|
|
10
|
+
"destination": "{{lib}}/auth.ts"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"type": "create-file",
|
|
14
|
+
"description": "Create email service for sending emails",
|
|
15
|
+
"source": "lib/email-service.ts",
|
|
16
|
+
"destination": "{{lib}}/email/email-service.ts"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "create-file",
|
|
20
|
+
"description": "Create email templates",
|
|
21
|
+
"source": "lib/email-templates.ts",
|
|
22
|
+
"destination": "{{lib}}/email/email-templates.ts"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"type": "create-file",
|
|
26
|
+
"description": "Create Better Auth API routes for Next.js",
|
|
27
|
+
"source": "api/auth/[...all]/route.ts",
|
|
28
|
+
"destination": "app/api/auth/[...all]/route.ts",
|
|
29
|
+
"condition": { "framework": "nextjs" }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "create-file",
|
|
33
|
+
"description": "Create Better Auth client for React",
|
|
34
|
+
"source": "lib/auth-client.ts",
|
|
35
|
+
"destination": "{{lib}}/auth-client.ts",
|
|
36
|
+
"condition": { "framework": "nextjs" }
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"type": "create-file",
|
|
40
|
+
"description": "Create Better Auth middleware for Next.js",
|
|
41
|
+
"destination": "middleware.ts",
|
|
42
|
+
"condition": { "framework": "nextjs" },
|
|
43
|
+
"content": "import { auth } from \"@/lib/auth\";\n\nexport default auth((req) => {\n console.log('middleware', req.auth)\n})\n\nexport const config = {\n matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],\n}"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"type": "patch-file",
|
|
47
|
+
"description": "Add Better Auth routes to Express app",
|
|
48
|
+
"file": "src/app.ts",
|
|
49
|
+
"condition": { "framework": "express" },
|
|
50
|
+
"operations": [
|
|
51
|
+
{
|
|
52
|
+
"type": "add-import",
|
|
53
|
+
"imports": [
|
|
54
|
+
"import { auth } from \"@/lib/auth\";",
|
|
55
|
+
"import { toNodeHandler } from \"better-auth/node\";"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "add-code",
|
|
60
|
+
"after": "// routes",
|
|
61
|
+
"code": "\napp.all(\"/api/auth/*splat\", toNodeHandler(auth));"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"better-auth": "^1.4.12",
|
|
68
|
+
"nodemailer": "^7.0.12"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@types/nodemailer": "^6.4.17"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {},
|
|
74
|
+
"envVars": {
|
|
75
|
+
"BETTER_AUTH_SECRET": "",
|
|
76
|
+
"BETTER_AUTH_URL": "http://localhost:3000",
|
|
77
|
+
"EMAIL_HOST": "smtp.gmail.com",
|
|
78
|
+
"EMAIL_PORT": "587",
|
|
79
|
+
"EMAIL_USER": "",
|
|
80
|
+
"EMAIL_PASS": "",
|
|
81
|
+
"EMAIL_FROM": "noreply@yourapp.com"
|
|
82
|
+
}
|
|
83
|
+
}
|