Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
eb985d3
feat: implement 2fa via email
pdji1602003 Jan 30, 2026
6405190
feat: verify OTP and complete login
pdji1602003 Jan 30, 2026
ae71f89
chore: reorder lifecycle methods
pdji1602003 Feb 3, 2026
4989c60
feat: add two factor setting frontend component and backend api endpo…
pdji1602003 Feb 3, 2026
37e0575
feat: implement 2fa via LDAP
pdji1602003 Feb 6, 2026
913a345
feat: extend user table with more login-required columns
pdji1602003 Feb 9, 2026
eae28f3
refactor: remove redundant columns
pdji1602003 Feb 9, 2026
6f3fb1d
refactor: update 2FA endpoints and remove previous LDAP and ORCID imp…
pdji1602003 Feb 9, 2026
1d22ae1
feat: implement orcid, totp, and saml
pdji1602003 Feb 10, 2026
78af46a
feat: implement 2fa workflow
pdji1602003 Feb 11, 2026
b40121c
feat: update orcid and LDAP login behaviour
pdji1602003 Feb 13, 2026
b97ea77
feat: remove first factor setting, add 2fa setting, refactor backend …
pdji1602003 Feb 13, 2026
044f155
fix: fix totp activating flow
pdji1602003 Feb 15, 2026
c392173
fix: remove passport-totp and adopt otpauth
pdji1602003 Feb 15, 2026
81bee8a
refactor: reorder route by category
pdji1602003 Feb 15, 2026
365eeea
refactor: reorder routes by category
pdji1602003 Feb 15, 2026
e78c0a9
refactor: reuse sendEmailOtp method
pdji1602003 Feb 15, 2026
cf558f8
chore: update server routes
pdji1602003 Feb 15, 2026
b13f8c2
refactor: extract login logic in 2fa-related routes
pdji1602003 Feb 15, 2026
cbc2100
chore: remove unused parameter
pdji1602003 Feb 15, 2026
cbac09c
feat: add login buttons
pdji1602003 Feb 15, 2026
fe4a8ce
chore: delete unused frontend page
pdji1602003 Feb 15, 2026
5bc119e
chore: update orcid url
pdji1602003 Feb 20, 2026
a59d496
chore: remove orcid linking routes and handler
pdji1602003 Feb 20, 2026
f764cc0
fix: remove email value constraint and set the full url for testing
pdji1602003 Feb 20, 2026
44457e3
feat: add Login page for ldap user
pdji1602003 Feb 22, 2026
cc7a293
refactor: improve ldap login logic
pdji1602003 Feb 22, 2026
7b84518
fix: fix user data leak between sessions by triggering page reload
pdji1602003 Feb 22, 2026
0a174be
refactor: remove the transaction from login procedure as it is just t…
pdji1602003 Feb 23, 2026
e18fa9e
refactor: replace repeated logic with generic method
pdji1602003 Feb 23, 2026
6cb13e7
chore: add TODO note
pdji1602003 Feb 23, 2026
b25b3fe
feat: create auth/2fa setting migration
pdji1602003 Feb 23, 2026
369b7f2
refactor: encapsulate each login strategy, refactor passport session …
pdji1602003 Feb 23, 2026
b7492a1
fix: restore to previous storage of full user object to avoid breakin…
pdji1602003 Feb 24, 2026
a2ade3f
feat: track auth provider status
pdji1602003 Feb 24, 2026
5caa59a
feat: add auth provider status guard to relevant routes
pdji1602003 Feb 24, 2026
3e2fcf9
style: reformat html
pdji1602003 Feb 25, 2026
5ffb851
feat: add text area to support other setting type
pdji1602003 Feb 25, 2026
e9a4469
feat: add third-party login buttons and add guard to ldap login page
pdji1602003 Feb 25, 2026
020f54a
feat: add enforced prop and add relevant checks in the 2fa enforcemen…
pdji1602003 Feb 26, 2026
b83a23c
feat: pops up TwoFactorSettingModal if 2fa is enforced
pdji1602003 Feb 26, 2026
edb5f5c
fix: fix infinitely loading issue due to app init state
pdji1602003 Feb 26, 2026
204f398
fix: fix method logic
pdji1602003 Feb 27, 2026
90f424f
fix: prevent page from reloading when login has errors
pdji1602003 Feb 27, 2026
3457b45
feat: prevent user from login the case of pending 2fa
pdji1602003 Feb 28, 2026
ef09a32
feat: use npm package for qr-code generation
pdji1602003 Feb 28, 2026
2b3f859
feat: add redirect baseUrl
pdji1602003 Feb 28, 2026
231e73e
feat: add error mapping handling
pdji1602003 Mar 1, 2026
7a0c069
refactor: simplify 2fa login flow
pdji1602003 Mar 1, 2026
9e19c86
refactor: unify login finalization flow
pdji1602003 Mar 1, 2026
d0b73a6
chore: remove unused user field
pdji1602003 Mar 1, 2026
7be6e37
feat: update description of email processing in terms
pdji1602003 Mar 1, 2026
32bc4cc
chore: remove testing env variables and todo notes
pdji1602003 Mar 1, 2026
ab6794c
chore: remove unused codes
pdji1602003 Mar 2, 2026
410371a
chore: remove rate limitation check on frontend
pdji1602003 Mar 2, 2026
0058701
style: adjust spacing
pdji1602003 Mar 16, 2026
ffc9d19
feat: update user email when email exists
pdji1602003 Mar 16, 2026
3772ad1
feat: group setting by each login method
pdji1602003 Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ PG_STATS_MIN_AGE_MS=1000
# Intended maximum number of long‑running active queries to log.
# NOTE: Current SQL has a hard LIMIT 10; change only matters if code is updated
# to parameterize the LIMIT.
PG_STATS_TOP_N=10
PG_STATS_TOP_N=10
79 changes: 79 additions & 0 deletions backend/db/migrations/20260126144237-extend-user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use strict";

