Skip to content

Commit 942960e

Browse files
authored
Add members page to dashboard (#2459)
1 parent 624789a commit 942960e

File tree

89 files changed

+1619
-467
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1619
-467
lines changed

packages/fern-dashboard/next.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const nextConfig: NextConfig = {
99
* pnpm list --filter=@fern-dashboard/ui --only-projects --prod --recursive --depth=Infinity --json | jq -r '[.. | objects | select(.version | .!=null) | select(.version | startswith("link:")) | .from] | unique'
1010
*/
1111
"@fern-api/fdr-sdk",
12+
"@fern-ui/loadable",
1213
],
1314
experimental: {
1415
optimizePackageImports: [],
@@ -31,6 +32,9 @@ const nextConfig: NextConfig = {
3132

3233
// vercel chokes on monorepo compilation and we run compile before building
3334
typescript: { ignoreBuildErrors: true },
35+
36+
// so it doesn't cover the theme toggle
37+
devIndicators: { position: "bottom-right" },
3438
};
3539

3640
export default nextConfig;

packages/fern-dashboard/package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,31 @@
3030
"@auth0/nextjs-auth0": "^4.1.0",
3131
"@aws-sdk/client-s3": "^3.744.0",
3232
"@fern-api/fdr-sdk": "workspace:*",
33-
"@fern-api/venus-api-sdk": "^0.10.1-5-ged06d22",
33+
"@fern-api/venus-api-sdk": "^0.13.4-3-g1fe7ae7",
34+
"@fern-ui/loadable": "workspace:*",
3435
"@heroicons/react": "^2.2.0",
36+
"@radix-ui/react-dialog": "^1.1.6",
3537
"@radix-ui/react-dropdown-menu": "^2.1.6",
3638
"@radix-ui/react-popover": "^1.1.6",
3739
"@radix-ui/react-select": "^2.1.6",
3840
"@radix-ui/react-slot": "^1.1.2",
3941
"@sparticuz/chromium": "^133.0.0",
42+
"@tanstack/react-query": "^5.71.1",
43+
"@tanstack/react-query-devtools": "^5.71.1",
4044
"auth0": "^4.20.0",
4145
"class-variance-authority": "^0.7.1",
4246
"clsx": "^2.1.1",
4347
"jsonwebtoken": "^9.0.2",
4448
"lucide-react": "^0.460.0",
4549
"next": "15.3.0-canary.1",
4650
"next-themes": "^0.4.4",
47-
"node-cache": "^5.1.2",
4851
"puppeteer-core": "^24.4.0",
4952
"react": "19.0.0",
5053
"react-dom": "19.0.0",
5154
"sharp": "^0.33.5",
55+
"sonner": "^1.5.0",
5256
"tailwind-merge": "^3.0.1",
53-
"tailwindcss-animate": "^1.0.7",
54-
"zustand": "^5.0.2"
57+
"tailwindcss-animate": "^1.0.7"
5558
},
5659
"devDependencies": {
5760
"@fern-platform/configs": "workspace:*",

packages/fern-dashboard/src/app/actions/AsyncCache.tsx

Lines changed: 0 additions & 19 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Server actions are for mutations

packages/fern-dashboard/src/app/actions/auth0.ts

Lines changed: 0 additions & 93 deletions
This file was deleted.
Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,86 @@
11
"use server";
22

3-
import { getAuth0ManagementClient, getCurrentSession } from "./auth0";
3+
import { User } from "@auth0/nextjs-auth0/types";
44

5-
export async function createPersonalProject() {
5+
import { FernVenusApi, FernVenusApiClient } from "@fern-api/venus-api-sdk";
6+
import { APIResponse } from "@fern-api/venus-api-sdk/core";
7+
8+
import { getCurrentSession } from "../services/auth0/getCurrentSession";
9+
import { Auth0OrgID, Auth0UserID } from "../services/auth0/types";
10+
import { getVenusClient } from "../services/venus/getVenusClient";
11+
12+
export async function createPersonalProject(): Promise<Auth0OrgID> {
613
const { session, userId } = await getCurrentSession();
14+
const venus = getVenusClient({ token: session.tokenSet.accessToken });
715

8-
const { data: organization } =
9-
await getAuth0ManagementClient().organizations.create({
10-
name: `${userId.replace("|", "-")}-personal-project`,
11-
display_name: `${session.user.name}'s Personal Project`,
12-
enabled_connections: [{ connection_id: "con_Z6Dd06NADtkpPpLZ" }],
13-
metadata: {
14-
isPersonalProject: "true",
15-
},
16-
});
16+
const orgId = await createPersonalProjectInVenus({
17+
userId,
18+
userName: getUserName(session.user),
19+
venus,
20+
});
21+
22+
return Auth0OrgID(orgId);
23+
}
24+
25+
async function createPersonalProjectInVenus({
26+
userId,
27+
userName,
28+
venus,
29+
}: {
30+
userId: Auth0UserID;
31+
userName: string | undefined;
32+
venus: FernVenusApiClient;
33+
}) {
34+
let createOrgResponse: APIResponse<
35+
FernVenusApi.CreateOrganizationResponse,
36+
FernVenusApi.organization.create.Error
37+
>;
38+
let attempt = 0;
1739

18-
await getAuth0ManagementClient().organizations.addMembers(
19-
{ id: organization.id },
20-
{ members: [userId] }
40+
do {
41+
createOrgResponse = await venus.organization.create({
42+
organizationId: FernVenusApi.OrganizationId(
43+
getPersonalProjectOrgId({ userId, userName, attempt: attempt++ })
44+
),
45+
enableGithubConnection: true,
46+
});
47+
} while (
48+
!createOrgResponse.ok &&
49+
createOrgResponse.error.error === "OrganizationAlreadyExistsError"
2150
);
2251

23-
return organization.id;
52+
if (!createOrgResponse.ok) {
53+
console.error("Failed to create organization", createOrgResponse.error);
54+
throw new Error("Failed to create organization");
55+
}
56+
57+
return createOrgResponse.body.organizationId;
58+
}
59+
60+
function getPersonalProjectOrgId({
61+
userId,
62+
userName,
63+
attempt,
64+
}: {
65+
userId: Auth0UserID;
66+
userName: string | undefined;
67+
attempt: number;
68+
}) {
69+
let orgId = `${userName ?? userId}-personal-project`
70+
.replaceAll(" ", "-")
71+
.toLowerCase();
72+
if (attempt > 0) {
73+
orgId += `-${attempt}`;
74+
}
75+
return orgId;
76+
}
77+
78+
function getUserName(user: User) {
79+
if (user.name != null) {
80+
return user.name;
81+
}
82+
if (user.given_name != null && user.family_name != null) {
83+
return `${user.given_name} ${user.family_name}`;
84+
}
85+
return user.given_name ?? user.family_name;
2486
}

packages/fern-dashboard/src/app/actions/getMyDocsSites.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

packages/fern-dashboard/src/app/actions/getMyOrganizations.ts

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use server";
2+
3+
import { getAuth0ManagementClient } from "../services/auth0/getAuth0ManagementClient";
4+
import { getCurrentSession } from "../services/auth0/getCurrentSession";
5+
import { getOrgMembers } from "../services/auth0/helpers";
6+
import { Auth0OrgID } from "../services/auth0/types";
7+
8+
/* eslint-disable turbo/no-undeclared-env-vars */
9+
10+
export async function inviteUserToOrg({
11+
orgId,
12+
inviteeEmail,
13+
}: {
14+
// include orgId on the request to avoid race conditions if the org is
15+
// changed, i.e. ensure that we are adding the org that the user was looking
16+
// at on the frontend
17+
orgId: Auth0OrgID;
18+
inviteeEmail: string;
19+
}) {
20+
const auth0 = getAuth0ManagementClient();
21+
const { session, userId } = await getCurrentSession();
22+
23+
const orgMembers = await getOrgMembers(orgId);
24+
if (!orgMembers.some((member) => member.user_id === userId)) {
25+
throw new Error(`User ${userId} is not in org ${orgId}`);
26+
}
27+
28+
await auth0.organizations.createInvitation(
29+
{ id: orgId },
30+
{
31+
inviter: { name: session.user.name ?? "" },
32+
invitee: { email: inviteeEmail },
33+
client_id: getAuth0ClientId(),
34+
send_invitation_email: true,
35+
}
36+
);
37+
}
38+
39+
function getAuth0ClientId() {
40+
if (process.env.AUTH0_CLIENT_ID == null) {
41+
throw new Error(
42+
"AUTH0_CLIENT_ID is not defined in the current environment"
43+
);
44+
}
45+
return process.env.AUTH0_CLIENT_ID;
46+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"use server";
2+
3+
import { getAuth0ManagementClient } from "../services/auth0/getAuth0ManagementClient";
4+
import { Auth0OrgID } from "../services/auth0/types";
5+
6+
export async function rescindInvitation({
7+
orgId,
8+
invitationId,
9+
}: {
10+
orgId: Auth0OrgID;
11+
invitationId: string;
12+
}) {
13+
await getAuth0ManagementClient().organizations.deleteInvitation({
14+
id: orgId,
15+
invitation_id: invitationId,
16+
});
17+
}

0 commit comments

Comments
 (0)