diff --git a/config.json b/config.json index 0164242b7..c6224c263 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,4 @@ { - "username": "admin", - "password": "secret", "userAuth": { "username": "user", "password": "secret" diff --git a/src/api/controller/api/login.js b/src/api/controller/api/login.js index 52643e77f..b9c458166 100644 --- a/src/api/controller/api/login.js +++ b/src/api/controller/api/login.js @@ -7,17 +7,22 @@ import { auth } from 'config'; import jwt from 'jsonwebtoken'; import { ADMIN_ROLE, ROOT_ROLE } from '../../../common/tools/tenantTools'; -export const postLogin = date => ctx => { +export const postLogin = date => async ctx => { if (!ctx.ezMasterConfig) { throw new Error('Invalid EzMaster configuration.'); } - if (!ctx.ezMasterConfig.username) { - throw new Error('Invalid EzMaster configuration: missing username'); + const { + username: usernameAdmin, + password: passwordAdmin, + } = await ctx.tenantCollection.findOneByName(ctx.tenant.toLowerCase()); + + if (!usernameAdmin) { + throw new Error('Invalid instance configuration: missing username'); } - if (!ctx.ezMasterConfig.password) { - throw new Error('Invalid EzMaster configuration: missing password.'); + if (!passwordAdmin) { + throw new Error('Invalid instance configuration: missing password.'); } const { username, password } = ctx.request.body; @@ -25,10 +30,7 @@ export const postLogin = date => ctx => { const rootAuth = get(ctx, 'ezMasterConfig.rootAuth', {}); let role; - if ( - username === ctx.ezMasterConfig.username && - password === ctx.ezMasterConfig.password - ) { + if (username === usernameAdmin && password === passwordAdmin) { role = ADMIN_ROLE; } diff --git a/src/api/controller/api/login.spec.js b/src/api/controller/api/login.spec.js index ca5162be0..d97500686 100644 --- a/src/api/controller/api/login.spec.js +++ b/src/api/controller/api/login.spec.js @@ -7,12 +7,16 @@ import { ADMIN_ROLE } from '../../../common/tools/tenantTools'; const expDate = Date.now(); describe('login', () => { - it('should set ctx.status to 401, if ctx.body.username do not match with config', () => { + it('should set ctx.status to 401, if ctx.body.username do not match with config', async () => { const ctx = { - ezMasterConfig: { - username: 'admin', - password: 'secret', + ezMasterConfig: {}, + tenantCollection: { + findOneByName: () => ({ + username: 'admin', + password: 'secret', + }), }, + tenant: 'default', request: { body: { username: 'not admin', @@ -20,16 +24,20 @@ describe('login', () => { }, }, }; - login(expDate)(ctx); + await login(expDate)(ctx); expect(ctx.status).toBe(401); }); - it('should set ctx.status to 401, if ctx.body.password do not match with config', () => { + it('should set ctx.status to 401, if ctx.body.password do not match with config', async () => { const ctx = { - ezMasterConfig: { - username: 'admin', - password: 'secret', + ezMasterConfig: {}, + tenantCollection: { + findOneByName: () => ({ + username: 'admin', + password: 'secret', + }), }, + tenant: 'default', request: { body: { username: 'user', @@ -37,21 +45,24 @@ describe('login', () => { }, }, }; - login(expDate)(ctx); + await login(expDate)(ctx); expect(ctx.status).toBe(401); }); - it('should return header token and set cookie with cookie token for admin when password and user name match config', () => { + it('should return header token and set cookie with cookie token for admin when password and user name match config', async () => { let setCall; const ctx = { - ezMasterConfig: { - username: 'user', - password: 'secret', + ezMasterConfig: {}, + tenantCollection: { + findOneByName: () => ({ + username: 'admin', + password: 'secret', + }), }, - tenant: 'test', + tenant: 'default', request: { body: { - username: 'user', + username: 'admin', password: 'secret', }, }, @@ -62,11 +73,11 @@ describe('login', () => { }, }; - login(expDate)(ctx); + await login(expDate)(ctx); expect(ctx.body).toEqual({ token: jwt.sign( { - username: 'user', + username: 'admin', role: ADMIN_ROLE, exp: Math.ceil(expDate / 1000) + auth.expiresIn, }, @@ -75,10 +86,10 @@ describe('login', () => { role: ADMIN_ROLE, }); expect(setCall).toEqual([ - 'lodex_token_test', + 'lodex_token_default', jwt.sign( { - username: 'user', + username: 'admin', role: ADMIN_ROLE, exp: Math.ceil(expDate / 1000) + auth.expiresIn, }, @@ -89,16 +100,21 @@ describe('login', () => { }); describe('user authentication', () => { - it('should set ctx.status to 401, if ctx.body.username do not match with userAuth config', () => { + it('should set ctx.status to 401, if ctx.body.username do not match with userAuth config', async () => { const ctx = { ezMasterConfig: { - username: 'admin', - password: 'secret', userAuth: { username: 'user', password: 'secret', }, }, + tenantCollection: { + findOneByName: () => ({ + username: 'admin', + password: 'secret', + }), + }, + tenant: 'default', request: { body: { username: 'not user', @@ -106,20 +122,25 @@ describe('login', () => { }, }, }; - login(expDate)(ctx); + await login(expDate)(ctx); expect(ctx.status).toBe(401); }); - it('should set ctx.status to 401, if ctx.body.password do not match with config', () => { + it('should set ctx.status to 401, if ctx.body.password do not match with config', async () => { const ctx = { ezMasterConfig: { - username: 'admin', - password: 'secret', userAuth: { username: 'user', password: 'secret', }, }, + tenantCollection: { + findOneByName: () => ({ + username: 'admin', + password: 'secret', + }), + }, + tenant: 'default', request: { body: { username: 'user', @@ -127,22 +148,26 @@ describe('login', () => { }, }, }; - login(expDate)(ctx); + await login(expDate)(ctx); expect(ctx.status).toBe(401); }); - it('should return header token and set cookie with cookie token for user when password and user name match userAuth config', () => { + it('should return header token and set cookie with cookie token for user when password and user name match userAuth config', async () => { let setCall; const ctx = { ezMasterConfig: { - username: 'admin', - password: 'secret', userAuth: { username: 'user', password: 'secret', }, }, - tenant: 'test', + tenantCollection: { + findOneByName: () => ({ + username: 'admin', + password: 'secret', + }), + }, + tenant: 'default', request: { body: { username: 'user', @@ -156,7 +181,7 @@ describe('login', () => { }, }; - login(expDate)(ctx); + await login(expDate)(ctx); expect(ctx.body).toEqual({ token: jwt.sign( { @@ -169,7 +194,7 @@ describe('login', () => { role: 'user', }); expect(setCall).toEqual([ - 'lodex_token_test', + 'lodex_token_default', jwt.sign( { username: 'user', diff --git a/src/api/controller/rootAdmin.js b/src/api/controller/rootAdmin.js index 146d48240..de10804ca 100644 --- a/src/api/controller/rootAdmin.js +++ b/src/api/controller/rootAdmin.js @@ -24,14 +24,14 @@ app.use(async (ctx, next) => { if (!ctx.state.cookie) { ctx.status = 401; ctx.cookies.set('lodex_token_root', '', { expires: new Date() }); - ctx.body = 'No authentication token found'; + ctx.body = { message: 'No authentication token found' }; return; } if (ctx.state.cookie.role !== ROOT_ROLE) { ctx.status = 401; ctx.cookies.set('lodex_token_root', '', { expires: new Date() }); - ctx.body = 'No root token found'; + ctx.body = { message: 'No root token found' }; return; } @@ -55,6 +55,8 @@ const postTenant = async ctx => { name, description, author, + username: 'admin', + password: 'secret', createdAt: new Date(), }); const queue = createWorkerQueue(name, 1); @@ -63,6 +65,19 @@ const postTenant = async ctx => { } }; +const putTenant = async (ctx, id) => { + const { description, author, username, password } = ctx.request.body; + const tenantExists = await ctx.tenantCollection.findOneById(id); + + if (!tenantExists) { + ctx.throw(403, `Invalid id: "${id}"`); + } + + const update = { description, author, username, password }; + await ctx.tenantCollection.update(id, update); + ctx.body = await ctx.tenantCollection.findAll(); +}; + const deleteTenant = async ctx => { const { _id, name } = ctx.request.body; const tenantExists = await ctx.tenantCollection.findOne({ @@ -82,6 +97,7 @@ const deleteTenant = async ctx => { app.use(route.get('/tenant', getTenant)); app.use(route.post('/tenant', postTenant)); +app.use(route.put('/tenant/:id', putTenant)); app.use(route.delete('/tenant', deleteTenant)); app.use(async ctx => { diff --git a/src/api/controller/testController.js b/src/api/controller/testController.js index 2861d16bd..9a09d1964 100644 --- a/src/api/controller/testController.js +++ b/src/api/controller/testController.js @@ -7,6 +7,7 @@ import mount from 'koa-mount'; import repositoryMiddleware, { mongoRootAdminClient, } from '../services/repositoryMiddleware'; +import { DEFAULT_TENANT } from '../../common/tools/tenantTools'; const app = new koa(); @@ -21,8 +22,9 @@ app.use( await ctx.db.collection('dataset').remove({}); await ctx.db.collection('subresource').remove({}); await ctx.db.collection('enrichment').remove({}); - await ctx.rootAdminDb.collection('tenant').remove({}); - + await ctx.rootAdminDb + .collection('tenant') + .remove({ name: { $ne: DEFAULT_TENANT } }); ctx.body = { status: 'ok' }; }), ); diff --git a/src/api/index.js b/src/api/index.js index f304281a5..e6f800ad2 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -69,17 +69,30 @@ serverAdapter.setBasePath('/bull'); const initQueueAndBullDashboard = async () => { bullBoard.initBullBoard(serverAdapter); - const defaultQueue = createWorkerQueue(DEFAULT_TENANT, 1); - bullBoard.addDashboardQueue(DEFAULT_TENANT, defaultQueue); - // Get current tenants const adminDb = await mongoClient('admin'); const tenantCollection = await tenant(adminDb); + const tenants = await tenantCollection.findAll(); tenants.forEach(tenant => { const queue = createWorkerQueue(tenant.name, 1); bullBoard.addDashboardQueue(tenant.name, queue); }); + + // if tenant `default` is not in the database, we add it + if (!tenants.find(tenant => tenant.name === DEFAULT_TENANT)) { + await tenantCollection.create({ + name: DEFAULT_TENANT, + description: 'Instance par défaut', + author: 'Root', + username: 'admin', + password: 'secret', + createdAt: new Date(), + }); + const defaultQueue = createWorkerQueue(DEFAULT_TENANT, 1); + bullBoard.addDashboardQueue(DEFAULT_TENANT, defaultQueue); + // TODO: create default instance config. + } }; initQueueAndBullDashboard(); diff --git a/src/api/services/ezMasterConfig.js b/src/api/services/ezMasterConfig.js index 95bef3e6e..1a542dd1a 100644 --- a/src/api/services/ezMasterConfig.js +++ b/src/api/services/ezMasterConfig.js @@ -1,11 +1,6 @@ import expect from 'expect'; export const validateConfig = config => { - expect(config).toMatchObject({ - username: /.+/, - password: /.+/, - }); - if (config.userAuth) { expect(config.userAuth).toMatchObject({ username: /.+/, diff --git a/src/api/services/ezMasterConfig.spec.js b/src/api/services/ezMasterConfig.spec.js index e4af19eb3..76f95d6f7 100644 --- a/src/api/services/ezMasterConfig.spec.js +++ b/src/api/services/ezMasterConfig.spec.js @@ -2,18 +2,6 @@ import ezMasterConfig, { validateConfig } from './ezMasterConfig'; describe('ezMasterConfig', () => { describe('validateConfig', () => { - it('should throw if username is not present', () => { - expect(() => validateConfig({})).toThrow(); - }); - - it('should throw if password is not present', () => { - expect(() => - validateConfig({ - username: 'toto', - }), - ).toThrow(); - }); - it('should throw if userAuth but no userAuth.username', () => { expect(() => validateConfig({ diff --git a/src/app/js/root-admin/Tenants.js b/src/app/js/root-admin/Tenants.js index a62e65c73..33e95b9b8 100644 --- a/src/app/js/root-admin/Tenants.js +++ b/src/app/js/root-admin/Tenants.js @@ -4,6 +4,7 @@ import 'react-toastify/dist/ReactToastify.css'; import PropTypes from 'prop-types'; import AddBoxIcon from '@mui/icons-material/AddBox'; import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; import { getHost } from '../../../common/uris'; import CreateTenantDialog from './CreateTenantDialog'; @@ -16,6 +17,7 @@ import { GridToolbarFilterButton, } from '@mui/x-data-grid'; import { Button, Tooltip } from '@mui/material'; +import UpdateTenantDialog from './UpdateTenantDialog'; const baseUrl = getHost(); @@ -23,6 +25,7 @@ const Tenants = ({ handleLogout }) => { const [tenants, setTenants] = useState([]); const [openCreateTenantDialog, setOpenCreateTenantDialog] = useState(false); const [openDeleteTenantDialog, setOpenDeleteTenantDialog] = useState(false); + const [tenantToUpdate, setTenantToUpdate] = useState(null); const onChangeTenants = changedTenants => { if (changedTenants instanceof Array) { @@ -95,6 +98,55 @@ const Tenants = ({ handleLogout }) => { }); }; + const updateTenant = (id, updatedTenant) => { + fetch(`/rootAdmin/tenant/${id}`, { + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'X-Lodex-Tenant': 'admin', + }, + method: 'PUT', + body: JSON.stringify(updatedTenant), + }) + .then(response => { + if (response.status === 401) { + handleLogout(); + return; + } + + if (response.status === 403) { + toast.error('Action non autorisée', { + position: 'top-right', + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + theme: 'light', + }); + return; + } + + if (response.status === 200) { + if (response.status === 200) { + toast.success('Instance modifiée', { + position: 'top-right', + autoClose: 5000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + theme: 'light', + }); + } + } + + return response.json(); + }) + .then(data => { + onChangeTenants(data); + setTenantToUpdate(null); + }); + }; + const deleteTenant = (_id, name) => { fetch('/rootAdmin/tenant', { credentials: 'include', @@ -230,6 +282,21 @@ const Tenants = ({ handleLogout }) => { ); }, }, + { + field: 'update', + headerName: 'Modifier', + width: 150, + renderCell: params => { + return ( + + ); + }, + }, { field: 'delete', headerName: 'Supprimer', @@ -264,6 +331,12 @@ const Tenants = ({ handleLogout }) => { handleClose={() => setOpenCreateTenantDialog(false)} createAction={addTenant} /> + setTenantToUpdate(null)} + updateAction={updateTenant} + /> + setOpenDeleteTenantDialog(false)} diff --git a/src/app/js/root-admin/UpdateTenantDialog.js b/src/app/js/root-admin/UpdateTenantDialog.js new file mode 100644 index 000000000..9e6f1d270 --- /dev/null +++ b/src/app/js/root-admin/UpdateTenantDialog.js @@ -0,0 +1,103 @@ +import React, { useEffect, useState } from 'react'; +import PropTypes from 'prop-types'; +import { + Dialog, + DialogContent, + DialogActions, + DialogTitle, + Button, + TextField, +} from '@mui/material'; + +const UpdateTenantDialog = ({ tenant, handleClose, updateAction }) => { + const [description, setDescription] = useState(''); + const [author, setAuthor] = useState(''); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + + useEffect(() => { + setDescription(tenant?.description || ''); + setAuthor(tenant?.author || ''); + setUsername(tenant?.username || ''); + setPassword(tenant?.password || ''); + }, [tenant]); + + return ( + + Modifier instance: {tenant?.name} + + setDescription(event.target.value)} + value={description} + sx={{ marginTop: '1em' }} + /> + + setAuthor(event.target.value)} + value={author} + sx={{ marginTop: '1em' }} + /> + setUsername(event.target.value)} + value={username} + sx={{ marginTop: '1em' }} + /> + setPassword(event.target.value)} + value={password} + sx={{ marginTop: '1em' }} + /> + + + + + + ); +}; + +UpdateTenantDialog.propTypes = { + tenant: PropTypes.object, + handleClose: PropTypes.func.isRequired, + updateAction: PropTypes.func.isRequired, +}; + +export default UpdateTenantDialog;