const columns = [
// Email OTP for 2FA
{
name: "twoFactorOtp",
type: "STRING",
allowNull: true,
defaultValue: null,
},
{
name: "twoFactorOtpExpiresAt",
type: "DATE",
allowNull: true,
defaultValue: null,
},
// LDAP support
{
name: "ldapUsername",
type: "STRING",
allowNull: true,
defaultValue: null,
},
// ORCID support
{
name: "orcidId",
type: "STRING",
allowNull: true,
defaultValue: null,
unique: true,
},
// SAML support
{
name: "samlNameId",
type: "STRING",
allowNull: true,
defaultValue: null,
unique: true,
},
// Multi-method 2FA configuration
{
name: "twoFactorMethods",
type: "JSON",
allowNull: false,
defaultValue: [],
},
{
name: "totpSecret",
type: "STRING",
allowNull: true,
defaultValue: null,
},
];

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.changeColumn("user", "email", {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[QUESTION] should this also be in the down function? changing back the column "email" as it was
Also, why are we allowing null for emails?

type: Sequelize.STRING,
allowNull: true,
unique: true
});

for (const column of columns) {
await queryInterface.addColumn("user", column.name, {
type: Sequelize[column.type],
defaultValue: column.defaultValue,
allowNull: column.allowNull,
...(column.unique ? { unique: true } : {}),
});
}
},

