stackkit 0.2.7 → 0.2.9
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 +91 -12
- package/dist/lib/fs/files.js +1 -1
- package/dist/lib/generation/code-generator.d.ts +2 -7
- package/dist/lib/generation/code-generator.js +120 -56
- package/dist/lib/utils/fs-helpers.d.ts +1 -1
- package/modules/auth/authjs/generator.json +16 -16
- package/modules/auth/better-auth/files/express/middlewares/authorize.ts +121 -41
- package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +257 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.interface.ts +23 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.route.ts +22 -0
- package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +403 -0
- package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +91 -0
- package/modules/auth/better-auth/files/express/templates/otp.ejs +87 -0
- package/modules/auth/better-auth/files/express/types/express.d.ts +6 -8
- package/modules/auth/better-auth/files/express/utils/cookie.ts +19 -0
- package/modules/auth/better-auth/files/express/utils/jwt.ts +34 -0
- package/modules/auth/better-auth/files/express/utils/token.ts +66 -0
- package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +1 -1
- package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +11 -1
- package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +74 -0
- package/modules/auth/better-auth/files/shared/config/env.ts +113 -0
- package/modules/auth/better-auth/files/shared/lib/auth-client.ts +1 -1
- package/modules/auth/better-auth/files/shared/lib/auth.ts +151 -62
- package/modules/auth/better-auth/files/shared/prisma/schema.prisma +22 -11
- package/modules/auth/better-auth/files/shared/utils/email.ts +71 -0
- package/modules/auth/better-auth/generator.json +159 -81
- package/modules/database/mongoose/generator.json +18 -18
- package/modules/database/prisma/generator.json +44 -44
- package/package.json +1 -1
- package/templates/express/env.example +2 -2
- package/templates/express/eslint.config.mjs +7 -0
- package/templates/express/node_modules/.bin/acorn +17 -0
- package/templates/express/node_modules/.bin/eslint +17 -0
- package/templates/express/node_modules/.bin/tsc +17 -0
- package/templates/express/node_modules/.bin/tsserver +17 -0
- package/templates/express/node_modules/.bin/tsx +17 -0
- package/templates/express/package.json +12 -6
- package/templates/express/src/app.ts +15 -7
- package/templates/express/src/config/cors.ts +8 -7
- package/templates/express/src/config/env.ts +26 -5
- package/templates/express/src/config/logger.ts +2 -2
- package/templates/express/src/config/rate-limit.ts +2 -2
- package/templates/express/src/modules/health/health.controller.ts +13 -11
- package/templates/express/src/routes/index.ts +1 -6
- package/templates/express/src/server.ts +12 -12
- package/templates/express/src/shared/errors/app-error.ts +16 -0
- package/templates/express/src/shared/middlewares/error.middleware.ts +154 -12
- package/templates/express/src/shared/middlewares/not-found.middleware.ts +2 -1
- package/templates/express/src/shared/utils/catch-async.ts +11 -0
- package/templates/express/src/shared/utils/pagination.ts +6 -1
- package/templates/express/src/shared/utils/send-response.ts +25 -0
- package/templates/nextjs/lib/env.ts +19 -8
- package/modules/auth/better-auth/files/shared/lib/email/email-service.ts +0 -33
- package/modules/auth/better-auth/files/shared/lib/email/email-templates.ts +0 -89
- package/templates/express/eslint.config.cjs +0 -42
- package/templates/express/src/config/helmet.ts +0 -5
- package/templates/express/src/modules/health/health.service.ts +0 -6
- package/templates/express/src/shared/errors/error-codes.ts +0 -9
- package/templates/express/src/shared/logger/logger.ts +0 -20
- package/templates/express/src/shared/utils/async-handler.ts +0 -9
- package/templates/express/src/shared/utils/response.ts +0 -9
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Response } from "express";
|
|
2
|
+
import { JwtPayload, SignOptions } from "jsonwebtoken";
|
|
3
|
+
import { envVars } from "../../config/env";
|
|
4
|
+
import { cookieUtils } from "./cookie";
|
|
5
|
+
import { jwtUtils } from "./jwt";
|
|
6
|
+
|
|
7
|
+
//Creating access token
|
|
8
|
+
const getAccessToken = (payload: JwtPayload) => {
|
|
9
|
+
const accessToken = jwtUtils.createToken(
|
|
10
|
+
payload,
|
|
11
|
+
envVars.ACCESS_TOKEN_SECRET,
|
|
12
|
+
{ expiresIn: envVars.ACCESS_TOKEN_EXPIRES_IN } as SignOptions
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return accessToken;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const getRefreshToken = (payload: JwtPayload) => {
|
|
19
|
+
const refreshToken = jwtUtils.createToken(
|
|
20
|
+
payload,
|
|
21
|
+
envVars.REFRESH_TOKEN_SECRET,
|
|
22
|
+
{ expiresIn: envVars.REFRESH_TOKEN_EXPIRES_IN } as SignOptions
|
|
23
|
+
);
|
|
24
|
+
return refreshToken;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const setAccessTokenCookie = (res: Response, token: string) => {
|
|
28
|
+
cookieUtils.setCookie(res, 'accessToken', token, {
|
|
29
|
+
httpOnly: true,
|
|
30
|
+
secure: true,
|
|
31
|
+
sameSite: "none",
|
|
32
|
+
path: '/',
|
|
33
|
+
//1 day
|
|
34
|
+
maxAge: 60 * 60 * 24 * 1000,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const setRefreshTokenCookie = (res: Response, token: string) => {
|
|
39
|
+
cookieUtils.setCookie(res, 'refreshToken', token, {
|
|
40
|
+
httpOnly: true,
|
|
41
|
+
secure: true,
|
|
42
|
+
sameSite: "none",
|
|
43
|
+
path: '/',
|
|
44
|
+
//7d
|
|
45
|
+
maxAge: 60 * 60 * 24 * 1000 * 7,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const setBetterAuthSessionCookie = (res: Response, token: string) => {
|
|
50
|
+
cookieUtils.setCookie(res, "better-auth.session_token", token, {
|
|
51
|
+
httpOnly: true,
|
|
52
|
+
secure: true,
|
|
53
|
+
sameSite: "none",
|
|
54
|
+
path: '/',
|
|
55
|
+
//1 day
|
|
56
|
+
maxAge: 60 * 60 * 24 * 1000,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const tokenUtils = {
|
|
61
|
+
getAccessToken,
|
|
62
|
+
getRefreshToken,
|
|
63
|
+
setAccessTokenCookie,
|
|
64
|
+
setRefreshTokenCookie,
|
|
65
|
+
setBetterAuthSessionCookie,
|
|
66
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { auth } from "@/lib/auth";
|
|
2
1
|
import { headers } from "next/headers";
|
|
3
2
|
import { redirect } from "next/navigation";
|
|
3
|
+
import { auth } from "./auth";
|
|
4
4
|
|
|
5
5
|
export async function getSession() {
|
|
6
6
|
const session = await auth.api.getSession({
|
|
@@ -10,6 +10,16 @@ export async function getSession() {
|
|
|
10
10
|
return session;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export async function requireSuperAdmin() {
|
|
14
|
+
const session = await getSession();
|
|
15
|
+
|
|
16
|
+
if (!session || session.user.role !== "SUPER_ADMIN") {
|
|
17
|
+
redirect("/login");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return session.user;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
export async function requireAdmin() {
|
|
14
24
|
const session = await getSession();
|
|
15
25
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
interface OtpTemplateProps {
|
|
2
|
+
name?: string | null;
|
|
3
|
+
otp: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const escapeHtml = (value: string) =>
|
|
7
|
+
value
|
|
8
|
+
.replaceAll("&", "&")
|
|
9
|
+
.replaceAll("<", "<")
|
|
10
|
+
.replaceAll(">", ">")
|
|
11
|
+
.replaceAll('"', """)
|
|
12
|
+
.replaceAll("'", "'");
|
|
13
|
+
|
|
14
|
+
const renderOtpTemplate = ({ name, otp }: OtpTemplateProps) => {
|
|
15
|
+
const safeName = name ? escapeHtml(name) : null;
|
|
16
|
+
const safeOtp = escapeHtml(otp);
|
|
17
|
+
|
|
18
|
+
return `<html>
|
|
19
|
+
<body
|
|
20
|
+
style="margin:0;padding:24px;background-color:#f8fafc;font-family:Arial,sans-serif;color:#0f172a;"
|
|
21
|
+
>
|
|
22
|
+
<table
|
|
23
|
+
role="presentation"
|
|
24
|
+
width="100%"
|
|
25
|
+
cellpadding="0"
|
|
26
|
+
cellspacing="0"
|
|
27
|
+
style="max-width:560px;margin:0 auto;background:#ffffff;"
|
|
28
|
+
>
|
|
29
|
+
<tbody>
|
|
30
|
+
<tr>
|
|
31
|
+
<td style="padding:28px 24px;">
|
|
32
|
+
<h2 style="margin:0 0 12px;font-size:22px;">Verification Code</h2>
|
|
33
|
+
<p style="margin:0 0 16px;font-size:15px;line-height:1.6;">
|
|
34
|
+
${safeName ? `Hi ${safeName},` : "Hi,"}
|
|
35
|
+
</p>
|
|
36
|
+
<p style="margin:0 0 16px;font-size:15px;line-height:1.6;">
|
|
37
|
+
Use the OTP below to continue. This code will expire in 2 minutes.
|
|
38
|
+
</p>
|
|
39
|
+
<div
|
|
40
|
+
style="display:inline-block;padding:12px 18px;border:1px solid #e2e8f0;border-radius:8px;letter-spacing:6px;font-size:22px;font-weight:700;background-color:#f1f5f9;"
|
|
41
|
+
>
|
|
42
|
+
${safeOtp}
|
|
43
|
+
</div>
|
|
44
|
+
<p style="margin:20px 0 0;font-size:13px;color:#475569;">
|
|
45
|
+
If you did not request this code, you can safely ignore this email.
|
|
46
|
+
</p>
|
|
47
|
+
</td>
|
|
48
|
+
</tr>
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
</body>
|
|
52
|
+
</html>`;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const renderEmailTemplate = (
|
|
56
|
+
templateName: string,
|
|
57
|
+
templateData: Record<string, unknown>,
|
|
58
|
+
) => {
|
|
59
|
+
switch (templateName) {
|
|
60
|
+
case "otp": {
|
|
61
|
+
const name =
|
|
62
|
+
typeof templateData.name === "string" ? templateData.name : null;
|
|
63
|
+
const otp = typeof templateData.otp === "string" ? templateData.otp : "";
|
|
64
|
+
|
|
65
|
+
if (!otp) {
|
|
66
|
+
throw new Error("OTP value is required for otp template");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return `<!DOCTYPE html>${renderOtpTemplate({ name, otp })}`;
|
|
70
|
+
}
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`Unknown email template: ${templateName}`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{{#if framework == "express" }}
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import status from "http-status";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { AppError } from "../shared/errors/app-error";
|
|
6
|
+
|
|
7
|
+
dotenv.config({ path: path.join(process.cwd(), ".env") });
|
|
8
|
+
{{/if}}
|
|
9
|
+
|
|
10
|
+
interface EnvConfig {
|
|
11
|
+
APP_URL?: string;
|
|
12
|
+
FRONTEND_URL?: string;
|
|
13
|
+
BETTER_AUTH_URL: string;
|
|
14
|
+
BETTER_AUTH_SECRET: string;
|
|
15
|
+
GOOGLE_CLIENT_ID?: string;
|
|
16
|
+
GOOGLE_CLIENT_SECRET?: string;
|
|
17
|
+
GOOGLE_CALLBACK_URL: string;
|
|
18
|
+
EMAIL_SENDER: {
|
|
19
|
+
SMTP_USER: string;
|
|
20
|
+
SMTP_PASS: string;
|
|
21
|
+
SMTP_HOST: string;
|
|
22
|
+
SMTP_PORT: string;
|
|
23
|
+
SMTP_FROM: string;
|
|
24
|
+
};
|
|
25
|
+
{{#if framework == "express" }}
|
|
26
|
+
NODE_ENV: string;
|
|
27
|
+
PORT: string;
|
|
28
|
+
BETTER_AUTH_SESSION_TOKEN_EXPIRES_IN: string;
|
|
29
|
+
BETTER_AUTH_SESSION_TOKEN_UPDATE_AGE: string;
|
|
30
|
+
ACCESS_TOKEN_SECRET: string;
|
|
31
|
+
ACCESS_TOKEN_EXPIRES_IN: string;
|
|
32
|
+
REFRESH_TOKEN_SECRET: string;
|
|
33
|
+
REFRESH_TOKEN_EXPIRES_IN: string;
|
|
34
|
+
SUPER_ADMIN_EMAIL: string;
|
|
35
|
+
SUPER_ADMIN_PASSWORD: string;
|
|
36
|
+
{{/if}}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const loadEnvVars = (): EnvConfig => {
|
|
40
|
+
const requiredEnvVars = [
|
|
41
|
+
"FRONTEND_URL",
|
|
42
|
+
"BETTER_AUTH_URL",
|
|
43
|
+
"BETTER_AUTH_SECRET",
|
|
44
|
+
"GOOGLE_CLIENT_ID",
|
|
45
|
+
"GOOGLE_CLIENT_SECRET",
|
|
46
|
+
"GOOGLE_CALLBACK_URL",
|
|
47
|
+
"EMAIL_SENDER_SMTP_USER",
|
|
48
|
+
"EMAIL_SENDER_SMTP_PASS",
|
|
49
|
+
"EMAIL_SENDER_SMTP_HOST",
|
|
50
|
+
"EMAIL_SENDER_SMTP_PORT",
|
|
51
|
+
"EMAIL_SENDER_SMTP_FROM",
|
|
52
|
+
{{#if framework == "express" }}
|
|
53
|
+
"NODE_ENV",
|
|
54
|
+
"PORT",
|
|
55
|
+
"BETTER_AUTH_SESSION_TOKEN_EXPIRES_IN",
|
|
56
|
+
"BETTER_AUTH_SESSION_TOKEN_UPDATE_AGE",
|
|
57
|
+
"ACCESS_TOKEN_SECRET",
|
|
58
|
+
"ACCESS_TOKEN_EXPIRES_IN",
|
|
59
|
+
"REFRESH_TOKEN_SECRET",
|
|
60
|
+
"REFRESH_TOKEN_EXPIRES_IN",
|
|
61
|
+
"SUPER_ADMIN_EMAIL",
|
|
62
|
+
"SUPER_ADMIN_PASSWORD",
|
|
63
|
+
{{/if}}
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
requiredEnvVars.forEach((varName) => {
|
|
67
|
+
if (!process.env[varName]) {
|
|
68
|
+
{{#if framework == "express" }}
|
|
69
|
+
throw new AppError(
|
|
70
|
+
status.INTERNAL_SERVER_ERROR,
|
|
71
|
+
`Environment variable ${varName} is required but not set in .env file.`,
|
|
72
|
+
);
|
|
73
|
+
{{else}}
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Environment variable ${varName} is required but not defined.`,
|
|
76
|
+
);
|
|
77
|
+
{{/if}}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
APP_URL: process.env.APP_URL as string,
|
|
83
|
+
FRONTEND_URL: process.env.FRONTEND_URL as string,
|
|
84
|
+
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL as string,
|
|
85
|
+
BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET as string,
|
|
86
|
+
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
|
87
|
+
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
|
|
88
|
+
GOOGLE_CALLBACK_URL: process.env.GOOGLE_CALLBACK_URL as string,
|
|
89
|
+
EMAIL_SENDER: {
|
|
90
|
+
SMTP_USER: process.env.EMAIL_SENDER_SMTP_USER as string,
|
|
91
|
+
SMTP_PASS: process.env.EMAIL_SENDER_SMTP_PASS as string,
|
|
92
|
+
SMTP_HOST: process.env.EMAIL_SENDER_SMTP_HOST as string,
|
|
93
|
+
SMTP_PORT: process.env.EMAIL_SENDER_SMTP_PORT as string,
|
|
94
|
+
SMTP_FROM: process.env.EMAIL_SENDER_SMTP_FROM as string,
|
|
95
|
+
},
|
|
96
|
+
{{#if framework == "express" }}
|
|
97
|
+
NODE_ENV: process.env.NODE_ENV as string,
|
|
98
|
+
PORT: process.env.PORT as string,
|
|
99
|
+
BETTER_AUTH_SESSION_TOKEN_EXPIRES_IN: process.env
|
|
100
|
+
.BETTER_AUTH_SESSION_TOKEN_EXPIRES_IN as string,
|
|
101
|
+
BETTER_AUTH_SESSION_TOKEN_UPDATE_AGE: process.env
|
|
102
|
+
.BETTER_AUTH_SESSION_TOKEN_UPDATE_AGE as string,
|
|
103
|
+
ACCESS_TOKEN_SECRET: process.env.ACCESS_TOKEN_SECRET as string,
|
|
104
|
+
ACCESS_TOKEN_EXPIRES_IN: process.env.ACCESS_TOKEN_EXPIRES_IN as string,
|
|
105
|
+
REFRESH_TOKEN_SECRET: process.env.REFRESH_TOKEN_SECRET as string,
|
|
106
|
+
REFRESH_TOKEN_EXPIRES_IN: process.env.REFRESH_TOKEN_EXPIRES_IN as string,
|
|
107
|
+
SUPER_ADMIN_EMAIL: process.env.SUPER_ADMIN_EMAIL as string,
|
|
108
|
+
SUPER_ADMIN_PASSWORD: process.env.SUPER_ADMIN_PASSWORD as string,
|
|
109
|
+
{{/if}}
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const envVars = loadEnvVars();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createAuthClient } from "better-auth/react";
|
|
2
2
|
|
|
3
3
|
export const authClient = createAuthClient({
|
|
4
|
-
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:
|
|
4
|
+
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:5000",
|
|
5
5
|
});
|
|
6
6
|
|
|
7
7
|
export const { signIn, signUp, signOut, useSession } = authClient;
|
|
@@ -1,46 +1,42 @@
|
|
|
1
|
+
import { Role, UserStatus } from "@prisma/client";
|
|
1
2
|
import { betterAuth } from "better-auth";
|
|
3
|
+
import { bearer, emailOTP } from "better-auth/plugins";
|
|
2
4
|
{{#if combo == "prisma:express"}}
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
getVerificationEmailTemplate,
|
|
7
|
-
} from "../../shared/email/email-templates";
|
|
8
|
-
import { prisma } from "../../database/prisma";
|
|
5
|
+
import { envVars } from "../config/env";
|
|
6
|
+
import { sendEmail } from "../shared/utils/email";
|
|
7
|
+
import { prisma } from "../database/prisma";
|
|
9
8
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
10
9
|
{{/if}}
|
|
11
10
|
|
|
12
11
|
{{#if combo == "prisma:nextjs"}}
|
|
13
|
-
import { getPasswordResetEmailTemplate, getVerificationEmailTemplate } from "../service/email/email-templates";
|
|
14
12
|
import { sendEmail } from "../service/email/email-service";
|
|
15
13
|
import { prisma } from "../database/prisma";
|
|
14
|
+
import { envVars } from "@/lib/env";
|
|
16
15
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
|
17
16
|
{{/if}}
|
|
18
17
|
|
|
19
18
|
{{#if combo == "mongoose:express"}}
|
|
19
|
+
import { envVars } from "../config/env";
|
|
20
20
|
import { sendEmail } from "../../shared/email/email-service";
|
|
21
|
-
import {
|
|
22
|
-
getPasswordResetEmailTemplate,
|
|
23
|
-
getVerificationEmailTemplate,
|
|
24
|
-
} from "../../shared/email/email-templates";
|
|
25
21
|
import { mongoose } from "../../database/mongoose";
|
|
26
22
|
import { mongodbAdapter } from "better-auth/adapters/mongodb";
|
|
27
23
|
{{/if}}
|
|
28
24
|
|
|
29
25
|
{{#if combo == "mongoose:nextjs"}}
|
|
30
|
-
import { getPasswordResetEmailTemplate, getVerificationEmailTemplate } from "../service/email/email-templates";
|
|
31
26
|
import { sendEmail } from "../service/email/email-service";
|
|
32
27
|
import { mongoose } from "../database/mongoose";
|
|
28
|
+
import { envVars } from "@/lib/env";
|
|
33
29
|
import { mongodbAdapter } from "better-auth/adapters/mongodb";
|
|
34
30
|
{{/if}}
|
|
35
31
|
|
|
36
|
-
export async function initAuth() {
|
|
37
32
|
{{#if database == "mongoose"}}
|
|
33
|
+
export async function initAuth() {
|
|
38
34
|
const mongooseInstance = await mongoose();
|
|
39
35
|
const client = mongooseInstance.connection.getClient();
|
|
40
36
|
const db = client.db();
|
|
41
37
|
{{/if}}
|
|
42
38
|
|
|
43
|
-
return betterAuth({
|
|
39
|
+
{{#if database == "mongoose"}}return{{else}}export const auth = {{/if}} betterAuth({
|
|
44
40
|
{{#if database == "prisma"}}
|
|
45
41
|
database: prismaAdapter(prisma, {
|
|
46
42
|
provider: "{{prismaProvider}}",
|
|
@@ -49,76 +45,169 @@ return betterAuth({
|
|
|
49
45
|
{{#if database == "mongoose"}}
|
|
50
46
|
database: mongodbAdapter(db, { client }),
|
|
51
47
|
{{/if}}
|
|
52
|
-
baseURL:
|
|
53
|
-
secret:
|
|
54
|
-
trustedOrigins: [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
role: {
|
|
58
|
-
type: "string",
|
|
59
|
-
defaultValue: "USER",
|
|
60
|
-
required: true,
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
48
|
+
baseURL: envVars.BETTER_AUTH_URL,
|
|
49
|
+
secret: envVars.BETTER_AUTH_SECRET,
|
|
50
|
+
trustedOrigins: [
|
|
51
|
+
envVars.FRONTEND_URL || envVars.BETTER_AUTH_URL || "http://localhost:3000",
|
|
52
|
+
],
|
|
64
53
|
emailAndPassword: {
|
|
65
54
|
enabled: true,
|
|
66
55
|
requireEmailVerification: true,
|
|
67
|
-
sendResetPassword: async ({ user, url }) => {
|
|
68
|
-
const { html, text } = getPasswordResetEmailTemplate(user, url);
|
|
69
|
-
await sendEmail({
|
|
70
|
-
to: user.email,
|
|
71
|
-
subject: "Reset Your Password",
|
|
72
|
-
text,
|
|
73
|
-
html,
|
|
74
|
-
});
|
|
75
|
-
},
|
|
76
56
|
},
|
|
77
57
|
socialProviders: {
|
|
78
58
|
google: {
|
|
79
|
-
|
|
59
|
+
clientId: envVars.GOOGLE_CLIENT_ID as string,
|
|
60
|
+
clientSecret: envVars.GOOGLE_CLIENT_SECRET as string,
|
|
61
|
+
accessType: "offline",
|
|
80
62
|
prompt: "select_account consent",
|
|
81
|
-
|
|
82
|
-
|
|
63
|
+
mapProfileToUser: () => {
|
|
64
|
+
return {
|
|
65
|
+
role: Role.USER,
|
|
66
|
+
status: UserStatus.ACTIVE,
|
|
67
|
+
needPasswordChange: false,
|
|
68
|
+
emailVerified: true,
|
|
69
|
+
isDeleted: false,
|
|
70
|
+
deletedAt: null,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
83
73
|
},
|
|
84
74
|
},
|
|
85
75
|
emailVerification: {
|
|
86
|
-
|
|
87
|
-
const { html, text } = getVerificationEmailTemplate(user, url);
|
|
88
|
-
await sendEmail({
|
|
89
|
-
to: user.email,
|
|
90
|
-
subject: "Verify Your Email Address",
|
|
91
|
-
text,
|
|
92
|
-
html,
|
|
93
|
-
});
|
|
94
|
-
},
|
|
76
|
+
sendOnSignUp: true,
|
|
95
77
|
sendOnSignIn: true,
|
|
78
|
+
autoSignInAfterVerification: true,
|
|
96
79
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
80
|
+
user: {
|
|
81
|
+
additionalFields: {
|
|
82
|
+
role: {
|
|
83
|
+
type: "string",
|
|
84
|
+
required: true,
|
|
85
|
+
defaultValue: Role.USER,
|
|
86
|
+
},
|
|
87
|
+
status: {
|
|
88
|
+
type: "string",
|
|
89
|
+
required: true,
|
|
90
|
+
defaultValue: UserStatus.ACTIVE,
|
|
91
|
+
},
|
|
92
|
+
needPasswordChange: {
|
|
93
|
+
type: "boolean",
|
|
94
|
+
required: true,
|
|
95
|
+
defaultValue: false,
|
|
96
|
+
},
|
|
97
|
+
isDeleted: {
|
|
98
|
+
type: "boolean",
|
|
99
|
+
required: true,
|
|
100
|
+
defaultValue: false,
|
|
101
|
+
},
|
|
102
|
+
deletedAt: {
|
|
103
|
+
type: "date",
|
|
104
|
+
required: false,
|
|
105
|
+
defaultValue: null,
|
|
106
|
+
},
|
|
105
107
|
},
|
|
106
108
|
},
|
|
109
|
+
plugins: [
|
|
110
|
+
bearer(),
|
|
111
|
+
emailOTP({
|
|
112
|
+
overrideDefaultEmailVerification: true,
|
|
113
|
+
async sendVerificationOTP({ email, otp, type }) {
|
|
114
|
+
if (type === "email-verification") {
|
|
115
|
+
const user = await prisma.user.findUnique({
|
|
116
|
+
where: {
|
|
117
|
+
email,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!user) {
|
|
122
|
+
console.error(
|
|
123
|
+
`User with email ${email} not found. Cannot send verification OTP.`,
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (user && user.role === Role.SUPER_ADMIN) {
|
|
129
|
+
console.log(
|
|
130
|
+
`User with email ${email} is a super admin. Skipping sending verification OTP.`,
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (user && !user.emailVerified) {
|
|
136
|
+
sendEmail({
|
|
137
|
+
to: email,
|
|
138
|
+
subject: "Verify your email",
|
|
139
|
+
templateName: "otp",
|
|
140
|
+
templateData: {
|
|
141
|
+
name: user.name,
|
|
142
|
+
otp,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
} else if (type === "forget-password") {
|
|
147
|
+
const user = await prisma.user.findUnique({
|
|
148
|
+
where: {
|
|
149
|
+
email,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (user) {
|
|
154
|
+
sendEmail({
|
|
155
|
+
to: email,
|
|
156
|
+
subject: "Password Reset OTP",
|
|
157
|
+
templateName: "otp",
|
|
158
|
+
templateData: {
|
|
159
|
+
name: user.name,
|
|
160
|
+
otp,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
expiresIn: 2 * 60, // 2 minutes in seconds
|
|
167
|
+
otpLength: 6,
|
|
168
|
+
}),
|
|
169
|
+
],
|
|
107
170
|
session: {
|
|
171
|
+
expiresIn: 60 * 60 * 60 * 24, // 1 day in seconds
|
|
172
|
+
updateAge: 60 * 60 * 60 * 24, // 1 day in seconds
|
|
108
173
|
cookieCache: {
|
|
109
174
|
enabled: true,
|
|
110
|
-
maxAge: 60 * 60 *
|
|
175
|
+
maxAge: 60 * 60 * 60 * 24, // 1 day in seconds
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
redirectURLs: {
|
|
179
|
+
signIn: `${envVars.BETTER_AUTH_URL}/api/v1/auth/google/success`,
|
|
180
|
+
},
|
|
181
|
+
advanced: {
|
|
182
|
+
// disableCSRFCheck: true,
|
|
183
|
+
useSecureCookies: false,
|
|
184
|
+
cookies: {
|
|
185
|
+
state: {
|
|
186
|
+
attributes: {
|
|
187
|
+
sameSite: "none",
|
|
188
|
+
secure: true,
|
|
189
|
+
httpOnly: true,
|
|
190
|
+
path: "/",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
sessionToken: {
|
|
194
|
+
attributes: {
|
|
195
|
+
sameSite: "none",
|
|
196
|
+
secure: true,
|
|
197
|
+
httpOnly: true,
|
|
198
|
+
path: "/",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
111
201
|
},
|
|
112
|
-
|
|
113
|
-
updateAge: 60 * 60 * 24,
|
|
114
|
-
cookieName: "better-auth.session_token",
|
|
115
|
-
}
|
|
202
|
+
},
|
|
116
203
|
})
|
|
204
|
+
{{#if database == "mongoose"}}
|
|
117
205
|
};
|
|
118
206
|
|
|
119
|
-
export let auth:
|
|
207
|
+
export let auth: ReturnType<typeof betterAuth> | undefined = undefined;
|
|
120
208
|
|
|
121
209
|
export async function setupAuth() {
|
|
122
210
|
auth = await initAuth();
|
|
123
211
|
return auth;
|
|
124
|
-
}
|
|
212
|
+
}
|
|
213
|
+
{{/if}}
|
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
{{#var defaultId = @default(cuid())}}
|
|
5
5
|
{{/if}}
|
|
6
6
|
model User {
|
|
7
|
-
id
|
|
8
|
-
name
|
|
9
|
-
email
|
|
10
|
-
emailVerified
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
id String @id {{defaultId}}
|
|
8
|
+
name String
|
|
9
|
+
email String
|
|
10
|
+
emailVerified Boolean @default(false)
|
|
11
|
+
role Role @default(USER)
|
|
12
|
+
status UserStatus @default(ACTIVE)
|
|
13
|
+
needPasswordChange Boolean @default(false)
|
|
14
|
+
isDeleted Boolean @default(false)
|
|
15
|
+
deletedAt DateTime?
|
|
16
|
+
image String?
|
|
17
|
+
createdAt DateTime @default(now())
|
|
18
|
+
updatedAt DateTime @updatedAt
|
|
19
|
+
sessions Session[]
|
|
20
|
+
accounts Account[]
|
|
17
21
|
|
|
18
22
|
@@unique([email])
|
|
19
23
|
@@map("user")
|
|
@@ -67,6 +71,13 @@ model Verification {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
enum Role {
|
|
70
|
-
|
|
74
|
+
SUPER_ADMIN
|
|
71
75
|
ADMIN
|
|
76
|
+
USER
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
enum UserStatus {
|
|
80
|
+
ACTIVE
|
|
81
|
+
BLOCKED
|
|
82
|
+
DELETED
|
|
72
83
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
{{#if framework == "express"}}
|
|
3
|
+
import ejs from "ejs";
|
|
4
|
+
import status from "http-status";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { envVars } from "../../config/env";
|
|
7
|
+
import { AppError } from "../errors/app-error";
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{#if framework == "nextjs"}}
|
|
10
|
+
import nodemailer from "nodemailer";
|
|
11
|
+
import { renderEmailTemplate } from "../email/otp-template";
|
|
12
|
+
import { envVars } from "../env";
|
|
13
|
+
{{/if}}
|
|
14
|
+
|
|
15
|
+
const transporter = nodemailer.createTransport({
|
|
16
|
+
host: envVars.EMAIL_SENDER.SMTP_HOST,
|
|
17
|
+
secure: true,
|
|
18
|
+
auth: {
|
|
19
|
+
user: envVars.EMAIL_SENDER.SMTP_USER,
|
|
20
|
+
pass: envVars.EMAIL_SENDER.SMTP_PASS,
|
|
21
|
+
},
|
|
22
|
+
port: Number(envVars.EMAIL_SENDER.SMTP_PORT),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
interface SendEmailOptions {
|
|
26
|
+
to: string;
|
|
27
|
+
subject: string;
|
|
28
|
+
templateName: string;
|
|
29
|
+
templateData: Record<string, string | number | boolean | object>;
|
|
30
|
+
attachments?: {
|
|
31
|
+
filename: string;
|
|
32
|
+
content: Buffer | string;
|
|
33
|
+
contentType: string;
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const sendEmail = async ({
|
|
38
|
+
subject,
|
|
39
|
+
templateData,
|
|
40
|
+
templateName,
|
|
41
|
+
to,
|
|
42
|
+
attachments,
|
|
43
|
+
}: SendEmailOptions) => {
|
|
44
|
+
try {
|
|
45
|
+
{{#if framework == "express"}}
|
|
46
|
+
const templatePath = path.resolve(
|
|
47
|
+
process.cwd(),
|
|
48
|
+
`src/templates/${templateName}.ejs`,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const html = await ejs.renderFile(templatePath, templateData);
|
|
52
|
+
{{/if}}
|
|
53
|
+
{{#if framework == "nextjs"}}
|
|
54
|
+
const html = renderEmailTemplate(templateName, templateData);
|
|
55
|
+
{{/if}}
|
|
56
|
+
|
|
57
|
+
await transporter.sendMail({
|
|
58
|
+
from: envVars.EMAIL_SENDER.SMTP_FROM,
|
|
59
|
+
to: to,
|
|
60
|
+
subject: subject,
|
|
61
|
+
html: html,
|
|
62
|
+
attachments: attachments?.map((attachment) => ({
|
|
63
|
+
filename: attachment.filename,
|
|
64
|
+
content: attachment.content,
|
|
65
|
+
contentType: attachment.contentType,
|
|
66
|
+
})),
|
|
67
|
+
});
|
|
68
|
+
} catch {
|
|
69
|
+
throw new AppError(status.INTERNAL_SERVER_ERROR, "Failed to send email");
|
|
70
|
+
}
|
|
71
|
+
};
|