mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-01 19:15:56 +01:00
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,6 +4,7 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
.pnp.js
|
.pnp.js
|
||||||
|
/cli/node_modules/
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
@@ -49,6 +50,7 @@ data/configs
|
|||||||
!.yarn/releases
|
!.yarn/releases
|
||||||
!.yarn/sdks
|
!.yarn/sdks
|
||||||
!.yarn/versions
|
!.yarn/versions
|
||||||
|
/cli/.yarn/
|
||||||
|
|
||||||
#envfiles
|
#envfiles
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ COPY ./drizzle ./drizzle
|
|||||||
|
|
||||||
COPY ./drizzle/migrate ./migrate
|
COPY ./drizzle/migrate ./migrate
|
||||||
COPY ./tsconfig.json ./migrate/tsconfig.json
|
COPY ./tsconfig.json ./migrate/tsconfig.json
|
||||||
|
COPY ./cli ./cli
|
||||||
|
|
||||||
RUN mkdir /data
|
RUN mkdir /data
|
||||||
|
|
||||||
@@ -43,6 +44,10 @@ RUN mv node_modules ./migrate/node_modules
|
|||||||
# Copy temp node_modules of app to app folder
|
# Copy temp node_modules of app to app folder
|
||||||
RUN mv _node_modules node_modules
|
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 the default application port
|
||||||
EXPOSE $PORT
|
EXPOSE $PORT
|
||||||
ENV PORT=${PORT}
|
ENV PORT=${PORT}
|
||||||
|
|||||||
30
cli/cli.js
Normal file
30
cli/cli.js
Normal file
@@ -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
cli/commands/reset-owner-password.js
Normal file
55
cli/commands/reset-owner-password.js
Normal file
@@ -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
cli/commands/reset-password.js
Normal file
56
cli/commands/reset-password.js
Normal file
@@ -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
cli/package.json
Normal file
12
cli/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
1371
cli/yarn.lock
Normal file
1371
cli/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user