Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ba24991
feat: add wizard_step table and model for setup wizard
mohammadsherif0 Feb 23, 2026
9d5e6cf
feat: add wizard columns to setting table (showInWizard, wizardOrder,…
mohammadsherif0 Feb 23, 2026
3130b7a
feat: add wizard steps and wizard completion settings
mohammadsherif0 Feb 23, 2026
a379667
feat: assign wizard settings to steps (general, mail, registration, m…
mohammadsherif0 Feb 23, 2026
bcaaad3
refactor: remove default admin to require setup wizard
mohammadsherif0 Feb 23, 2026
59a6fc5
feat: add getWizardSettingsByStep and setup config route
mohammadsherif0 Feb 23, 2026
b3eb522
feat: add setup wizard support to auth routes (check, setup-admin)
mohammadsherif0 Feb 23, 2026
e427b62
feat: add Collapsible form component for sections
mohammadsherif0 Feb 23, 2026
0736ee8
feat: add SetupWizard with 6 steps, collapsibles, and JSON import modal
mohammadsherif0 Feb 23, 2026
0cf693d
feat: add router guard to redirect when wizard not completed
mohammadsherif0 Feb 23, 2026
e686bc1
feat : add app_state migration for wizard state storage
mohammadsherif0 Feb 23, 2026
69459e6
feat : add AppState model with get and set methods
mohammadsherif0 Feb 23, 2026
17dcdbe
refactor : use AppState for wizardCompleted in auth check
mohammadsherif0 Feb 23, 2026
8a5d985
feat : add PATCH /setup/state endpoint for wizard state
mohammadsherif0 Feb 23, 2026
376f870
refactor : save wizard state via PATCH /setup/state in SetupWizard
mohammadsherif0 Feb 23, 2026
692a370
feat: add displayName and displayGroup columns to settings
mohammadsherif0 Feb 26, 2026
21653b8
feat: add app.register.enabled to wizard registration step
mohammadsherif0 Feb 26, 2026
c6f48be
refactor: use displayGroup for settings page layout
mohammadsherif0 Feb 26, 2026
5ad0ea0
refactor: use displayName from backend in setup wizard
mohammadsherif0 Feb 26, 2026
91a7311
feat: add displaySubsection to settings
mohammadsherif0 Mar 1, 2026
ba11464
refactor: use displayGroup and displaySubsection for Settings layout
mohammadsherif0 Mar 1, 2026
9bc2213
refactor: use displaySubsection in SetupWizard
mohammadsherif0 Mar 1, 2026
f861e64
refactor: simplify wizard steps and settings mapping
mohammadsherif0 Mar 26, 2026
9fbd2d8
refactor: simplify setup wizard and create admin on finish
mohammadsherif0 Mar 26, 2026
18c22c8
refactor: show terms of service in registration settings
mohammadsherif0 Mar 26, 2026
e3c2e33
feat: add optional moodle keys to wizard settings migration
mohammadsherif0 Mar 29, 2026
595252f
docs: clarify moodle wizard note in setup route
mohammadsherif0 Mar 29, 2026
512420e
feat: show setting help beside label in setting item
mohammadsherif0 Mar 29, 2026
7536e2a
feat: add moodle block, import flow, and terms modal to setup wizard
mohammadsherif0 Mar 29, 2026
1764831
feat : reinitialize mail transport after mail settings save
mohammadsherif0 Apr 7, 2026
4b0d8e8
feat : add admin mail send test socket and shared mail test helpers
mohammadsherif0 Apr 7, 2026
59bed69
feat : add setup config and test-mail endpoints
mohammadsherif0 Apr 7, 2026
8be8026
feat : update setup wizard general step with download template and te…
mohammadsherif0 Apr 7, 2026
e1eabbb
feat : add mail test area via settings section footer slot and switch…
mohammadsherif0 Apr 7, 2026
26e2199
fix : sync editor modal inner state from model value using immediate …
mohammadsherif0 Apr 7, 2026
dc87a7f
refactor : use basicbutton and btn-group for import settings modal
mohammadsherif0 Apr 19, 2026
a7d5fa6
refactor : extract initial admin creation into shared helper
mohammadsherif0 Apr 19, 2026
527b691
feat : add dev admin setup to skip wizard on boot
mohammadsherif0 Apr 19, 2026
b8b26c4
chore : add make dev-no-wizard
mohammadsherif0 Apr 19, 2026
e780806
refactor : remove unused rerun wizard parameter
mohammadsherif0 Apr 19, 2026
d5a5c2a
fix : redirect /wizard to home when setup is complete
mohammadsherif0 Apr 19, 2026
c6db20b
Merge branch 'dev' into feat-99-setup-wizard
mohammadsherif0 Apr 19, 2026
dcc0f7a
feat: add migration for new settings display metadata
mohammadsherif0 Apr 19, 2026
4d37883
feat: extend dashboard settings subsection ordering
mohammadsherif0 Apr 19, 2026
e692b5b
docs: revise settingitem row description
mohammadsherif0 Apr 19, 2026
6197fd1
fix: return json payload from auth check endpoint
mohammadsherif0 Apr 19, 2026
04a3575
refactor: make dev skip setup wizard by default
mohammadsherif0 Apr 20, 2026
7ce292c
fix: validate wizard JSON import keys and show import result toasts
mohammadsherif0 Apr 20, 2026
9f68bdc
fix: remove duplicate modal close trigger from editor footer button
mohammadsherif0 Apr 20, 2026
6e964f6
docs : add setup wizard and settings documentation
mohammadsherif0 Apr 20, 2026
b19e565
Merge branch 'dev' into feat-99-setup-wizard
mohammadsherif0 Apr 20, 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
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ default: help
.PHONY: help
help:
@echo "make help Show this help message"
@echo "make dev Run in development mode (only unix)"
@echo "make dev Run in development mode, skipping the first-time setup wizard (only unix)"
@echo "make dev-wizard Run dev mode and go through the first-time setup wizard"
@echo "make doc Build the documentation"
@echo "make dev-build Build the frontend (make dev-build-frontend) and run the backend in development mode"
@echo "make dev-backend Run backend in development mode"
Expand Down Expand Up @@ -79,6 +80,10 @@ init: modules db

.PHONY: dev
dev: frontend/node_modules/.uptodate backend/node_modules/.uptodate
cd frontend && npm run frontend-dev & cd backend && DEV_SKIP_WIZARD=true npm run start

.PHONY: dev-wizard
dev-wizard: frontend/node_modules/.uptodate backend/node_modules/.uptodate
cd frontend && npm run frontend-dev & cd backend && npm run start

.PHONY: dev-frontend
Expand Down
56 changes: 56 additions & 0 deletions backend/db/migrations/20260119185828-create-wizard_step.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('wizard_step', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
key: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
order: {
type: Sequelize.INTEGER,
allowNull: false,
},
title: {
type: Sequelize.STRING,
allowNull: false,
},
description: {
type: Sequelize.STRING,
allowNull: true,
},
type: {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we use integer instead of strings in that table? I guess the types are predefined somehow?

type: Sequelize.STRING,
allowNull: false,
},
deleted: {
type: Sequelize.BOOLEAN,
defaultValue: false,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
deletedAt: {
allowNull: true,
type: Sequelize.DATE,
},
});
},

async down(queryInterface, Sequelize) {
await queryInterface.dropTable('wizard_step');
},
};
30 changes: 30 additions & 0 deletions backend/db/migrations/20260119185850-extend-setting-wizard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn('setting', 'showInWizard', {
type: Sequelize.BOOLEAN,
defaultValue: false,
});
await queryInterface.addColumn('setting', 'wizardOrder', {
type: Sequelize.INTEGER,
allowNull: true,
});
await queryInterface.addColumn('setting', 'requiredInWizard', {
type: Sequelize.BOOLEAN,
defaultValue: false,
});
await queryInterface.addColumn('setting', 'wizardStep', {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can't that be a FK to the id of the wizard_step table?

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

async down(queryInterface, Sequelize) {
await queryInterface.removeColumn('setting', 'showInWizard');
await queryInterface.removeColumn('setting', 'wizardOrder');
await queryInterface.removeColumn('setting', 'requiredInWizard');
await queryInterface.removeColumn('setting', 'wizardStep');
},
};
21 changes: 21 additions & 0 deletions backend/db/migrations/20260119192230-basic-wizard_steps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
const now = new Date();
await queryInterface.bulkInsert('wizard_step', [
{ key: 'admin', order: 1, title: 'Admin Account', description: 'Create the administrator account', type: 'admin', deleted: false, createdAt: now, updatedAt: now, deletedAt: null },
{ key: 'general', order: 2, title: 'General Settings', description: 'Copyright, consent, guest login, external links', type: 'general', deleted: false, createdAt: now, updatedAt: now, deletedAt: null },
{ key: 'mail', order: 3, title: 'Mail Configuration', description: 'Enable email service and configure SMTP/sendmail', type: 'mail', deleted: false, createdAt: now, updatedAt: now, deletedAt: null },
{ key: 'registration', order: 4, title: 'User Registration', description: 'What is required at signup', type: 'registration', deleted: false, createdAt: now, updatedAt: now, deletedAt: null },
{ key: 'summary', order: 6, title: 'Summary', description: 'Review your choices before finishing', type: 'summary', deleted: false, createdAt: now, updatedAt: now, deletedAt: null },
], {});
},