async down(queryInterface, Sequelize) {
for (const column of columns) {
await queryInterface.removeColumn("user", column.name);
}
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use strict";

const settings = [
{
key: "system.auth.orcid.enabled",
type: "boolean",
value: false,
description: "Enable ORCID login flow"
},
{
key: "system.auth.ldap.enabled",
type: "boolean",
value: false,
description: "Enable LDAP login flow"
},
{
key: "system.auth.saml.enabled",
type: "boolean",
value: false,
description: "Enable SAML login flow"
},
{
key: "system.auth.local.2fa.required",
type: "boolean",
value: false,
description: "Require 2FA setup for local login users"
},
{
key: "system.auth.orcid.2fa.required",
type: "boolean",
value: false,
description: "Require 2FA setup for ORCID login users"
},
{
key: "system.auth.ldap.2fa.required",
type: "boolean",
value: false,
description: "Require 2FA setup for LDAP login users"
},
{
key: "system.auth.saml.2fa.required",
type: "boolean",
value: false,
description: "Require 2FA setup for SAML login users"
},
{
key: "system.auth.redirect.baseUrl",
type: "string",
value: "http://localhost:3000",
description: "Frontend base URL for authentication redirects (login success, OAuth callback, and 2FA verification pages)"
},
{
key: "system.auth.orcid.clientId",
type: "string",
value: "",
description: "ORCID OAuth client id (changes require a restart of the server)",
onlyAdmin: true
},
{
key: "system.auth.orcid.clientSecret",
type: "string",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[QUESTION] i'm not very knowledgable about security, but is this (and a few others in this file) fine to put as a string in the database?

value: "",
description: "ORCID OAuth client secret (changes require a restart of the server)",
onlyAdmin: true
},
{
key: "system.auth.orcid.callbackUrl",
type: "string",
value: "",
description: "ORCID callback URL (changes require a restart of the server)"
},
{
key: "system.auth.orcid.sandbox",
type: "boolean",
value: true,
description: "Use ORCID sandbox mode (changes require a restart of the server)"
},
{
key: "system.auth.ldap.url",
type: "string",
value: "",
description: "LDAP server URL (changes require a restart of the server)"
},
{
key: "system.auth.ldap.bindDN",
type: "string",
value: "",
description: "LDAP bind DN (changes require a restart of the server)",
onlyAdmin: true
},
{
key: "system.auth.ldap.bindCredentials",
type: "string",
value: "",
description: "LDAP bind password (changes require a restart of the server)",
onlyAdmin: true
},
{
key: "system.auth.ldap.searchBase",
type: "string",
value: "",
description: "LDAP user search base (changes require a restart of the server)"
},
{
key: "system.auth.ldap.searchFilter",
type: "string",
value: "(uid={{username}})",
description: "LDAP search filter template (changes require a restart of the server)"
},
{
key: "system.auth.saml.entryPoint",
type: "string",
value: "",
description: "SAML identity provider entry point (changes require a restart of the server)"
},
{
key: "system.auth.saml.issuer",
type: "string",
value: "",
description: "SAML service provider issuer (changes require a restart of the server)"
},
{
key: "system.auth.saml.cert",
type: "text",
value: "",
description: "SAML identity provider certificate (changes require a restart of the server)",
onlyAdmin: true
},
{
key: "system.auth.saml.callbackUrl",
type: "string",
value: "",
description: "SAML callback URL (changes require a restart of the server)"
},
];

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.bulkInsert("setting",
settings.map((setting) => ({
...setting,
createdAt: new Date(),
updatedAt: new Date(),
})),
);
},

async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete("setting", {
key: settings.map((setting) => setting.key)
}, {});
}
};
134 changes: 134 additions & 0 deletions backend/db/migrations/20260301134602-extend-setting-terms_2fa_email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"use strict";

const previousTermsTemplate = {
ops: [
{ insert: "Einwilligungserklärung\n", attributes: { header: 2 } },
{ insert: "Ich willige ein, dass meine personenbezogenen Daten (z. B. Name, Benutzerkennung, eingereichte Inhalte, Bewertungen, Interaktionen und IP-Adresse) zum Zweck der Durchführung der Anwendung verarbeitet werden dürfen. Zusätzlich willige ich in die Verarbeitung meiner personenbezogenen Daten für Forschungszwecke ein, sofern ich separat zugestimmt habe.\n\n" },

{ insert: "Informationen und Datenschutzerklärung\n", attributes: { header: 2 } },
{ insert: "Die Datenverarbeitung erfolgt unter Beachtung geltender Datenschutzgesetze (z. B. DSGVO). Alle Daten werden ausschließlich zu den im Informationsblatt beschriebenen Zwecken verwendet.\n\n" },

{ insert: "1. Bezeichnung der Verarbeitungstätigkeit\n", attributes: { header: 3 } },
{ insert: "Verarbeitung personenbezogener Daten zur Durchführung der Anwendung und optional zur wissenschaftlichen Auswertung von Nutzungsverhalten und Feedback.\n\n" },

{ insert: "2. Datenverantwortlicher\n", attributes: { header: 3 } },
{ insert: "Verantwortliche Organisation:\n", attributes: { bold: true } },
{ insert: "[Name der Organisation]\n[Adresse]\n[E-Mail-Adresse oder Kontaktlink]\n\n" },

{ insert: "3. Datenschutzbeauftragter\n", attributes: { header: 3 } },
{ insert: "[Name/Titel des Datenschutzbeauftragten]\n[Organisation oder Kontaktstelle]\n[Adresse]\n[E-Mail oder Datenschutzlink]\n\n" },

{ insert: "4. Zweck(e) und Rechtsgrundlage(n) der Verarbeitung\n", attributes: { header: 3 } },
{ insert: "Zwecke:\n", attributes: { bold: true } },
{ insert: "Bewertung und Durchführung der Anwendung\n", attributes: { list: "bullet" } },
{ insert: "Eindeutige Zuordnung über Benutzerkennungen\n", attributes: { list: "bullet" } },
{ insert: "Nutzung anonymisierter Daten für Forschung und Publikation (bei Zustimmung)\n", attributes: { list: "bullet" } },
{ insert: "Analyse des Nutzungsverhaltens (z. B. Klicks, Navigation, Zeitstempel)\n", attributes: { list: "bullet" } },
{ insert: "Technisch notwendige IP-Verarbeitung (nicht gespeichert)\n", attributes: { list: "bullet" } },
{ insert: "\nRechtsgrundlage:\n", attributes: { bold: true } },
{ insert: "Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)\n\n" },

{ insert: "5. Dauer der Speicherung\n", attributes: { header: 3 } },
{ insert: "Personenbezogene Daten werden für einen definierten Zeitraum (z. B. zwei Jahre nach Abschluss) gespeichert und anschließend gelöscht oder anonymisiert.\n\n" },

{ insert: "6. Rechte der betroffenen Personen\n", attributes: { header: 3 } },
{ insert: "Recht auf Auskunft\n", attributes: { list: "bullet" } },
{ insert: "Recht auf Berichtigung\n", attributes: { list: "bullet" } },
{ insert: "Recht auf Löschung oder Einschränkung\n", attributes: { list: "bullet" } },
{ insert: "Recht auf Widerspruch\n", attributes: { list: "bullet" } },
{ insert: "Recht auf Datenübertragbarkeit\n", attributes: { list: "bullet" } },
{ insert: "Beschwerderecht bei einer Aufsichtsbehörde\n", attributes: { list: "bullet" } },
{ insert: "\n" },

{ insert: "7. Widerrufsrecht\n", attributes: { header: 3 } },
{ insert: "Die Einwilligung kann jederzeit widerrufen werden. Die Rechtmäßigkeit der bis dahin erfolgten Verarbeitung bleibt unberührt. Nach Anonymisierung ist keine nachträgliche Löschung mehr möglich.\n\n" },

{ insert: "8. Veröffentlichung anonymisierter Daten\n", attributes: { header: 3 } },
{ insert: "Anonymisierte Ergebnisse können in wissenschaftlichen Publikationen veröffentlicht werden. Die Daten werden unter einer offenen Lizenz (z. B. CC BY-NC 4.0) in geeigneten Repositorien veröffentlicht.\n" }
]
};

