diff --git a/apps/dashboard/src/@/actions/confirmEmail.ts b/apps/dashboard/src/@/actions/confirmEmail.ts index 6f1216a9fea..7ebb6952d87 100644 --- a/apps/dashboard/src/@/actions/confirmEmail.ts +++ b/apps/dashboard/src/@/actions/confirmEmail.ts @@ -1,6 +1,8 @@ "use server"; +import { revalidateTag } from "next/cache"; import { getAuthToken } from "../../app/api/lib/getAuthToken"; +import { accountCacheTag } from "../constants/cacheTags"; import { API_SERVER_URL } from "../constants/env"; export async function confirmEmailWithOTP(otp: string) { @@ -36,4 +38,6 @@ export async function confirmEmailWithOTP(otp: string) { errorMessage: "Failed to confirm email", }; } + + revalidateTag(accountCacheTag(token)); } diff --git a/apps/dashboard/src/@/actions/proxies.ts b/apps/dashboard/src/@/actions/proxies.ts index 0d5551689d3..c39cd28b4a0 100644 --- a/apps/dashboard/src/@/actions/proxies.ts +++ b/apps/dashboard/src/@/actions/proxies.ts @@ -1,6 +1,11 @@ "use server"; - +import { revalidateTag } from "next/cache"; import { getAuthToken } from "../../app/api/lib/getAuthToken"; +import { + accountCacheTag, + projectsCacheTag, + teamsCacheTag, +} from "../constants/cacheTags"; import { API_SERVER_URL } from "../constants/env"; type ProxyActionParams = { @@ -9,6 +14,13 @@ type ProxyActionParams = { method: "GET" | "POST" | "PUT" | "DELETE"; body?: string; headers?: Record; + revalidateCacheTags?: RevalidateCacheTagOptions; +}; + +type RevalidateCacheTagOptions = { + teamTag?: true; + projectTag?: true; + accountTag?: true; }; type ProxyActionResult = @@ -28,6 +40,15 @@ async function proxy( params: ProxyActionParams, ): Promise> { const authToken = await getAuthToken(); + const { revalidateCacheTags: revalidateCacheTag } = params; + + if (!authToken) { + return { + status: 401, + ok: false, + error: "Unauthorized", + }; + } // build URL const url = new URL(baseUrl); @@ -64,6 +85,20 @@ async function proxy( } } + if (revalidateCacheTag) { + if (revalidateCacheTag.teamTag) { + revalidateTag(teamsCacheTag(authToken)); + } + + if (revalidateCacheTag.projectTag) { + revalidateTag(projectsCacheTag(authToken)); + } + + if (revalidateCacheTag.accountTag) { + revalidateTag(accountCacheTag(authToken)); + } + } + return { status: res.status, ok: true, diff --git a/apps/dashboard/src/@/actions/updateAccount.ts b/apps/dashboard/src/@/actions/updateAccount.ts index 0c4d2378d47..02265ea0446 100644 --- a/apps/dashboard/src/@/actions/updateAccount.ts +++ b/apps/dashboard/src/@/actions/updateAccount.ts @@ -1,5 +1,7 @@ "use server"; +import { revalidateTag } from "next/cache"; import { getAuthToken } from "../../app/api/lib/getAuthToken"; +import { accountCacheTag } from "../constants/cacheTags"; import { API_SERVER_URL } from "../constants/env"; export async function updateAccount(values: { @@ -7,9 +9,9 @@ export async function updateAccount(values: { email?: string; image?: string | null; }) { - const token = await getAuthToken(); + const authToken = await getAuthToken(); - if (!token) { + if (!authToken) { throw new Error("No Auth token"); } @@ -17,7 +19,7 @@ export async function updateAccount(values: { method: "PUT", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${authToken}`, }, body: JSON.stringify(values), }); @@ -35,4 +37,6 @@ export async function updateAccount(values: { errorMessage: "Failed To Update Account", }; } + + revalidateTag(accountCacheTag(authToken)); } diff --git a/apps/dashboard/src/@/actions/validLogin.ts b/apps/dashboard/src/@/actions/validLogin.ts index 03d637d2b06..0a76ac1ed48 100644 --- a/apps/dashboard/src/@/actions/validLogin.ts +++ b/apps/dashboard/src/@/actions/validLogin.ts @@ -3,8 +3,8 @@ import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; import { cookies } from "next/headers"; import { getAddress } from "thirdweb"; +import { getCachedRawAccountForAuthToken } from "../../app/account/settings/getAccount"; import { COOKIE_PREFIX_TOKEN } from "../constants/cookie"; -import { API_SERVER_URL } from "../constants/env"; /** * Check that the connected wallet is valid for the current active account @@ -30,19 +30,12 @@ export async function isWalletValidForActiveAccount(params: { } // authToken should be valid - const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, { - method: "GET", - headers: { - Authorization: `Bearer ${authTokenForAddress}`, - }, - }); + const account = await getCachedRawAccountForAuthToken(authTokenForAddress); - if (accountRes.status !== 200) { + if (!account) { return false; } - const account = (await accountRes.json()).data as Account; - // the current account should match the account fetched for the authToken return account.id === params.account.id; } diff --git a/apps/dashboard/src/@/api/projects.ts b/apps/dashboard/src/@/api/projects.ts index f45ecf02496..2fb4718b53e 100644 --- a/apps/dashboard/src/@/api/projects.ts +++ b/apps/dashboard/src/@/api/projects.ts @@ -1,6 +1,8 @@ import "server-only"; import { API_SERVER_URL } from "@/constants/env"; +import { unstable_cache } from "next/cache"; import { getAuthToken } from "../../app/api/lib/getAuthToken"; +import { projectsCacheTag } from "../constants/cacheTags"; export type Project = { id: string; @@ -26,37 +28,74 @@ export async function getProjects(teamSlug: string) { return []; } - const teamsRes = await fetch( - `${API_SERVER_URL}/v1/teams/${teamSlug}/projects`, + const getCachedProjects = unstable_cache( + getProjectsForAuthToken, + ["getProjects"], { - headers: { - Authorization: `Bearer ${token}`, - }, + tags: [projectsCacheTag(token)], + revalidate: 3600, // 1 hour }, ); - if (teamsRes.ok) { - return (await teamsRes.json())?.result as Project[]; + + return getCachedProjects(token, teamSlug); +} + +export async function getProjectsForAuthToken( + authToken: string, + teamSlug: string, +) { + console.log("FETCHING PROJECTS ------------------------"); + const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}/projects`, { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }); + if (res.ok) { + return (await res.json())?.result as Project[]; } return []; } export async function getProject(teamSlug: string, projectSlug: string) { - const token = await getAuthToken(); + const authToken = await getAuthToken(); - if (!token) { + if (!authToken) { return null; } - const teamsRes = await fetch( + const getCachedProject = unstable_cache( + getProjectForAuthToken, + ["getProject"], + { + tags: [projectsCacheTag(authToken)], + revalidate: 3600, // 1 hour + }, + ); + + return getCachedProject(authToken, teamSlug, projectSlug); +} + +async function getProjectForAuthToken( + authToken: string, + teamSlug: string, + projectSlug: string, +) { + console.log( + "FETCHING PROJECT ------------------------", + teamSlug, + projectSlug, + ); + const res = await fetch( `${API_SERVER_URL}/v1/teams/${teamSlug}/projects/${projectSlug}`, { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${authToken}`, }, }, ); - if (teamsRes.ok) { - return (await teamsRes.json())?.result as Project; + + if (res.ok) { + return (await res.json())?.result as Project; } return null; } diff --git a/apps/dashboard/src/@/api/team.ts b/apps/dashboard/src/@/api/team.ts index 78ea7550851..5355821c469 100644 --- a/apps/dashboard/src/@/api/team.ts +++ b/apps/dashboard/src/@/api/team.ts @@ -1,6 +1,8 @@ import "server-only"; import { API_SERVER_URL } from "@/constants/env"; +import { unstable_cache } from "next/cache"; import { getAuthToken } from "../../app/api/lib/getAuthToken"; +import { teamsCacheTag } from "../constants/cacheTags"; type EnabledTeamScope = | "pay" @@ -29,16 +31,31 @@ export type Team = { enabledScopes: EnabledTeamScope[]; }; -export async function getTeamBySlug(slug: string) { - const token = await getAuthToken(); +export async function getTeamBySlug(teamSlug: string) { + const authToken = await getAuthToken(); - if (!token) { + if (!authToken) { return null; } - const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${slug}`, { + const getCachedTeam = unstable_cache( + getTeamBySlugForAuthToken, + ["getTeamBySlug"], + { + tags: [teamsCacheTag(authToken)], + revalidate: 3600, // 1 hour + }, + ); + + return getCachedTeam(teamSlug, authToken); +} + +async function getTeamBySlugForAuthToken(teamSlug: string, authToken: string) { + console.log("FETCHING TEAM ------------------------", teamSlug); + + const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}`, { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${authToken}`, }, }); if (teamRes.ok) { @@ -48,14 +65,26 @@ export async function getTeamBySlug(slug: string) { } export async function getTeams() { - const token = await getAuthToken(); - if (!token) { + const authToken = await getAuthToken(); + + if (!authToken) { return null; } + const getCachedTeams = unstable_cache(getTeamsForAuthToken, ["getTeams"], { + tags: [teamsCacheTag(authToken)], + revalidate: 3600, // 1 hour + }); + + return getCachedTeams(authToken); +} + +async function getTeamsForAuthToken(authToken: string) { + console.log("FETCHING ALL TEAMs ------------------------"); + const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${authToken}`, }, }); if (teamsRes.ok) { @@ -70,17 +99,33 @@ type TeamNebulaWaitList = { }; export async function getTeamNebulaWaitList(teamSlug: string) { - const token = await getAuthToken(); + const authToken = await getAuthToken(); - if (!token) { + if (!authToken) { return null; } + const getCachedNebulaWaitlist = unstable_cache( + getTeamNebulaWaitListForAuthToken, + ["getTeamNebulaWaitList"], + { + tags: [teamsCacheTag(authToken)], + revalidate: 3600, // 1 hour + }, + ); + + return getCachedNebulaWaitlist(teamSlug, authToken); +} + +async function getTeamNebulaWaitListForAuthToken( + teamSlug: string, + authToken: string, +) { const res = await fetch( `${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist?scope=nebula`, { headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${authToken}`, }, }, ); @@ -88,6 +133,5 @@ export async function getTeamNebulaWaitList(teamSlug: string) { if (res.ok) { return (await res.json()).result as TeamNebulaWaitList; } - return null; } diff --git a/apps/dashboard/src/@/constants/cacheTags.ts b/apps/dashboard/src/@/constants/cacheTags.ts new file mode 100644 index 00000000000..623fc0d83e4 --- /dev/null +++ b/apps/dashboard/src/@/constants/cacheTags.ts @@ -0,0 +1,20 @@ +import { keccak256 } from "thirdweb"; + +export function teamsCacheTag(authToken: string) { + return `${shortenAuthToken(authToken)}/teams`; +} + +export function projectsCacheTag(authToken: string) { + return `${shortenAuthToken(authToken)}/projects`; +} + +export function accountCacheTag(authToken: string) { + return `${shortenAuthToken(authToken)}/account`; +} + +function shortenAuthToken(authToken: string) { + // authToken is too long for the next.js cache tag, we have to shorten it + // shorten auth token by creating a hash of it + const authTokenHash = keccak256(new TextEncoder().encode(authToken)); + return authTokenHash; +} diff --git a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts index cfb6b1c753b..21ed1dac2e1 100644 --- a/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts +++ b/apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts @@ -51,6 +51,7 @@ export type Account = { billing: "email" | "none"; updates: "email" | "none"; }; + plan: string; // This is being used in ticket creation flow // TODO - add image URL }; @@ -222,6 +223,7 @@ export function useAccountCredits() { error?: { message: string }; }; + // TODO - this can be cached in server action const res = await apiServerProxy({ pathname: "/v1/account/credits", method: "GET", @@ -397,6 +399,9 @@ export function useUpdateAccount() { "Content-Type": "application/json", }, body: JSON.stringify(input), + revalidateCacheTags: { + accountTag: true, + }, }); if (!res.ok) { @@ -438,6 +443,9 @@ export function useUpdateNotifications() { "Content-Type": "application/json", }, body: JSON.stringify({ preferences: input }), + revalidateCacheTags: { + accountTag: true, + }, }); if (!res.ok) { @@ -478,6 +486,9 @@ export function useConfirmEmail() { "Content-Type": "application/json", }, body: JSON.stringify(input), + revalidateCacheTags: { + accountTag: true, + }, }); if (!res.ok) { @@ -527,6 +538,9 @@ export function useResendEmailConfirmation() { "Content-Type": "application/json", }, body: JSON.stringify({}), + revalidateCacheTags: { + accountTag: true, + }, }); if (!res.ok) { @@ -567,6 +581,9 @@ export function useCreateApiKey() { "Content-Type": "application/json", }, body: JSON.stringify(input), + revalidateCacheTags: { + projectTag: true, + }, }); if (!res.ok) { @@ -607,6 +624,9 @@ export function useUpdateApiKey() { "Content-Type": "application/json", }, body: JSON.stringify(input), + revalidateCacheTags: { + projectTag: true, + }, }); if (!res.ok) { @@ -647,6 +667,9 @@ export function useRevokeApiKey() { "Content-Type": "application/json", }, body: JSON.stringify({}), + revalidateCacheTags: { + projectTag: true, + }, }); if (!res.ok) { diff --git a/apps/dashboard/src/app/(dashboard)/support/create-ticket/components/create-ticket.action.ts b/apps/dashboard/src/app/(dashboard)/support/create-ticket/components/create-ticket.action.ts index a18e208994d..4eebc201ee8 100644 --- a/apps/dashboard/src/app/(dashboard)/support/create-ticket/components/create-ticket.action.ts +++ b/apps/dashboard/src/app/(dashboard)/support/create-ticket/components/create-ticket.action.ts @@ -2,9 +2,9 @@ import "server-only"; import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; -import { API_SERVER_URL } from "@/constants/env"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; +import { getCachedRawAccountForAuthToken } from "../../../../account/settings/getAccount"; type State = { success: boolean; @@ -76,28 +76,22 @@ export async function createTicketAction( const token = activeAccount ? cookieManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value : null; + if (!activeAccount || !token) { // user is not logged in, make them log in redirect(`/login?next=${encodeURIComponent("/support")}`); } - const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - if (accountRes.status !== 200) { + + const account = await getCachedRawAccountForAuthToken(token); + + if (!account) { // user is not logged in, make them log in redirect(`/login?next=${encodeURIComponent("/support")}`); } - const account = (await accountRes.json()) as { - data: { name: string; email: string; plan: string; id: string }; - }; + const { plan } = account; - const customerId = isValidPlan(account.data.plan) - ? planToCustomerId[account.data.plan] - : undefined; + const customerId = isValidPlan(plan) ? planToCustomerId[plan] : undefined; const product = formData.get("product")?.toString() || ""; const problemArea = formData.get("extraInfo_Problem_Area")?.toString() || ""; @@ -105,8 +99,8 @@ export async function createTicketAction( const title = prepareEmailTitle( product, problemArea, - account.data.email, - account.data.name, + account.email || "", + account.name || "", ); const keyVal: Record = {}; @@ -117,8 +111,8 @@ export async function createTicketAction( const markdown = prepareEmailBody({ product, markdownInput: keyVal.markdown || "", - email: account.data.email, - name: account.data.name, + email: account.email || "", + name: account.name || "", extraInfoInput: keyVal, walletAddress: activeAccount, }); @@ -129,9 +123,9 @@ export async function createTicketAction( markdown, status: "open", onBehalfOf: { - email: account.data.email, - name: account.data.name, - id: account.data.id, + email: account.email, + name: account.name, + id: account.id, }, customerId, emailInboxId: process.env.UNTHREAD_EMAIL_INBOX_ID, diff --git a/apps/dashboard/src/app/account/settings/getAccount.ts b/apps/dashboard/src/app/account/settings/getAccount.ts index fe1ea5f1612..cf6b71b34c1 100644 --- a/apps/dashboard/src/app/account/settings/getAccount.ts +++ b/apps/dashboard/src/app/account/settings/getAccount.ts @@ -1,5 +1,7 @@ +import { accountCacheTag } from "@/constants/cacheTags"; import { API_SERVER_URL } from "@/constants/env"; import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; +import { unstable_cache } from "next/cache"; import { getAuthToken } from "../../api/lib/getAuthToken"; import { loginRedirect } from "../../login/loginRedirect"; import { isOnboardingComplete } from "../../login/onboarding/isOnboardingRequired"; @@ -15,6 +17,23 @@ export async function getRawAccount() { return undefined; } + return getCachedRawAccountForAuthToken(authToken); +} + +export async function getCachedRawAccountForAuthToken(authToken: string) { + const getCachedAccount = unstable_cache( + getRawAccountForAuthToken, + ["getRawAccount"], + { + tags: [accountCacheTag(authToken)], + }, + ); + + return getCachedAccount(authToken); +} + +async function getRawAccountForAuthToken(authToken: string) { + console.log("FETCHING ACCOUNT -------------"); const res = await fetch(`${API_SERVER_URL}/v1/account/me`, { method: "GET", headers: { diff --git a/apps/dashboard/src/app/api/auth/ensure-login/route.ts b/apps/dashboard/src/app/api/auth/ensure-login/route.ts deleted file mode 100644 index 5ba7c408e16..00000000000 --- a/apps/dashboard/src/app/api/auth/ensure-login/route.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; -import { API_SERVER_URL } from "@/constants/env"; -import { cookies } from "next/headers"; -import { type NextRequest, NextResponse } from "next/server"; -import { getAddress } from "thirdweb/utils"; - -export type EnsureLoginResponse = { - isLoggedIn: boolean; - jwt?: string; -}; - -export const GET = async (req: NextRequest) => { - const address = req.nextUrl.searchParams.get("address"); - - const cookieStore = await cookies(); - // if we are "disconnected" we are not logged in, clear the cookie and redirect to login - // this is the "log out" case (for now) - if (!address) { - // delete all cookies that start with the token prefix - const allCookies = cookieStore.getAll(); - for (const cookie of allCookies) { - if (cookie.name.startsWith(COOKIE_PREFIX_TOKEN)) { - cookieStore.delete(cookie.name); - } - } - // also delete the active account cookie - cookieStore.delete(COOKIE_ACTIVE_ACCOUNT); - return NextResponse.json({ - isLoggedIn: false, - }); - } - - const authCookieName = COOKIE_PREFIX_TOKEN + getAddress(address); - - // check if we have a token - const token = cookieStore.get(authCookieName)?.value; - - // if no token, not logged in, redirect to login - if (!token) { - // delete the active account cookie (the account is not logged in) - cookieStore.delete(COOKIE_ACTIVE_ACCOUNT); - return NextResponse.json({ - isLoggedIn: false, - }); - } - - // check that the token is valid by checking for the user account - const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - if (accountRes.status !== 200) { - // if the account is not found, clear the token and redirect to login - cookieStore.delete(authCookieName); - return NextResponse.json({ - isLoggedIn: false, - }); - } - - // make sure the active account cookie is set to the correct address - const activeAccountCookie = cookieStore.get(COOKIE_ACTIVE_ACCOUNT); - if ( - !activeAccountCookie || - getAddress(activeAccountCookie.value) !== getAddress(address) - ) { - cookieStore.set(COOKIE_ACTIVE_ACCOUNT, getAddress(address), { - httpOnly: true, - secure: true, - sameSite: "strict", - // 3 days - maxAge: 3 * 24 * 60 * 60, - }); - } - - // if everything is good simply return true - return NextResponse.json({ isLoggedIn: true, jwt: token }); -}; diff --git a/apps/dashboard/src/app/api/auth/get-auth-token/route.ts b/apps/dashboard/src/app/api/auth/get-auth-token/route.ts index 5d9fd4cfbf7..065b9573f00 100644 --- a/apps/dashboard/src/app/api/auth/get-auth-token/route.ts +++ b/apps/dashboard/src/app/api/auth/get-auth-token/route.ts @@ -1,8 +1,8 @@ import { COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; -import { API_SERVER_URL } from "@/constants/env"; import { cookies } from "next/headers"; import { type NextRequest, NextResponse } from "next/server"; import { getAddress } from "thirdweb/utils"; +import { getCachedRawAccountForAuthToken } from "../../../account/settings/getAccount"; export type GetAuthTokenResponse = { jwt: string | null; @@ -41,14 +41,9 @@ export const GET = async (req: NextRequest) => { } // check token validity - const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, { - method: "GET", - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (accountRes.status !== 200) { + const account = await getCachedRawAccountForAuthToken(token); + + if (!account) { return respond(null); } diff --git a/apps/dashboard/src/app/api/lib/getAPIKeys.ts b/apps/dashboard/src/app/api/lib/getAPIKeys.ts index c10e7d8e577..b265c937778 100644 --- a/apps/dashboard/src/app/api/lib/getAPIKeys.ts +++ b/apps/dashboard/src/app/api/lib/getAPIKeys.ts @@ -1,5 +1,7 @@ +import { projectsCacheTag } from "@/constants/cacheTags"; import { API_SERVER_URL } from "@/constants/env"; import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi"; +import { unstable_cache } from "next/cache"; import { getAuthToken } from "./getAuthToken"; // TODO - Fix the `/v1/keys/${apiKeyId}` endpoint in API server @@ -38,6 +40,23 @@ async function getAPIKey(apiKeyId: string) { async function getApiKeys() { const authToken = await getAuthToken(); + if (!authToken) { + return []; + } + + const getCachedAPIKeys = unstable_cache( + getAPIKeysForAuthToken, + ["getApiKeys"], + { + tags: [projectsCacheTag(authToken)], + revalidate: 3600, // 1 hour + }, + ); + + return getCachedAPIKeys(authToken); +} + +async function getAPIKeysForAuthToken(authToken: string) { const res = await fetch(`${API_SERVER_URL}/v1/keys`, { method: "GET", headers: { diff --git a/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts b/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts index f2d7d0d6e87..ce461f0fe2b 100644 --- a/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts +++ b/apps/dashboard/src/app/api/testnet-faucet/claim/route.ts @@ -1,16 +1,15 @@ import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie"; import { - API_SERVER_URL, THIRDWEB_ACCESS_TOKEN, THIRDWEB_ENGINE_FAUCET_WALLET, THIRDWEB_ENGINE_URL, } from "@/constants/env"; -import type { Account } from "@3rdweb-sdk/react/hooks/useApi"; import { ipAddress } from "@vercel/functions"; import { startOfToday } from "date-fns"; import { cacheGet, cacheSet } from "lib/redis"; import { type NextRequest, NextResponse } from "next/server"; import { ZERO_ADDRESS, getAddress } from "thirdweb"; +import { getCachedRawAccountForAuthToken } from "../../../account/settings/getAccount"; import { getFaucetClaimAmount } from "./claim-amount"; interface RequestTestnetFundsPayload { @@ -53,15 +52,12 @@ export const POST = async (req: NextRequest) => { ); } + const authToken = authCookie.value; + // Make sure the connected wallet has a thirdweb account - const accountRes = await fetch(`${API_SERVER_URL}/v1/account/me`, { - method: "GET", - headers: { - Authorization: `Bearer ${authCookie.value}`, - }, - }); + const account = await getCachedRawAccountForAuthToken(authToken); - if (accountRes.status !== 200) { + if (!account) { // Account not found on this connected address return NextResponse.json( { @@ -71,10 +67,8 @@ export const POST = async (req: NextRequest) => { ); } - const account: { data: Account } = await accountRes.json(); - // Make sure the logged-in account has verified its email - if (!account.data.email) { + if (!account.email) { return NextResponse.json( { error: "Account owner hasn't verified email", @@ -152,7 +146,7 @@ export const POST = async (req: NextRequest) => { const ipCacheKey = `testnet-faucet:${chainId}:${ip}`; const addressCacheKey = `testnet-faucet:${chainId}:${toAddress}`; - const accountCacheKey = `testnet-faucet:${chainId}:${account.data.id}`; + const accountCacheKey = `testnet-faucet:${chainId}:${account.id}`; // Assert 1 request per IP/chain every 24 hours. // get the cached value diff --git a/apps/dashboard/src/app/login/auth-actions.ts b/apps/dashboard/src/app/login/auth-actions.ts index 57631b09f33..a612d596568 100644 --- a/apps/dashboard/src/app/login/auth-actions.ts +++ b/apps/dashboard/src/app/login/auth-actions.ts @@ -10,6 +10,7 @@ import type { LoginPayload, VerifyLoginPayloadParams, } from "thirdweb/auth"; +import { getCachedRawAccountForAuthToken } from "../account/settings/getAccount"; const THIRDWEB_API_SECRET = process.env.API_SERVER_SECRET || ""; @@ -154,28 +155,9 @@ export async function isLoggedIn(address: string) { return false; } - const res = await fetch(`${API_SERVER_URL}/v1/account/me`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - }); - - if (!res.ok) { - console.error( - "Failed to check if logged in - api call failed", - res.status, - res.statusText, - ); - // not logged in - // clear the cookie - cookieStore.delete(cookieName); - return false; - } - const json = await res.json(); + const account = await getCachedRawAccountForAuthToken(token); - if (!json) { + if (!account) { // not logged in // clear the cookie cookieStore.delete(cookieName); diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx index a5d6f7d07e5..56486fc9c59 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx @@ -1,9 +1,13 @@ import { getWalletConnections } from "@/api/analytics"; -import { type Project, getProjects } from "@/api/projects"; +import { getProjectsForAuthToken } from "@/api/projects"; import { getTeamBySlug } from "@/api/team"; +import { projectsCacheTag } from "@/constants/cacheTags"; import { Changelog } from "components/dashboard/Changelog"; import { subDays } from "date-fns"; +import { unstable_cache } from "next/cache"; import { redirect } from "next/navigation"; +import { getAuthToken } from "../../../api/lib/getAuthToken"; +import { loginRedirect } from "../../../login/loginRedirect"; import { type ProjectWithAnalytics, TeamProjectsPage, @@ -13,14 +17,33 @@ export default async function Page(props: { params: Promise<{ team_slug: string }>; }) { const params = await props.params; - const team = await getTeamBySlug(params.team_slug); + + const [team, authToken] = await Promise.all([ + getTeamBySlug(params.team_slug), + getAuthToken(), + ]); + + if (!authToken) { + loginRedirect(`/team/${params.team_slug}`); + } if (!team) { redirect("/team"); } - const projects = await getProjects(params.team_slug); - const projectsWithTotalWallets = await getProjectsWithAnalytics(projects); + const getCachedProjectsWithAnalytics = unstable_cache( + getProjectsWithAnalytics, + ["getProjectsWithAnalytics"], + { + revalidate: 3600, // 1 hour, + tags: [projectsCacheTag(authToken)], + }, + ); + + const projectsWithTotalWallets = await getCachedProjectsWithAnalytics( + authToken, + params.team_slug, + ); return (
@@ -39,8 +62,12 @@ export default async function Page(props: { } async function getProjectsWithAnalytics( - projects: Project[], + authToken: string, + teamSlug: string, ): Promise> { + console.log("FETCHING PROJECTS WITH ANALYTICS -------------", teamSlug); + const projects = await getProjectsForAuthToken(authToken, teamSlug); + return Promise.all( projects.map(async (p) => { try { diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/updateTeam.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/updateTeam.ts index b00713dc6d2..b577965292a 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/updateTeam.ts +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/updateTeam.ts @@ -1,7 +1,9 @@ "use server"; import type { Team } from "@/api/team"; +import { teamsCacheTag } from "@/constants/cacheTags"; import { API_SERVER_URL } from "@/constants/env"; +import { revalidateTag } from "next/cache"; import { getAuthToken } from "../../../../../../api/lib/getAuthToken"; export async function updateTeam(params: { @@ -26,4 +28,6 @@ export async function updateTeam(params: { if (!res.ok) { throw new Error("failed to update team"); } + + revalidateTag(teamsCacheTag(authToken)); } diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/getAccountUsage.ts b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/getAccountUsage.ts index 1fad3d1934c..c8b34a08f67 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/getAccountUsage.ts +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/getAccountUsage.ts @@ -1,5 +1,7 @@ +import { accountCacheTag } from "@/constants/cacheTags"; import { API_SERVER_URL } from "@/constants/env"; import type { UsageBillableByService } from "@3rdweb-sdk/react/hooks/useApi"; +import { unstable_cache } from "next/cache"; import { getAuthToken } from "../../../../../api/lib/getAuthToken"; export async function getAccountUsage() { @@ -9,11 +11,24 @@ export async function getAccountUsage() { return undefined; } + const getCachedAccountUsage = unstable_cache( + getAccountUsageForAuthToken, + ["getAccountUsage"], + { + tags: [accountCacheTag(token)], + revalidate: 3600, // 1 hour + }, + ); + + return getCachedAccountUsage(token); +} + +async function getAccountUsageForAuthToken(authToken: string) { const res = await fetch(`${API_SERVER_URL}/v1/account/usage`, { method: "GET", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${authToken}`, }, }); diff --git a/apps/dashboard/src/stories/stubs.ts b/apps/dashboard/src/stories/stubs.ts index 68349aee83b..247a7431f8b 100644 --- a/apps/dashboard/src/stories/stubs.ts +++ b/apps/dashboard/src/stories/stubs.ts @@ -314,6 +314,7 @@ export function accountStub(overrides?: Partial): Account { isStaff: false, advancedEnabled: false, creatorWalletAddress: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37", + plan: "free", ...overrides, }; }