import _ from 'lodash' import type { RouteHandler } from 'fastify' import * as z from 'zod' import { sql } from 'kysely' import config from '../../../config.ts' import StatusError from '../../../lib/status_error.ts' import { StatusErrorSchema } from '../../../schemas/status_error.ts' import { verifyPassword } from '../helpers.ts' const { errors, maxLoginAttempts, requireVerification } = config.auth const BodySchema = z.object({ email: z.string(), password: z.string(), remember: z.boolean().optional(), }) const ResponseSchema = { 200: z.object({ id: { type: 'integer' }, email: { type: 'string' }, password: { type: 'string' }, emailVerifiedAt: { type: 'string' }, bannedAt: { type: 'string' }, blockedAt: { type: 'string' }, loginAttempts: { type: 'integer' }, }), '4XX': StatusErrorSchema, } const login: RouteHandler<{ Body: z.infer }> = async function (request, reply) { const { db } = request.server try { const user = await db.selectFrom('user').selectAll().where('email', '=', request.body.email).executeTakeFirst() if (!user) { throw new StatusError(...errors.noUserFound) } else if (!user.password) { throw new StatusError(...errors.notLocal) } else if (user.loginAttempts && user.loginAttempts >= maxLoginAttempts) { throw new StatusError(...errors.tooManyLoginAttempts) } const result = await verifyPassword(request.body.password, user.password) if (!result) { await db .updateTable('user') .set({ lastLoginAttemptAt: sql`now()`, loginAttempts: sql`"loginAttempts" + 1` }) .where('id', '=', user.id) .execute() throw new StatusError(...errors.wrongPassword) } if (requireVerification && !user.emailVerifiedAt) { throw new StatusError(...errors.emailNotVerified) } else if (user.blockedAt) { throw new StatusError(...errors.blocked) } else if (user.bannedAt) { throw new StatusError(...errors.banned) } if (request.body.remember) { request.session.cookie.expires = new Date('2038-01-19T03:14:07.000Z') } await request.login(user) await db .updateTable('user') .set({ lastLoginAt: sql`now()`, loginAttempts: 0, lastLoginAttemptAt: null }) .where('id', '=', user.id) .execute() return reply.status(200).send(_.omit(user, 'password')) } catch (err) { this.log.error(err) if (err instanceof StatusError) return reply.status(err.status).send(err.toJSON()) else throw err } } export default { handler: login, schema: { body: BodySchema, schema: ResponseSchema, }, }