diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/actions.ts b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/actions.ts
new file mode 100644
index 0000000000..5da92edd66
--- /dev/null
+++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/actions.ts
@@ -0,0 +1,39 @@
+"use server";
+import { stackServerApp } from "@/stack";
+
+export async function revokeInvitation(teamId: string, invitationId: string) {
+ "use server";
+ const user = await stackServerApp.getUser();
+ const team = await user?.getTeam(teamId);
+ if (!team) {
+ throw new Error("Team not found");
+ }
+ const invite = await team.listInvitations().then(invites => invites.find(invite => invite.id === invitationId));
+ if (!invite) {
+ throw new Error("Invitation not found");
+ }
+ await invite.revoke();
+}
+
+export async function listInvitations(teamId: string) {
+ const user = await stackServerApp.getUser();
+ const team = await user?.getTeam(teamId);
+ if (!team) {
+ throw new Error("Team not found");
+ }
+ const invitations = await team.listInvitations();
+ return invitations.map(invite => ({
+ id: invite.id,
+ recipientEmail: invite.recipientEmail,
+ expiresAt: invite.expiresAt,
+ }));
+}
+
+export async function inviteUser(teamId: string, email: string, callbackUrl: string) {
+ const user = await stackServerApp.getUser();
+ const team = await user?.getTeam(teamId);
+ if (!team) {
+ throw new Error("Team not found");
+ }
+ await team.inviteUser({ email, callbackUrl });
+}
diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
index f4e35d9fcc..51da8ff45a 100644
--- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
@@ -6,12 +6,13 @@ import { SearchBar } from "@/components/search-bar";
import { AdminOwnedProject, Team, useUser } from "@stackframe/stack";
import { strictEmailSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields";
import { groupBy } from "@stackframe/stack-shared/dist/utils/arrays";
-import { wait } from "@stackframe/stack-shared/dist/utils/promises";
+import { runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises";
import { stringCompare } from "@stackframe/stack-shared/dist/utils/strings";
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, Skeleton, Typography, toast } from "@stackframe/stack-ui";
import { Settings } from "lucide-react";
-import { Suspense, useEffect, useMemo, useState } from "react";
+import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
import * as yup from "yup";
+import { inviteUser, listInvitations, revokeInvitation } from "./actions";
export default function PageClient() {
const user = useUser({ or: 'redirect', projectIdMustMatch: "internal" });
@@ -102,9 +103,7 @@ export default function PageClient() {
{teamId ? teamIdMap.get(teamId) : "No Team"}
{team && (
-