142 lines
3.7 KiB
TypeScript

import { AuthService, Credentials, NewUser, UserProfile, trim } from '@rondo.dev/common'
import Validator from '@rondo.dev/validator'
import { compare, hash } from 'bcrypt'
import { validate as validateEmail } from 'email-validator'
import createError from 'http-errors'
import { Database } from '../database/Database'
import { User } from '../entities/User'
import { UserEmail } from '../entities/UserEmail'
const SALT_ROUNDS = 10
const MIN_PASSWORD_LENGTH = 10
export class SQLAuthService implements AuthService {
constructor(protected readonly db: Database) {}
async createUser(payload: NewUser): Promise<UserProfile> {
const newUser = {
username: trim(payload.username),
firstName: trim(payload.firstName),
lastName: trim(payload.lastName),
}
if (!validateEmail(newUser.username)) {
throw createError(400, 'Username is not a valid e-mail')
}
if (payload.password.length < MIN_PASSWORD_LENGTH) {
throw createError(400,
`Password must be at least ${MIN_PASSWORD_LENGTH} characters long`)
}
new Validator(newUser)
.ensure('username')
.ensure('firstName')
.ensure('lastName')
.throw()
const password = await this.hash(payload.password)
const user = await this.db.getRepository(User).save({
...newUser,
password,
})
await this.db.getRepository(UserEmail).save({
email: newUser.username,
userId: user.id,
})
return {
id: user.id,
...newUser,
}
}
async findOne(id: number) {
const user = await this.db.getRepository(User).findOne(id, {
relations: ['emails'],
})
if (!user) {
return undefined
}
return {
id: user.id,
username: user.emails[0] ? user.emails[0].email : '',
firstName: user.firstName,
lastName: user.lastName,
}
}
async findUserByEmail(email: string) {
const userEmail = await this.db.getRepository(UserEmail)
.findOne({ email }, {
relations: ['user'],
})
if (!userEmail) {
return
}
const user = userEmail.user!
return {
id: userEmail.userId!,
username: userEmail.email,
firstName: user.firstName,
lastName: user.lastName,
}
}
async changePassword(params: {
userId: number
oldPassword: string
newPassword: string
}) {
const {userId, oldPassword, newPassword} = params
const userRepository = this.db.getRepository(User)
const user = await userRepository
.createQueryBuilder('user')
.select('user')
.addSelect('user.password')
.whereInIds([ userId ])
.getOne()
const isValid = await compare(oldPassword, user ? user.password! : '')
if (!(user && isValid)) {
throw createError(400, 'Passwords do not match')
}
const password = await this.hash(newPassword)
await userRepository
.update(userId, { password })
}
async validateCredentials(credentials: Credentials) {
const {username, password} = credentials
const user = await this.db.getRepository(User)
.createQueryBuilder('user')
.select('user')
.addSelect('user.password')
.addSelect('emails')
.innerJoin('user.emails', 'emails', 'emails.email = :email', {
email: username,
})
.getOne()
const isValid = await compare(password, user ? user.password! : '')
if (user && isValid) {
return {
id: user.id,
username: user.emails[0].email,
firstName: user.firstName,
lastName: user.lastName,
}
}
}
async findUserEmails(userId: number) {
return this.db.getRepository(UserEmail).find({ userId })
}
protected async hash(password: string): Promise<string> {
return hash(password, SALT_ROUNDS)
}
}