Registration page: the initial commit

This commit is contained in:
yu-i-i
2026-09-22 02:41:43 +02:00
parent 479c9fce50
commit fea291b72e
13 changed files with 349 additions and 5 deletions
@@ -11,6 +11,7 @@ import EmailHelper from '../Helpers/EmailHelper.mjs'
import EditorRealTimeController from '../Editor/EditorRealTimeController.mjs'
import AnalyticsManager from '../Analytics/AnalyticsManager.mjs'
import SessionManager from '../Authentication/SessionManager.mjs'
import Features from '../../infrastructure/Features.mjs'
import { RateLimiter } from '../../infrastructure/RateLimiter.mjs'
import { z, zz, parseReq } from '../../infrastructure/Validation.mjs'
import { expressify } from '@overleaf/promise-utils'
@@ -386,7 +387,11 @@ async function viewInvite(req, res) {
user_first_name: owner.first_name,
}
AuthenticationController.setRedirectInSession(req)
return res.redirect('/register')
if (Features.hasFeature('registration-page')) {
return res.redirect('/register')
} else {
return res.redirect('/login')
}
}
// cleanup if set for register page
@@ -442,6 +447,12 @@ async function viewSharingLink(req, res) {
if (!isPublicSharingLink) {
AuthenticationController.setRedirectInSession(req)
return res.redirect('/register')
AuthenticationController.setRedirectInSession(req)
if (Features.hasFeature('registration-page')) {
return res.redirect('/register')
} else {
return res.redirect('/login')
}
}
}
@@ -34,6 +34,7 @@ const Features = {
return (
(Boolean(Settings.ldap) && Boolean(Settings.ldap.enable)) ||
(Boolean(Settings.saml) && Boolean(Settings.saml.enable)) ||
(Boolean(Settings.oidc) && Boolean(Settings.oidc.enable)) ||
Boolean(Settings.overleaf)
)
},
@@ -51,10 +52,7 @@ const Features = {
case 'homepage':
return Boolean(Settings.enableHomepage)
case 'registration-page':
return (
!Features.externalAuthenticationSystemUsed() ||
Boolean(Settings.overleaf) || Settings.oidc?.allowedOIDCEmailDomains
)
return Boolean(Settings.enableRegistrationPage)
case 'registration':
return Boolean(Settings.overleaf)
case 'chat':
+1
View File
@@ -1187,6 +1187,7 @@ module.exports = {
'authentication/saml',
'authentication/oidc',
'admin-tools', // import after authentication
'registration-page', // import after authentication
'template-gallery',
'git-bridge',
'github-sync',
+6
View File
@@ -75,6 +75,7 @@
"account_not_linked_to_dropbox": "Dein Konto ist nicht mit Dropbox verknüpft",
"account_settings": "Kontoeinstellungen",
"account_with_email_exists": "Anscheinend existiert bereits ein <b>__appName__</b>-Konto mit der E-Mail-Adresse <b>__email__</b>.",
"account_with_this_email_exists": "Anscheinend existiert bereits ein <b>__appName__</b>-Konto mit dieser E-Mail-Adresse.",
"acct_linked_to_institution_acct_2": "Du kannst dich jetzt mit dem Konto deiner Institution <0>__institutionName__</0> bei Overleaf anmelden.",
"actions": "Aktionen",
"activate": "Aktivieren",
@@ -572,6 +573,7 @@
"export_project_to_github": "Projekt nach GitHub exportieren",
"failed_to_publish_as_a_template": "Veröffentlichung als Vorlage fehlgeschlagen.",
"failed_to_send_managed_user_invite_to_email": "Der Versand der Einladung für Verwaltete Benutzer an <0>__email__</0> hat nicht funktioniert. Bitte versuche es später noch einmal.",
"failed_to_send_registration_email": "Die Registrierungs-E-Mail konnte nicht gesendet werden. Bitte überprüfe die E-Mail-Adresse.",
"faq_how_does_free_trial_works_answer": "Während deines __len__-tägigen Probe-Abonnements erhältst du vollen Zugriff auf die Funktionen des von dir gewählten __appName__-Abonnements. Es besteht keine Verpflichtung, über die Testperiode hinaus fortzufahren. Deine Karte wird am Ende des __len__-tägigen Testzeitraums belastet, sofern du nicht vorher gekündigt hast. Um zu kündigen, gehe zu deinen Abonnementeinstellungen in deinem Konto.",
"fast": "Schnell",
"fast_draft": "Schnell [Entwurf]",
@@ -875,6 +877,7 @@
"include_caption": "Beschriftung anzeigen",
"include_label": "Label anzeigen",
"included_as_part_of_your_writefull_subscription": "Als Teil deines Writefull-Abonnements enthalten",
"including_all_subdomains": "einschließlich aller Subdomains",
"increased_compile_timeout": "Zeitlimit beim Kompilieren erhöhen",
"info": "Info",
"inr_discount_modal_info": "Erhalte Dokumentenverlauf, Änderungsverfolgung, weitere Mitarbeiter und mehr zu Preisen nach Kaufkraftparität.",
@@ -1390,11 +1393,13 @@
"regards": "Viele Grüße",
"register": "Registrieren",
"register_error": "Registrierungsfehler",
"register_success": "Registrierung erfolgreich. Bitte überprüfen Sie Ihre E-Mail, um Ihr Konto zu aktivieren.",
"register_intercept_sso": "Du kannst dein __authProviderName__-Konto nach der Anmeldung auf der Seite Kontoeinstellungen verknüpfen.",
"register_to_edit_template": "Bitte registriere dich um die __templateName__ Vorlage zu bearbeiten",
"register_with_another_email": "<a href=\"__link__\">Registriere Dich bei __appName__</a> mit einer anderen E-Mail-Adresse.",
"registered": "Registriert",
"registering": "Registrieren",
"registration_is_restricted_to_the_following_email_domains": "Die Registrierung ist auf die folgenden E-Mail-Domänen beschränkt:",
"reject": "Verwerfen",
"related_tags": "Ähnliche Stichwörter",
"relink_your_account": "Verknüpfe dein Konto neu",
@@ -1575,6 +1580,7 @@
"stops_compiling_after_the_first_error_so_you_can_fix_issues_one_at_a_time": "Stoppt das Kompilieren beim ersten Fehler, damit du Probleme einzeln beheben kannst",
"store_your_work": "Speichere deine Arbeit auf deiner eigenen Infrastruktur",
"student": "Student",
"subdomains_of_this_domain_only": "nur Subdomains dieser Domain",
"subject": "Betreff",
"submit": "Absenden",
"submit_title": "Einreichen",
+6
View File
@@ -86,6 +86,7 @@
"account_not_linked_to_dropbox": "Your account is not linked to Dropbox",
"account_settings": "Account settings",
"account_with_email_exists": "It looks like an <b>__appName__</b> account with the email <b>__email__</b> already exists.",
"account_with_this_email_exists": "It looks like an <b>__appName__</b> account with this email address already exists.",
"acct_linked_to_institution_acct_2": "You can <0>log in</0> to Overleaf through your <0>__institutionName__</0> institutional login.",
"action_needed": "Action needed",
"actions": "Actions",
@@ -928,6 +929,7 @@
"failed_to_publish_as_a_template": "Failed to publish as a template.",
"failed_to_send_group_invite_to_email": "Failed to send group invite to <0>__email__</0>. Please try again later.",
"failed_to_send_managed_user_invite_to_email": "Failed to send Managed User invite to <0>__email__</0>. Please try again later.",
"failed_to_send_registration_email": "Failed to send the registration email. Please check the email address.",
"failed_to_send_sso_link_invite_to_email": "Failed to send SSO invite reminder to <0>__email__</0>. Please try again later.",
"fair_usage_policy_applies": "Fair usage policy applies.",
"faq_how_does_free_trial_works_answer": "You get full access to your chosen __appName__ plan during your __len__-day free trial. There is no obligation to continue beyond the trial. Your card will be charged at the end of your __len__ day trial unless you cancel before then. You can cancel via your subscription settings.",
@@ -1380,6 +1382,7 @@
"include_the_error_message_and_ai_response": "Include the error message and AI response",
"included_as_part_of_your_writefull_subscription": "Included as part of your Writefull subscription",
"includes": "includes",
"including_all_subdomains": "including all subdomains",
"increase_indent": "Increase indentation",
"increased_ai_allowance": "Increased AI allowance",
"increased_ai_allowance_tooltip": "10 AI uses per day across all AI tools.",
@@ -2331,12 +2334,14 @@
"regards": "Regards",
"register": "Register",
"register_error": "Registration error",
"register_success": "Registration successful. Please check your email to activate your account.",
"register_intercept_sso": "You can link your __authProviderName__ account from the Account settings page after logging in.",
"register_to_accept_invitation": "Register to accept invitation",
"register_to_edit_template": "Please register to edit the __templateName__ template",
"register_with_another_email": "<a href=\"__link__\">Register with __appName__</a> using another email.",
"registered": "Registered",
"registering": "Registering",
"registration_is_restricted_to_the_following_email_domains": "The registration is restricted to the following email domains:",
"reject": "Reject",
"reject_change": "Reject change",
"reject_selected_changes": "Reject selected changes",
@@ -2786,6 +2791,7 @@
"student_verification_rate_limited_info": "We’ve had too many verification requests recently. Please wait a few minutes and try again.",
"student_verification_required": "Student verification required",
"students": "Students",
"subdomains_of_this_domain_only": "subdomains of this domain only",
"subject": "Subject",
"subject_area": "Subject area",
"submit": "submit",
+6
View File
@@ -48,6 +48,7 @@
"account_information": "Информация об аккаунте",
"account_not_linked_to_dropbox": "Ваш аккаунт не синхронизирован с Dropbox",
"account_settings": "Настройки профиля",
"account_with_this_email_exists": "Похоже, что учётная запись <b>__appName__</b> с этим адресом электронной почты уже существует.",
"actions": "Действия",
"activate": "Активировать",
"activate_account": "Активируйте Ваш аккаунт",
@@ -224,6 +225,7 @@
"explore_plans": "Посмотреть тарифы",
"export_project_to_github": "Экспорт проекта на GitHub",
"failed_to_publish_as_a_template": "Не удалось создать шаблон.",
"failed_to_send_registration_email": "Не удалось отправить письмо для подтверждения регистрации. Пожалуйста, проверьте адрес электронной почты.",
"fast": "быстрый",
"feature_controls": "Управление функциями",
"feature_settings_subtext": "Просмотр и управление функциями, доступными участникам Вашей группы",
@@ -377,6 +379,7 @@
"importing_and_merging_changes_in_github": "Импорт и слияние изменений в GitHub",
"inactive_projects": "Неактивные проекты",
"included_as_part_of_your_writefull_subscription": "Входит в Вашу подписку Writefull",
"including_all_subdomains": "включая все поддомены",
"info": "Информация",
"institution": "Организация",
"integrations_like_github": "Интеграции, такие как синхронизация с GitHub",
@@ -599,9 +602,11 @@
"refresh_page_after_starting_free_trial": "Пожалуйста, обновите страницу после начала бесплатного пробного периода.",
"regards": "С уважением",
"register": "Регистрация",
"register_success": "Регистрация успешно завершена. Пожалуйста, проверьте свою электронную почту, чтобы активировать учётную запись.",
"register_to_edit_template": "Пожалуйста, зарегистрируйтесь, чтобы редактировать шаблон __templateName__",
"registered": "Зарегистрирован",
"registering": "Создание аккаунта",
"registration_is_restricted_to_the_following_email_domains": "Регистрация разрешена только для следующих доменов электронной почты:",
"remind_before_trial_ends": "Мы напомним Вам до окончания пробного периода",
"remove_from_group": "Удалить из группы",
"removed": "удалено",
@@ -684,6 +689,7 @@
"stop_compile": "Остановить компиляцию",
"stop_on_validation_error": "Проверить синтаксис перед компиляцией",
"student": "Студент",
"subdomains_of_this_domain_only": "только поддомены этого домена",
"subject": "Тема",
"subscribe": "Подписаться",
"subscribe_now": "Подписаться сейчас",
@@ -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