Merge pull request #1867 from automatisch/custom-user-seed
add POST /api/v1/installation/users to seed user
This commit is contained in:
@@ -2,6 +2,7 @@ import appConfig from '../../src/config/app.js';
|
|||||||
import logger from '../../src/helpers/logger.js';
|
import logger from '../../src/helpers/logger.js';
|
||||||
import client from './client.js';
|
import client from './client.js';
|
||||||
import User from '../../src/models/user.js';
|
import User from '../../src/models/user.js';
|
||||||
|
import Config from '../../src/models/config.js';
|
||||||
import Role from '../../src/models/role.js';
|
import Role from '../../src/models/role.js';
|
||||||
import '../../src/config/orm.js';
|
import '../../src/config/orm.js';
|
||||||
import process from 'process';
|
import process from 'process';
|
||||||
@@ -21,6 +22,14 @@ export async function createUser(
|
|||||||
email = 'user@automatisch.io',
|
email = 'user@automatisch.io',
|
||||||
password = 'sample'
|
password = 'sample'
|
||||||
) {
|
) {
|
||||||
|
if (appConfig.disableSeedUser) {
|
||||||
|
logger.info('Seed user is disabled.');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const UNIQUE_VIOLATION_CODE = '23505';
|
const UNIQUE_VIOLATION_CODE = '23505';
|
||||||
|
|
||||||
const role = await fetchAdminRole();
|
const role = await fetchAdminRole();
|
||||||
@@ -37,6 +46,8 @@ export async function createUser(
|
|||||||
if (userCount === 0) {
|
if (userCount === 0) {
|
||||||
const user = await User.query().insertAndFetch(userParams);
|
const user = await User.query().insertAndFetch(userParams);
|
||||||
logger.info(`User has been saved: ${user.email}`);
|
logger.info(`User has been saved: ${user.email}`);
|
||||||
|
|
||||||
|
await Config.markInstallationCompleted();
|
||||||
} else {
|
} else {
|
||||||
logger.info('No need to seed a user.');
|
logger.info('No need to seed a user.');
|
||||||
}
|
}
|
||||||
|
@@ -98,6 +98,7 @@ const appConfig = {
|
|||||||
disableFavicon: process.env.DISABLE_FAVICON === 'true',
|
disableFavicon: process.env.DISABLE_FAVICON === 'true',
|
||||||
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
|
additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK,
|
||||||
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
|
additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT,
|
||||||
|
disableSeedUser: process.env.DISABLE_SEED_USER === 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!appConfig.encryptionKey) {
|
if (!appConfig.encryptionKey) {
|
||||||
|
@@ -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();
|
||||||
|
};
|
@@ -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);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
@@ -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
packages/backend/src/helpers/allow-installation.js
Normal file
16
packages/backend/src/helpers/allow-installation.js
Normal file
@@ -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();
|
||||||
|
};
|
@@ -13,6 +13,28 @@ class Config extends Base {
|
|||||||
value: { type: 'object' },
|
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;
|
export default Config;
|
||||||
|
@@ -45,6 +45,10 @@ class Role extends Base {
|
|||||||
get isAdmin() {
|
get isAdmin() {
|
||||||
return this.key === 'admin';
|
return this.key === 'admin';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async findAdmin() {
|
||||||
|
return await this.query().findOne({ key: 'admin' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Role;
|
export default Role;
|
||||||
|
@@ -10,6 +10,7 @@ import Base from './base.js';
|
|||||||
import App from './app.js';
|
import App from './app.js';
|
||||||
import AccessToken from './access-token.js';
|
import AccessToken from './access-token.js';
|
||||||
import Connection from './connection.js';
|
import Connection from './connection.js';
|
||||||
|
import Config from './config.js';
|
||||||
import Execution from './execution.js';
|
import Execution from './execution.js';
|
||||||
import Flow from './flow.js';
|
import Flow from './flow.js';
|
||||||
import Identity from './identity.ee.js';
|
import Identity from './identity.ee.js';
|
||||||
@@ -373,6 +374,21 @@ class User extends Base {
|
|||||||
return apps;
|
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) {
|
async $beforeInsert(queryContext) {
|
||||||
await super.$beforeInsert(queryContext);
|
await super.$beforeInsert(queryContext);
|
||||||
|
|
||||||
|
14
packages/backend/src/routes/api/v1/installation/users.js
Normal file
14
packages/backend/src/routes/api/v1/installation/users.js
Normal file
@@ -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;
|
@@ -18,6 +18,7 @@ import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.
|
|||||||
import rolesRouter from './api/v1/admin/roles.ee.js';
|
import rolesRouter from './api/v1/admin/roles.ee.js';
|
||||||
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
import permissionsRouter from './api/v1/admin/permissions.ee.js';
|
||||||
import adminUsersRouter from './api/v1/admin/users.ee.js';
|
import adminUsersRouter from './api/v1/admin/users.ee.js';
|
||||||
|
import installationUsersRouter from './api/v1/installation/users.js';
|
||||||
|
|
||||||
const router = Router();
|
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/roles', rolesRouter);
|
||||||
router.use('/api/v1/admin/permissions', permissionsRouter);
|
router.use('/api/v1/admin/permissions', permissionsRouter);
|
||||||
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
|
router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter);
|
||||||
|
router.use('/api/v1/installation/users', installationUsersRouter);
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
@@ -11,3 +11,7 @@ export const createConfig = async (params = {}) => {
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createInstallationCompletedConfig = async () => {
|
||||||
|
return await createConfig({ key: 'installation.completed', value: { data: true } });
|
||||||
|
}
|
||||||
|
@@ -8,7 +8,7 @@ global.beforeAll(async () => {
|
|||||||
logger.silent = true;
|
logger.silent = true;
|
||||||
|
|
||||||
// Remove default roles and permissions before running the test suite
|
// 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 () => {
|
global.beforeEach(async () => {
|
||||||
|
Reference in New Issue
Block a user