added 2FA

This commit is contained in:
2024-04-26 22:16:21 +03:00
parent 53cadc289a
commit f17a002ac6
38 changed files with 1036 additions and 414 deletions

45
actions/logger.ts Normal file
View File

@@ -0,0 +1,45 @@
import pino from 'pino'
const pinoConfigProd: pino.LoggerOptions = {
transport: {
targets: [
{
target: 'pino/file', options: {
destination: './production.log', mkdir: true, minLength: 4096, sync: false,
},
},
],
},
level: 'error',
redact: {
paths: ['password', '*.password'], remove: true,
},
}
const pinoConfigDev: pino.LoggerOptions = {
redact: {
paths: ['password', '*.password'], remove: false,
},
// formatters: {
// bindings: (bindings) => {
// return { pid: bindings.pid, host: bindings.hostname, node_version: process.version }
// },
// },
transport: {
targets: [
{
//target: 'pino/file',
target: 'pino-pretty', options: { destination: `./pretty.log`, mkdir: true, colorize: false }, level: 'error',
}, {
target: 'pino-pretty', level: 'trace',
}],
},
}
const journal = process.env.NODE_ENV === 'production'
? pino(pinoConfigProd)
: pino(pinoConfigDev)
export default journal
// TODO: wait for newer version of https://betterstack.com/docs/logs/javascript/pino/

View File

@@ -6,7 +6,15 @@ import { signIn } from '@/config/auth'
import { DEFAULT_LOGIN_REDIRECT } from '@/config/routes'
import { AuthError } from 'next-auth'
import { getUserByEmail } from '@/data/user'
import { sendVerificationEmail } from '@/actions/send-verification-email'
import { sendTwoFactorTokenEmail, sendVerificationEmail } from '@/actions/send-verification-email'
import { generateTwoFactorToken } from '@/lib/tokens'
import { deleteTwoFactorToken, getTwoFactorTokenByEmail } from '@/data/two-factor-token'
import {
createTwoFactoComfirmation,
deleteTwoFactoComfirmation,
getTwoFactorConfirmationByUserId,
} from '@/data/two-factor-confirmation'
import journal from '@/actions/logger'
export const login = async (values: zInfer<typeof LoginSchema>) => {
const validatedFields = LoginSchema.safeParse(values)
@@ -15,7 +23,7 @@ export const login = async (values: zInfer<typeof LoginSchema>) => {
return { error: 'auth.form.error.invalid_fields' }
}
const { email, password } = validatedFields.data
const { email, password, code } = validatedFields.data
const existingUser = await getUserByEmail(email)
@@ -27,6 +35,40 @@ export const login = async (values: zInfer<typeof LoginSchema>) => {
return await sendVerificationEmail(existingUser.email, existingUser.name)
}
if (existingUser.isTwoFactorEnabled && existingUser.email) {
if (code) {
const twoFactorToken = await getTwoFactorTokenByEmail(existingUser.email)
if (!twoFactorToken || twoFactorToken.token !== code) {
return { error: 'auth.form.error.invalid_code' }
}
const hasExpired = new Date(twoFactorToken.expires) < new Date()
if (hasExpired) {
return { error: 'auth.form.error.expired_token' }
}
await deleteTwoFactorToken(twoFactorToken.id)
const existingConfirmation = await getTwoFactorConfirmationByUserId(existingUser.id)
if (existingConfirmation) {
await deleteTwoFactoComfirmation(existingConfirmation.id)
}
await createTwoFactoComfirmation(existingUser.id)
} else {
const twoFactorToken = await generateTwoFactorToken(existingUser.email)
if (twoFactorToken) {
const isOk = await sendTwoFactorTokenEmail(twoFactorToken.email, twoFactorToken.token, existingUser.name)
return { twoFactor: isOk }
}
console.error('ERROR.TYPE: could not send token')
return { error: 'common.something_went_wrong' }
}
}
try {
await signIn('credentials', {
email, password, redirectTo: DEFAULT_LOGIN_REDIRECT,

View File

@@ -2,11 +2,32 @@
import mailer from '@/lib/mailer'
import { AUTH_NEW_PASSWORD_URL, AUTH_USER_VERIFICATION_URL } from '@/config/routes'
import { generatePasswordResetToken, generateVerificationToken } from '@/lib/tokens'
import { generatePasswordResetToken, generateTwoFactorToken, generateVerificationToken } from '@/lib/tokens'
import { env } from '@/lib/utils'
import { __ct } from '@/lib/translate'
import { body } from '@/templates/email/send-verification-email'
export const sendTwoFactorTokenEmail = async (email: string, token: string, name?: string | null) => {
const { isOk, code, info, error } = await mailer({
to: name ? { name: name?.toString(), address: email } : email,
subject: await __ct({
key: 'mailer.subject.send_2FA_code',
params: { site_name: env('SITE_NAME') },
}),
text: `Your 2FA code: ${token}`,
html: `<p>Your 2FA code: ${token}</p>`,
})
return isOk
// TODO: Log this action
// if (isOk && code === 250) {
// //return //'auth.email.success._2FA_email_sent'
// return { success: code === 250 ? 'auth.email.success._2FA_email_sent' : info?.response }
// } else {
// return { error: env('DEBUG') === 'true' ? error?.response : 'auth.email.error._2FA_email_sending_error' }
// }
}
const sendVerificationEmail = async (
email: string,
name?: string | null,

View File

@@ -1,8 +1,7 @@
'use server'
import db from '@/lib/db'
import { getVerificationTokenByToken } from '@/data/verification-token'
import { getUserByEmail } from '@/data/user'
import { deleteVerificationToken, getVerificationTokenByToken } from '@/data/verification-token'
import { getUserByEmail, updateUserEmailVerified } from '@/data/user'
export const userVerification = async (token: string) => {
const existingToken = await getVerificationTokenByToken(token)
@@ -17,25 +16,9 @@ export const userVerification = async (token: string) => {
if (!existingUser) return { error: 'Email associated with token not found!' }
try {
await db.user.update({
where: { id: existingUser.id }, data: {
email: existingToken.email, emailVerified: new Date(),
},
})
} catch (e) {
console.error(e)
return { error: 'db.error.update.user_data' }
}
await updateUserEmailVerified(existingUser.id, existingToken.email)
try {
await db.verificationToken.delete({
where: { id: existingToken.id },
})
} catch (e) {
// TODO: log error on disc or db
console.error(e)
}
await deleteVerificationToken(existingToken.id)
return { success: 'User verified!' }
}