Skip to content

Commit

Permalink
Merge pull request #1867 from automatisch/custom-user-seed
Browse files Browse the repository at this point in the history
add POST /api/v1/installation/users to seed user
  • Loading branch information
barinali committed May 13, 2024
2 parents 5aeb4f8 + 4144944 commit 9548c93
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 1 deletion.
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 = 'user@automatisch.io',
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: '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);
});

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: 'user@automatisch.io',
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: '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);
});
})
});
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

0 comments on commit 9548c93

Please sign in to comment.