const updatedTermsTemplate = {
ops: [
{ insert: "Einwilligungserklärung\n", attributes: { header: 2 } },
{ insert: "Ich willige ein, dass meine personenbezogenen Daten (z. B. Name, Benutzerkennung, eingereichte Inhalte, Bewertungen, Interaktionen und IP-Adresse) zum Zweck der Durchführung der Anwendung verarbeitet werden dürfen. Zusätzlich willige ich in die Verarbeitung meiner personenbezogenen Daten für Forschungszwecke ein, sofern ich separat zugestimmt habe.\n\n" },

{ insert: "Informationen und Datenschutzerklärung\n", attributes: { header: 2 } },
{ insert: "Die Datenverarbeitung erfolgt unter Beachtung geltender Datenschutzgesetze (z. B. DSGVO). Alle Daten werden ausschließlich zu den im Informationsblatt beschriebenen Zwecken verwendet.\n\n" },

{ insert: "1. Bezeichnung der Verarbeitungstätigkeit\n", attributes: { header: 3 } },
{ insert: "Verarbeitung personenbezogener Daten zur Durchführung der Anwendung und optional zur wissenschaftlichen Auswertung von Nutzungsverhalten und Feedback.\n\n" },

{ insert: "2. Datenverantwortlicher\n", attributes: { header: 3 } },
{ insert: "Verantwortliche Organisation:\n", attributes: { bold: true } },
{ insert: "[Name der Organisation]\n[Adresse]\n[E-Mail-Adresse oder Kontaktlink]\n\n" },

{ insert: "3. Datenschutzbeauftragter\n", attributes: { header: 3 } },
{ insert: "[Name/Titel des Datenschutzbeauftragten]\n[Organisation oder Kontaktstelle]\n[Adresse]\n[E-Mail oder Datenschutzlink]\n\n" },

{ insert: "4. Zweck(e) und Rechtsgrundlage(n) der Verarbeitung\n", attributes: { header: 3 } },
{ insert: "Zwecke:\n", attributes: { bold: true } },
{ insert: "Bewertung und Durchführung der Anwendung\n", attributes: { list: "bullet" } },
{ insert: "Eindeutige Zuordnung über Benutzerkennungen\n", attributes: { list: "bullet" } },
{ insert: "Sicherstellung der Kontosicherheit durch Zwei-Faktor-Authentifizierung (2FA), einschließlich Versand einmaliger Verifizierungscodes per E-Mail\n", attributes: { list: "bullet" } },
{ insert: "Nutzung anonymisierter Daten für Forschung und Publikation (bei Zustimmung)\n", attributes: { list: "bullet" } },
{ insert: "Analyse des Nutzungsverhaltens (z. B. Klicks, Navigation, Zeitstempel)\n", attributes: { list: "bullet" } },
{ insert: "Technisch notwendige IP-Verarbeitung (nicht gespeichert)\n", attributes: { list: "bullet" } },
{ insert: "\nRechtsgrundlage:\n", attributes: { bold: true } },
{ insert: "Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)\n\n" },

{ insert: "4a. Hinweise zur E-Mail-Verifizierung (2FA)\n", attributes: { header: 3 } },
{ insert: "Wenn E-Mail-2FA aktiviert ist, werden bei der Anmeldung einmalige Sicherheitscodes an die hinterlegte E-Mail-Adresse gesendet.\n" },
{ insert: "Verarbeitet werden dabei insbesondere:\n", attributes: { bold: true } },
{ insert: "E-Mail-Adresse\n", attributes: { list: "bullet" } },
{ insert: "Zeitpunkt des Versands und Ablaufzeitpunkt des Codes\n", attributes: { list: "bullet" } },
{ insert: "Technische Metadaten zur Missbrauchs- und Fehlerprävention\n", attributes: { list: "bullet" } },
{ insert: "Die Codes werden ausschließlich zur Anmeldung und Kontosicherheit verwendet, nicht für Werbung oder Newsletter.\n\n" },

{ insert: "5. Dauer der Speicherung\n", attributes: { header: 3 } },
{ insert: "Personenbezogene Daten werden für einen definierten Zeitraum (z. B. zwei Jahre nach Abschluss) gespeichert und anschließend gelöscht oder anonymisiert.\n\n" },

{ insert: "6. Rechte der betroffenen Personen\n", attributes: { header: 3 } },
{ insert: "Recht auf Auskunft\n", attributes: { list: "bullet" } },
{ insert: "Recht auf Berichtigung\n", attributes: { list: "bullet" } },
{ insert: "Recht auf Löschung oder Einschränkung\n", attributes: { list: "bullet" } },
{ insert: "Recht auf Widerspruch\n", attributes: { list: "bullet" } },
{ insert: "Recht auf Datenübertragbarkeit\n", attributes: { list: "bullet" } },
{ insert: "Beschwerderecht bei einer Aufsichtsbehörde\n", attributes: { list: "bullet" } },
{ insert: "\n" },

{ insert: "7. Widerrufsrecht\n", attributes: { header: 3 } },
{ insert: "Die Einwilligung kann jederzeit widerrufen werden. Die Rechtmäßigkeit der bis dahin erfolgten Verarbeitung bleibt unberührt. Nach Anonymisierung ist keine nachträgliche Löschung mehr möglich.\n\n" },

{ insert: "8. Veröffentlichung anonymisierter Daten\n", attributes: { header: 3 } },
{ insert: "Anonymisierte Ergebnisse können in wissenschaftlichen Publikationen veröffentlicht werden. Die Daten werden unter einer offenen Lizenz (z. B. CC BY-NC 4.0) in geeigneten Repositorien veröffentlicht.\n" }
]
};

