Farhan Dwi Hartanto
fix: remove await when sent otp email
87a4ba9
Raw
History Blame Contribute Delete
7.34 kB
import { Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaPg } from '@prisma/adapter-pg';
import bcrypt from 'bcrypt';
import { APIError, betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { createAuthMiddleware } from 'better-auth/api';
import { bearer, emailOTP, openAPI } from 'better-auth/plugins';
import { jwt } from 'better-auth/plugins/jwt';
import 'dotenv/config';
import * as nodemailer from 'nodemailer';
import { SignUpBody } from './dto/sign-up.dto';
import { PrismaClient } from '~/generated/client';
const configService = new ConfigService();
const transporter = nodemailer.createTransport({
host: configService.get<string>('SMTP_HOST'),
port: configService.get<number>('SMTP_PORT'),
secure: false,
auth: {
user: configService.get<string>('SMTP_USER'),
pass: configService.get<string>('SMTP_PASS'),
},
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const adapter = new PrismaPg({
connectionString: configService.get<string>('DATABASE_URL'),
});
const prisma = new PrismaClient({ adapter });
const logger: Logger = new Logger(betterAuth.name);
export const auth = betterAuth({
basePath: '/api/auth',
secret: configService.get<string>('BETTER_AUTH_SECRET'),
baseURL: configService.get<string>('BETTER_AUTH_URL'),
logger: {
disabled: false,
disableColors: false,
level: 'info',
log: (level, message, ...args) => {
// Custom logging implementation
logger.log(`[${level}] ${message}`, ...args);
},
},
database: prismaAdapter(prisma, {
provider: 'postgresql',
usePlural: true,
}),
user: {
additionalFields: {
role: {
type: [
'super_admin',
'admin_finance',
'admin_operational',
'admin_sales',
'mentor',
'student',
],
required: false,
defaultValue: 'student',
input: false,
},
phoneNumber: {
type: 'string',
required: false,
input: true,
},
},
},
hooks: {
before: createAuthMiddleware(async (ctx) => {
if (!ctx.path.endsWith('/sign-up/email')) {
return;
}
const body = (ctx.body || {}) as SignUpBody;
const { name, email, phoneNumber, password, confirmPassword, image } =
body;
// Is all field filled
if (!name || !email || !phoneNumber || !password || !confirmPassword) {
throw new APIError('BAD_REQUEST', {
message:
'name, email, phoneNumber, password, and confirmPassword must be filled',
});
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new APIError('BAD_REQUEST', {
message: 'Invalid email format',
});
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const existingUser = await prisma.users.findUnique({
where: {
email,
},
});
// Is email already registered, but not verified
if (existingUser && !existingUser?.emailVerified) {
throw new APIError('CONFLICT', {
message: 'Email already registered, but not verified',
});
}
// Is email already verified
if (existingUser && existingUser?.emailVerified) {
throw new APIError('CONFLICT', {
message: 'Email already verified, please sign in',
});
}
// Validate phone number
const phoneRegex = /^08\d{8,14}$/;
if (!phoneRegex.test(phoneNumber)) {
throw new APIError('BAD_REQUEST', {
message: 'Invalid phone number',
});
}
// Validate password
if (password !== confirmPassword) {
throw new APIError('BAD_REQUEST', {
message: 'Password and confirm password mismatch',
});
}
if (password.length < 8) {
throw new APIError('BAD_REQUEST', {
message: 'Password must be at least 8 characters',
});
}
// If image not provided
if (!image) {
(ctx.body as SignUpBody).image =
`https://ui-avatars.com/api/?background=1565D8&color=fff&rounded=true&size=128&length=2&bold=true&font-size=0.33&name=${name}`;
}
}),
},
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
password: {
async hash(password) {
return await bcrypt.hash(password, 10);
},
async verify(data) {
return await bcrypt.compare(data.password, data.hash);
},
},
},
trustedOrigins: ['*'],
advanced: {
disableOriginCheck: true,
disableCSRFCheck: true,
},
plugins: [
bearer(),
openAPI(),
jwt({
jwt: {
expiresIn: configService.get<string>('JWT_EXPIRES_IN'),
definePayload: ({ user }) => ({
sub: user.id,
email: user.email,
role: user.role as string,
}),
},
}),
emailOTP({
sendVerificationOnSignUp: true,
storeOTP: 'hashed',
expiresIn: configService.get<number>('OTP_EXPIRES_IN'),
async sendVerificationOTP(data: {
email: string;
otp: string;
type: 'sign-in' | 'email-verification' | 'forget-password';
}): Promise<void> {
const subject = `Kode Verifikasi TeraSkil: ${data.otp}`;
const expiresInMinutes = Math.floor(
(configService.get<number>('OTP_EXPIRES_IN') ?? 300) / 60,
);
const html = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px;">
<h2 style="color: #2563EB; text-align: center;">TeraSkill</h2>
<p style="color: #333;">Halo,</p>
<p style="color: #333;">Gunakan kode di bawah ini untuk memverifikasi akun Anda. Kode ini berlaku selama ${expiresInMinutes} menit.</p>
<div style="background-color: #f3f4f6; padding: 15px; text-align: center; border-radius: 5px; margin: 20px 0;">
<span style="font-size: 24px; font-weight: bold; letter-spacing: 5px; color: #1f2937;">${data.otp}</span>
</div>
<p style="color: #666; font-size: 12px; text-align: center;">
Jika Anda tidak meminta kode ini, abaikan saja email ini.
</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 20px 0;">
<p style="color: #999; font-size: 10px; text-align: center;">&copy; ${new Date().getFullYear()} PT Media Kreasi Abadi. All rights reserved.</p>
</div>
`;
try {
transporter.sendMail({
from: configService.get<string>('SMTP_FROM'),
to: data.email,
subject,
html,
});
console.log(`OTP sent to ${data.email}`);
} catch (error) {
console.error(`Failed to send OTP:`, error);
}
},
overrideDefaultEmailVerification: true,
}),
],
socialProviders: {
google: {
clientId: configService.get<string>('GOOGLE_CLIENT_ID_WEB')!,
clientSecret: configService.get<string>('GOOGLE_CLIENT_SECRET_WEB')!,
prompt: 'select_account',
},
},
});