Skip to content

Commit 44c74ee

Browse files
committed
IAM panel to invite entra ID tenant users
1 parent c953122 commit 44c74ee

File tree

19 files changed

+314
-68
lines changed

19 files changed

+314
-68
lines changed

.eslintrc

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{
2-
"extends": ["plugin:prettier/recommended", "plugin:@typescript-eslint/recommended"],
2+
"extends": [
3+
"plugin:prettier/recommended",
4+
"plugin:@typescript-eslint/recommended"
5+
],
36
"plugins": ["import"],
47
"rules": {
5-
// turn on errors for missing imports
68
"import/no-unresolved": "error",
79
"import/extensions": [
810
"error",
@@ -13,26 +15,32 @@
1315
"ts": "never",
1416
"tsx": "never"
1517
}
16-
],
17-
"no-unused-vars": "off",
18-
"max-classes-per-file": "off",
19-
"func-names": "off",
20-
"@typescript-eslint/no-unused-vars": [
21-
"warn", // or "error"
22-
{
23-
"argsIgnorePattern": "^_",
24-
"varsIgnorePattern": "^_",
25-
"caughtErrorsIgnorePattern": "^_"
26-
}
27-
]
18+
],
19+
"no-unused-vars": "off",
20+
"max-classes-per-file": "off",
21+
"func-names": "off",
22+
"@typescript-eslint/no-unused-vars": [
23+
"warn",
24+
{
25+
"argsIgnorePattern": "^_",
26+
"varsIgnorePattern": "^_",
27+
"caughtErrorsIgnorePattern": "^_"
28+
}
29+
],
30+
"@typescript-eslint/no-explicit-any": "warn",
31+
"@typescript-eslint/explicit-module-boundary-types": "off"
2832
},
2933
"settings": {
3034
"import/parsers": {
3135
"@typescript-eslint/parser": [".ts", ".tsx", ".js", ".jsx"]
3236
},
3337
"import/resolver": {
3438
"typescript": {
35-
"alwaysTryTypes": true
39+
"alwaysTryTypes": true,
40+
"project": [
41+
"src/api/tsconfig.json", // Path to tsconfig.json in src/api
42+
"src/ui/tsconfig.json" // Path to tsconfig.json in src/ui
43+
]
3644
}
3745
}
3846
},
@@ -44,7 +52,7 @@
4452
}
4553
},
4654
{
47-
"files": ["src/ui/*", "src/ui/**/*"], // Or *.test.js
55+
"files": ["src/ui/*", "src/ui/**/*"],
4856
"rules": {
4957
"@typescript-eslint/no-explicit-any": "off",
5058
"@typescript-eslint/no-unused-vars": "off"

src/api/routes/iam.ts

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { FastifyPluginAsync } from "fastify";
22
import { AppRoles } from "../../common/roles.js";
3-
import { z } from "zod";
43
import { zodToJsonSchema } from "zod-to-json-schema";
54
import { addToTenant, getEntraIdToken } from "../functions/entraId.js";
65
import {
@@ -18,33 +17,13 @@ import {
1817
} from "@aws-sdk/client-dynamodb";
1918
import { genericConfig } from "../../common/config.js";
2019
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
21-
22-
const invitePostRequestSchema = z.object({
23-
emails: z.array(z.string()),
24-
});
25-
export type InviteUserPostRequest = z.infer<typeof invitePostRequestSchema>;
26-
27-
const groupMappingCreatePostSchema = z.object({
28-
roles: z
29-
.array(z.nativeEnum(AppRoles))
30-
.min(1)
31-
.refine((items) => new Set(items).size === items.length, {
32-
message: "All roles must be unique, no duplicate values allowed",
33-
}),
34-
});
35-
36-
export type GroupMappingCreatePostRequest = z.infer<
37-
typeof groupMappingCreatePostSchema
38-
>;
39-
40-
const invitePostResponseSchema = zodToJsonSchema(
41-
z.object({
42-
success: z.array(z.object({ email: z.string() })).optional(),
43-
failure: z
44-
.array(z.object({ email: z.string(), message: z.string() }))
45-
.optional(),
46-
}),
47-
);
20+
import {
21+
InviteUserPostRequest,
22+
invitePostRequestSchema,
23+
GroupMappingCreatePostRequest,
24+
groupMappingCreatePostSchema,
25+
invitePostResponseSchema,
26+
} from "../../common/types/iam.js";
4827

4928
const dynamoClient = new DynamoDBClient({
5029
region: genericConfig.AwsRegion,
@@ -155,13 +134,13 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
155134
"/inviteUsers",
156135
{
157136
schema: {
158-
response: { 200: invitePostResponseSchema },
137+
response: { 200: zodToJsonSchema(invitePostResponseSchema) },
159138
},
160139
preValidation: async (request, reply) => {
161140
await fastify.zodValidateBody(request, reply, invitePostRequestSchema);
162141
},
163142
onRequest: async (request, reply) => {
164-
await fastify.authorize(request, reply, [AppRoles.SSO_INVITE_USER]);
143+
await fastify.authorize(request, reply, [AppRoles.IAM_INVITE_ONLY]);
165144
},
166145
},
167146
async (request, reply) => {
@@ -194,11 +173,7 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => {
194173
}
195174
}
196175
}
197-
let statusCode = 201;
198-
if (response.success.length === 0) {
199-
statusCode = 500;
200-
}
201-
reply.status(statusCode).send(response);
176+
reply.status(202).send(response);
202177
},
203178
);
204179
};

src/api/tsconfig.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
{
22
"extends": "@tsconfig/node20/tsconfig.json",
33
"compilerOptions": {
4-
"module": "Node16",
5-
"outDir": "../../dist",
4+
"module": "Node16",
5+
"rootDir": "../",
6+
"outDir": "../../dist",
7+
"baseUrl": "../",
68
},
79
"ts-node": {
810
"esm": true
911
},
10-
"include": ["./*"],
12+
"include": [
13+
"../api/**/*.ts",
14+
"../common/**/*.ts"
15+
],
16+
"exclude": [
17+
"../../node_modules",
18+
"../../dist"
19+
]
1120
}

src/common/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const environmentConfig: EnvironmentConfigType = {
8080
"ff49e948-4587-416b-8224-65147540d5fc": allAppRoles, // Officers
8181
"ad81254b-4eeb-4c96-8191-3acdce9194b1": [
8282
AppRoles.EVENTS_MANAGER,
83-
AppRoles.SSO_INVITE_USER,
83+
AppRoles.IAM_INVITE_ONLY,
8484
], // Exec
8585
},
8686
UserRoleMapping: {

src/common/errors/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export class EntraInvitationError extends BaseError<"EntraInvitationError"> {
141141
name: "EntraInvitationError",
142142
id: 108,
143143
message: message || "Could not invite user to Entra ID.",
144-
httpStatusCode: 500,
144+
httpStatusCode: 400,
145145
});
146146
this.email = email;
147147
}

