Skip to content

Commit

Permalink
#1616 Recovery script in Docker container
Browse files Browse the repository at this point in the history
  • Loading branch information
manuel-rw committed Dec 2, 2023
1 parent cca8be0 commit 34b548a
Show file tree
Hide file tree
Showing 7 changed files with 1,531 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/node_modules
/.pnp
.pnp.js
/cli/node_modules/

# testing
/coverage
Expand Down Expand Up @@ -49,6 +50,7 @@ data/configs
!.yarn/releases
!.yarn/sdks
!.yarn/versions
/cli/.yarn/

#envfiles
.env
Expand Down
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ COPY ./drizzle ./drizzle

COPY ./drizzle/migrate ./migrate
COPY ./tsconfig.json ./migrate/tsconfig.json
COPY ./cli ./cli

RUN mkdir /data

Expand All @@ -43,6 +44,10 @@ RUN mv node_modules ./migrate/node_modules
# Copy temp node_modules of app to app folder
RUN mv _node_modules node_modules

RUN echo '#!/bin/bash\nnode /app/cli/cli.js "$@"' > /usr/bin/homarr
RUN chmod +x /usr/bin/homarr
RUN cd /app/cli && yarn --immutable

# Expose the default application port
EXPOSE $PORT
ENV PORT=${PORT}
Expand Down
30 changes: 30 additions & 0 deletions cli/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import yargs from 'yargs';

import { resetPasswordForOwner } from './commands/reset-owner-password.js';
import { resetPasswordForUsername } from './commands/reset-password.js';

yargs(process.argv.slice(2))
.scriptName('homarr')
.usage('$0 <cmd> [args]')
.command('reset-owner-password', 'Resets the current owner password without UI access', async () => {
await resetPasswordForOwner();
})
.command(
'reset-password',
'Reset the password of a specific user without UI access',
(yargs) => {
yargs.option('username', {
type: 'string',
describe: 'Username of user',
demandOption: true
});
},
async (argv) => {
await resetPasswordForUsername(argv.username);
}
)
.version(false)
.showHelpOnFail(true)
.alias('h', 'help')
.demandCommand()
.help().argv;
55 changes: 55 additions & 0 deletions cli/commands/reset-owner-password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import bcrypt from 'bcryptjs';

import Database from 'better-sqlite3';

import boxen from 'boxen';

import chalk from 'chalk';

import Consola from 'consola';

import crypto from 'crypto';

import { sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/better-sqlite3';

export async function resetPasswordForOwner() {
if (!process.env.DATABASE_URL) {
Consola.error('Unable to connect to database due to missing database URL environment variable');
return;
}

Consola.info('Connecting to the database...');
const sqlite = new Database(process.env.DATABASE_URL.replace('file:', ''));
const db = drizzle(sqlite);

Consola.info('Connected to the database ' + chalk.green('✓'));
Consola.info('Generating new random password...');

const newPassword = crypto.randomUUID();
const salt = bcrypt.genSaltSync(10);
const hashedPassword = bcrypt.hashSync(newPassword, salt);

try {
await db.transaction((tx) => {
tx.run(
sql`DELETE FROM session WHERE userId = (SELECT id FROM user WHERE is_owner = 1 LIMIT 1)`
);
tx.run(sql`UPDATE user SET password = ${hashedPassword} WHERE is_owner = 1 LIMIT 1;`);
});
console.log(
boxen(`New owner password is '${chalk.red(newPassword)}'. You can now log in with this password.\nExising sessions have been destroyed and need to login again with the new passowrd.`, {
dimBorder: true,
borderStyle: 'round',
padding: {
left: 1,
right: 1
}
})
);
} catch (err) {
Consola.error('Failed to update password', err);
} finally {
Consola.info('Command has completed');
}
}
56 changes: 56 additions & 0 deletions cli/commands/reset-password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import bcrypt from 'bcryptjs';

import Database from 'better-sqlite3';

import Consola from 'consola';

import crypto from 'crypto';

import boxen from 'boxen';

import chalk from 'chalk';

import { sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/better-sqlite3';

export async function resetPasswordForUsername(username) {
if (!process.env.DATABASE_URL) {
Consola.error('Unable to connect to database due to missing database URL environment variable');
return;
}

Consola.info('Connecting to the database...');
const sqlite = new Database(process.env.DATABASE_URL.replace('file:', ''));
const db = drizzle(sqlite);

Consola.info('Generating new random password...');

const newPassword = crypto.randomUUID();
const salt = bcrypt.genSaltSync(10);
const hashedPassword = bcrypt.hashSync(newPassword, salt);

Consola.info(`Updating password for user '${username}'`);

try {
await db.transaction((tx) => {
tx.run(
sql`DELETE FROM session WHERE userId = (SELECT id FROM user WHERE name = ${username} LIMIT 1)`
);
tx.run(sql`UPDATE user SET password = ${hashedPassword} WHERE id = (SELECT id FROM user WHERE name = ${username} LIMIT 1) LIMIT 1`);
});
console.log(
boxen(`New password for '${username}' is '${chalk.red(newPassword)}'. You can now log in with this password.\nExising sessions have been destroyed and need to login again with the new passowrd.`, {
dimBorder: true,
borderStyle: 'round',
padding: {
left: 1,
right: 1
}
})
);
} catch (err) {
Consola.error('Failed to update password', err);
} finally {
Consola.info('Command has completed');
}
}
12 changes: 12 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"dependencies": {
"bcryptjs": "^2.4.3",
"better-sqlite3": "^8.6.0",
"boxen": "^7.1.1",
"chalk": "^5.3.0",
"consola": "^3.0.0",
"drizzle-orm": "^0.28.6",
"yargs": "^17.7.2"
},
"type": "module"
}
Loading

0 comments on commit 34b548a

Please sign in to comment.