93 lines
2.6 KiB
TypeScript
93 lines
2.6 KiB
TypeScript
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<typeof BodySchema> }> = 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,
|
|
},
|
|
}
|