import OError from '@overleaf/o-error' import { expressify } from '@overleaf/promise-utils' import Settings from '@overleaf/settings' import Path from 'node:path' import { fileURLToPath } from 'node:url' import logger from '@overleaf/logger' import UserRegistrationHandler from '../../../../app/src/Features/User/UserRegistrationHandler.js' import EmailHandler from '../../../../app/src/Features/Email/EmailHandler.js' import UserGetter from '../../../../app/src/Features/User/UserGetter.js' import { User } from '../../../../app/src/models/User.js' import AuthenticationManager from '../../../../app/src/Features/Authentication/AuthenticationManager.js' import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.js' import SessionManager from '../../../../app/src/Features/Authentication/SessionManager.js' import { hasAdminAccess } from '../../../../app/src/Features/Helpers/AdminAuthorizationHelper.js' const __dirname = fileURLToPath(new URL('.', import.meta.url)) /** * Container for functions that need to be mocked in tests * * TODO: Rewrite tests in terms of exported functions only */ const _mocks = {} _mocks._atLeastOneAdminExists = async () => { const user = await UserGetter.promises.getUser( { isAdmin: true }, { _id: 1, isAdmin: 1 } ) return Boolean(user) } async function _atLeastOneAdminExists() { return await _mocks._atLeastOneAdminExists() } function getAuthMethod() { if (Settings.ldap) { return 'ldap' } else if (Settings.saml) { return 'saml' } else { return 'local' } } async function launchpadPage(req, res) { // TODO: check if we're using external auth? // * how does all this work with ldap and saml? const sessionUser = SessionManager.getSessionUser(req.session) const authMethod = getAuthMethod() const adminUserExists = await _atLeastOneAdminExists() if (!sessionUser) { if (!adminUserExists) { res.render(Path.resolve(__dirname, '../views/launchpad'), { adminUserExists, authMethod, }) } else { AuthenticationController.setRedirectInSession(req) res.redirect('/login') } } else { const user = await UserGetter.promises.getUser(sessionUser._id, { isAdmin: 1, }) if (hasAdminAccess(user)) { res.render(Path.resolve(__dirname, '../views/launchpad'), { wsUrl: Settings.wsUrl, adminUserExists, authMethod, }) } else { res.redirect('/restricted') } } } async function sendTestEmail(req, res) { const { email } = req.body if (!email) { logger.debug({}, 'no email address supplied') return res.status(400).json({ message: 'no email address supplied', }) } logger.debug({ email }, 'sending test email') const emailOptions = { to: email } try { await EmailHandler.promises.sendEmail('testEmail', emailOptions) logger.debug({ email }, 'sent test email') res.json({ message: res.locals.translate('email_sent') }) } catch (err) { OError.tag(err, 'error sending test email', { email, }) throw err } } function registerExternalAuthAdmin(authMethod) { return expressify(async function (req, res) { if (getAuthMethod() !== authMethod) { logger.debug( { authMethod }, 'trying to register external admin, but that auth service is not enabled, disallow' ) return res.sendStatus(403) } const { email } = req.body if (!email) { logger.debug({ authMethod }, 'no email supplied, disallow') return res.sendStatus(400) } logger.debug({ email }, 'attempted register first admin user') const exists = await _atLeastOneAdminExists() if (exists) { logger.debug({ email }, 'already have at least one admin user, disallow') return res.sendStatus(403) } const body = { email, password: 'password_here', first_name: email, last_name: '', } logger.debug( { body, authMethod }, 'creating admin account for specified external-auth user' ) let user try { user = await UserRegistrationHandler.promises.registerNewUser(body) } catch (err) { OError.tag(err, 'error with registerNewUser', { email, authMethod, }) throw err } try { const reversedHostname = user.email .split('@')[1] .split('') .reverse() .join('') await User.updateOne( { _id: user._id }, { $set: { isAdmin: true, emails: [{ email, reversedHostname }] }, } ).exec() } catch (err) { OError.tag(err, 'error setting user to admin', { user_id: user._id, }) throw err } AuthenticationController.setRedirectInSession(req, '/launchpad') logger.debug( { email, userId: user._id, authMethod }, 'created first admin account' ) res.json({ redir: '/launchpad', email }) }) } async function registerAdmin(req, res) { const { email } = req.body const { password } = req.body if (!email || !password) { logger.debug({}, 'must supply both email and password, disallow') return res.sendStatus(400) } logger.debug({ email }, 'attempted register first admin user') const exists = await _atLeastOneAdminExists() if (exists) { logger.debug( { email: req.body.email }, 'already have at least one admin user, disallow' ) return res.status(403).json({ message: { type: 'error', text: 'admin user already exists' }, }) } const invalidEmail = AuthenticationManager.validateEmail(email) if (invalidEmail) { return res .status(400) .json({ message: { type: 'error', text: invalidEmail.message } }) } const invalidPassword = AuthenticationManager.validatePassword( password, email ) if (invalidPassword) { return res .status(400) .json({ message: { type: 'error', text: invalidPassword.message } }) } const body = { email, password } const user = await UserRegistrationHandler.promises.registerNewUser(body) logger.debug({ userId: user._id }, 'making user an admin') try { const reversedHostname = user.email .split('@')[1] .split('') .reverse() .join('') await User.updateOne( { _id: user._id }, { $set: { isAdmin: true, emails: [{ email, reversedHostname }], }, } ).exec() } catch (err) { OError.tag(err, 'error setting user to admin', { user_id: user._id, }) throw err } logger.debug({ email, userId: user._id }, 'created first admin account') res.json({ redir: '/launchpad' }) } const LaunchpadController = { launchpadPage: expressify(launchpadPage), registerAdmin: expressify(registerAdmin), registerExternalAuthAdmin, sendTestEmail: expressify(sendTestEmail), _atLeastOneAdminExists, _mocks, } export default LaunchpadController