From 2fdd5a72f038e046f95e7cf295a720f4e42ed36d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 10 Mar 2026 11:18:59 -0700 Subject: [PATCH] type authController correctly, update services accordingly --- server/src/controllers/authController.ts | 69 ++++++++------- server/src/controllers/controllerUtils.ts | 4 +- server/src/service/business/userService.ts | 88 ++++++++++---------- server/src/service/system/settingsService.ts | 6 +- 4 files changed, 89 insertions(+), 78 deletions(-) diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 3f837f632..cdca8fb52 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -1,7 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { AppError } from "@/utils/AppError.js"; -import { requireTeamId, requireUserRoles } from "./controllerUtils.js"; -import type { UserRole } from "@/types/user.js"; +import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js"; import { registrationBodyValidation, @@ -20,15 +19,16 @@ import { editSuperadminUserByIdBodyValidation, editUserPasswordByIdBodyValidation, } from "@/validation/userValidation.js"; +import { IUserService } from "@/service/index.js"; const SERVICE_NAME = "authController"; class AuthController { static SERVICE_NAME = SERVICE_NAME; - private userService: any; + private userService: IUserService; - constructor(userService: any) { + constructor(userService: IUserService) { this.userService = userService; } @@ -41,11 +41,12 @@ class AuthController { const newUser = req.body.user; const newUserToken = req.body.token; if (newUser?.email) { - newUser.email = newUser.email.toLowerCase(); + const newUserEmail = requireUserEmail(newUser.email); + newUser.email = newUserEmail.toLowerCase(); } registrationBodyValidation.parse(newUser); - const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); + const { user, token } = await this.userService.registerUser(newUser, newUserToken, req?.file ?? null); res.status(200).json({ success: true, msg: "User registered successfully", @@ -62,11 +63,11 @@ class AuthController { if (userData?.email) { userData.email = userData.email.toLowerCase(); } - createUserBodyValidation.parse(userData); + const validatedBody = createUserBodyValidation.parse(userData); const teamId = requireTeamId(req.user?.teamId); - const actorRoles = requireUserRoles(req.user?.role) as UserRole[]; - const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file); + const actorRoles = requireUserRoles(req.user?.role); + const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null); res.status(201).json({ success: true, msg: "User created successfully", @@ -101,7 +102,9 @@ class AuthController { editUser = async (req: Request, res: Response, next: NextFunction) => { try { const validatedBody = editUserBodyValidation.parse(req.body); - const updatedUser = await this.userService.editUser(validatedBody, req.file, req.user); + const userId = requireUserId(req.user?.id); + const userEmail = requireUserEmail(req.user?.email); + const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail); res.status(200).json({ success: true, @@ -170,7 +173,15 @@ class AuthController { deleteUser = async (req: Request, res: Response, next: NextFunction) => { try { - await this.userService.deleteUser(req.user); + const userId = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); + const roles = requireUserRoles(req.user?.role); + + await this.userService.deleteUser({ + userId, + teamId, + roles, + }); return res.status(200).json({ success: true, msg: "User deleted successfully", @@ -182,9 +193,12 @@ class AuthController { deleteUserById = async (req: Request, res: Response, next: NextFunction) => { try { - getUserByIdParamValidation.parse(req.params); - const targetUserId = req.params.userId; - await this.userService.deleteUserById(req.user, targetUserId); + const validatedParams = getUserByIdParamValidation.parse(req.params); + const targetUserId = validatedParams.userId; + const actorId = requireUserId(req.user?.id); + const actorTeamId = requireTeamId(req.user?.teamId); + const actorRoles = requireUserRoles(req.user?.role); + await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId }); return res.status(200).json({ success: true, msg: "User removed successfully", @@ -231,24 +245,19 @@ class AuthController { editUserById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = req?.user?.role; + const roles = requireUserRoles(req.user?.role); + const actorId = requireUserId(req.user?.id); if (!roles || !roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } - const userId = req.params.userId as string; - const user = { ...req.body }; - - editUserByIdParamValidation.parse(req.params); + const validatedParams = editUserByIdParamValidation.parse(req.params); // If this is superadmin self edit, allow "superadmin" role - if (userId === req.user?.id) { - editSuperadminUserByIdBodyValidation.parse(req.body); - } else { - editUserByIdBodyValidation.parse(req.body); - } + const validatedBody = + validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body); - await this.userService.editUserById(userId, user); + await this.userService.editUserById(validatedParams.userId, validatedBody); return res.status(200).json({ success: true, msg: "ok" }); } catch (error) { next(error); @@ -257,16 +266,14 @@ class AuthController { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = req?.user?.role; + const roles = requireUserRoles(req.user?.role); if (!roles || !roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } - const userId = req.params.userId as string; - editUserByIdParamValidation.parse(req.params); - editUserPasswordByIdBodyValidation.parse(req.body); - const updatedPassword = req.body.password; - await this.userService.setPasswordByUserId(userId, updatedPassword); + const validatedParams = editUserByIdParamValidation.parse(req.params); + const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body); + await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password); return res.status(200).json({ success: true, msg: "Password reset successfully" }); } catch (error) { next(error); diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index 99c60807e..e5c852591 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -1,5 +1,5 @@ import { AppError } from "@/utils/AppError.js"; -import { type MonitorType, MonitorTypes } from "@/types/index.js"; +import { type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => { const monitorUrl = new URL(monitor.url); @@ -112,7 +112,7 @@ const requireUserEmail = (userEmail?: string): string => { return userEmail; }; -export const requireUserRoles = (userRoles?: string[]): string[] => { +export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => { if (!userRoles || userRoles.length === 0) { throw new AppError({ message: "User roles are required", status: 400 }); } diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 3337e491d..e5b6f9e68 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -12,24 +12,31 @@ import bcrypt from "bcryptjs"; import { AppError } from "@/utils/AppError.js"; import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; import { IEmailService } from "@/service/infrastructure/emailService.js"; -import { ISettingsService } from "@/service/system/settingsService.js"; +import { EnvConfig, ISettingsService } from "@/service/system/settingsService.js"; import { ILogger } from "@/utils/logger.js"; import jwt from "jsonwebtoken"; +import crypto from "crypto"; +type CryptoType = typeof crypto; type JwtType = typeof jwt; const SERVICE_NAME = "userService"; export interface IUserService { - issueToken(payload: Partial, appSettings: { jwtTTL?: string; jwtSecret?: string }): string; - registerUser(user: Partial, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>; - createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: unknown): Promise; + issueToken(payload: Partial, appSettings: EnvConfig): string; + registerUser(user: Partial, inviteToken: string, file: Express.Multer.File | null): Promise<{ user: User; token: string }>; + createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise; loginUser(email: string, password: string): Promise<{ user: User; token: string }>; - editUser(updates: Partial, file: unknown, currentUser: User): Promise; + editUser( + updates: Partial, + file: Express.Multer.File | null, + currentUserId: string, + currentUserEmail: string + ): Promise; checkSuperadminExists(): Promise; requestRecovery(email: string): Promise; validateRecovery(recoveryToken: string): Promise; resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>; - deleteUser(user: User): Promise; - deleteUserById(actor: User, targetUserId: string): Promise; + deleteUser(params: { userId: string; teamId: string; roles: UserRole[] }): Promise; + deleteUserById(params: { actorId: string; actorTeamId: string; actorRoles: UserRole[]; targetUserId: string }): Promise; getAllUsers(): Promise; getUserById(roles: UserRole[], userId: string): Promise; editUserById(userId: string, patch: Partial): Promise; @@ -49,7 +56,7 @@ export class UserService implements IUserService { private logger: ILogger; private jwt: JwtType; private jobQueue: ISuperSimpleQueue; - private crypto: any; + private crypto: CryptoType; private monitorsRepository: IMonitorsRepository; private usersRepository: IUsersRepository; private invitesRepository: IInvitesRepository; @@ -71,7 +78,7 @@ export class UserService implements IUserService { settingsRepository, teamsRepository, }: { - crypto: any; + crypto: CryptoType; emailService: IEmailService; settingsService: ISettingsService; logger: ILogger; @@ -102,14 +109,14 @@ export class UserService implements IUserService { return UserService.SERVICE_NAME; } - issueToken = (payload: any, appSettings: any) => { + issueToken = (payload: Partial, appSettings: EnvConfig) => { const tokenTTL = appSettings?.jwtTTL ?? "2h"; const tokenSecret = appSettings?.jwtSecret; const payloadData = payload; return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); }; - registerUser = async (user: Partial, inviteToken: string, file: any) => { + registerUser = async (user: Partial, inviteToken: string, file: Express.Multer.File | null) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.usersRepository.findSuperAdmin(); @@ -179,7 +186,7 @@ export class UserService implements IUserService { return { user: newUser, token }; }; - createUser = async (userData: Partial, teamId: string, actorRoles: UserRole[], file: any) => { + createUser = async (userData: Partial, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null) => { // Validate that the creator can assign the requested roles const targetRoles = userData.role ?? []; for (const targetRole of targetRoles) { @@ -239,14 +246,19 @@ export class UserService implements IUserService { return { user: userWithoutPassword, token }; }; - editUser = async (updates: Partial, file: any, currentUser: any) => { + editUser = async ( + updates: Partial, + file: Express.Multer.File | null, + currentUserId: string, + currentUserEmail: string + ) => { // Change Password check if (updates?.password && updates?.newPassword) { // Get user's email // Add user email to body for DB operation - updates.email = currentUser.email; + updates.email = currentUserEmail; // Get user - const user = await this.usersRepository.findByEmail(currentUser.email); + const user = await this.usersRepository.findByEmail(currentUserEmail); // Compare passwords const match = await bcrypt.compare(updates?.password, user.password); // If not a match, throw a 403 @@ -259,7 +271,7 @@ export class UserService implements IUserService { delete updates.newPassword; } - return await this.usersRepository.updateById(currentUser.id, updates, file); + return await this.usersRepository.updateById(currentUserId, updates, file); }; checkSuperadminExists = async () => { @@ -319,24 +331,7 @@ export class UserService implements IUserService { return { user: existingUser, token }; }; - deleteUser = async (user: User) => { - const email = user?.email; - if (!email) { - throw new AppError({ message: "No email in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); - } - - const teamId = user?.teamId; - const userId = user?.id; - - if (!teamId) { - throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); - } - - if (!userId) { - throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); - } - - const roles = user?.role; + deleteUser = async ({ userId, teamId, roles }: { userId: string; teamId: string; roles: UserRole[] }) => { if (roles.includes("demo")) { throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUser", status: 400 }); } @@ -358,14 +353,24 @@ export class UserService implements IUserService { await this.usersRepository.deleteById(userId); }; - deleteUserById = async (actor: User, targetUserId: string) => { - if (actor.id === targetUserId) { + deleteUserById = async ({ + actorId, + actorRoles, + actorTeamId, + targetUserId, + }: { + actorId: string; + actorTeamId: string; + actorRoles: UserRole[]; + targetUserId: string; + }) => { + if (actorId === targetUserId) { throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 }); } const targetUser = await this.usersRepository.findById(targetUserId); - if (targetUser.teamId !== actor.teamId) { + if (targetUser.teamId !== actorTeamId) { throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 }); } @@ -373,7 +378,6 @@ export class UserService implements IUserService { throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 }); } - const actorRoles = actor.role; const targetRoles = targetUser.role; // Check actor can manage all of target's roles @@ -392,7 +396,7 @@ export class UserService implements IUserService { await this.usersRepository.deleteById(targetUserId); this.logger.info({ - message: `User ${targetUserId} deleted by ${actor.id}`, + message: `User ${targetUserId} deleted by ${actorId}`, service: SERVICE_NAME, method: "deleteUserById", }); @@ -402,7 +406,7 @@ export class UserService implements IUserService { return await this.usersRepository.findAll(); }; - getUserById = async (roles: any, userId: any) => { + getUserById = async (roles: UserRole[], userId: string) => { if (!roles.includes("superadmin") && !roles.includes("admin")) { throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 }); } @@ -411,11 +415,11 @@ export class UserService implements IUserService { return user; }; - editUserById = async (userId: any, patch: Partial) => { + editUserById = async (userId: string, patch: Partial) => { await this.usersRepository.updateById(userId, patch, null); }; - setPasswordByUserId = async (userId: any, password: string) => { + setPasswordByUserId = async (userId: string, password: string) => { const hashedPassword = this.hashPassword(password); const updatedUser = await this.usersRepository.updateById(userId, { password: hashedPassword }, null); return updatedUser; diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 1d75c0a36..ac4dfc425 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -2,12 +2,12 @@ import { ISettingsRepository } from "@/repositories/index.js"; import { Settings } from "@/types/index.js"; import { AppError } from "@/utils/AppError.js"; import { ValidatedEnv } from "@/validation/envValidation.js"; - +import type { StringValue } from "ms"; const SERVICE_NAME = "SettingsService"; export type EnvConfig = { jwtSecret: string; - jwtTTL: string; + jwtTTL: StringValue; nodeEnv: string; logLevel: string; clientHost: string; @@ -30,7 +30,7 @@ export class SettingsService implements ISettingsService { this.settingsRepository = settingsRepository; this.settings = { jwtSecret: env.JWT_SECRET, - jwtTTL: env.TOKEN_TTL, + jwtTTL: env.TOKEN_TTL as StringValue, nodeEnv: env.NODE_ENV, logLevel: env.LOG_LEVEL, clientHost: env.CLIENT_HOST,