import { FastifyReply, FastifyRequest, FastifyInstance } from 'fastify'; import { Controller, GET, POST, PATCH, FastifyInstanceToken, Inject, DELETE } from 'fastify-decorators'; import { Repository } from 'typeorm'; import { User } from '../entities/User'; import { Zipline } from '../entities/Zipline'; import { UserNotFoundError, MissingBodyData, LoginError, UserExistsError } from '../lib/api/APIErrors'; import { Configuration, ConfigWebhooks } from '../lib/Config'; import { Console } from '../lib/logger'; import { checkPassword, createBaseCookie, createToken, encryptPassword, getFirst, readBaseCookie } from '../lib/Util'; import { WebhookType, WebhookHelper } from '../lib/Webhooks'; const config = Configuration.readConfig(); @Controller('/api/user') export class UserController { @Inject(FastifyInstanceToken) private instance!: FastifyInstance; private users: Repository = this.instance.orm.getRepository(User); private logger: Console = Console.logger(User); private webhooks: ConfigWebhooks = WebhookHelper.conf(config); @GET('/login-status') async loginStatus(req: FastifyRequest, reply: FastifyReply) { return reply.send({ user: !!req.cookies.zipline }); } @GET('/') async currentUser(req: FastifyRequest, reply: FastifyReply) { if (!req.cookies.zipline) throw new LoginError('Not logged in.'); const user = await this.users.findOne({ where: { id: readBaseCookie(req.cookies.zipline) } }); // eslint-disable-next-line quotes if (!user) throw new UserExistsError("User doesn't exist"); delete user.password; return reply.send(user); } @PATCH('/') async editUser( req: FastifyRequest<{ Body: { username: string; password: string } }>, reply: FastifyReply ) { if (!req.cookies.zipline) throw new LoginError('Not logged in.'); const user = await this.users.findOne({ where: { id: readBaseCookie(req.cookies.zipline) } }); // eslint-disable-next-line quotes if (!user) throw new UserExistsError("User doesn't exist"); this.logger.verbose(`attempting to save ${user.username} (${user.id})`); user.username = req.body.username; user.password = encryptPassword(req.body.password); await this.users.save(user); this.logger.info(`saved ${user.username} (${user.id})`); if (this.webhooks.events.includes(WebhookType.USER_EDIT)) WebhookHelper.sendWebhook(this.webhooks.user_edit.content, { user }); delete user.password; return reply.send(user); } @POST('/login') async login( req: FastifyRequest<{ Body: { username: string; password: string } }>, reply: FastifyReply ) { if (req.cookies.zipline) throw new LoginError('Already logged in.'); if (!req.body.username) throw new MissingBodyData('Missing username.'); if (!req.body.password) throw new MissingBodyData('Missing uassword.'); const user = await this.users.findOne({ where: { username: req.body.username } }); if (!user) throw new UserNotFoundError(`User "${req.body.username}" was not found.`); if (!checkPassword(req.body.password, user.password)) { this.logger.error( `${user.username} (${user.id}) tried to login but failed` ); throw new LoginError('Wrong credentials!'); } delete user.password; this.logger.verbose(`set cookie for ${user.username} (${user.id})`); reply.setCookie('zipline', createBaseCookie(user.id), { path: '/', maxAge: 1036800000 }); this.logger.info(`${user.username} (${user.id}) logged in`); if (this.webhooks.events.includes(WebhookType.LOGIN)) WebhookHelper.sendWebhook(this.webhooks.login.content, { user }); return reply.send(user); } @POST('/logout') async logout(req: FastifyRequest, reply: FastifyReply) { if (!req.cookies.zipline) throw new LoginError('Not logged in.'); try { reply.clearCookie('zipline', { path: '/' }); return reply.send({ clearStore: true }); } catch (e) { reply.send({ clearStore: false }); } } @POST('/reset-token') async resetToken(req: FastifyRequest, reply: FastifyReply) { if (!req.cookies.zipline) throw new LoginError('Not logged in.'); const user = await this.users.findOne({ where: { id: readBaseCookie(req.cookies.zipline) } }); if (!user) throw new UserNotFoundError('User was not found.'); this.logger.verbose( `attempting to reset token ${user.username} (${user.id})` ); user.token = createToken(); await this.users.save(user); this.logger.info(`reset token ${user.username} (${user.id})`); if (this.webhooks.events.includes(WebhookType.TOKEN_RESET)) WebhookHelper.sendWebhook(this.webhooks.token_reset.content, { user }); return reply.send({ updated: true }); } @POST('/create') async create( req: FastifyRequest<{ Body: { username: string; password: string; administrator: boolean }; }>, reply: FastifyReply ) { if (!req.body.username) throw new MissingBodyData('Missing username.'); if (!req.body.password) throw new MissingBodyData('Missing uassword.'); const existing = await this.users.findOne({ where: { username: req.body.username } }); if (existing) throw new UserExistsError('User exists already'); try { this.logger.verbose(`attempting to create ${req.body.username}`); const user = await this.users.save( new User( req.body.username, encryptPassword(req.body.password), createToken(), req.body.administrator || false ) ); this.logger.info(`created user ${user.username} (${user.id})`); if (this.webhooks.events.includes(WebhookType.CREATE_USER)) WebhookHelper.sendWebhook(this.webhooks.create_user.content, { user }); const firstSetup = await getFirst(this.instance.orm); if (firstSetup) await this.instance.orm.getRepository(Zipline).update( { id: 'zipline' }, { first: false } ); delete user.password; return reply.send(user); } catch (e) { throw new Error(`Could not create user: ${e.message}`); } } @DELETE('/:id') async delete( req: FastifyRequest<{ Params: { id: string; }; }>, reply: FastifyReply ) { const existing = await this.users.findOne({ where: { id: req.params.id } }); if (!existing) throw new UserExistsError('User doesnt exist'); try { this.logger.verbose( `attempting to delete ${existing.username} (${existing.id})` ); await this.users.delete({ id: existing.id }); this.logger.info(`deleted ${existing.username} (${existing.id})`); if (this.webhooks.events.includes(WebhookType.USER_DELETE)) WebhookHelper.sendWebhook(this.webhooks.user_delete.content, { user: existing }); return reply.send({ ok: true }); } catch (e) { throw new Error(`Could not delete user: ${e.message}`); } } // @Hook('preValidation') // public async preValidation(req: FastifyRequest, reply: FastifyReply) { // // const adminRoutes = ['/api/user/create']; // // if (adminRoutes.includes(req.routerPath)) { // // if (!req.cookies.zipline) return reply.send({ error: "You are not logged in" }); // // const admin = await this.instance.mongo.db.collection('zipline_users').findOne({ _id: req.cookies.zipline }); // // if (!admin) return reply.send({ error: "You are not an administrator" }); // // return; // // } // // return; // } }