From 50e816dfb3335091a7fbda673fb7628cbc24a85f Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Tue, 4 Mar 2025 12:03:02 -0700 Subject: [PATCH 1/5] migration to add locked row to user table --- .../20250304185826_lock-column-users-table.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/database/migrations/20250304185826_lock-column-users-table.ts diff --git a/src/database/migrations/20250304185826_lock-column-users-table.ts b/src/database/migrations/20250304185826_lock-column-users-table.ts new file mode 100644 index 00000000..482428f3 --- /dev/null +++ b/src/database/migrations/20250304185826_lock-column-users-table.ts @@ -0,0 +1,14 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable('users', (table) => { + table.boolean('locked').defaultTo(false).notNullable(); + }); + await knex('users').update({ locked: false }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable('users', (table) => { + table.dropColumn('locked'); + }); +} From febde23b96a5ceb4c4f783b01a6bbb0c443c06f7 Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Tue, 4 Mar 2025 15:48:20 -0700 Subject: [PATCH 2/5] disallow access to locked users --- src/api/entities/User.ts | 2 ++ src/api/middleware/usersMiddleware.ts | 3 +++ src/web/App.tsx | 4 ++++ src/web/components/Navigation/LockedUserView.scss | 14 ++++++++++++++ src/web/components/Navigation/LockedUserView.tsx | 9 +++++++++ src/web/contexts/CurrentUserProvider.tsx | 3 ++- src/web/services/userAccount.ts | 15 +++++++++++---- 7 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 src/web/components/Navigation/LockedUserView.scss create mode 100644 src/web/components/Navigation/LockedUserView.tsx diff --git a/src/api/entities/User.ts b/src/api/entities/User.ts index 77e2c53e..8c7a083b 100644 --- a/src/api/entities/User.ts +++ b/src/api/entities/User.ts @@ -63,6 +63,7 @@ export class User extends BaseModel { declare jobFunction: UserJobFunction; declare participants?: Participant[]; declare acceptedTerms: boolean; + declare locked?: boolean; declare userToParticipantRoles?: UserToParticipantRole[]; static readonly modifiers = { @@ -85,6 +86,7 @@ export const UserSchema = z.object({ phone: z.string().optional(), jobFunction: z.nativeEnum(UserJobFunction).optional(), acceptedTerms: z.boolean(), + locked: z.boolean().optional(), }); export const UserCreationPartial = UserSchema.pick({ diff --git a/src/api/middleware/usersMiddleware.ts b/src/api/middleware/usersMiddleware.ts index d162a650..05626971 100644 --- a/src/api/middleware/usersMiddleware.ts +++ b/src/api/middleware/usersMiddleware.ts @@ -46,6 +46,9 @@ export const enrichCurrentUser = async (req: UserRequest, res: Response, next: N if (!user) { return res.status(404).send([{ message: 'The user cannot be found.' }]); } + if (user.locked) { + return res.status(403).send([{ message: 'Unauthorized.' }]); + } req.user = user; return next(); }; diff --git a/src/web/App.tsx b/src/web/App.tsx index d2a9fe4f..64479659 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -6,6 +6,7 @@ import { EnvironmentBanner } from './components/Core/Banner/EnvironmentBanner'; import { ErrorView } from './components/Core/ErrorView/ErrorView'; import { Loading } from './components/Core/Loading/Loading'; import { ToastContainerWrapper } from './components/Core/Popups/Toast'; +import { LockedUserView } from './components/Navigation/LockedUserView'; import { NoParticipantAccessView } from './components/Navigation/NoParticipantAccessView'; import { PortalHeader } from './components/PortalHeader/PortalHeader'; import { UpdatesTour } from './components/SiteTour/UpdatesTour'; @@ -25,6 +26,9 @@ function AppContent() { const { participant } = useContext(ParticipantContext); const isLocalDev = process.env.NODE_ENV === 'development'; + if (LoggedInUser?.isLocked) { + return ; + } if (LoggedInUser?.user?.participants!.length === 0) { return ; } diff --git a/src/web/components/Navigation/LockedUserView.scss b/src/web/components/Navigation/LockedUserView.scss new file mode 100644 index 00000000..767a6e33 --- /dev/null +++ b/src/web/components/Navigation/LockedUserView.scss @@ -0,0 +1,14 @@ +.user-locked-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 4.5rem 6.5rem; + font-size: 2rem; + background-color: var(--theme-background-content); + height: 100vh; + + .no-access-text { + font-weight: bold; + margin-bottom: 0.25rem; + } +} diff --git a/src/web/components/Navigation/LockedUserView.tsx b/src/web/components/Navigation/LockedUserView.tsx new file mode 100644 index 00000000..544d7216 --- /dev/null +++ b/src/web/components/Navigation/LockedUserView.tsx @@ -0,0 +1,9 @@ +import './LockedUserView.scss'; + +export function LockedUserView() { + return ( +
+

Access Forbidden.

+
+ ); +} diff --git a/src/web/contexts/CurrentUserProvider.tsx b/src/web/contexts/CurrentUserProvider.tsx index f61d1b9d..da1c1bb2 100644 --- a/src/web/contexts/CurrentUserProvider.tsx +++ b/src/web/contexts/CurrentUserProvider.tsx @@ -26,10 +26,11 @@ function CurrentUserProvider({ children }: Readonly<{ children: ReactNode }>) { setIsLoading(true); try { const profile = await keycloak.loadUserProfile(); - const user = await GetLoggedInUserAccount(); + const { user, isLocked } = await GetLoggedInUserAccount(); SetLoggedInUser({ profile, user, + isLocked, }); } catch (e: unknown) { if (e instanceof Error) throwError(e); diff --git a/src/web/services/userAccount.ts b/src/web/services/userAccount.ts index 7bf54cd2..21c34ee7 100644 --- a/src/web/services/userAccount.ts +++ b/src/web/services/userAccount.ts @@ -10,6 +10,7 @@ import { backendError } from '../utils/apiError'; export type UserAccount = { profile: KeycloakProfile; user: UserWithParticipantRoles | null; + isLocked?: boolean; }; export type InviteTeamMemberForm = { @@ -20,17 +21,23 @@ export type InviteTeamMemberForm = { userRoleId?: number; }; +export type LoggedInUser = { + user: UserWithParticipantRoles | null; + isLocked?: boolean; +}; + export type UpdateTeamMemberForm = Omit; export type UserPayload = z.infer; -export async function GetLoggedInUserAccount(): Promise { +export async function GetLoggedInUserAccount(): Promise { try { const result = await axios.get(`/users/current`, { - validateStatus: (status) => [200, 404].includes(status), + validateStatus: (status) => [200, 403, 404].includes(status), }); - if (result.status === 200) return result.data; - return null; + if (result.status === 403) return { user: null, isLocked: true }; + if (result.status === 200) return { user: result.data }; + return { user: null }; } catch (e: unknown) { throw backendError(e, 'Could not get user account'); } From 8b664c3cf20bdbc890580633b55743cd7e443d2a Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Wed, 5 Mar 2025 16:11:48 -0700 Subject: [PATCH 3/5] lock user through UI --- src/api/entities/AuditTrail.ts | 1 + src/api/routers/managementRouter.ts | 10 +++++ src/api/services/userService.ts | 22 ++++++++++- src/api/services/usersService.ts | 1 + .../components/PortalHeader/PortalHeader.scss | 2 +- .../UserManagement/UserManagementItem.scss | 38 +++++++++++++++++++ .../UserManagement/UserManagementItem.tsx | 27 +++++++++++-- .../UserManagement/UserManagementTable.tsx | 6 +-- src/web/screens/manageUsers.tsx | 9 ++++- src/web/services/userAccount.ts | 8 ++++ 10 files changed, 114 insertions(+), 10 deletions(-) diff --git a/src/api/entities/AuditTrail.ts b/src/api/entities/AuditTrail.ts index 442c1fa5..55b353d4 100644 --- a/src/api/entities/AuditTrail.ts +++ b/src/api/entities/AuditTrail.ts @@ -19,6 +19,7 @@ export enum AuditTrailEvents { UpdateDomainNames = 'UpdateDomainNames', UpdateAppNames = 'UpdateAppNames', ManageTeamMembers = 'ManageTeamMembers', + ChangeUserLock = 'ChangeUserLock', } export class AuditTrail extends BaseModel { diff --git a/src/api/routers/managementRouter.ts b/src/api/routers/managementRouter.ts index e67709ff..2673f0c1 100644 --- a/src/api/routers/managementRouter.ts +++ b/src/api/routers/managementRouter.ts @@ -1,7 +1,9 @@ import express, { Response } from 'express'; +import { z } from 'zod'; import { isSuperUserCheck } from '../middleware/userRoleMiddleware'; import { ParticipantRequest } from '../services/participantsService'; +import { UserService } from '../services/userService'; import { getAllUsersList } from '../services/usersService'; const handleGetAllUsers = async (req: ParticipantRequest, res: Response) => { @@ -9,10 +11,18 @@ const handleGetAllUsers = async (req: ParticipantRequest, res: Response) => { return res.status(200).json(userList); }; +const handleChangeUserLock = async (req: ParticipantRequest, res: Response) => { + const userService = new UserService(); + const { userId } = z.object({ userId: z.coerce.number() }).parse(req.params); + const { isLocked } = z.object({ isLocked: z.boolean() }).parse(req.body); + await userService.updateUserLock(req, userId, isLocked); +}; + export function createManagementRouter() { const managementRouter = express.Router(); managementRouter.get('/users', isSuperUserCheck, handleGetAllUsers); + managementRouter.patch('/:userId/changeLock', isSuperUserCheck, handleChangeUserLock); return managementRouter; } diff --git a/src/api/services/userService.ts b/src/api/services/userService.ts index 7bdf3bf1..66e63e3a 100644 --- a/src/api/services/userService.ts +++ b/src/api/services/userService.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { AuditAction, AuditTrailEvents } from '../entities/AuditTrail'; import { ParticipantType } from '../entities/ParticipantType'; -import { UserJobFunction } from '../entities/User'; +import { User, UserJobFunction } from '../entities/User'; import { getUserRoleById, UserRoleId } from '../entities/UserRole'; import { UserToParticipantRole } from '../entities/UserToParticipantRole'; import { getTraceId } from '../helpers/loggingHelpers'; @@ -142,4 +142,24 @@ export class UserService { }); }); } + + public async updateUserLock(req: UserParticipantRequest, userId: number, isLocked: boolean) { + const requestingUser = await findUserByEmail(req.auth?.payload.email as string); + const updatedUser = await User.query().where('id', userId).first(); + const traceId = getTraceId(req); + + const auditTrailInsertObject = constructAuditTrailObject( + requestingUser!, + AuditTrailEvents.ChangeUserLock, + { + action: AuditAction.Update, + email: updatedUser?.email, + locked: isLocked, + } + ); + + await performAsyncOperationWithAuditTrail(auditTrailInsertObject, traceId, async () => { + await User.query().where('id', userId).update({ locked: isLocked }); + }); + } } diff --git a/src/api/services/usersService.ts b/src/api/services/usersService.ts index c4bdadaa..bc6f96a9 100644 --- a/src/api/services/usersService.ts +++ b/src/api/services/usersService.ts @@ -199,6 +199,7 @@ export const inviteUserToParticipant = async ( } }; +// should this be moved somewhere for only superuser actions? managementService? export const getAllUsersList = async () => { const userList = await User.query().where('deleted', 0).orderBy('email'); return userList; diff --git a/src/web/components/PortalHeader/PortalHeader.scss b/src/web/components/PortalHeader/PortalHeader.scss index e3b1187e..115c63f2 100644 --- a/src/web/components/PortalHeader/PortalHeader.scss +++ b/src/web/components/PortalHeader/PortalHeader.scss @@ -97,7 +97,7 @@ } .theme-toggle[data-state='checked'] { - background-color: 'black'; + background-color: black; } .thumb { diff --git a/src/web/components/UserManagement/UserManagementItem.scss b/src/web/components/UserManagement/UserManagementItem.scss index 28659ac5..fd0e1860 100644 --- a/src/web/components/UserManagement/UserManagementItem.scss +++ b/src/web/components/UserManagement/UserManagementItem.scss @@ -17,4 +17,42 @@ .approver-name { margin-right: 40px; } + + .theme-switch { + button { + all: unset; + } + + color: var(--theme-secondary); + display: flex; + justify-content: space-between; + margin-top: 10px; + margin-bottom: 10px; + + .theme-toggle { + width: 42px; + height: 25px; + background-color: gray; + border-radius: 9999px; + margin-left: 10px; + cursor: pointer; + } + + .theme-toggle[data-state='checked'] { + background-color: crimson; + } + + .thumb { + display: block; + width: 21px; + height: 21px; + background-color: white; + border-radius: 9999px; + transform: translateX(2px); + } + + .thumb[data-state='checked'] { + transform: translateX(19px); + } + } } diff --git a/src/web/components/UserManagement/UserManagementItem.tsx b/src/web/components/UserManagement/UserManagementItem.tsx index e2eb57b5..257e6107 100644 --- a/src/web/components/UserManagement/UserManagementItem.tsx +++ b/src/web/components/UserManagement/UserManagementItem.tsx @@ -1,12 +1,23 @@ +import * as Switch from '@radix-ui/react-switch'; +import { useState } from 'react'; + import { UserDTO } from '../../../api/entities/User'; import './UserManagementItem.scss'; type UserManagementItemProps = Readonly<{ user: UserDTO; + onChangeUserLock: (userId: number, isLocked: boolean) => Promise; }>; -export function UserManagementItem({ user }: UserManagementItemProps) { +export function UserManagementItem({ user, onChangeUserLock }: UserManagementItemProps) { + const [lockedState, setLockedState] = useState(user.locked); + + const onLockedToggle = async () => { + await onChangeUserLock(user.id, !lockedState); + setLockedState(!lockedState); + }; + return ( {user.email} @@ -14,8 +25,18 @@ export function UserManagementItem({ user }: UserManagementItemProps) { {user.lastName} {user.jobFunction} {user.acceptedTerms ? 'True' : 'False'} - - + +
+ + + +
+ ); } diff --git a/src/web/components/UserManagement/UserManagementTable.tsx b/src/web/components/UserManagement/UserManagementTable.tsx index c807fe40..ccf0f3b6 100644 --- a/src/web/components/UserManagement/UserManagementTable.tsx +++ b/src/web/components/UserManagement/UserManagementTable.tsx @@ -13,6 +13,7 @@ import './UserManagementTable.scss'; type UserManagementTableProps = Readonly<{ users: UserDTO[]; + onChangeUserLock: (userId: number, isLocked: boolean) => Promise; }>; function NoUsers() { @@ -23,7 +24,7 @@ function NoUsers() { ); } -function UserManagementTableContent({ users }: UserManagementTableProps) { +function UserManagementTableContent({ users, onChangeUserLock }: UserManagementTableProps) { const initialRowsPerPage = 25; const initialPageNumber = 1; @@ -96,14 +97,13 @@ function UserManagementTableContent({ users }: UserManagementTableProps) { sortKey='lastName' header='Last Name' /> sortKey='jobFunction' header='Job Function' /> Accepted Terms - Delete Action Here Lock Action Here {pagedRows.map((user) => ( - + ))} diff --git a/src/web/screens/manageUsers.tsx b/src/web/screens/manageUsers.tsx index 638858a3..038c3bc9 100644 --- a/src/web/screens/manageUsers.tsx +++ b/src/web/screens/manageUsers.tsx @@ -4,7 +4,7 @@ import { defer, useLoaderData } from 'react-router-typesafe'; import { Loading } from '../components/Core/Loading/Loading'; import { ScreenContentContainer } from '../components/Core/ScreenContentContainer/ScreenContentContainer'; import UserManagementTable from '../components/UserManagement/UserManagementTable'; -import { GetAllUsers } from '../services/userAccount'; +import { ChangeUserLock, GetAllUsers } from '../services/userAccount'; import { AwaitTypesafe } from '../utils/AwaitTypesafe'; import { RouteErrorBoundary } from '../utils/RouteErrorBoundary'; import { PortalRoute } from './routeUtils'; @@ -17,6 +17,11 @@ const loader = () => { function ManageUsers() { const data = useLoaderData(); + const onChangeUserLock = async (userId: number, isLocked: boolean) => { + console.log(`locking user: ${userId}`); + await ChangeUserLock(userId, isLocked); + }; + return ( <>

Manage Users

@@ -24,7 +29,7 @@ function ManageUsers() { }> - {(users) => } + {(users) => } diff --git a/src/web/services/userAccount.ts b/src/web/services/userAccount.ts index 21c34ee7..62f944c5 100644 --- a/src/web/services/userAccount.ts +++ b/src/web/services/userAccount.ts @@ -120,3 +120,11 @@ export async function GetAllUsers() { throw backendError(e, 'Unable to get user list.'); } } + +export async function ChangeUserLock(userId: number, isLocked: boolean) { + try { + return await axios.patch(`/manage/${userId}/changeLock`, { userId, isLocked }); + } catch (e: unknown) { + throw backendError(e, 'Unable to update user lock status.'); + } +} From 788e4ccb5f815cfa58cac25fbe1a0d4acd1bbb3b Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Thu, 6 Mar 2025 13:08:01 -0700 Subject: [PATCH 4/5] lock user through UI --- src/api/routers/managementRouter.ts | 7 ++-- src/api/services/managementService.ts | 38 +++++++++++++++++++ src/api/services/userService.ts | 22 +---------- src/api/services/usersService.ts | 6 --- .../UserManagement/UserManagementItem.tsx | 10 ++--- .../UserManagement/UserManagementTable.tsx | 2 +- src/web/screens/manageUsers.tsx | 4 +- src/web/services/userAccount.ts | 3 +- 8 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 src/api/services/managementService.ts diff --git a/src/api/routers/managementRouter.ts b/src/api/routers/managementRouter.ts index 2673f0c1..790c92a8 100644 --- a/src/api/routers/managementRouter.ts +++ b/src/api/routers/managementRouter.ts @@ -2,9 +2,8 @@ import express, { Response } from 'express'; import { z } from 'zod'; import { isSuperUserCheck } from '../middleware/userRoleMiddleware'; +import { getAllUsersList, updateUserLock } from '../services/managementService'; import { ParticipantRequest } from '../services/participantsService'; -import { UserService } from '../services/userService'; -import { getAllUsersList } from '../services/usersService'; const handleGetAllUsers = async (req: ParticipantRequest, res: Response) => { const userList = await getAllUsersList(); @@ -12,10 +11,10 @@ const handleGetAllUsers = async (req: ParticipantRequest, res: Response) => { }; const handleChangeUserLock = async (req: ParticipantRequest, res: Response) => { - const userService = new UserService(); const { userId } = z.object({ userId: z.coerce.number() }).parse(req.params); const { isLocked } = z.object({ isLocked: z.boolean() }).parse(req.body); - await userService.updateUserLock(req, userId, isLocked); + await updateUserLock(req, userId, isLocked); + return res.status(200).end(); }; export function createManagementRouter() { diff --git a/src/api/services/managementService.ts b/src/api/services/managementService.ts new file mode 100644 index 00000000..a6502958 --- /dev/null +++ b/src/api/services/managementService.ts @@ -0,0 +1,38 @@ +import { AuditAction, AuditTrailEvents } from '../entities/AuditTrail'; +import { User } from '../entities/User'; +import { getTraceId } from '../helpers/loggingHelpers'; +import { + constructAuditTrailObject, + performAsyncOperationWithAuditTrail, +} from './auditTrailService'; +import { UserParticipantRequest } from './participantsService'; +import { findUserByEmail } from './usersService'; + +export const getAllUsersList = async () => { + const userList = await User.query().where('deleted', 0).orderBy('email'); + return userList; +}; + +export const updateUserLock = async ( + req: UserParticipantRequest, + userId: number, + isLocked: boolean +) => { + const requestingUser = await findUserByEmail(req.auth?.payload.email as string); + const updatedUser = await User.query().where('id', userId).first(); + const traceId = getTraceId(req); + + const auditTrailInsertObject = constructAuditTrailObject( + requestingUser!, + AuditTrailEvents.ChangeUserLock, + { + action: AuditAction.Update, + email: updatedUser?.email, + locked: isLocked, + } + ); + + await performAsyncOperationWithAuditTrail(auditTrailInsertObject, traceId, async () => { + await User.query().where('id', userId).update({ locked: isLocked }); + }); +}; diff --git a/src/api/services/userService.ts b/src/api/services/userService.ts index 66e63e3a..7bdf3bf1 100644 --- a/src/api/services/userService.ts +++ b/src/api/services/userService.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { AuditAction, AuditTrailEvents } from '../entities/AuditTrail'; import { ParticipantType } from '../entities/ParticipantType'; -import { User, UserJobFunction } from '../entities/User'; +import { UserJobFunction } from '../entities/User'; import { getUserRoleById, UserRoleId } from '../entities/UserRole'; import { UserToParticipantRole } from '../entities/UserToParticipantRole'; import { getTraceId } from '../helpers/loggingHelpers'; @@ -142,24 +142,4 @@ export class UserService { }); }); } - - public async updateUserLock(req: UserParticipantRequest, userId: number, isLocked: boolean) { - const requestingUser = await findUserByEmail(req.auth?.payload.email as string); - const updatedUser = await User.query().where('id', userId).first(); - const traceId = getTraceId(req); - - const auditTrailInsertObject = constructAuditTrailObject( - requestingUser!, - AuditTrailEvents.ChangeUserLock, - { - action: AuditAction.Update, - email: updatedUser?.email, - locked: isLocked, - } - ); - - await performAsyncOperationWithAuditTrail(auditTrailInsertObject, traceId, async () => { - await User.query().where('id', userId).update({ locked: isLocked }); - }); - } } diff --git a/src/api/services/usersService.ts b/src/api/services/usersService.ts index bc6f96a9..d253cfb2 100644 --- a/src/api/services/usersService.ts +++ b/src/api/services/usersService.ts @@ -198,9 +198,3 @@ export const inviteUserToParticipant = async ( await createUserInPortal(userPartial, participant!.id, userRoleId); } }; - -// should this be moved somewhere for only superuser actions? managementService? -export const getAllUsersList = async () => { - const userList = await User.query().where('deleted', 0).orderBy('email'); - return userList; -}; diff --git a/src/web/components/UserManagement/UserManagementItem.tsx b/src/web/components/UserManagement/UserManagementItem.tsx index 257e6107..4debe487 100644 --- a/src/web/components/UserManagement/UserManagementItem.tsx +++ b/src/web/components/UserManagement/UserManagementItem.tsx @@ -1,5 +1,4 @@ import * as Switch from '@radix-ui/react-switch'; -import { useState } from 'react'; import { UserDTO } from '../../../api/entities/User'; @@ -11,11 +10,8 @@ type UserManagementItemProps = Readonly<{ }>; export function UserManagementItem({ user, onChangeUserLock }: UserManagementItemProps) { - const [lockedState, setLockedState] = useState(user.locked); - const onLockedToggle = async () => { - await onChangeUserLock(user.id, !lockedState); - setLockedState(!lockedState); + await onChangeUserLock(user.id, !user.locked); }; return ( @@ -26,10 +22,10 @@ export function UserManagementItem({ user, onChangeUserLock }: UserManagementIte {user.jobFunction} {user.acceptedTerms ? 'True' : 'False'} -
+
diff --git a/src/web/components/UserManagement/UserManagementTable.tsx b/src/web/components/UserManagement/UserManagementTable.tsx index ccf0f3b6..85de69aa 100644 --- a/src/web/components/UserManagement/UserManagementTable.tsx +++ b/src/web/components/UserManagement/UserManagementTable.tsx @@ -97,7 +97,7 @@ function UserManagementTableContent({ users, onChangeUserLock }: UserManagementT sortKey='lastName' header='Last Name' /> sortKey='jobFunction' header='Job Function' /> Accepted Terms - Lock Action Here + Locked diff --git a/src/web/screens/manageUsers.tsx b/src/web/screens/manageUsers.tsx index 038c3bc9..5d6b948b 100644 --- a/src/web/screens/manageUsers.tsx +++ b/src/web/screens/manageUsers.tsx @@ -1,4 +1,5 @@ import { Suspense } from 'react'; +import { useRevalidator } from 'react-router-dom'; import { defer, useLoaderData } from 'react-router-typesafe'; import { Loading } from '../components/Core/Loading/Loading'; @@ -16,10 +17,11 @@ const loader = () => { function ManageUsers() { const data = useLoaderData(); + const reloader = useRevalidator(); const onChangeUserLock = async (userId: number, isLocked: boolean) => { - console.log(`locking user: ${userId}`); await ChangeUserLock(userId, isLocked); + reloader.revalidate(); }; return ( diff --git a/src/web/services/userAccount.ts b/src/web/services/userAccount.ts index 62f944c5..b204e250 100644 --- a/src/web/services/userAccount.ts +++ b/src/web/services/userAccount.ts @@ -123,7 +123,8 @@ export async function GetAllUsers() { export async function ChangeUserLock(userId: number, isLocked: boolean) { try { - return await axios.patch(`/manage/${userId}/changeLock`, { userId, isLocked }); + const res = await axios.patch(`/manage/${userId}/changeLock`, { userId, isLocked }); + return res; } catch (e: unknown) { throw backendError(e, 'Unable to update user lock status.'); } From 70e7e9abcc46621a811db3c9cee3daa3dd6857db Mon Sep 17 00:00:00 2001 From: Scott Sundahl Date: Thu, 6 Mar 2025 13:54:17 -0700 Subject: [PATCH 5/5] filter locked team members. prevent locking self --- src/api/routers/managementRouter.ts | 7 ++++++- src/api/services/managementService.ts | 5 +++++ src/api/services/usersService.ts | 4 +++- src/web/components/UserManagement/UserManagementItem.tsx | 2 +- src/web/services/userAccount.ts | 3 +-- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/api/routers/managementRouter.ts b/src/api/routers/managementRouter.ts index 790c92a8..2cbb4f05 100644 --- a/src/api/routers/managementRouter.ts +++ b/src/api/routers/managementRouter.ts @@ -2,7 +2,7 @@ import express, { Response } from 'express'; import { z } from 'zod'; import { isSuperUserCheck } from '../middleware/userRoleMiddleware'; -import { getAllUsersList, updateUserLock } from '../services/managementService'; +import { getAllUsersList, getUserById, updateUserLock } from '../services/managementService'; import { ParticipantRequest } from '../services/participantsService'; const handleGetAllUsers = async (req: ParticipantRequest, res: Response) => { @@ -13,6 +13,11 @@ const handleGetAllUsers = async (req: ParticipantRequest, res: Response) => { const handleChangeUserLock = async (req: ParticipantRequest, res: Response) => { const { userId } = z.object({ userId: z.coerce.number() }).parse(req.params); const { isLocked } = z.object({ isLocked: z.boolean() }).parse(req.body); + const user = await getUserById(userId); + if (req.auth?.payload?.email === user?.email) { + res.status(403).send([{ message: 'You cannot lock yourself.' }]); + return; + } await updateUserLock(req, userId, isLocked); return res.status(200).end(); }; diff --git a/src/api/services/managementService.ts b/src/api/services/managementService.ts index a6502958..d01682ae 100644 --- a/src/api/services/managementService.ts +++ b/src/api/services/managementService.ts @@ -13,6 +13,11 @@ export const getAllUsersList = async () => { return userList; }; +export const getUserById = async (userId: number) => { + const user = await User.query().where('id', userId).first(); + return user; +}; + export const updateUserLock = async ( req: UserParticipantRequest, userId: number, diff --git a/src/api/services/usersService.ts b/src/api/services/usersService.ts index d253cfb2..842a80a0 100644 --- a/src/api/services/usersService.ts +++ b/src/api/services/usersService.ts @@ -74,6 +74,7 @@ export const findUserByEmail = async (email: string) => { const user = await User.query() .findOne('email', email) .where('deleted', 0) + .where('locked', 0) .modify('withParticipants'); if (user?.participants) { @@ -107,6 +108,7 @@ export const getAllUsersFromParticipantWithRoles = async (participant: Participa const usersWithParticipants = await User.query() .whereIn('id', participantUserIds) .where('deleted', 0) + .where('locked', 0) .withGraphFetched('userToParticipantRoles'); return mapUsersWithParticipantRoles(usersWithParticipants, participant.id); @@ -117,7 +119,7 @@ export const getAllUsersFromParticipant = async (participant: Participant) => { await UserToParticipantRole.query().where('participantId', participant.id) ).map((userToParticipantRole) => userToParticipantRole.userId); - return User.query().whereIn('id', participantUserIds).where('deleted', 0); + return User.query().whereIn('id', participantUserIds).where('deleted', 0).where('locked', 0); }; export const sendInviteEmailToExistingUser = ( diff --git a/src/web/components/UserManagement/UserManagementItem.tsx b/src/web/components/UserManagement/UserManagementItem.tsx index 4debe487..1240faea 100644 --- a/src/web/components/UserManagement/UserManagementItem.tsx +++ b/src/web/components/UserManagement/UserManagementItem.tsx @@ -24,7 +24,7 @@ export function UserManagementItem({ user, onChangeUserLock }: UserManagementIte