module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.bulkUpdate(
"setting",
{
value: updatedTermsTemplate,
updatedAt: new Date(),
},
{
key: "app.register.terms",
},
);
},

async down(queryInterface, Sequelize) {
await queryInterface.bulkUpdate(
"setting",
{
value: previousTermsTemplate,
updatedAt: new Date(),
},
{
key: "app.register.terms",
},
);
},
};
14 changes: 14 additions & 0 deletions backend/db/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,20 @@ module.exports = (sequelize, DataTypes) => {
emailVerificationToken: DataTypes.STRING,
lastPasswordResetEmailSent: DataTypes.DATE,
lastVerificationEmailSent: DataTypes.DATE,
// Email OTP for 2FA
twoFactorOtp: DataTypes.STRING,
twoFactorOtpExpiresAt: DataTypes.DATE,
// Multi-method 2FA configuration
twoFactorMethods: {
type: DataTypes.JSON,
defaultValue: [],
},
// TOTP for 2FA
totpSecret: DataTypes.STRING,
// External login method identifiers
orcidId: DataTypes.STRING,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] should we add unique: true here too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually set the extra rules in the migration files; here we do not need to add unique: true.

ldapUsername: DataTypes.STRING,
samlNameId: DataTypes.STRING,
},
{
sequelize,
Expand Down
Loading