diff --git a/messages/en-US.json b/messages/en-US.json index 0a45b32e..178a9bb9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -150,7 +150,6 @@ "resourceAdd": "Add Resource", "resourceErrorDelte": "Error deleting resource", "authentication": "Authentication", - "authPages": "Auth Page", "protected": "Protected", "notProtected": "Not Protected", "resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index d08457e5..411ada44 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -123,7 +123,9 @@ export enum ActionsEnum { getBlueprint = "getBlueprint", applyBlueprint = "applyBlueprint", viewLogs = "viewLogs", - exportLogs = "exportLogs" + exportLogs = "exportLogs", + updateOrgAuthPage = "updateOrgAuthPage", + getOrgAuthPage = "getOrgAuthPage" } export async function checkUserActionPermission( diff --git a/server/lib/createResponseBodySchema.ts b/server/lib/createResponseBodySchema.ts new file mode 100644 index 00000000..478cc0c3 --- /dev/null +++ b/server/lib/createResponseBodySchema.ts @@ -0,0 +1,13 @@ +import z, { type ZodSchema } from "zod"; + +export function createResponseBodySchema(dataSchema: T) { + return z.object({ + data: dataSchema.nullable(), + success: z.boolean(), + error: z.boolean(), + message: z.string(), + status: z.number() + }); +} + +export default createResponseBodySchema; diff --git a/server/private/routers/authPage/getOrgAuthPage.ts b/server/private/routers/authPage/getOrgAuthPage.ts new file mode 100644 index 00000000..03c03ac9 --- /dev/null +++ b/server/private/routers/authPage/getOrgAuthPage.ts @@ -0,0 +1,107 @@ +import { eq } from "drizzle-orm"; +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, orgAuthPages } from "@server/db"; +import { orgs } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import createResponseBodySchema from "@server/lib/createResponseBodySchema"; + +const getOrgAuthPageParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const reponseSchema = createResponseBodySchema( + z + .object({ + logoUrl: z.string().url(), + logoWidth: z.number().min(1), + logoHeight: z.number().min(1), + title: z.string(), + subtitle: z.string().optional(), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional() + }) + .strict() +); + +export type GetOrgAuthPageResponse = z.infer; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/auth-page", + description: "Get an organization auth page", + tags: [OpenAPITags.Org], + request: { + params: getOrgAuthPageParamsSchema + }, + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: reponseSchema + } + } + } + } +}); + +export async function getOrgAuthPage( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getOrgAuthPageParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const [orgAuthPage] = await db + .select() + .from(orgAuthPages) + .leftJoin(orgs, eq(orgs.orgId, orgAuthPages.orgId)) + .where(eq(orgs.orgId, orgId)) + .limit(1); + + return response(res, { + data: orgAuthPage?.orgAuthPages ?? null, + success: true, + error: false, + message: "Organization auth page retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/authPage/index.ts b/server/private/routers/authPage/index.ts new file mode 100644 index 00000000..2a01a879 --- /dev/null +++ b/server/private/routers/authPage/index.ts @@ -0,0 +1,15 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./updateOrgAuthPage"; +export * from "./getOrgAuthPage"; diff --git a/server/private/routers/authPage/updateOrgAuthPage.ts b/server/private/routers/authPage/updateOrgAuthPage.ts new file mode 100644 index 00000000..e6fa04f4 --- /dev/null +++ b/server/private/routers/authPage/updateOrgAuthPage.ts @@ -0,0 +1,141 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, orgAuthPages } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import createResponseBodySchema from "@server/lib/createResponseBodySchema"; + +const updateOrgAuthPageParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const updateOrgAuthPageBodySchema = z + .object({ + logoUrl: z.string().url(), + logoWidth: z.number().min(1), + logoHeight: z.number().min(1), + title: z.string(), + subtitle: z.string().optional(), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional() + }) + .strict(); + +const reponseSchema = createResponseBodySchema(updateOrgAuthPageBodySchema); + +export type UpdateOrgAuthPageResponse = z.infer; + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/auth-page", + description: "Update an organization auth page", + tags: [OpenAPITags.Org], + request: { + params: updateOrgAuthPageParamsSchema, + body: { + content: { + "application/json": { + schema: updateOrgAuthPageBodySchema + } + } + } + }, + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: reponseSchema + } + } + } + } +}); + +export async function updateOrgAuthPage( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = updateOrgAuthPageParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = updateOrgAuthPageBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + const body = parsedBody.data; + + const updatedOrgAuthPages = await db + .insert(orgAuthPages) + .values({ + ...body, + orgId + }) + .onConflictDoUpdate({ + target: orgAuthPages.orgId, + set: { + ...body + } + }) + .returning(); + + if (updatedOrgAuthPages.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Organization with ID ${orgId} not found` + ) + ); + } + + return response(res, { + data: updatedOrgAuthPages[0], + success: true, + error: false, + message: "Organization auth page updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 00ad117f..6d1cf6df 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -17,6 +17,7 @@ import * as billing from "#private/routers/billing"; import * as remoteExitNode from "#private/routers/remoteExitNode"; import * as loginPage from "#private/routers/loginPage"; import * as orgIdp from "#private/routers/orgIdp"; +import * as authPage from "#private/routers/authPage"; import * as domain from "#private/routers/domain"; import * as auth from "#private/routers/auth"; import * as license from "#private/routers/license"; @@ -403,3 +404,23 @@ authenticated.get( logActionAudit(ActionsEnum.exportLogs), logs.exportAccessAuditLogs ); + +authenticated.put( + "/org/:orgId/auth-page", + verifyValidLicense, + verifyValidSubscription, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateOrgAuthPage), + logActionAudit(ActionsEnum.updateOrgAuthPage), + authPage.updateOrgAuthPage +); + +authenticated.get( + "/org/:orgId/auth-page", + verifyValidLicense, + verifyValidSubscription, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getOrgAuthPage), + logActionAudit(ActionsEnum.getOrgAuthPage), + authPage.getOrgAuthPage +); diff --git a/server/routers/external.ts b/server/routers/external.ts index 5c235902..0099aeea 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -80,7 +80,7 @@ authenticated.post( verifyOrgAccess, verifyUserHasAction(ActionsEnum.updateOrg), logActionAudit(ActionsEnum.updateOrg), - org.updateOrg, + org.updateOrg ); if (build !== "saas") { @@ -90,7 +90,7 @@ if (build !== "saas") { verifyUserIsOrgOwner, verifyUserHasAction(ActionsEnum.deleteOrg), logActionAudit(ActionsEnum.deleteOrg), - org.deleteOrg, + org.deleteOrg ); } @@ -157,7 +157,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createClient), logActionAudit(ActionsEnum.createClient), - client.createClient, + client.createClient ); authenticated.delete( @@ -166,7 +166,7 @@ authenticated.delete( verifyClientAccess, verifyUserHasAction(ActionsEnum.deleteClient), logActionAudit(ActionsEnum.deleteClient), - client.deleteClient, + client.deleteClient ); authenticated.post( @@ -175,7 +175,7 @@ authenticated.post( verifyClientAccess, // this will check if the user has access to the client verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client logActionAudit(ActionsEnum.updateClient), - client.updateClient, + client.updateClient ); // authenticated.get( @@ -189,14 +189,14 @@ authenticated.post( verifySiteAccess, verifyUserHasAction(ActionsEnum.updateSite), logActionAudit(ActionsEnum.updateSite), - site.updateSite, + site.updateSite ); authenticated.delete( "/site/:siteId", verifySiteAccess, verifyUserHasAction(ActionsEnum.deleteSite), logActionAudit(ActionsEnum.deleteSite), - site.deleteSite, + site.deleteSite ); // TODO: BREAK OUT THESE ACTIONS SO THEY ARE NOT ALL "getSite" @@ -216,13 +216,13 @@ authenticated.post( "/site/:siteId/docker/check", verifySiteAccess, verifyUserHasAction(ActionsEnum.getSite), - site.checkDockerSocket, + site.checkDockerSocket ); authenticated.post( "/site/:siteId/docker/trigger", verifySiteAccess, verifyUserHasAction(ActionsEnum.getSite), - site.triggerFetchContainers, + site.triggerFetchContainers ); authenticated.get( "/site/:siteId/docker/containers", @@ -238,7 +238,7 @@ authenticated.put( verifySiteAccess, verifyUserHasAction(ActionsEnum.createSiteResource), logActionAudit(ActionsEnum.createSiteResource), - siteResource.createSiteResource, + siteResource.createSiteResource ); authenticated.get( @@ -272,7 +272,7 @@ authenticated.post( verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.updateSiteResource), logActionAudit(ActionsEnum.updateSiteResource), - siteResource.updateSiteResource, + siteResource.updateSiteResource ); authenticated.delete( @@ -282,7 +282,7 @@ authenticated.delete( verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.deleteSiteResource), logActionAudit(ActionsEnum.deleteSiteResource), - siteResource.deleteSiteResource, + siteResource.deleteSiteResource ); authenticated.put( @@ -290,7 +290,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createResource), logActionAudit(ActionsEnum.createResource), - resource.createResource, + resource.createResource ); authenticated.get( @@ -352,7 +352,7 @@ authenticated.delete( verifyOrgAccess, verifyUserHasAction(ActionsEnum.removeInvitation), logActionAudit(ActionsEnum.removeInvitation), - user.removeInvitation, + user.removeInvitation ); authenticated.post( @@ -360,7 +360,7 @@ authenticated.post( verifyOrgAccess, verifyUserHasAction(ActionsEnum.inviteUser), logActionAudit(ActionsEnum.inviteUser), - user.inviteUser, + user.inviteUser ); // maybe make this /invite/create instead unauthenticated.post("/invite/accept", user.acceptInvite); // this is supposed to be unauthenticated @@ -396,14 +396,14 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.updateResource), logActionAudit(ActionsEnum.updateResource), - resource.updateResource, + resource.updateResource ); authenticated.delete( "/resource/:resourceId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.deleteResource), logActionAudit(ActionsEnum.deleteResource), - resource.deleteResource, + resource.deleteResource ); authenticated.put( @@ -411,7 +411,7 @@ authenticated.put( verifyResourceAccess, verifyUserHasAction(ActionsEnum.createTarget), logActionAudit(ActionsEnum.createTarget), - target.createTarget, + target.createTarget ); authenticated.get( "/resource/:resourceId/targets", @@ -425,7 +425,7 @@ authenticated.put( verifyResourceAccess, verifyUserHasAction(ActionsEnum.createResourceRule), logActionAudit(ActionsEnum.createResourceRule), - resource.createResourceRule, + resource.createResourceRule ); authenticated.get( "/resource/:resourceId/rules", @@ -438,14 +438,14 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.updateResourceRule), logActionAudit(ActionsEnum.updateResourceRule), - resource.updateResourceRule, + resource.updateResourceRule ); authenticated.delete( "/resource/:resourceId/rule/:ruleId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.deleteResourceRule), logActionAudit(ActionsEnum.deleteResourceRule), - resource.deleteResourceRule, + resource.deleteResourceRule ); authenticated.get( @@ -459,14 +459,14 @@ authenticated.post( verifyTargetAccess, verifyUserHasAction(ActionsEnum.updateTarget), logActionAudit(ActionsEnum.updateTarget), - target.updateTarget, + target.updateTarget ); authenticated.delete( "/target/:targetId", verifyTargetAccess, verifyUserHasAction(ActionsEnum.deleteTarget), logActionAudit(ActionsEnum.deleteTarget), - target.deleteTarget, + target.deleteTarget ); authenticated.put( @@ -474,7 +474,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createRole), logActionAudit(ActionsEnum.createRole), - role.createRole, + role.createRole ); authenticated.get( "/org/:orgId/roles", @@ -500,7 +500,7 @@ authenticated.delete( verifyRoleAccess, verifyUserHasAction(ActionsEnum.deleteRole), logActionAudit(ActionsEnum.deleteRole), - role.deleteRole, + role.deleteRole ); authenticated.post( "/role/:roleId/add/:userId", @@ -508,7 +508,7 @@ authenticated.post( verifyUserAccess, verifyUserHasAction(ActionsEnum.addUserRole), logActionAudit(ActionsEnum.addUserRole), - user.addUserRole, + user.addUserRole ); authenticated.post( @@ -517,7 +517,7 @@ authenticated.post( verifyRoleAccess, verifyUserHasAction(ActionsEnum.setResourceRoles), logActionAudit(ActionsEnum.setResourceRoles), - resource.setResourceRoles, + resource.setResourceRoles ); authenticated.post( @@ -526,7 +526,7 @@ authenticated.post( verifySetResourceUsers, verifyUserHasAction(ActionsEnum.setResourceUsers), logActionAudit(ActionsEnum.setResourceUsers), - resource.setResourceUsers, + resource.setResourceUsers ); authenticated.post( @@ -534,7 +534,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourcePassword), logActionAudit(ActionsEnum.setResourcePassword), - resource.setResourcePassword, + resource.setResourcePassword ); authenticated.post( @@ -542,7 +542,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourcePincode), logActionAudit(ActionsEnum.setResourcePincode), - resource.setResourcePincode, + resource.setResourcePincode ); authenticated.post( @@ -550,7 +550,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourceHeaderAuth), logActionAudit(ActionsEnum.setResourceHeaderAuth), - resource.setResourceHeaderAuth, + resource.setResourceHeaderAuth ); authenticated.post( @@ -558,7 +558,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourceWhitelist), logActionAudit(ActionsEnum.setResourceWhitelist), - resource.setResourceWhitelist, + resource.setResourceWhitelist ); authenticated.get( @@ -573,7 +573,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.generateAccessToken), logActionAudit(ActionsEnum.generateAccessToken), - accessToken.generateAccessToken, + accessToken.generateAccessToken ); authenticated.delete( @@ -581,7 +581,7 @@ authenticated.delete( verifyAccessTokenAccess, verifyUserHasAction(ActionsEnum.deleteAcessToken), logActionAudit(ActionsEnum.deleteAcessToken), - accessToken.deleteAccessToken, + accessToken.deleteAccessToken ); authenticated.get( @@ -655,7 +655,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createOrgUser), logActionAudit(ActionsEnum.createOrgUser), - user.createOrgUser, + user.createOrgUser ); authenticated.post( @@ -664,7 +664,7 @@ authenticated.post( verifyUserAccess, verifyUserHasAction(ActionsEnum.updateOrgUser), logActionAudit(ActionsEnum.updateOrgUser), - user.updateOrgUser, + user.updateOrgUser ); authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); @@ -688,7 +688,7 @@ authenticated.delete( verifyUserAccess, verifyUserHasAction(ActionsEnum.removeUser), logActionAudit(ActionsEnum.removeUser), - user.removeUserOrg, + user.removeUserOrg ); // authenticated.put( @@ -819,7 +819,7 @@ authenticated.post( verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.setApiKeyActions), logActionAudit(ActionsEnum.setApiKeyActions), - apiKeys.setApiKeyActions, + apiKeys.setApiKeyActions ); authenticated.get( @@ -835,7 +835,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createApiKey), logActionAudit(ActionsEnum.createApiKey), - apiKeys.createOrgApiKey, + apiKeys.createOrgApiKey ); authenticated.delete( @@ -844,7 +844,7 @@ authenticated.delete( verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.deleteApiKey), logActionAudit(ActionsEnum.deleteApiKey), - apiKeys.deleteOrgApiKey, + apiKeys.deleteOrgApiKey ); authenticated.get( @@ -860,7 +860,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createOrgDomain), logActionAudit(ActionsEnum.createOrgDomain), - domain.createOrgDomain, + domain.createOrgDomain ); authenticated.post( @@ -869,7 +869,7 @@ authenticated.post( verifyDomainAccess, verifyUserHasAction(ActionsEnum.restartOrgDomain), logActionAudit(ActionsEnum.restartOrgDomain), - domain.restartOrgDomain, + domain.restartOrgDomain ); authenticated.delete( @@ -878,7 +878,7 @@ authenticated.delete( verifyDomainAccess, verifyUserHasAction(ActionsEnum.deleteOrgDomain), logActionAudit(ActionsEnum.deleteOrgDomain), - domain.deleteAccountDomain, + domain.deleteAccountDomain ); authenticated.get( @@ -1237,4 +1237,4 @@ authRouter.delete( store: createStore() }), auth.deleteSecurityKey -); \ No newline at end of file +); diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx index 538c7fde..c4048bcc 100644 --- a/src/app/[orgId]/settings/(private)/billing/layout.tsx +++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx @@ -1,16 +1,11 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import { cache } from "react"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; type BillingSettingsProps = { children: React.ReactNode; @@ -19,12 +14,11 @@ type BillingSettingsProps = { export default async function BillingSettingsPage({ children, - params, + params }: BillingSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +26,7 @@ export default async function BillingSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader(), - ), - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,13 +34,7 @@ export default async function BillingSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader(), - ), - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); @@ -65,11 +47,11 @@ export default async function BillingSettingsPage({ - {children} + {children} diff --git a/src/app/[orgId]/settings/general/auth-pages/page.tsx b/src/app/[orgId]/settings/general/auth-pages/page.tsx index 2ab90bdb..79065513 100644 --- a/src/app/[orgId]/settings/general/auth-pages/page.tsx +++ b/src/app/[orgId]/settings/general/auth-pages/page.tsx @@ -1,5 +1,11 @@ import AuthPageCustomizationForm from "@app/components/AuthPagesCustomizationForm"; import { SettingsContainer } from "@app/components/Settings"; +import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; +import { pullEnv } from "@app/lib/pullEnv"; +import { build } from "@server/build"; +import { TierId } from "@server/lib/billing/tiers"; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; +import { redirect } from "next/navigation"; export interface AuthPageProps { params: Promise<{ orgId: string }>; @@ -7,6 +13,21 @@ export interface AuthPageProps { export default async function AuthPage(props: AuthPageProps) { const orgId = (await props.params).orgId; + const env = pullEnv(); + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + + if (!subscribed) { + redirect(env.app.dashboardUrl); + } + return ( diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index fc501d82..5ace97ca 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -9,8 +9,14 @@ import { GetOrgResponse } from "@server/routers/org"; import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import { cache } from "react"; + import { getTranslations } from "next-intl/server"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { GetOrgTierResponse } from "@server/routers/billing/types"; +import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; +import { build } from "@server/build"; +import { TierId } from "@server/lib/billing/tiers"; type GeneralSettingsProps = { children: React.ReactNode; @@ -23,8 +29,7 @@ export default async function GeneralSettingsPage({ }: GeneralSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +37,7 @@ export default async function GeneralSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader() - ) - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,18 +45,22 @@ export default async function GeneralSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader() - ) - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); } + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + const t = await getTranslations(); const navItems: TabItem[] = [ @@ -65,12 +68,14 @@ export default async function GeneralSettingsPage({ title: t("general"), href: `/{orgId}/settings/general`, exact: true - }, - { - title: t("authPages"), - href: `/{orgId}/settings/general/auth-pages` } ]; + if (subscribed) { + navItems.push({ + title: t("authPage"), + href: `/{orgId}/settings/general/auth-pages` + }); + } return ( <> diff --git a/src/lib/api/getCachedOrgUser.ts b/src/lib/api/getCachedOrgUser.ts new file mode 100644 index 00000000..89632d97 --- /dev/null +++ b/src/lib/api/getCachedOrgUser.ts @@ -0,0 +1,13 @@ +import type { GetOrgResponse } from "@server/routers/org"; +import type { AxiosResponse } from "axios"; +import { cache } from "react"; +import { authCookieHeader } from "./cookies"; +import { internal } from "."; +import type { GetOrgUserResponse } from "@server/routers/user"; + +export const getCachedOrgUser = cache(async (orgId: string, userId: string) => + internal.get>( + `/org/${orgId}/user/${userId}`, + await authCookieHeader() + ) +); diff --git a/src/lib/api/getCachedSubscription.ts b/src/lib/api/getCachedSubscription.ts new file mode 100644 index 00000000..dbffee5d --- /dev/null +++ b/src/lib/api/getCachedSubscription.ts @@ -0,0 +1,8 @@ +import type { AxiosResponse } from "axios"; +import { cache } from "react"; +import { priv } from "."; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; + +export const getCachedSubscription = cache(async (orgId: string) => + priv.get>(`/org/${orgId}/billing/tier`) +); diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 5cef9f0e..6c4613e9 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -60,4 +60,3 @@ export const priv = axios.create({ }); export * from "./formatAxiosError"; - diff --git a/src/lib/auth/verifySession.ts b/src/lib/auth/verifySession.ts index e51c5096..d679e7b5 100644 --- a/src/lib/auth/verifySession.ts +++ b/src/lib/auth/verifySession.ts @@ -3,9 +3,10 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { GetUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { pullEnv } from "../pullEnv"; +import { cache } from "react"; -export async function verifySession({ - skipCheckVerifyEmail, +export const verifySession = cache(async function ({ + skipCheckVerifyEmail }: { skipCheckVerifyEmail?: boolean; } = {}): Promise { @@ -14,7 +15,7 @@ export async function verifySession({ try { const res = await internal.get>( "/user", - await authCookieHeader(), + await authCookieHeader() ); const user = res.data.data; @@ -35,4 +36,4 @@ export async function verifySession({ } catch (e) { return null; } -} +});