-
Notifications
You must be signed in to change notification settings - Fork 1
Feat 99 setup wizard #173
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Feat 99 setup wizard #173
Changes from all commits
ba24991
9d5e6cf
3130b7a
a379667
bcaaad3
59a6fc5
b3eb522
e427b62
0736ee8
0cf693d
e686bc1
69459e6
17dcdbe
8a5d985
376f870
692a370
21653b8
c6f48be
5ad0ea0
91a7311
ba11464
9bc2213
f861e64
9fbd2d8
18c22c8
e3c2e33
595252f
512420e
7536e2a
1764831
4b0d8e8
59bed69
8be8026
e1eabbb
26e2199
dc87a7f
a7d5fa6
527b691
b8b26c4
e780806
d5a5c2a
c6db20b
dcc0f7a
4d37883
e692b5b
6197fd1
04a3575
7ce292c
9f68bdc
6e964f6
b19e565
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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: { | ||
| 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'); | ||
| }, | ||
| }; | ||
| 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', { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can't that be a FK to the id of the |
||
| 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'); | ||
| }, | ||
| }; | ||
| 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'], | ||
| }, {}); | ||
| }, | ||
| }; |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| }], {}); | ||
| } | ||
| }, | ||
| }; |
| 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 }, | ||
| { 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, | ||
| } | ||
| ); | ||
| } | ||
| }, | ||
| }; |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
| }, | ||
| }; |
There was a problem hiding this comment.
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?