mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 06:03:45 +01:00
Registration page: the initial commit
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import Path from 'path'
|
||||
import logger from '@overleaf/logger'
|
||||
import registerNewUserAndSendActivationEmail from './UserRegistrationHandler.mjs'
|
||||
import EmailHelper from '../../../../app/src/Features/Helpers/EmailHelper.mjs'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
async function registrationPage(req, res, next) {
|
||||
// Check if the user is already logged in
|
||||
if (req.user != null) {
|
||||
return res.redirect(`/`)
|
||||
}
|
||||
|
||||
const sharedProjectData = req.session.sharedProjectData || {}
|
||||
|
||||
const newTemplateData = {}
|
||||
if (req.session.templateData != null) {
|
||||
newTemplateData.templateName = req.session.templateData.templateName
|
||||
}
|
||||
|
||||
const allowedDomains = Settings.allowedRegistrationEmailDomains || []
|
||||
const displayDomains = new Map()
|
||||
|
||||
for (const domain of allowedDomains) {
|
||||
const subdomainsOnly = domain.startsWith('*.')
|
||||
const base = subdomainsOnly ? domain.substring(2) : domain
|
||||
|
||||
const existing = displayDomains.get(base)
|
||||
|
||||
displayDomains.set(base, {
|
||||
domain: base,
|
||||
exact: existing?.exact || !subdomainsOnly,
|
||||
subdomains: existing?.subdomains || subdomainsOnly,
|
||||
})
|
||||
}
|
||||
|
||||
const __dirname = Path.dirname(new URL(import.meta.url).pathname)
|
||||
res.render(Path.resolve(__dirname, '../views/register'), {
|
||||
title: 'register',
|
||||
sharedProjectData,
|
||||
newTemplateData,
|
||||
displayDomains: [...displayDomains.values()],
|
||||
csrfToken: req.csrfToken(),
|
||||
})
|
||||
}
|
||||
|
||||
async function registerNewUser(req, res, next) {
|
||||
const { email, first_name, last_name } = req.body
|
||||
|
||||
if (
|
||||
typeof first_name !== 'string' || first_name.length > 100 ||
|
||||
typeof last_name !== 'string' || last_name.length > 100
|
||||
) {
|
||||
return res.status(400).json({ message: 'Too long name.' })
|
||||
}
|
||||
|
||||
const parsedEmail = EmailHelper.parseEmail(email)
|
||||
if (!parsedEmail) {
|
||||
return res.status(400).json({ message: 'Invalid email address.' })
|
||||
}
|
||||
|
||||
// If registration is restricted to a specific email domains,
|
||||
// check that the email domain is allowed
|
||||
const domain = parsedEmail.split('@').pop()
|
||||
const allowedDomains = Settings.allowedRegistrationEmailDomains
|
||||
|
||||
if (
|
||||
allowedDomains &&
|
||||
!allowedDomains.some(pattern => {
|
||||
if (pattern.startsWith('*.')) {
|
||||
const base = pattern.slice(2)
|
||||
return domain.endsWith(`.${base}`)
|
||||
}
|
||||
return domain === pattern
|
||||
})
|
||||
) {
|
||||
return res.status(403).json({
|
||||
message: 'Registration is not available for this email domain.',
|
||||
})
|
||||
}
|
||||
|
||||
const t = req.i18n.translate
|
||||
try {
|
||||
const userDetails = { email, first_name, last_name }
|
||||
const success = await registerNewUserAndSendActivationEmail(userDetails)
|
||||
if (success) {
|
||||
return res.status(200).json({ message: t('register_success') })
|
||||
} else {
|
||||
return res.status(409).json({ message: { key: 'account_with_this_email_exists' } })
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message === 'error sending message') {
|
||||
return res.status(422).json({ message: t('failed_to_send_registration_email') })
|
||||
}
|
||||
return next(error)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
registrationPage,
|
||||
registerNewUser
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import logger from "@overleaf/logger"
|
||||
import Settings from "@overleaf/settings"
|
||||
import RegistrationPageController from './RegistrationPageController.mjs'
|
||||
import AuthenticationController from '../../../../app/src/Features/Authentication/AuthenticationController.mjs'
|
||||
import RateLimiterMiddleware from "../../../../app/src/Features/Security/RateLimiterMiddleware.mjs"
|
||||
import { RateLimiter } from "../../../../app/src/infrastructure/RateLimiter.mjs"
|
||||
|
||||
// Limit registration attempts to 3 per minute per IP
|
||||
const registrationRateLimiters = {
|
||||
postRegister: new RateLimiter('postRegister', {
|
||||
points: 5,
|
||||
duration: 60,
|
||||
}),
|
||||
}
|
||||
|
||||
export default {
|
||||
apply(webRouter) {
|
||||
logger.debug({}, 'Init Registration Page module')
|
||||
|
||||
// remove default registration router if it exists
|
||||
webRouter.stack = webRouter.stack.filter(layer => {
|
||||
return !(layer.route && layer.route.path === '/register' && layer.route.methods.get)
|
||||
})
|
||||
|
||||
webRouter.get('/register', RegistrationPageController.registrationPage)
|
||||
|
||||
webRouter.post(
|
||||
'/register',
|
||||
RateLimiterMiddleware.rateLimit(registrationRateLimiters.postRegister),
|
||||
RegistrationPageController.registerNewUser
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import crypto from 'node:crypto'
|
||||
import settings from '@overleaf/settings'
|
||||
import logger from '@overleaf/logger'
|
||||
import UserRegistrationHandler from '../../../../app/src/Features/User/UserRegistrationHandler.mjs'
|
||||
import UserDeleter from '../../../../app/src/Features/User/UserDeleter.mjs'
|
||||
import EmailHandler from '../../../../app/src/Features/Email/EmailHandler.mjs'
|
||||
import OneTimeTokenHandler from '../../../../app/src/Features/Security/OneTimeTokenHandler.mjs'
|
||||
|
||||
const ONE_WEEK = 7 * 24 * 60 * 60 // seconds
|
||||
|
||||
export default async function registerNewUserAndSendActivationEmail(userData) {
|
||||
let user
|
||||
try {
|
||||
const password = crypto.randomBytes(32).toString('hex')
|
||||
user = await UserRegistrationHandler.promises.registerNewUser({ ...userData, password })
|
||||
} catch (error) {
|
||||
if (error.message === 'EmailAlreadyRegistered') {
|
||||
logger.debug({ email: userData.email }, 'user already registered')
|
||||
return false
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
try {
|
||||
const token = await OneTimeTokenHandler.promises.getNewToken(
|
||||
'password',
|
||||
{ user_id: user._id.toString(), email: user.email },
|
||||
{ expiresIn: ONE_WEEK }
|
||||
)
|
||||
|
||||
const setNewPasswordUrl = `${settings.siteUrl}/user/activate?token=${token}&user_id=${user._id}`
|
||||
|
||||
await EmailHandler.promises.sendEmail('registered', {
|
||||
to: user.email,
|
||||
setNewPasswordUrl,
|
||||
})
|
||||
} catch (error) {
|
||||
try {
|
||||
await UserDeleter.promises.deleteUser(user._id, {
|
||||
ipAddress: '127.0.0.1',
|
||||
skipEmail: true,
|
||||
})
|
||||
await UserDeleter.promises.expireDeletedUser(user._id)
|
||||
} catch (err) {
|
||||
logger.error({ err, userId: user._id }, 'failed delete user')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "extends": "../../../../tsconfig.backend.json" }
|
||||
@@ -0,0 +1,98 @@
|
||||
extends ../../../../app/views/layout-website-redesign
|
||||
|
||||
block vars
|
||||
- isWebsiteRedesign = true
|
||||
|
||||
block content
|
||||
main#main-content.content
|
||||
.container
|
||||
.row
|
||||
.registration-message
|
||||
if sharedProjectData.user_first_name !== undefined
|
||||
h1 #{translate("user_wants_you_to_see_project", {username:sharedProjectData.user_first_name, projectname:""})}
|
||||
em #{sharedProjectData.project_name}
|
||||
div
|
||||
| #{translate("join_sl_to_view_project")}.
|
||||
div
|
||||
| #{translate("already_have_sl_account")}
|
||||
|
|
||||
a(href='/login') #{translate("login_here")}
|
||||
else if newTemplateData.templateName !== undefined
|
||||
h1 #{translate("register_to_edit_template", {templateName:newTemplateData.templateName})}
|
||||
|
||||
div #{translate("already_have_sl_account")}
|
||||
|
|
||||
a(href='/login') #{translate("login_here")}
|
||||
.row
|
||||
.col-lg-8.offset-lg-2.col-xl-6.offset-xl-3
|
||||
.page-header
|
||||
h1.login-register-header-heading-focus.h3 #{translate("register")}
|
||||
if displayDomains.length
|
||||
.alert.alert-info
|
||||
div.small.fst-italic.mb-2 #{translate("registration_is_restricted_to_the_following_email_domains")}
|
||||
ul.mb-0.small
|
||||
each item in displayDomains
|
||||
li
|
||||
span.small.fst-italic
|
||||
code #{item.domain}
|
||||
if item.subdomains && item.exact
|
||||
| (#{translate("including_all_subdomains")})
|
||||
else if item.subdomains
|
||||
| (#{translate("subdomains_of_this_domain_only")})
|
||||
form(
|
||||
data-ol-async-form
|
||||
name="registrationForm"
|
||||
action="/register",
|
||||
method="POST",
|
||||
aria-label=translate('create_account')
|
||||
)
|
||||
input(name='_csrf' type='hidden' value=csrfToken)
|
||||
+formMessagesNewStyle()
|
||||
+customFormMessageNewStyle('account_with_this_email_exists', 'warn')
|
||||
| !{translate("account_with_this_email_exists")}
|
||||
a.d-block.mt-1(href='/user/password/reset') #{translate("forgot_your_password")}?
|
||||
.form-group
|
||||
label(for='firstNameField') #{translate("first_name")}
|
||||
input.form-control(
|
||||
aria-label="first_name"
|
||||
id='firstNameField'
|
||||
type='text',
|
||||
name='first_name',
|
||||
placeholder=translate("first_name"),
|
||||
required,
|
||||
autocomplete="given-name",
|
||||
maxlength="100"
|
||||
autofocus
|
||||
)
|
||||
.form-group
|
||||
label(for='lastNameField') #{translate("last_name")}
|
||||
input.form-control(
|
||||
aria-label="last_name"
|
||||
id='lastNameField'
|
||||
type='text',
|
||||
name='last_name',
|
||||
placeholder=translate("last_name"),
|
||||
required,
|
||||
autocomplete="family-name",
|
||||
maxlength="100"
|
||||
)
|
||||
.form-group
|
||||
label(for='emailField') #{translate("email")}
|
||||
input.form-control(
|
||||
aria-label="email"
|
||||
id='emailField'
|
||||
type='email',
|
||||
name='email',
|
||||
placeholder=translate("email"),
|
||||
required,
|
||||
autocomplete="username",
|
||||
maxlength="100"
|
||||
)
|
||||
div(style="margin-top: 3rem")
|
||||
.actions
|
||||
button.btn.btn-primary.w-100(
|
||||
type='submit',
|
||||
data-ol-disabled-inflight
|
||||
)
|
||||
span(data-ol-inflight="idle") #{translate("create_account")}
|
||||
span(hidden data-ol-inflight="pending") #{translate("registering")}…
|
||||
@@ -0,0 +1,30 @@
|
||||
import Settings from '@overleaf/settings'
|
||||
import { boolFromEnv } from '../authentication/utils.mjs'
|
||||
|
||||
let RegistrationPageModule = {}
|
||||
|
||||
let enableRegistrationPage = boolFromEnv(process.env.OVERLEAF_ENABLE_REGISTRATION_PAGE)
|
||||
|
||||
// if the env var is not set, enable registration page iff the external authentication is not enabled
|
||||
if (enableRegistrationPage === undefined) {
|
||||
enableRegistrationPage = !(Settings.ldap?.enable || Settings.saml?.enable || Settings.oidc?.enable)
|
||||
}
|
||||
|
||||
if (enableRegistrationPage) {
|
||||
const { default: router } = await import('./app/src/RegistrationPageRouter.mjs')
|
||||
|
||||
Settings.enableRegistrationPage = true
|
||||
|
||||
if (process.env.OVERLEAF_ALLOWED_REGISTRATION_EMAIL_DOMAINS) {
|
||||
Settings.allowedRegistrationEmailDomains = process.env.OVERLEAF_ALLOWED_REGISTRATION_EMAIL_DOMAINS
|
||||
.split(/[,\s]+/)
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
RegistrationPageModule = {
|
||||
name: 'registration-page',
|
||||
router: router,
|
||||
}
|
||||
}
|
||||
|
||||
export default RegistrationPageModule
|
||||
Reference in New Issue
Block a user