mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
feat(freight:backoffice): migrated smart-office user-management
This commit is contained in:
@@ -16,6 +16,7 @@ import { BillingModule } from "./modules/billing/billing.module";
|
|||||||
import { NotificationsModule } from "./modules/notifications/notifications.module";
|
import { NotificationsModule } from "./modules/notifications/notifications.module";
|
||||||
import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module";
|
import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module";
|
||||||
import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module";
|
import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module";
|
||||||
|
import { BackofficeModule } from "./modules/backoffice/backoffice.module";
|
||||||
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -40,6 +41,7 @@ import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
|||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
FileUploadSettingsModule,
|
FileUploadSettingsModule,
|
||||||
DropdownSettingsModule,
|
DropdownSettingsModule,
|
||||||
|
BackofficeModule,
|
||||||
],
|
],
|
||||||
providers: [EdrOrgSeeder],
|
providers: [EdrOrgSeeder],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Put,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { BackofficeService } from "./backoffice.service";
|
||||||
|
import { UpdateEmployeeUserRolesDto } from "./dto/update-employee-user-roles.dto";
|
||||||
|
|
||||||
|
@ApiTags("backoffice")
|
||||||
|
@Controller("backoffice")
|
||||||
|
export class BackofficeController {
|
||||||
|
constructor(private readonly backofficeService: BackofficeService) {}
|
||||||
|
|
||||||
|
@Get("organizations/:orgId/employee-users/:userId/roles")
|
||||||
|
@ApiOperation({ summary: "Get org-scoped roles assigned to an employee user" })
|
||||||
|
getEmployeeUserRoles(
|
||||||
|
@Param("orgId", ParseUUIDPipe) organizationId: string,
|
||||||
|
@Param("userId", ParseUUIDPipe) userId: string,
|
||||||
|
) {
|
||||||
|
return this.backofficeService.getEmployeeUserRoles(organizationId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("organizations/:orgId/employee-users/:userId/roles")
|
||||||
|
@ApiOperation({ summary: "Replace org-scoped roles assigned to an employee user" })
|
||||||
|
replaceEmployeeUserRoles(
|
||||||
|
@Param("orgId", ParseUUIDPipe) organizationId: string,
|
||||||
|
@Param("userId", ParseUUIDPipe) userId: string,
|
||||||
|
@Body() dto: UpdateEmployeeUserRolesDto,
|
||||||
|
) {
|
||||||
|
return this.backofficeService.replaceEmployeeUserRoles(
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
dto.roleIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
|
import { Role } from "@tria-plc/iamapi-common/entities/iam/user/role.entity";
|
||||||
|
import { UserRole } from "@tria-plc/iamapi-common/entities/iam/user/user-role.entity";
|
||||||
|
import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity";
|
||||||
|
|
||||||
|
import { BackofficeController } from "./backoffice.controller";
|
||||||
|
import { BackofficeService } from "./backoffice.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Role, UserRole, User])],
|
||||||
|
controllers: [BackofficeController],
|
||||||
|
providers: [BackofficeService],
|
||||||
|
exports: [BackofficeService],
|
||||||
|
})
|
||||||
|
export class BackofficeModule {}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { DataSource, In, IsNull, Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { Role } from "@tria-plc/iamapi-common/entities/iam/user/role.entity";
|
||||||
|
import { UserRole } from "@tria-plc/iamapi-common/entities/iam/user/user-role.entity";
|
||||||
|
import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity";
|
||||||
|
|
||||||
|
const RESERVED_ROLE_KEYS = new Set([
|
||||||
|
"super_admin",
|
||||||
|
"organization_admin",
|
||||||
|
"unit_admin",
|
||||||
|
]);
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BackofficeService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Role)
|
||||||
|
private readonly roleRepository: Repository<Role>,
|
||||||
|
@InjectRepository(UserRole)
|
||||||
|
private readonly userRoleRepository: Repository<UserRole>,
|
||||||
|
@InjectRepository(User)
|
||||||
|
private readonly userRepository: Repository<User>,
|
||||||
|
private readonly dataSource: DataSource,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getEmployeeUserRoles(organizationId: string, userId: string) {
|
||||||
|
await this.assertUserBelongsToOrganization(organizationId, userId);
|
||||||
|
|
||||||
|
const userRoles = await this.userRoleRepository.find({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
unitId: IsNull(),
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
role: {
|
||||||
|
key: "ASC",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return userRoles
|
||||||
|
.map((userRole) => userRole.role)
|
||||||
|
.filter((role): role is Role => Boolean(role))
|
||||||
|
.map((role) => ({
|
||||||
|
id: role.id,
|
||||||
|
key: role.key,
|
||||||
|
name: role.name,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async replaceEmployeeUserRoles(
|
||||||
|
organizationId: string,
|
||||||
|
userId: string,
|
||||||
|
roleIds: string[],
|
||||||
|
) {
|
||||||
|
await this.assertUserBelongsToOrganization(organizationId, userId);
|
||||||
|
|
||||||
|
const uniqueRoleIds = [...new Set(roleIds)];
|
||||||
|
const roles = uniqueRoleIds.length
|
||||||
|
? await this.roleRepository.find({
|
||||||
|
where: {
|
||||||
|
id: In(uniqueRoleIds),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (roles.length !== uniqueRoleIds.length) {
|
||||||
|
throw new NotFoundException("one_or_more_roles_not_found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const reservedRoles = roles.filter((role) => RESERVED_ROLE_KEYS.has(role.key));
|
||||||
|
if (reservedRoles.length) {
|
||||||
|
throw new BadRequestException("reserved_roles_must_use_admin_actions");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.dataSource.transaction(async (manager) => {
|
||||||
|
await manager.getRepository(UserRole).delete({
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
unitId: IsNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!roles.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.getRepository(UserRole).insert(
|
||||||
|
roles.map((role) => ({
|
||||||
|
userId,
|
||||||
|
roleId: role.id,
|
||||||
|
organizationId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.getEmployeeUserRoles(organizationId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertUserBelongsToOrganization(
|
||||||
|
organizationId: string,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
const exists = await this.userRepository
|
||||||
|
.createQueryBuilder("user")
|
||||||
|
.innerJoin(
|
||||||
|
"user.employee",
|
||||||
|
"employee",
|
||||||
|
"employee.organizationId = :organizationId AND employee.isCurrent = true",
|
||||||
|
{ organizationId },
|
||||||
|
)
|
||||||
|
.where("user.id = :userId", { userId })
|
||||||
|
.getExists();
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
throw new NotFoundException("user_not_found_in_organization");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsArray, IsUUID } from "class-validator";
|
||||||
|
|
||||||
|
export class UpdateEmployeeUserRolesDto {
|
||||||
|
@ApiProperty({ type: [String] })
|
||||||
|
@IsArray()
|
||||||
|
@IsUUID("4", { each: true })
|
||||||
|
roleIds!: string[];
|
||||||
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { DashboardLayout, type SidebarItem } from "@edr/ui-common";
|
import { DashboardLayout, type SidebarItem } from "@edr/ui-common";
|
||||||
import { LayoutDashboard, ShieldCheck, Users, Network } from "lucide-react";
|
import { LayoutDashboard, Network } from "lucide-react";
|
||||||
|
|
||||||
import { useAuth } from "./auth/useAuth";
|
import { useAuth } from "./auth/useAuth";
|
||||||
import LoginPage from "./pages/auth/LoginPage";
|
import LoginPage from "./pages/auth/LoginPage";
|
||||||
import OverviewPage from "./pages/dashboard/OverviewPage";
|
import OverviewPage from "./pages/dashboard/OverviewPage";
|
||||||
import UsersPage from "./pages/dashboard/user-management/UsersPage";
|
import UserManagementPage from "./pages/dashboard/user-management/UserManagementPage";
|
||||||
import RolesPage from "./pages/dashboard/user-management/RolesPage";
|
|
||||||
import DepartmentsPage from "./pages/dashboard/user-management/DepartmentsPage";
|
|
||||||
import OrgStructurePage from "./pages/dashboard/org-structure/OrgStructurePage";
|
|
||||||
import LoadingScreen from "./components/LoadingScreen";
|
import LoadingScreen from "./components/LoadingScreen";
|
||||||
|
|
||||||
const sidebarItems: SidebarItem[] = [
|
const sidebarItems: SidebarItem[] = [
|
||||||
@@ -18,8 +15,8 @@ const sidebarItems: SidebarItem[] = [
|
|||||||
icon: <LayoutDashboard />,
|
icon: <LayoutDashboard />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Org structure",
|
label: "User management",
|
||||||
href: "/dashboard/org-structure",
|
href: "/dashboard/user-management",
|
||||||
icon: <Network />,
|
icon: <Network />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -69,9 +66,9 @@ const App = () => {
|
|||||||
<Route path="/dashboard" element={<Navigate to="/dashboard/overview" replace />} />
|
<Route path="/dashboard" element={<Navigate to="/dashboard/overview" replace />} />
|
||||||
<Route path="/dashboard" element={<DashboardShell />}>
|
<Route path="/dashboard" element={<DashboardShell />}>
|
||||||
<Route path="overview" element={<OverviewPage />} />
|
<Route path="overview" element={<OverviewPage />} />
|
||||||
<Route path="org-structure" element={<OrgStructurePage />} />
|
<Route path="user-management" element={<UserManagementPage />} />
|
||||||
<Route path="org-structure/units/:unitId" element={<OrgStructurePage />} />
|
<Route path="org-structure" element={<Navigate to="/dashboard/user-management" replace />} />
|
||||||
<Route path="org-structure/units/:unitId/:section" element={<OrgStructurePage />} />
|
<Route path="org-structure/*" element={<Navigate to="/dashboard/user-management" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/dashboard/overview" replace />} />
|
<Route path="*" element={<Navigate to="/dashboard/overview" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -125,8 +125,15 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
mfaEmailRef.current = null;
|
mfaEmailRef.current = null;
|
||||||
},
|
},
|
||||||
logout: () => {
|
logout: () => {
|
||||||
|
const preservedTheme = window.localStorage.getItem("edr-theme");
|
||||||
|
|
||||||
clearSessionCookies();
|
clearSessionCookies();
|
||||||
localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
|
||||||
|
if (preservedTheme === "dark" || preservedTheme === "light") {
|
||||||
|
window.localStorage.setItem("edr-theme", preservedTheme);
|
||||||
|
}
|
||||||
|
|
||||||
setUser(null);
|
setUser(null);
|
||||||
window.location.replace("/auth");
|
window.location.replace("/auth");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,47 @@
|
|||||||
|
interface LocaleText {
|
||||||
|
en?: string;
|
||||||
|
am?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthRole {
|
||||||
|
id?: string;
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthPermission {
|
||||||
|
id?: string;
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthEmployeePosition {
|
||||||
|
id?: string;
|
||||||
|
employeePositionId?: string;
|
||||||
|
name?: LocaleText;
|
||||||
|
key?: string;
|
||||||
|
isDelegate?: boolean;
|
||||||
|
parentPositionId?: string | null;
|
||||||
|
permissions?: AuthPermission[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthEmployeeRecord {
|
||||||
|
id?: string;
|
||||||
|
organizationId?: string;
|
||||||
|
unitId?: string;
|
||||||
|
name?: LocaleText;
|
||||||
|
positions?: AuthEmployeePosition[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AuthUser {
|
export interface AuthUser {
|
||||||
id?: string;
|
id?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
name?: {
|
phoneNumber?: string;
|
||||||
en?: string;
|
name?: LocaleText;
|
||||||
am?: string;
|
roles?: AuthRole[];
|
||||||
};
|
permissions?: AuthPermission[];
|
||||||
|
employee?: AuthEmployeeRecord[];
|
||||||
|
hasSetPassword?: boolean;
|
||||||
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthTokens {
|
export interface AuthTokens {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user