Skip to content

Commit c06410e

Browse files
committed
feat: Organisation Invite Flow and SMTP setup
1 parent f336a22 commit c06410e

34 files changed

Lines changed: 4264 additions & 165 deletions

File tree

API_DOCUMENTATION.md

Lines changed: 327 additions & 34 deletions
Large diffs are not rendered by default.

Cargo.lock

Lines changed: 416 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

airborne_dashboard/app/dashboard/[orgId]/[appId]/users/page.tsx

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
"use client";
2+
import { useState } from "react";
23
import useSWR from "swr";
34
import { apiFetch } from "@/lib/api";
45
import { useAppContext } from "@/providers/app-context";
56
import UsersLoading from "@/app/users/loading";
6-
import { UserManagement, type AccessLevel, type User } from "@/components/user-management";
7+
import { canUpdateUsers, UserManagement, type AccessLevel, type User } from "@/components/user-management";
8+
import { ApplicationAccessModal } from "@/components/application-access-modal";
9+
import { Button } from "@/components/ui/button";
10+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
11+
import { Users, Building2 } from "lucide-react";
12+
import { toastSuccess, toastError } from "@/hooks/use-toast";
13+
import { useRouter } from "next/navigation";
714

815
type OrgUsers = { users: User[] };
916

1017
export default function ApplicationUsersPage() {
1118
const { token, org, app, getAppAccess, getOrgAccess, updateOrgs } = useAppContext();
19+
const router = useRouter();
20+
const [isAppAccessModalOpen, setIsAppAccessModalOpen] = useState(false);
21+
22+
// Application users data
1223
const { data, isLoading, error, mutate } = useSWR<OrgUsers>(
1324
token && org ? "/organisations/applications/user/list" : null,
1425
(url: string) => apiFetch<any>(url, {}, { token, org, app })
1526
);
1627

28+
// Organization users data (for the application access modal)
29+
const { data: orgUsersData, isLoading: orgUsersLoading } = useSWR<OrgUsers>(
30+
token && org ? "/organisations/user/list" : null,
31+
(url: string) => apiFetch<any>(url, {}, { token, org })
32+
);
33+
1734
const addUser = async (user: string, access: AccessLevel) => {
1835
await apiFetch(
1936
"/organisations/applications/user/create",
@@ -40,6 +57,46 @@ export default function ApplicationUsersPage() {
4057
updateOrgs();
4158
};
4259

60+
// Handle application access invites (grant access to existing org users)
61+
const handleApplicationInvite = async (invites: { userId: string; role: string }[]) => {
62+
try {
63+
// Call the API for each user in parallel
64+
const promises = invites.map((invite) =>
65+
apiFetch(
66+
"/organisations/applications/user/create",
67+
{
68+
method: "POST",
69+
body: {
70+
user: invite.userId,
71+
access: invite.role as AccessLevel,
72+
},
73+
},
74+
{ token, org, app }
75+
)
76+
);
77+
78+
// Wait for all API calls to complete
79+
await Promise.all(promises);
80+
81+
toastSuccess(
82+
"Access Granted",
83+
`Successfully granted ${app} access to ${invites.length} user${invites.length !== 1 ? "s" : ""}`
84+
);
85+
86+
setIsAppAccessModalOpen(false); // Close the modal
87+
mutate(); // Refresh the users list
88+
updateOrgs(); // Update organizations data
89+
} catch (error: any) {
90+
console.error("Failed to grant application access:", error);
91+
toastError("Failed to Grant Access", error.message || "Could not grant application access");
92+
}
93+
};
94+
95+
// Handle redirect to organization users page
96+
const handleRedirectToOrgUsers = () => {
97+
router.push(`/dashboard/${org}/users`);
98+
};
99+
43100
if (isLoading) {
44101
return <UsersLoading />;
45102
}
@@ -48,8 +105,63 @@ export default function ApplicationUsersPage() {
48105
return <div className="p-6">Error loading users</div>;
49106
}
50107

108+
// Prepare org users for the application access modal (exclude users already in app)
109+
const currentAppUsernames = new Set((data?.users || []).map((user) => user.username));
110+
const orgUsers =
111+
orgUsersData?.users
112+
?.filter((user) => !currentAppUsernames.has(user.username)) // Filter out existing app users
113+
?.map((user) => ({
114+
id: user.username,
115+
name: user.username,
116+
email: user.username, // Assuming username is email for now
117+
username: user.username,
118+
roles: user.roles,
119+
})) || [];
120+
121+
const canUpdateAppUsers = canUpdateUsers("application", getOrgAccess(org), getAppAccess(org, app));
122+
const canUpdateOrgUsers = canUpdateUsers("organisation", getOrgAccess(org), getAppAccess(org, app));
123+
51124
return (
52-
<div className="container mx-auto p-6">
125+
<div className="container mx-auto p-6 space-y-6">
126+
{/* Add User to Application Card */}
127+
{(canUpdateAppUsers || canUpdateOrgUsers) && (
128+
<Card>
129+
<CardHeader>
130+
<CardTitle className="flex items-center gap-2">
131+
<Users className="h-5 w-5" />
132+
Grant Application Access
133+
</CardTitle>
134+
<p className="text-sm text-muted-foreground">
135+
Add existing organization members to this application (excluding current app users)
136+
</p>
137+
</CardHeader>
138+
<CardContent className="space-y-4">
139+
<div className="flex flex-col sm:flex-row gap-3 sm:justify-start">
140+
{canUpdateAppUsers && (
141+
<Button onClick={() => setIsAppAccessModalOpen(true)} size="sm" disabled={orgUsers.length === 0}>
142+
<Users className="h-4 w-4 mr-2" />
143+
Add User to Application
144+
</Button>
145+
)}
146+
{canUpdateOrgUsers && (
147+
<Button variant="outline" onClick={handleRedirectToOrgUsers} size="sm">
148+
<Building2 className="h-4 w-4 mr-2" />
149+
Add someone to organisation
150+
</Button>
151+
)}
152+
</div>
153+
{canUpdateAppUsers && (
154+
<p className="text-xs text-muted-foreground">
155+
{orgUsers.length === 0
156+
? "All organization users already have access to this application. Use 'Add someone to organisation' to invite new users."
157+
: `${orgUsers.length} organization member${orgUsers.length !== 1 ? "s" : ""} available to add to this application.`}
158+
</p>
159+
)}
160+
</CardContent>
161+
</Card>
162+
)}
163+
164+
{/* Current Application Users */}
53165
<UserManagement
54166
users={data?.users || []}
55167
currentUserAppAccess={getAppAccess(org, app)}
@@ -60,6 +172,18 @@ export default function ApplicationUsersPage() {
60172
title="Application Users"
61173
description="Manage users and their access levels for this application"
62174
entityType="application"
175+
hideAddUserButton={true}
176+
/>
177+
178+
{/* Application Access Modal */}
179+
<ApplicationAccessModal
180+
isOpen={isAppAccessModalOpen}
181+
onClose={() => setIsAppAccessModalOpen(false)}
182+
onSubmit={handleApplicationInvite}
183+
orgUsers={orgUsers}
184+
applicationName={app || ""}
185+
availableRoles={["read", "write", "admin"]}
186+
isLoading={orgUsersLoading}
63187
/>
64188
</div>
65189
);

0 commit comments

Comments
 (0)