async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete('wizard_step', {
key: ['admin', 'general', 'mail', 'registration', 'moodle', 'summary'],
}, {});
},
};
94 changes: 94 additions & 0 deletions backend/db/migrations/20260119192356-transform-user-admin.js
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

quick verification question: are they still visible for everyone after that?

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict';

const { genSalt, genPwdHash } = require('../../utils/auth');

/**
* Names of the 5 Exposé configurations created by basic-configuration (20250919125851).
* They are reassigned to Bot (userId 2) before deleting the admin to satisfy the
* configuration.userId FK to user. POST /auth/setup-admin then reassigns them to the new admin.
*/
const EXPOSE_CONFIG_NAMES = [
'Exposé assessment configuration',
'Exposé feedback configuration',
'UKP Exposé Submission Validator',
'Exposé assessment configuration (German)',
'Exposé feedback configuration (German)',
];

/**
* Removes the default admin user created by basic-users so the first admin
* must be created via the setup wizard. Runs after basic-configuration; the 5
* Exposé configs are first reassigned to Bot (userId 2) to satisfy the FK, then
* the admin is deleted. POST /auth/setup-admin reassigns them from Bot to the new admin.
*
* @type {import('sequelize-cli').Migration}
*/
module.exports = {
async up(queryInterface, Sequelize) {
const rows = await queryInterface.sequelize.query(
'SELECT id FROM "user" WHERE "userName" = \'admin\' AND "deleted" = false',
{ type: queryInterface.sequelize.QueryTypes.SELECT }
);
const admin = rows && rows[0];
if (!admin) {
return;
}
const adminId = admin.id;
const BOT_USER_ID = 2;

for (const name of EXPOSE_CONFIG_NAMES) {
await queryInterface.sequelize.query(
`UPDATE configuration SET "userId" = :botId, "updatedAt" = :now WHERE name = :name AND "userId" = :adminId`,
{
replacements: { botId: BOT_USER_ID, adminId, name, now: new Date() },
type: Sequelize.QueryTypes.UPDATE,
}
);
}

await queryInterface.bulkDelete('user_role_matching', { userId: adminId }, {});
await queryInterface.bulkDelete('user', { userName: 'admin' }, {});
},

async down(queryInterface, Sequelize) {
const salt = genSalt();
const passwordHash = await genPwdHash(process.env.ADMIN_PWD || 'admin', salt);
const email = process.env.ADMIN_EMAIL || 'admin@localhost';
const now = new Date();

await queryInterface.bulkInsert('user', [{
firstName: 'admin',
lastName: 'user',
userName: 'admin',
email: email,
passwordHash: passwordHash,
salt: salt,
acceptStats: true,
acceptTerms: true,
deleted: false,
createdAt: now,
updatedAt: now,
}], {});

const userRows = await queryInterface.sequelize.query(
'SELECT id FROM "user" WHERE "userName" = \'admin\'',
{ type: queryInterface.sequelize.QueryTypes.SELECT }
);
const roleRows = await queryInterface.sequelize.query(
'SELECT id FROM "user_role" WHERE name = \'admin\'',
{ type: queryInterface.sequelize.QueryTypes.SELECT }
);
const inserted = userRows && userRows[0];
const adminRole = roleRows && roleRows[0];
if (inserted && adminRole) {
await queryInterface.bulkInsert('user_role_matching', [{
userId: inserted.id,
userRoleId: adminRole.id,
deleted: false,
createdAt: now,
updatedAt: now,
deletedAt: null,
}], {});
}
},
};
75 changes: 75 additions & 0 deletions backend/db/migrations/20260119194252-transform-setting-wizard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';

