From 6dcb65cedbd5c06b3d6e5893c4f9217d015cce05 Mon Sep 17 00:00:00 2001 From: Ong Chung Yau <33013947+chungyau97@users.noreply.github.com> Date: Sat, 7 Jun 2025 02:16:16 +0800 Subject: [PATCH] feature/cli-reset-password (#4585) * feat: add cli to reset password * chore: add information for password reset command * fix: add information for password reset command --- package.json | 3 ++ packages/server/src/commands/base.ts | 4 +- packages/server/src/commands/user.ts | 80 ++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/commands/user.ts diff --git a/package.json b/package.json index f0a83893..a29f8cda 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "start-worker": "run-script-os", "start-worker:windows": "cd packages/server/bin && run worker", "start-worker:default": "cd packages/server/bin && ./run worker", + "user": "run-script-os", + "user:windows": "cd packages/server/bin && run user", + "user:default": "cd packages/server/bin && ./run user", "test": "turbo run test", "clean": "pnpm --filter \"./packages/**\" clean", "nuke": "pnpm --filter \"./packages/**\" nuke && rimraf node_modules .turbo", diff --git a/packages/server/src/commands/base.ts b/packages/server/src/commands/base.ts index 86222497..bdffb8f6 100644 --- a/packages/server/src/commands/base.ts +++ b/packages/server/src/commands/base.ts @@ -1,6 +1,6 @@ import { Command, Flags } from '@oclif/core' -import path from 'path' import dotenv from 'dotenv' +import path from 'path' import logger from '../utils/logger' dotenv.config({ path: path.join(__dirname, '..', '..', '.env'), override: true }) @@ -120,7 +120,7 @@ export abstract class BaseCommand extends Command { logger.error('unhandledRejection: ', err) }) - const { flags } = await this.parse(BaseCommand) + const { flags } = await this.parse(this.constructor as any) if (flags.PORT) process.env.PORT = flags.PORT if (flags.CORS_ORIGINS) process.env.CORS_ORIGINS = flags.CORS_ORIGINS if (flags.IFRAME_ORIGINS) process.env.IFRAME_ORIGINS = flags.IFRAME_ORIGINS diff --git a/packages/server/src/commands/user.ts b/packages/server/src/commands/user.ts new file mode 100644 index 00000000..1eecaaa2 --- /dev/null +++ b/packages/server/src/commands/user.ts @@ -0,0 +1,80 @@ +import { Args } from '@oclif/core' +import { QueryRunner } from 'typeorm' +import * as DataSource from '../DataSource' +import { User } from '../enterprise/database/entities/user.entity' +import { getHash } from '../enterprise/utils/encryption.util' +import { isInvalidPassword } from '../enterprise/utils/validation.util' +import logger from '../utils/logger' +import { BaseCommand } from './base' + +export default class user extends BaseCommand { + static args = { + email: Args.string({ + description: 'Email address to search for in the user database' + }), + password: Args.string({ + description: 'New password for that user' + }) + } + + async run(): Promise { + const { args } = await this.parse(user) + + let queryRunner: QueryRunner | undefined + try { + logger.info('Initializing DataSource') + const dataSource = await DataSource.getDataSource() + await dataSource.initialize() + + queryRunner = dataSource.createQueryRunner() + await queryRunner.connect() + + if (args.email && args.password) { + logger.info('Running resetPassword') + await this.resetPassword(queryRunner, args.email, args.password) + } else { + logger.info('Running listUserEmails') + await this.listUserEmails(queryRunner) + } + } catch (error) { + logger.error(error) + } finally { + if (queryRunner && !queryRunner.isReleased) await queryRunner.release() + await this.gracefullyExit() + } + } + + async listUserEmails(queryRunner: QueryRunner) { + logger.info('Listing all user emails') + const users = await queryRunner.manager.find(User, { + select: ['email'] + }) + + const emails = users.map((user) => user.email) + logger.info(`Email addresses: ${emails.join(', ')}`) + logger.info(`Email count: ${emails.length}`) + logger.info('To reset user password, run the following command: pnpm user --email "myEmail" --password "myPassword"') + } + + async resetPassword(queryRunner: QueryRunner, email: string, password: string) { + logger.info(`Finding user by email: ${email}`) + const user = await queryRunner.manager.findOne(User, { + where: { email } + }) + if (!user) throw new Error(`User not found with email: ${email}`) + + if (isInvalidPassword(password)) { + const errors = [] + if (!/(?=.*[a-z])/.test(password)) errors.push('at least one lowercase letter') + if (!/(?=.*[A-Z])/.test(password)) errors.push('at least one uppercase letter') + if (!/(?=.*\d)/.test(password)) errors.push('at least one number') + if (!/(?=.*[^a-zA-Z0-9])/.test(password)) errors.push('at least one special character') + if (password.length < 8) errors.push('minimum length of 8 characters') + throw new Error(`Invalid password: Must contain ${errors.join(', ')}`) + } + + user.credential = getHash(password) + await queryRunner.manager.save(user) + logger.info(`Password reset for user: ${email}`) + } +}