diff --git a/server/bin/migrations/UserInDatabase2.ts b/server/bin/migrations/UserInDatabase2.ts new file mode 100644 index 00000000..e98d41e3 --- /dev/null +++ b/server/bin/migrations/UserInDatabase2.ts @@ -0,0 +1,96 @@ +import {AccessLevel} from '../../../shared/schema/Auth'; +import Users from '../../models/Users'; + +import type {Credentials} from '../../../shared/schema/Auth'; +import type {RTorrentConnectionSettings} from '../../../shared/schema/ClientConnectionSettings'; +import type {UserInDatabase1} from './types/UserInDatabase1'; + +const migrationError = (err?: Error) => { + if (err) { + console.error(err); + } + console.error('Migration failed. You need to reset the databases manually.'); + process.exit(); +}; + +const migration = () => { + return new Promise((resolve, _reject) => { + Users.listUsers((users, err) => { + if (users == null || err) { + return; + } + + Promise.all( + users.map((user) => { + return new Promise((migratedResolve, _migratedReject) => { + if (user.client != null) { + // No need to migrate. + migratedResolve(); + return; + } + + const userV1 = (user as unknown) as UserInDatabase1; + + let connectionSettings: RTorrentConnectionSettings | null = null; + if (userV1.socketPath != null) { + connectionSettings = { + client: 'rTorrent', + type: 'socket', + version: 1, + socket: userV1.socketPath, + }; + } else if (userV1.host != null && userV1.port != null) { + connectionSettings = { + client: 'rTorrent', + type: 'tcp', + version: 1, + host: userV1.host, + port: userV1.port, + }; + } + + if (connectionSettings == null) { + migrationError(new Error('Corrupted client connection settings.')); + return; + } + + const userV2: Credentials = { + username: userV1.username, + password: userV1.password, + client: connectionSettings, + level: userV1.isAdmin ? AccessLevel.ADMINISTRATOR : AccessLevel.USER, + }; + + Users.removeUser(userV1.username, (id, errRemoval) => { + if (errRemoval) { + migrationError(errRemoval); + return; + } + + if (id == null) { + migrationError(new Error('Wrong user ID')); + return; + } + + Users.createUser( + userV2, + (_username, errCreation) => { + if (errCreation) { + migrationError(errCreation); + } + + migratedResolve(); + }, + false, + ); + }); + }); + }), + ).then(() => { + resolve(); + }); + }); + }); +}; + +export default migration; diff --git a/server/bin/migrations/run.ts b/server/bin/migrations/run.ts index 2f3c8e46..81921e8b 100644 --- a/server/bin/migrations/run.ts +++ b/server/bin/migrations/run.ts @@ -1,8 +1,6 @@ -const migrations = [ - () => { - // do nothing. there is no migration at the moment. - }, -]; +import UserInDatabase2 from './UserInDatabase2'; + +const migrations = [UserInDatabase2]; const migrate = () => Promise.all(migrations.map((migration) => migration())); diff --git a/server/bin/migrations/types/UserInDatabase1.ts b/server/bin/migrations/types/UserInDatabase1.ts new file mode 100644 index 00000000..9dbd6bfd --- /dev/null +++ b/server/bin/migrations/types/UserInDatabase1.ts @@ -0,0 +1,10 @@ +// Deprecated data structure. Not used outside of migration. +export type UserInDatabase1 = { + _id: string; + username: string; + password: string; + host?: string | null; + port?: number | null; + socketPath?: string | null; + isAdmin: boolean; +}; diff --git a/server/bin/start.ts b/server/bin/start.ts index 96bae1cd..a44153c2 100755 --- a/server/bin/start.ts +++ b/server/bin/start.ts @@ -4,13 +4,16 @@ import chalk from 'chalk'; import enforcePrerequisites from './enforce-prerequisites'; import migrateData from './migrations/run'; -import startWebServer from './web-server'; process.env.NODE_ENV = process.env.NODE_ENV !== 'development' ? 'production' : 'development'; enforcePrerequisites() .then(migrateData) - .then(startWebServer) + .then(() => { + // We do this because we don't want the side effects of importing server functions before migration is completed. + const startWebServer = require('./web-server').default; // eslint-disable-line global-require + return startWebServer(); + }) .catch((error) => { console.log(chalk.red('Failed to start Flood:')); console.trace(error); diff --git a/server/models/Users.ts b/server/models/Users.ts index f1bf997a..6248a0f4 100644 --- a/server/models/Users.ts +++ b/server/models/Users.ts @@ -75,7 +75,8 @@ class Users { createUser( credentials: Credentials, - callback: (data: {username: Required} | null, error?: Error) => void, + callback: (user: UserInDatabase | null, error?: Error) => void, + shouldHash = true, ): void { if (this.db == null) { return callback(null, new Error('Users database is not ready.')); @@ -87,7 +88,7 @@ class Users { this.db.insert( { ...credentials, - password: hash.encoded, + password: shouldHash ? hash.encoded : credentials.password, }, (error, user) => { if (error) { @@ -98,8 +99,7 @@ class Users { return callback(null, error); } - services.bootstrapServicesForUser(user as UserInDatabase); - return callback({username: credentials.username}); + return callback(user as UserInDatabase); }, ); }) diff --git a/server/routes/api/auth.ts b/server/routes/api/auth.ts index 9222076a..31082428 100644 --- a/server/routes/api/auth.ts +++ b/server/routes/api/auth.ts @@ -92,11 +92,7 @@ router.post('/authenticate', (req, const credentials = parsedResult.data; - Users.comparePassword(credentials, (isMatch, level, err) => { - if (err) { - return; - } - + Users.comparePassword(credentials, (isMatch, level, _err) => { if (isMatch === true && level != null) { sendAuthenticationResponse(res, { ...credentials, @@ -164,14 +160,16 @@ router.post('/regis const credentials = parsedResult.data; // Attempt to save the user - Users.createUser(credentials, (createUserResponse, createUserError) => { - if (createUserError) { - ajaxUtil.getResponseFn(res)(createUserResponse, createUserError); + Users.createUser(credentials, (user, error) => { + if (error || user == null) { + ajaxUtil.getResponseFn(res)({username: credentials.username}, error); return; } + services.bootstrapServicesForUser(user); + if (req.query.cookie === 'false') { - ajaxUtil.getResponseFn(res)(createUserResponse); + ajaxUtil.getResponseFn(res)({username: user.username}); return; }