/**
* Wizard settings per step requiredInWizard: true only where the setting must be filled.
*/
const WIZARD_SETTINGS = [
// general
{ key: 'app.config.copyright', wizardStep: 'general', wizardOrder: 1, requiredInWizard: true },
{ key: 'app.config.consent.enabled', wizardStep: 'general', wizardOrder: 2, requiredInWizard: false },
{ key: 'app.login.guest', wizardStep: 'general', wizardOrder: 3, requiredInWizard: false },
{ key: 'app.login.forgotPassword', wizardStep: 'general', wizardOrder: 4, requiredInWizard: false },
{ key: 'app.landing.showDocs', wizardStep: 'general', wizardOrder: 6, requiredInWizard: false },
{ key: 'app.landing.linkDocs', wizardStep: 'general', wizardOrder: 7, requiredInWizard: true },
{ key: 'app.landing.showProject', wizardStep: 'general', wizardOrder: 8, requiredInWizard: false },
{ key: 'app.landing.linkProject', wizardStep: 'general', wizardOrder: 9, requiredInWizard: false },
{ key: 'app.landing.showFeedback', wizardStep: 'general', wizardOrder: 10, requiredInWizard: false },
{ key: 'app.landing.linkFeedback', wizardStep: 'general', wizardOrder: 11, requiredInWizard: false },
// mail
{ key: 'system.mailService.enabled', wizardStep: 'mail', wizardOrder: 12, requiredInWizard: false },
{ key: 'system.mailService.sendMail.enabled', wizardStep: 'mail', wizardOrder: 13, requiredInWizard: false },
{ key: 'system.mailService.sendMail.path', wizardStep: 'mail', wizardOrder: 14, requiredInWizard: false },
{ key: 'system.mailService.senderAddress', wizardStep: 'mail', wizardOrder: 15, requiredInWizard: false },
{ key: 'system.mailService.smtp.enabled', wizardStep: 'mail', wizardOrder: 16, requiredInWizard: false },
{ key: 'system.mailService.smtp.host', wizardStep: 'mail', wizardOrder: 17, requiredInWizard: false },
{ key: 'system.mailService.smtp.port', wizardStep: 'mail', wizardOrder: 18, requiredInWizard: false },
{ key: 'system.mailService.smtp.secure', wizardStep: 'mail', wizardOrder: 19, requiredInWizard: false },
{ key: 'system.mailService.smtp.auth.enabled', wizardStep: 'mail', wizardOrder: 20, requiredInWizard: false },
{ key: 'system.mailService.smtp.auth.user', wizardStep: 'mail', wizardOrder: 21, requiredInWizard: false },
{ key: 'system.mailService.smtp.auth.pass', wizardStep: 'mail', wizardOrder: 22, requiredInWizard: false },
{ key: 'system.baseUrl', wizardStep: 'mail', wizardOrder: 23, requiredInWizard: false },
{ key: 'app.register.emailVerification', wizardStep: 'mail', wizardOrder: 24, requiredInWizard: false },
// app.login.forgotPassword already in general; in mail step it's a toggle in UI, same key
// registration
{ key: 'app.register.enabled', wizardStep: 'registration', wizardOrder: 25, requiredInWizard: false },
{ key: 'app.register.requestName', wizardStep: 'registration', wizardOrder: 26, requiredInWizard: false },
{ key: 'app.register.requestStats', wizardStep: 'registration', wizardOrder: 27, requiredInWizard: false },
{ key: 'app.register.requestData', wizardStep: 'registration', wizardOrder: 28, requiredInWizard: false },
{ key: 'app.register.acceptStats.default', wizardStep: 'registration', wizardOrder: 29, requiredInWizard: false },
{ key: 'app.register.acceptDataSharing.default', wizardStep: 'registration', wizardOrder: 30, requiredInWizard: false },
{ key: 'app.register.terms', wizardStep: 'general', wizardOrder: 5, requiredInWizard: false },
// Optional Moodle (wizardStep moodle keeps Settings > Moodle grouping; shown on General screen in SetupWizard)
{ key: 'rpc.moodleAPI.apiUrl', wizardStep: 'moodle', wizardOrder: 31, requiredInWizard: false },
{ key: 'rpc.moodleAPI.apiKey', wizardStep: 'moodle', wizardOrder: 32, requiredInWizard: false },
{ key: 'rpc.moodleAPI.courseID', wizardStep: 'moodle', wizardOrder: 33, requiredInWizard: false },
{ key: 'rpc.moodleAPI.showInput.apiUrl', wizardStep: 'moodle', wizardOrder: 34, requiredInWizard: false },
{ key: 'rpc.moodleAPI.showInput.apiKey', wizardStep: 'moodle', wizardOrder: 35, requiredInWizard: false },
{ key: 'rpc.moodleAPI.showInput.courseID', wizardStep: 'moodle', wizardOrder: 36, requiredInWizard: false },
];

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
for (const { key, wizardStep, wizardOrder, requiredInWizard } of WIZARD_SETTINGS) {
await queryInterface.sequelize.query(
`UPDATE setting SET "showInWizard" = true, "wizardStep" = :wizardStep, "wizardOrder" = :wizardOrder, "requiredInWizard" = :requiredInWizard, "updatedAt" = :now WHERE key = :key`,
{
replacements: { key, wizardStep, wizardOrder, requiredInWizard, now: new Date() },
type: Sequelize.QueryTypes.UPDATE,
}
);
}
},

async down(queryInterface, Sequelize) {
for (const { key } of WIZARD_SETTINGS) {
await queryInterface.sequelize.query(
`UPDATE setting SET "showInWizard" = false, "wizardStep" = NULL, "wizardOrder" = NULL, "requiredInWizard" = false, "updatedAt" = :now WHERE key = :key`,
{
replacements: { key, now: new Date() },
type: Sequelize.QueryTypes.UPDATE,
}
);
}
},
};
35 changes: 35 additions & 0 deletions backend/db/migrations/20260223130010-create-app_state.js
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we move those settings just to our settings table instead of creating a new table just for those two entries?

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('app_state', {
key: {
allowNull: false,
type: Sequelize.STRING,
primaryKey: true,
},
value: {
type: Sequelize.TEXT,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});

const now = new Date();
await queryInterface.bulkInsert('app_state', [
{ key: 'setup.wizardCompleted', value: 'false', createdAt: now, updatedAt: now },
{ key: 'setup.wizardCurrentStep', value: '0', createdAt: now, updatedAt: now },
], {});
},

async down(queryInterface, Sequelize) {
await queryInterface.dropTable('app_state');
},
};
Loading
Loading