blob: 7dfa24f6d5fb26a82deaa43252260c5e1483c46e [file] [log] [blame]
Michelle Sepkap Sime6967fb92022-11-08 08:39:36 -05001/*
2 * Copyright (C) 2022 Savoir-faire Linux Inc.
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation; either version 3 of the
7 * License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
13 *
14 * You should have received a copy of the GNU Affero General Public
15 * License along with this program. If not, see
16 * <https://www.gnu.org/licenses/>.
17 */
18import argon2 from 'argon2';
19import { Router } from 'express';
20import asyncHandler from 'express-async-handler';
21import { ParamsDictionary, Request } from 'express-serve-static-core';
22import { HttpStatusCode } from 'jami-web-common';
23import { SignJWT } from 'jose';
24import { Container } from 'typedi';
25
26import { AdminConfig } from '../admin-config.js';
27import { checkAdminSetup } from '../middleware/setup.js';
28import { Vault } from '../vault.js';
29
30export const setupRouter = Router();
31
32const vault = Container.get(Vault);
33const adminConfig = Container.get(AdminConfig);
34
35setupRouter.get('/check', (_req, res, _next) => {
36 const isSetupComplete = adminConfig.get() !== undefined;
37 res.send({ isSetupComplete });
38});
39
40setupRouter.post(
41 '/admin/create',
42 asyncHandler(async (req: Request<ParamsDictionary, string, { password?: string }>, res, _next) => {
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050043 const { password } = req.body;
44 if (password === undefined) {
45 res.status(HttpStatusCode.BadRequest).send('Missing password in body');
Michelle Sepkap Sime6967fb92022-11-08 08:39:36 -050046 return;
47 }
48
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050049 if (password === '') {
50 res.status(HttpStatusCode.BadRequest).send('Password may not be empty');
51 return;
52 }
53
54 const isAdminCreated = adminConfig.get() !== undefined;
55 if (isAdminCreated) {
56 res.status(HttpStatusCode.Conflict).send('Admin already exists');
Michelle Sepkap Sime6967fb92022-11-08 08:39:36 -050057 return;
58 }
59
60 const hashedPassword = await argon2.hash(password, { type: argon2.argon2id });
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050061
Michelle Sepkap Sime6967fb92022-11-08 08:39:36 -050062 adminConfig.set(hashedPassword);
63 await adminConfig.save();
64
65 res.sendStatus(HttpStatusCode.Created);
66 })
67);
68
69// Every request handler after this line will be submitted to this middleware
70// in order to ensure that the admin account is set up before proceeding with
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050071// setup related requests
Michelle Sepkap Sime6967fb92022-11-08 08:39:36 -050072setupRouter.use(checkAdminSetup);
73
74setupRouter.post(
75 '/admin/login',
76 asyncHandler(
77 async (req: Request<ParamsDictionary, { accessToken: string } | string, { password: string }>, res, _next) => {
78 const { password } = req.body;
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050079 if (password === undefined) {
80 res.status(HttpStatusCode.BadRequest).send('Missing password in body');
Michelle Sepkap Sime6967fb92022-11-08 08:39:36 -050081 return;
82 }
83
84 const hashedPassword = adminConfig.get();
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050085
Michelle Sepkap Sime6967fb92022-11-08 08:39:36 -050086 const isPasswordVerified = await argon2.verify(hashedPassword, password);
87 if (!isPasswordVerified) {
Misha Krieger-Raynauldcb11bba2022-11-11 18:08:33 -050088 res.status(HttpStatusCode.Forbidden).send('Incorrect password');
Michelle Sepkap Sime6967fb92022-11-08 08:39:36 -050089 return;
90 }
91
92 const jwt = await new SignJWT({ id: 'admin' })
93 .setProtectedHeader({ alg: 'EdDSA' })
94 .setIssuedAt()
95 // TODO: use valid issuer and audience
96 .setIssuer('urn:example:issuer')
97 .setAudience('urn:example:audience')
98 .setExpirationTime('2h')
99 .sign(vault.privateKey);
100 res.send({ accessToken: jwt });
101 }
102 )
103);