Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add POST /api/v1/installation/users to seed user #1867

Merged
merged 8 commits into from
May 13, 2024
11 changes: 11 additions & 0 deletions packages/backend/bin/database/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import appConfig from '../../src/config/app.js';
import logger from '../../src/helpers/logger.js';
import client from './client.js';
import User from '../../src/models/user.js';
import Config from '../../src/models/config.js';
import Role from '../../src/models/role.js';
import '../../src/config/orm.js';
import process from 'process';
Expand All @@ -21,6 +22,14 @@ export async function createUser(
email = '[email protected]',
password = 'sample'
) {
if (appConfig.disableSeedUser) {
logger.info('Seed user is disabled.');

process.exit(0);

return;
}

const UNIQUE_VIOLATION_CODE = '23505';

const role = await fetchAdminRole();
Expand All @@ -37,6 +46,8 @@ export async function createUser(
if (userCount === 0) {
const user = await User.query().insertAndFetch(userParams);
logger.info(`User has been saved: ${user.email}`);

await Config.markInstallationCompleted();
} else {
logger.info('No need to seed a user.');
}
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/config/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const appConfig = {
disableFavicon: process.env.DISABLE_FAVICON === 'true',
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
disableSeedUser: process.env.DISABLE_SEED_USER === 'true',
};

if (!appConfig.encryptionKey) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import User from '../../../../../models/user.js';

export default async (request, response) => {
const { email, password, fullName } = request.body;

await User.createAdmin({ email, password, fullName });

response.status(204).end();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 { createUser } from '../../../../../../test/factories/user';
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 when no user', async () => {
expect(await Config.isInstallationCompleted()).toBe(false);

await request(app)
.post('/api/v1/installation/users')
.send({
email: '[email protected]',
password: 'password',
fullName: 'Initial admin'
})
.expect(204);

const user = await User.query().findOne({ email: '[email protected]' });

expect(user.roleId).toBe(adminRole.id);
expect(await Config.isInstallationCompleted()).toBe(true);
});

it('should respond with HTTP 403 with correct payload when one user exists at least', async () => {
expect(await Config.isInstallationCompleted()).toBe(false);

await createUser();

const usersCountBefore = await User.query().resultSize();

await request(app)
.post('/api/v1/installation/users')
.send({
email: '[email protected]',
password: 'password',
fullName: 'Initial admin'
})
.expect(403);

const usersCountAfter = await User.query().resultSize();

expect(usersCountBefore).toEqual(usersCountAfter);
});
});

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: '[email protected]',
password: 'password',
fullName: 'Initial admin'
})
.expect(403);

const user = await User.query().findOne({ email: '[email protected]' });

expect(user).toBeUndefined();
expect(await Config.isInstallationCompleted()).toBe(true);
});
})
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export async function up(knex) {
const users = await knex('users').limit(1);

// no user implies installation is not completed yet.
if (users.length === 0) return;

await knex('config').insert({
key: 'installation.completed',
value: {
data: true
}
});
};

export async function down(knex) {
await knex('config').where({ key: 'installation.completed' }).delete();
};
16 changes: 16 additions & 0 deletions packages/backend/src/helpers/allow-installation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Config from '../models/config.js';
import User from '../models/user.js';

export async function allowInstallation(request, response, next) {
if (await Config.isInstallationCompleted()) {
return response.status(403).end();
}

const hasAnyUsers = await User.query().resultSize() > 0;

if (hasAnyUsers) {
return response.status(403).end();
}

next();
};
22 changes: 22 additions & 0 deletions packages/backend/src/models/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
4 changes: 4 additions & 0 deletions packages/backend/src/models/role.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
16 changes: 16 additions & 0 deletions packages/backend/src/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Base from './base.js';
import App from './app.js';
import AccessToken from './access-token.js';
import Connection from './connection.js';
import Config from './config.js';
import Execution from './execution.js';
import Flow from './flow.js';
import Identity from './identity.ee.js';
Expand Down Expand Up @@ -373,6 +374,21 @@ class User extends Base {
return apps;
}

static async createAdmin({ email, password, fullName }) {
const adminRole = await Role.findAdmin();

const adminUser = await this.query().insert({
email,
password,
fullName,
roleId: adminRole.id
});

await Config.markInstallationCompleted();

return adminUser;
}

async $beforeInsert(queryContext) {
await super.$beforeInsert(queryContext);

Expand Down
14 changes: 14 additions & 0 deletions packages/backend/src/routes/api/v1/installation/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Router } from 'express';
import asyncHandler from 'express-async-handler';
import { allowInstallation } from '../../../../helpers/allow-installation.js';
import createUserAction from '../../../../controllers/api/v1/installation/users/create-user.js';

const router = Router();

router.post(
'/',
allowInstallation,
asyncHandler(createUserAction)
);

export default router;
3 changes: 3 additions & 0 deletions packages/backend/src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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;
4 changes: 4 additions & 0 deletions packages/backend/test/factories/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ export const createConfig = async (params = {}) => {

return config;
};

export const createInstallationCompletedConfig = async () => {
return await createConfig({ key: 'installation.completed', value: { data: true } });
}
2 changes: 1 addition & 1 deletion packages/backend/test/setup/global-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ global.beforeAll(async () => {
logger.silent = true;

// Remove default roles and permissions before running the test suite
await knex.raw('TRUNCATE TABLE roles, permissions CASCADE');
await knex.raw('TRUNCATE TABLE config, roles, permissions CASCADE');
});

global.beforeEach(async () => {
Expand Down