Skip to content

Commit ffa7384

Browse files
authored
Merge branch 'app' into zk/update-homepage-images
2 parents 2476951 + fb22e68 commit ffa7384

Some content is hidden

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

72 files changed

+1747
-156
lines changed

.github/workflows/dashboard.yml

-41
This file was deleted.

packages/fern-dashboard/next.config.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
/* eslint-disable turbo/no-undeclared-env-vars */
22
import type { NextConfig } from "next";
33

4+
const APP_BASE_URL =
5+
process.env.VERCEL_ENV === "preview"
6+
? `https://${process.env.VERCEL_BRANCH_URL}`
7+
: process.env.NEXT_PUBLIC_APP_BASE_URL;
8+
49
const nextConfig: NextConfig = {
510
transpilePackages: [
611
/**
@@ -11,7 +16,7 @@ const nextConfig: NextConfig = {
1116
"@fern-api/fdr-sdk",
1217
],
1318
experimental: {
14-
optimizePackageImports: ["@fern-api/fdr-sdk"],
19+
optimizePackageImports: [],
1520
},
1621
images: {
1722
remotePatterns: [
@@ -25,15 +30,19 @@ const nextConfig: NextConfig = {
2530
],
2631
},
2732
env: {
28-
APP_BASE_URL:
29-
process.env.VERCEL_ENV === "preview"
30-
? `https://${process.env.VERCEL_BRANCH_URL}`
31-
: process.env.APP_BASE_URL,
33+
// need this to be NEXT_PUBLIC_ so it's accessible in auth0.ts
34+
NEXT_PUBLIC_APP_BASE_URL: APP_BASE_URL,
35+
36+
// Auth0 expects APP_BASE_URL to exist
37+
APP_BASE_URL,
3238
},
3339
webpack: (webpackConfig) => {
3440
webpackConfig.externals.push("sharp");
3541
return webpackConfig;
3642
},
43+
44+
// vercel chokes on monorepo compilation and we run compile before building
45+
typescript: { ignoreBuildErrors: true },
3746
};
3847

3948
export default nextConfig;

packages/fern-dashboard/package.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,32 @@
3131
"@aws-sdk/client-s3": "^3.744.0",
3232
"@fern-api/fdr-sdk": "workspace:*",
3333
"@fern-api/venus-api-sdk": "^0.10.1-5-ged06d22",
34+
"@heroicons/react": "^2.2.0",
3435
"@radix-ui/react-dropdown-menu": "^2.1.6",
36+
"@radix-ui/react-popover": "^1.1.6",
37+
"@radix-ui/react-select": "^2.1.6",
3538
"@radix-ui/react-slot": "^1.1.2",
3639
"@sparticuz/chromium": "^133.0.0",
40+
"auth0": "^4.20.0",
3741
"class-variance-authority": "^0.7.1",
3842
"clsx": "^2.1.1",
43+
"jsonwebtoken": "^9.0.2",
3944
"lucide-react": "^0.460.0",
4045
"next": "15.3.0-canary.1",
4146
"next-themes": "^0.4.4",
47+
"node-cache": "^5.1.2",
4248
"puppeteer-core": "^24.4.0",
4349
"react": "19.0.0",
4450
"react-dom": "19.0.0",
4551
"sharp": "^0.33.5",
4652
"tailwind-merge": "^3.0.1",
47-
"tailwindcss-animate": "^1.0.7"
53+
"tailwindcss-animate": "^1.0.7",
54+
"zustand": "^5.0.2"
4855
},
4956
"devDependencies": {
5057
"@fern-platform/configs": "workspace:*",
5158
"@tailwindcss/postcss": "^4.0.9",
59+
"@types/jsonwebtoken": "^9.0.9",
5260
"@types/node": "^18.11.9",
5361
"@types/react": "19.0.10",
5462
"@types/react-dom": "19.0.4",
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Page404 } from "@/components/Page404";
2+
3+
export default async function Page() {
4+
return <Page404 />;
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import NodeCache from "node-cache";
2+
3+
export class AsyncCache<K extends NodeCache.Key, T> {
4+
private cache: NodeCache;
5+
6+
constructor({ ttlInSeconds }: { ttlInSeconds: number }) {
7+
this.cache = new NodeCache({ stdTTL: ttlInSeconds });
8+
}
9+
10+
public async get(key: K, getter: () => T | Promise<T>): Promise<T> {
11+
const cachedValue = this.cache.get<T>(key);
12+
if (cachedValue != null) {
13+
return cachedValue;
14+
}
15+
const newValue = await getter();
16+
this.cache.set(key, newValue);
17+
return newValue;
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { GetOrganizations200ResponseOneOfInner, ManagementClient } from "auth0";
2+
import jwt from "jsonwebtoken";
3+
4+
import { auth0 } from "@/lib/auth0";
5+
6+
import { AsyncCache } from "./AsyncCache";
7+
import { Auth0OrgName } from "./types";
8+
9+
let AUTH0_MANAGEMENT_CLIENT: ManagementClient | undefined;
10+
11+
export function getAuth0ManagementClient() {
12+
if (AUTH0_MANAGEMENT_CLIENT == null) {
13+
// eslint-disable-next-line turbo/no-undeclared-env-vars
14+
const { AUTH0_DOMAIN, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET } = process.env;
15+
16+
if (AUTH0_DOMAIN == null) {
17+
throw new Error("AUTH0_DOMAIN is not defined");
18+
}
19+
if (AUTH0_CLIENT_ID == null) {
20+
throw new Error("AUTH0_CLIENT_ID is not defined");
21+
}
22+
if (AUTH0_CLIENT_SECRET == null) {
23+
throw new Error("AUTH0_CLIENT_SECRET is not defined");
24+
}
25+
26+
AUTH0_MANAGEMENT_CLIENT = new ManagementClient({
27+
domain: AUTH0_DOMAIN,
28+
clientId: AUTH0_CLIENT_ID,
29+
clientSecret: AUTH0_CLIENT_SECRET,
30+
});
31+
}
32+
33+
return AUTH0_MANAGEMENT_CLIENT;
34+
}
35+
36+
export async function getCurrentSession() {
37+
const session = await auth0.getSession();
38+
if (session == null) {
39+
throw new Error("Not authenticated");
40+
}
41+
if (session.idToken == null) {
42+
throw new Error("idToken is not present on session");
43+
}
44+
if (typeof session.idToken !== "string") {
45+
throw new Error(
46+
`idToken is of type ${typeof session.idToken} (expected string)`
47+
);
48+
}
49+
50+
const jwtPayload = jwt.decode(session.idToken);
51+
if (jwtPayload == null) {
52+
throw new Error("JWT payload is not defined");
53+
}
54+
if (typeof jwtPayload !== "object") {
55+
throw new Error("JWT payload is not an object");
56+
}
57+
if (jwtPayload?.sub == null) {
58+
throw new Error("JWT payload does not include 'sub'");
59+
}
60+
61+
return { session, userId: jwtPayload.sub };
62+
}
63+
64+
export async function getCurrentOrgId() {
65+
const session = await auth0.getSession();
66+
67+
if (session?.user.org_id == null) {
68+
throw new Error("org_id is not defined");
69+
}
70+
71+
return session.user.org_id;
72+
}
73+
74+
const ORGANIZATIONS_CACHE = new AsyncCache<
75+
Auth0OrgName,
76+
GetOrganizations200ResponseOneOfInner
77+
>({
78+
ttlInSeconds: 10,
79+
});
80+
81+
export async function getCurrentOrg() {
82+
const orgId = await getCurrentOrgId();
83+
84+
return await ORGANIZATIONS_CACHE.get(orgId, async () => {
85+
const { data: organization } =
86+
await getAuth0ManagementClient().organizations.get({
87+
id: orgId,
88+
});
89+
return organization;
90+
});
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use server";
2+
3+
import { getAuth0ManagementClient, getCurrentSession } from "./auth0";
4+
5+
export async function createPersonalProject() {
6+
const { session, userId } = await getCurrentSession();
7+
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+
});
17+
18+
await getAuth0ManagementClient().organizations.addMembers(
19+
{ id: organization.id },
20+
{ members: [userId] }
21+
);
22+
23+
return organization.id;
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use server";
2+
3+
import { FdrAPI } from "@fern-api/fdr-sdk";
4+
5+
import { getFdrClient } from "../services/fdr";
6+
import { AsyncCache } from "./AsyncCache";
7+
import { getCurrentOrg, getCurrentSession } from "./auth0";
8+
import { Auth0OrgName } from "./types";
9+
10+
const MY_DOCS_SITE_CACHE = new AsyncCache<
11+
Auth0OrgName,
12+
FdrAPI.dashboard.GetDocsSitesForOrgResponse
13+
>({
14+
ttlInSeconds: 10,
15+
});
16+
17+
export async function getMyDocsSites(): Promise<FdrAPI.dashboard.GetDocsSitesForOrgResponse> {
18+
const currentOrg = await getCurrentOrg();
19+
const orgId = currentOrg.name;
20+
21+
return MY_DOCS_SITE_CACHE.get(orgId, async () => {
22+
const { session } = await getCurrentSession();
23+
const fdr = getFdrClient({ token: session.tokenSet.accessToken });
24+
const docsSites = await fdr.dashboard.getDocsSitesForOrg({
25+
orgId: FdrAPI.OrgId(orgId),
26+
});
27+
if (!docsSites.ok) {
28+
console.error("Failed to load docs sites", docsSites.error);
29+
throw new Error("Failed to load docs sites");
30+
}
31+
return docsSites.body;
32+
});
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"use server";
2+
3+
import { GetOrganizations200ResponseOneOfInner } from "auth0";
4+
5+
import { AsyncCache } from "./AsyncCache";
6+
import { getAuth0ManagementClient, getCurrentSession } from "./auth0";
7+
import { Auth0UserID } from "./types";
8+
9+
const MY_ORGANIZATIONS_CACHE = new AsyncCache<
10+
Auth0UserID,
11+
GetOrganizations200ResponseOneOfInner[]
12+
>({
13+
ttlInSeconds: 10,
14+
});
15+
16+
export async function getMyOrganizations() {
17+
const { userId } = await getCurrentSession();
18+
19+
return await MY_ORGANIZATIONS_CACHE.get(userId, async () => {
20+
const { data: organizations } =
21+
await getAuth0ManagementClient().users.getUserOrganizations({
22+
id: userId,
23+
});
24+
return organizations;
25+
});
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type Auth0OrgID = string;
2+
export type Auth0OrgName = string;
3+
export type Auth0UserID = string;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default async function Page() {
2+
return <div>api keys!</div>;
3+
}

packages/fern-dashboard/src/app/api/generate-homepage-images/fdr.ts

-19
This file was deleted.

packages/fern-dashboard/src/app/api/generate-homepage-images/route.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable turbo/no-undeclared-env-vars */
12
import { NextRequest, NextResponse } from "next/server";
23

34
import { PutObjectCommand } from "@aws-sdk/client-s3";
@@ -9,10 +10,10 @@ import { setTimeout } from "timers/promises";
910
import { FdrAPI } from "@fern-api/fdr-sdk";
1011
import { FernVenusApi } from "@fern-api/venus-api-sdk";
1112

12-
import { getFdrClient } from "./fdr";
13+
import { getFdrClient } from "../../services/fdr";
14+
import { getS3Client } from "../../services/s3";
15+
import { getVenusClient } from "../../services/venus";
1316
import { parseAuthHeader } from "./parseAuthHeader";
14-
import { getS3Client } from "./s3";
15-
import { getVenusClient } from "./venus";
1617

1718
export const maxDuration = 60;
1819

@@ -131,9 +132,15 @@ async function takeScreenshotAndWriteToAws({
131132
[IMAGE_FILETYPE]({ quality: 50 })
132133
.toBuffer();
133134

135+
if (process.env.HOMEPAGE_IMAGES_S3_BUCKET_NAME == null) {
136+
throw new Error(
137+
"HOMEPAGE_IMAGES_S3_BUCKET_NAME is not defined in the environment"
138+
);
139+
}
140+
134141
await getS3Client().send(
135142
new PutObjectCommand({
136-
Bucket: "dev2-docs-homepage-images",
143+
Bucket: process.env.HOMEPAGE_IMAGES_S3_BUCKET_NAME,
137144
Key: getS3KeyForHomepageScreenshot({ url, theme }),
138145
Body: compressedScreenshotBuffer,
139146
ContentType: `image/${IMAGE_FILETYPE}`,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default async function Page() {
2+
return <div>billing</div>;
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default async function Page(_props: {
2+
params: Promise<{ docsUrl: string }>;
3+
}) {
4+
return <div>ai search</div>;
5+
}

0 commit comments

Comments
 (0)