diff --git a/packages/nocodb/src/helpers/initAdminFromEnv.ts b/packages/nocodb/src/helpers/initAdminFromEnv.ts new file mode 100644 index 0000000000..d41b3ece8f --- /dev/null +++ b/packages/nocodb/src/helpers/initAdminFromEnv.ts @@ -0,0 +1,281 @@ +import { promisify } from 'util'; +import { v4 as uuidv4 } from 'uuid'; +import bcrypt from 'bcryptjs'; +import { validatePassword } from 'nocodb-sdk'; +import boxen from 'boxen'; +import { T } from 'nc-help'; +import { isEmail } from 'validator'; +import NocoCache from '../cache/NocoCache'; +import { ProjectUser, User } from '../models'; +import Noco from '../Noco'; +import { CacheScope, MetaTable } from '../utils/globals'; + +const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 }; + +export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { + if (process.env.NC_ADMIN_EMAIL && process.env.NC_ADMIN_PASSWORD) { + if (!isEmail(process.env.NC_ADMIN_EMAIL?.trim())) { + console.log( + '\n', + boxen( + `Provided admin email '${process.env.NC_ADMIN_EMAIL}' is not valid`, + { + title: 'Invalid admin email', + padding: 1, + borderStyle: 'double', + titleAlignment: 'center', + borderColor: 'red', + }, + ), + '\n', + ); + process.exit(1); + } + + const { valid, error, hint } = validatePassword( + process.env.NC_ADMIN_PASSWORD, + ); + if (!valid) { + console.log( + '\n', + boxen(`${error}${hint ? `\n\n${hint}` : ''}`, { + title: 'Invalid admin password', + padding: 1, + borderStyle: 'double', + titleAlignment: 'center', + borderColor: 'red', + }), + '\n', + ); + process.exit(1); + } + + let ncMeta; + try { + ncMeta = await _ncMeta.startTransaction(); + const email = process.env.NC_ADMIN_EMAIL.toLowerCase().trim(); + + const salt = await promisify(bcrypt.genSalt)(10); + const password = await promisify(bcrypt.hash)( + process.env.NC_ADMIN_PASSWORD, + salt, + ); + const email_verification_token = uuidv4(); + const roles = 'user,super'; + + // if super admin not present + if (await User.isFirst(ncMeta)) { + // roles = 'owner,creator,editor' + T.emit('evt', { + evt_type: 'project:invite', + count: 1, + }); + + await User.insert( + { + firstname: '', + lastname: '', + email, + salt, + password, + email_verification_token, + roles, + }, + ncMeta, + ); + } else { + const salt = await promisify(bcrypt.genSalt)(10); + const password = await promisify(bcrypt.hash)( + process.env.NC_ADMIN_PASSWORD, + salt, + ); + const email_verification_token = uuidv4(); + const superUser = await ncMeta.metaGet2(null, null, MetaTable.USERS, { + roles: 'user,super', + }); + + if (!superUser?.id) { + const existingUserWithNewEmail = await User.getByEmail(email, ncMeta); + if (existingUserWithNewEmail?.id) { + // clear cache + await NocoCache.delAll( + CacheScope.USER, + `${existingUserWithNewEmail.email}___*`, + ); + await NocoCache.del( + `${CacheScope.USER}:${existingUserWithNewEmail.id}`, + ); + await NocoCache.del( + `${CacheScope.USER}:${existingUserWithNewEmail.email}`, + ); + + // Update email and password of super admin account + await User.update( + existingUserWithNewEmail.id, + { + salt, + email, + password, + email_verification_token, + token_version: null, + refresh_token: null, + roles, + }, + ncMeta, + ); + } else { + T.emit('evt', { + evt_type: 'project:invite', + count: 1, + }); + + await User.insert( + { + firstname: '', + lastname: '', + email, + salt, + password, + email_verification_token, + roles, + }, + ncMeta, + ); + } + } else if (email !== superUser.email) { + // update admin email and password and migrate projects + // if user already present and associated with some project + + // check user account already present with the new admin email + const existingUserWithNewEmail = await User.getByEmail(email, ncMeta); + + if (existingUserWithNewEmail?.id) { + // get all project access belongs to the existing account + // and migrate to the admin account + const existingUserProjects = await ncMeta.metaList2( + null, + null, + MetaTable.PROJECT_USERS, + { + condition: { fk_user_id: existingUserWithNewEmail.id }, + }, + ); + + for (const existingUserProject of existingUserProjects) { + const userProject = await ProjectUser.get( + existingUserProject.project_id, + superUser.id, + ncMeta, + ); + + // if admin user already have access to the project + // then update role based on the highest access level + if (userProject) { + if ( + rolesLevel[userProject.roles] > + rolesLevel[existingUserProject.roles] + ) { + await ProjectUser.update( + userProject.project_id, + superUser.id, + existingUserProject.roles, + ncMeta, + ); + } + } else { + // if super doesn't have access then add the access + await ProjectUser.insert( + { + ...existingUserProject, + fk_user_id: superUser.id, + }, + ncMeta, + ); + } + // delete the old project access entry from DB + await ProjectUser.delete( + existingUserProject.project_id, + existingUserProject.fk_user_id, + ncMeta, + ); + } + + // delete existing user + await ncMeta.metaDelete( + null, + null, + MetaTable.USERS, + existingUserWithNewEmail.id, + ); + + // clear cache + await NocoCache.delAll( + CacheScope.USER, + `${existingUserWithNewEmail.email}___*`, + ); + await NocoCache.del( + `${CacheScope.USER}:${existingUserWithNewEmail.id}`, + ); + await NocoCache.del( + `${CacheScope.USER}:${existingUserWithNewEmail.email}`, + ); + + // Update email and password of super admin account + await User.update( + superUser.id, + { + salt, + email, + password, + email_verification_token, + token_version: null, + refresh_token: null, + }, + ncMeta, + ); + } else { + // if email's are not different update the password and hash + await User.update( + superUser.id, + { + salt, + email, + password, + email_verification_token, + token_version: null, + refresh_token: null, + }, + ncMeta, + ); + } + } else { + const newPasswordHash = await promisify(bcrypt.hash)( + process.env.NC_ADMIN_PASSWORD, + superUser.salt, + ); + + if (newPasswordHash !== superUser.password) { + // if email's are same and passwords are different + // then update the password and token version + await User.update( + superUser.id, + { + salt, + password, + email_verification_token, + token_version: null, + refresh_token: null, + }, + ncMeta, + ); + } + } + } + await ncMeta.commit(); + } catch (e) { + console.log('Error occurred while updating/creating admin user'); + console.log(e); + await ncMeta.rollback(e); + } + } +} diff --git a/packages/nocodb/src/services/app-init.service.ts b/packages/nocodb/src/services/app-init.service.ts index f08263fb38..c913fcaefb 100644 --- a/packages/nocodb/src/services/app-init.service.ts +++ b/packages/nocodb/src/services/app-init.service.ts @@ -1,9 +1,12 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { T } from 'nc-help'; import NocoCache from '../cache/NocoCache'; import { Connection } from '../connection/connection'; +import initAdminFromEnv from '../helpers/initAdminFromEnv'; import NcPluginMgrv2 from '../helpers/NcPluginMgrv2'; import { MetaService } from '../meta/meta.service'; +import { User } from '../models' import Noco from '../Noco'; +import getInstance from '../utils/getInstance'; import NcConfigFactory from '../utils/NcConfigFactory'; import NcUpgrader from '../version-upgrader/NcUpgrader'; import type { IEventEmitter } from '../modules/event-emitter/event-emitter.interface'; @@ -54,6 +57,9 @@ export const appInitServiceProvider: Provider = { // init jwt secret await Noco.initJwt(); + // load super admin user from env if env is set + await initAdminFromEnv(metaService); + // init plugin manager await NcPluginMgrv2.init(Noco.ncMeta); await Noco.loadEEState(); @@ -61,6 +67,11 @@ export const appInitServiceProvider: Provider = { // run upgrader await NcUpgrader.upgrade({ ncMeta: Noco._ncMeta }); + T.init({ + instance: getInstance, + }); + T.emit('evt_app_started', await User.count()); + // todo: move app config to app-init service return new AppInitService(connection.config); },