From 10290ce6e31e272a3d5b67e3d4e09f7a1ba22d50 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 7 May 2024 15:39:22 +0000 Subject: [PATCH] feat: add POST /api/v1/installation/users to seed user --- .../api/v1/installation/users/create-user.js | 12 ++++ .../v1/installation/users/create-user.test.js | 62 +++++++++++++++++++ .../src/helpers/authorize-installation.js | 9 +++ packages/backend/src/models/config.js | 22 +++++++ packages/backend/src/models/role.js | 4 ++ packages/backend/src/models/user.js | 13 ++++ .../src/routes/api/v1/installation/users.js | 14 +++++ packages/backend/src/routes/index.js | 3 + packages/backend/test/factories/config.js | 4 ++ 9 files changed, 143 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/installation/users/create-user.js create mode 100644 packages/backend/src/controllers/api/v1/installation/users/create-user.test.js create mode 100644 packages/backend/src/helpers/authorize-installation.js create mode 100644 packages/backend/src/routes/api/v1/installation/users.js diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.js new file mode 100644 index 00000000..ee6a5ce7 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.js @@ -0,0 +1,12 @@ +import User from '../../../../../models/user.js'; +import Config from '../../../../../models/config.js'; + +export default async (request, response) => { + const { email, password, fullName } = request.body; + + await User.createAdminUser({ email, password, fullName }); + + await Config.markInstallationCompleted(); + + response.status(204).end(); +}; diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js new file mode 100644 index 00000000..17de0174 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import Config from '../../../../../models/config.js'; +import User from '../../../../../models/user.js'; +import { createRole } from '../../../../../../test/factories/role'; +import { createInstallationCompletedConfig } from '../../../../../../test/factories/config'; + +describe('POST /api/v1/installation/users', () => { + let adminRole; + + beforeEach(async () => { + adminRole = await createRole({ + name: 'Admin', + key: 'admin', + }) + }); + + describe('for incomplete installations', () => { + it('should respond with HTTP 204 with correct payload', async () => { + expect(await Config.isInstallationCompleted()).toBe(false); + + await request(app) + .post('/api/v1/installation/users') + .send({ + email: 'user@automatisch.io', + password: 'password', + fullName: 'Initial admin' + }) + .expect(204); + + const user = await User.query().findOne({ email: 'user@automatisch.io' }); + + expect(user.roleId).toBe(adminRole.id); + expect(await Config.isInstallationCompleted()).toBe(true); + }); + }); + + describe('for completed installations', () => { + beforeEach(async () => { + await createInstallationCompletedConfig(); + }); + + it('should respond with HTTP 403 when installation completed', async () => { + expect(await Config.isInstallationCompleted()).toBe(true); + + await request(app) + .post('/api/v1/installation/users') + .send({ + email: 'user@automatisch.io', + password: 'password', + fullName: 'Initial admin' + }) + .expect(403); + + const user = await User.query().findOne({ email: 'user@automatisch.io' }); + + expect(user).toBeUndefined(); + expect(await Config.isInstallationCompleted()).toBe(true); + }); + }) +}); diff --git a/packages/backend/src/helpers/authorize-installation.js b/packages/backend/src/helpers/authorize-installation.js new file mode 100644 index 00000000..09a2b733 --- /dev/null +++ b/packages/backend/src/helpers/authorize-installation.js @@ -0,0 +1,9 @@ +import Config from '../models/config.js'; + +export async function authorizeInstallation(request, response, next) { + if (await Config.isInstallationCompleted()) { + return response.status(403).end(); + } else { + next(); + } +}; diff --git a/packages/backend/src/models/config.js b/packages/backend/src/models/config.js index 949c6aaf..b65b6ece 100644 --- a/packages/backend/src/models/config.js +++ b/packages/backend/src/models/config.js @@ -13,6 +13,28 @@ class Config extends Base { value: { type: 'object' }, }, }; + + static async isInstallationCompleted() { + const installationCompletedEntry = await this + .query() + .where({ + key: 'installation.completed' + }) + .first(); + + const installationCompleted = installationCompletedEntry?.value?.data === true; + + return installationCompleted; + } + + static async markInstallationCompleted() { + return await this.query().insert({ + key: 'installation.completed', + value: { + data: true, + }, + }); + } } export default Config; diff --git a/packages/backend/src/models/role.js b/packages/backend/src/models/role.js index af6ccafa..08b19673 100644 --- a/packages/backend/src/models/role.js +++ b/packages/backend/src/models/role.js @@ -45,6 +45,10 @@ class Role extends Base { get isAdmin() { return this.key === 'admin'; } + + static async findAdmin() { + return await this.query().findOne({ key: 'admin' }); + } } export default Role; diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js index 893f057e..636034a0 100644 --- a/packages/backend/src/models/user.js +++ b/packages/backend/src/models/user.js @@ -373,6 +373,19 @@ class User extends Base { return apps; } + static async createAdminUser({ email, password, fullName }) { + const adminRole = await Role.findAdmin(); + + const adminUser = await this.query().insert({ + email, + password, + fullName, + roleId: adminRole.id + }); + + return adminUser; + } + async $beforeInsert(queryContext) { await super.$beforeInsert(queryContext); diff --git a/packages/backend/src/routes/api/v1/installation/users.js b/packages/backend/src/routes/api/v1/installation/users.js new file mode 100644 index 00000000..f2b16feb --- /dev/null +++ b/packages/backend/src/routes/api/v1/installation/users.js @@ -0,0 +1,14 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authorizeInstallation } from '../../../../helpers/authorize-installation.js'; +import createUserAction from '../../../../controllers/api/v1/installation/users/create-user.js'; + +const router = Router(); + +router.post( + '/', + authorizeInstallation, + asyncHandler(createUserAction) +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index a7f8c30a..c67a9cf3 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -18,6 +18,7 @@ import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee. import rolesRouter from './api/v1/admin/roles.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js'; import adminUsersRouter from './api/v1/admin/users.ee.js'; +import installationUsersRouter from './api/v1/installation/users.js'; const router = Router(); @@ -40,5 +41,7 @@ router.use('/api/v1/admin/users', adminUsersRouter); router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/permissions', permissionsRouter); router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter); +router.use('/api/v1/installation/users', installationUsersRouter); + export default router; diff --git a/packages/backend/test/factories/config.js b/packages/backend/test/factories/config.js index a8d59787..5a8e316c 100644 --- a/packages/backend/test/factories/config.js +++ b/packages/backend/test/factories/config.js @@ -11,3 +11,7 @@ export const createConfig = async (params = {}) => { return config; }; + +export const createInstallationCompletedConfig = async () => { + return await createConfig({ key: 'installation.completed', value: { data: true } }); +}