src/common/roles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ export const runEnvironments = ["dev", "prod"] as const;
33
export type RunEnvironment = (typeof runEnvironments)[number];
44
export enum AppRoles {
55
EVENTS_MANAGER = "manage:events",
6-
SSO_INVITE_USER = "invite:sso",
76
TICKETS_SCANNER = "scan:tickets",
87
TICKETS_MANAGER = "manage:tickets",
98
IAM_ADMIN = "admin:iam",
9+
IAM_INVITE_ONLY = "invite:iam",
1010
}
1111
export const allAppRoles = Object.values(AppRoles).filter(
1212
(value) => typeof value === "string",

src/common/types/iam.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { AppRoles } from "../roles.js";
2+
import { z } from "zod";
3+
4+
export const invitePostRequestSchema = z.object({
5+
emails: z.array(z.string()),
6+
});
7+
8+
export type InviteUserPostRequest = z.infer<typeof invitePostRequestSchema>;
9+
10+
export const groupMappingCreatePostSchema = z.object({
11+
roles: z
12+
.array(z.nativeEnum(AppRoles))
13+
.min(1)
14+
.refine((items) => new Set(items).size === items.length, {
15+
message: "All roles must be unique, no duplicate values allowed",
16+
}),
17+
});
18+
19+
export type GroupMappingCreatePostRequest = z.infer<
20+
typeof groupMappingCreatePostSchema
21+
>;
22+
23+
export const invitePostResponseSchema = z.object({
24+
success: z.array(z.object({ email: z.string() })).optional(),
25+
failure: z
26+
.array(z.object({ email: z.string(), message: z.string() }))
27+
.optional(),
28+
});
29+
30+
export type InvitePostResponse = z.infer<typeof invitePostResponseSchema>;

src/ui/Router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { ViewEventsPage } from './pages/events/ViewEvents.page';
1616
import { ScanTicketsPage } from './pages/tickets/ScanTickets.page';
1717
import { SelectTicketsPage } from './pages/tickets/SelectEventId.page';
1818
import { ViewTicketsPage } from './pages/tickets/ViewTickets.page';
19+
import { ManageIamPage } from './pages/iam/ManageIam.page';
1920

2021
// Component to handle redirects to login with return path
2122
const LoginRedirect: React.FC = () => {
@@ -110,6 +111,10 @@ const authenticatedRouter = createBrowserRouter([
110111
path: '/tickets',
111112
element: <SelectTicketsPage />,
112113
},
114+
{
115+
path: '/iam',
116+
element: <ManageIamPage />,
117+
},
113118
{
114119
path: '/tickets/manage/:eventId',
115120
element: <ViewTicketsPage />,

src/ui/components/AppShell/index.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
IconFileDollar,
1616
IconPizza,
1717
IconTicket,
18+
IconLock,
1819
} from '@tabler/icons-react';
1920
import { ReactNode } from 'react';
2021
import { useNavigate } from 'react-router-dom';
@@ -44,6 +45,12 @@ export const navItems = [
4445
icon: IconTicket,
4546
description: null,
4647
},
48+
{
49+
link: '/iam',
50+
name: 'IAM',
51+
icon: IconLock,
52+
description: null,
53+
},
4754
];
4855

4956
export const extLinks = [

src/ui/components/AuthGuard/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AcmAppShell } from '@ui/components/AppShell';
55
import FullScreenLoader from '@ui/components/AuthContext/LoadingScreen';
66
import { getRunEnvironmentConfig, ValidService } from '@ui/config';
77
import { useApi } from '@ui/util/api';
8+
import { AppRoles } from '@common/roles';
89

910
export const CACHE_KEY_PREFIX = 'auth_response_cache_';
1011
const CACHE_DURATION = 2 * 60 * 60 * 1000; // 2 hours in milliseconds
@@ -16,7 +17,7 @@ type CacheData = {
1617

1718
export type ResourceDefinition = {
1819
service: ValidService;
19-
validRoles: string[];
20+
validRoles: AppRoles[];
2021
};
2122

2223
const getAuthCacheKey = (service: ValidService, route: string) =>

0 commit comments

Comments
 (0)