Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions packages/server/src/commands/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 { validatePasswordOrThrow } from '../enterprise/utils/validation.util'
import logger from '../utils/logger'
import { BaseCommand } from './base'

Expand Down Expand Up @@ -63,15 +63,7 @@ export default class user extends BaseCommand {
})
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(', ')}`)
}
validatePasswordOrThrow(password)

user.credential = getHash(password)
await queryRunner.manager.save(user)
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/enterprise/Interface.Enterprise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const OrgSetupSchema = z
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.max(128, 'Password must not be more than 128 characters')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/\d/, 'Password must contain at least one digit')
Expand All @@ -122,6 +123,7 @@ export const RegisterUserSchema = z
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.max(128, 'Password must not be more than 128 characters')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/\d/, 'Password must contain at least one digit')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express'
import { NextFunction, Request, Response } from 'express'
import { StatusCodes } from 'http-status-codes'
import { AccountService } from '../services/account.service'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { AccountService } from '../services/account.service'

export class AccountController {
public async register(req: Request, res: Response, next: NextFunction) {
Expand Down Expand Up @@ -68,7 +68,7 @@ export class AccountController {
try {
const accountService = new AccountService()
const data = await accountService.resetPassword(req.body)
return res.status(StatusCodes.CREATED).json(data)
return res.status(StatusCodes.OK).json(data)
} catch (error) {
next(error)
}
Expand Down
14 changes: 9 additions & 5 deletions packages/server/src/enterprise/services/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,18 @@ import { User, UserStatus } from '../database/entities/user.entity'
import { WorkspaceUser, WorkspaceUserStatus } from '../database/entities/workspace-user.entity'
import { Workspace, WorkspaceName } from '../database/entities/workspace.entity'
import { LoggedInUser, LoginActivityCode } from '../Interface.Enterprise'
import { destroyAllSessionsForUser } from '../middleware/passport/SessionPersistance'
import { compareHash } from '../utils/encryption.util'
import { sendPasswordResetEmail, sendVerificationEmailForCloud, sendWorkspaceAdd, sendWorkspaceInvite } from '../utils/sendEmail'
import { generateTempToken } from '../utils/tempTokenUtils'
import { validatePasswordOrThrow } from '../utils/validation.util'
import auditService from './audit'
import { OrganizationUserErrorMessage, OrganizationUserService } from './organization-user.service'
import { OrganizationErrorMessage, OrganizationService } from './organization.service'
import { RoleErrorMessage, RoleService } from './role.service'
import { UserErrorMessage, UserService } from './user.service'
import { WorkspaceUserErrorMessage, WorkspaceUserService } from './workspace-user.service'
import { WorkspaceErrorMessage, WorkspaceService } from './workspace.service'
import { destroyAllSessionsForUser } from '../middleware/passport/SessionPersistance'

type AccountDTO = {
user: Partial<User>
Expand Down Expand Up @@ -564,11 +565,14 @@ export class AccountService {
const diff = now.diff(tokenExpiry, 'minutes')
if (Math.abs(diff) > expiryInMins) throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.EXPIRED_TEMP_TOKEN)

// @ts-ignore
const password = data.user.password
validatePasswordOrThrow(password)

// all checks are done, now update the user password, don't forget to hash it and do not forget to clear the temp token
// leave the user status and other details as is
const salt = bcrypt.genSaltSync(parseInt(process.env.PASSWORD_SALT_HASH_ROUNDS || '5'))
// @ts-ignore
const hash = bcrypt.hashSync(data.user.password, salt)
const hash = bcrypt.hashSync(password, salt)
data.user = user
data.user.credential = hash
data.user.tempToken = ''
Expand All @@ -582,10 +586,10 @@ export class AccountService {
// Invalidate all sessions for this user after password reset
await destroyAllSessionsForUser(user.id as string)
} catch (error) {
await queryRunner.rollbackTransaction()
if (queryRunner && queryRunner.isTransactionActive) await queryRunner.rollbackTransaction()
throw error
} finally {
await queryRunner.release()
if (queryRunner && !queryRunner.isReleased) await queryRunner.release()
}

return { message: 'success' }
Expand Down
15 changes: 7 additions & 8 deletions packages/server/src/enterprise/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { StatusCodes } from 'http-status-codes'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { Telemetry, TelemetryEventType } from '../../utils/telemetry'
import { User, UserStatus } from '../database/entities/user.entity'
import { isInvalidEmail, isInvalidName, isInvalidPassword, isInvalidUUID } from '../utils/validation.util'
import { DataSource, ILike, QueryRunner } from 'typeorm'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { generateId } from '../../utils'
import { GeneralErrorMessage } from '../../utils/constants'
import { compareHash, getHash } from '../utils/encryption.util'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { sanitizeUser } from '../../utils/sanitize.util'
import { Telemetry, TelemetryEventType } from '../../utils/telemetry'
import { User, UserStatus } from '../database/entities/user.entity'
import { destroyAllSessionsForUser } from '../middleware/passport/SessionPersistance'
import { compareHash, getHash } from '../utils/encryption.util'
import { isInvalidEmail, isInvalidName, isInvalidPassword, isInvalidUUID } from '../utils/validation.util'

export const enum UserErrorMessage {
EXPIRED_TEMP_TOKEN = 'Expired Temporary Token',
Expand Down Expand Up @@ -170,8 +170,7 @@ export class UserService {
if (newUserData.newPassword !== newUserData.confirmPassword) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, UserErrorMessage.PASSWORDS_DO_NOT_MATCH)
}
const hash = getHash(newUserData.newPassword)
newUserData.credential = hash
newUserData.credential = this.encryptUserCredential(newUserData.newPassword)
newUserData.tempToken = ''
newUserData.tokenExpiry = undefined
}
Expand Down
23 changes: 23 additions & 0 deletions packages/server/src/enterprise/utils/validation.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,26 @@ export function isInvalidPassword(password: unknown): boolean {
const regexPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^a-zA-Z0-9]).{8,}$/
return !regexPassword.test(password)
}

/**
* Validates the password and throws an Error with a descriptive message if invalid.
* No-op when the password is valid.
* @throws Error with message "Invalid password: Must contain ..." or "Invalid password: Password is required."
*/
export function validatePasswordOrThrow(password: unknown): void {
if (!isInvalidPassword(password)) return

if (typeof password !== 'string') {
throw new Error('Invalid password: Password is required.')
}

const errors: string[] = []
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')
if (password.length > 128) errors.push('less than or equal to 128 characters')

throw new Error(`Invalid password: Must contain ${errors.join(', ')}`)
}
1 change: 1 addition & 0 deletions packages/ui/src/utils/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from 'zod'
export const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
.max(128, 'Password must not be more than 128 characters')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/\d/, 'Password must contain at least one digit')
Expand Down