Skip to content

Move workspaceId to use createId #2171

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Mar 19, 2025
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/web/app/api/admin/impersonate/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { prefixWorkspaceId } from "@/lib/api/workspace-id";
import { hashToken, withAdmin } from "@/lib/auth";
import { prisma } from "@dub/prisma";
import { APP_DOMAIN, PARTNERS_DOMAIN } from "@dub/utils";
Expand Down Expand Up @@ -50,7 +51,7 @@ export const POST = withAdmin(async ({ req }) => {
email: response.email,
workspaces: response.projects.map(({ project }) => ({
...project,
id: `ws_${project.id}`,
id: prefixWorkspaceId(project.id),
clicks: project.usage,
links: project.linksUsage,
sales: project.salesUsage,
Expand Down
4 changes: 3 additions & 1 deletion apps/web/app/api/ai/completion/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { handleAndReturnErrorResponse } from "@/lib/api/errors";
import { throwIfAIUsageExceeded } from "@/lib/api/links/usage-checks";
import { normalizeWorkspaceId } from "@/lib/api/workspace-id";
import { withWorkspace } from "@/lib/auth";
import z from "@/lib/zod";
import { anthropic } from "@ai-sdk/anthropic";
Expand Down Expand Up @@ -36,11 +37,12 @@ export const POST = withWorkspace(async ({ req, workspace }) => {
],
maxTokens: 300,
});

// only count usage for the sonnet model
if (model === "claude-3-5-sonnet-latest") {
waitUntil(
prismaEdge.project.update({
where: { id: workspace.id.replace("ws_", "") },
where: { id: normalizeWorkspaceId(workspace.id) },
data: {
aiUsage: {
increment: 1,
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/callback/bitly/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { normalizeWorkspaceId } from "@/lib/api/workspace-id";
import { redis } from "@/lib/upstash";
import { prisma } from "@dub/prisma";
import { APP_DOMAIN, APP_DOMAIN_WITH_NGROK } from "@dub/utils";
Expand All @@ -18,7 +19,7 @@ export async function GET(req: Request) {
const stateJSON = JSON.parse(state);
let { workspaceId, folderId } = stateJSON;

workspaceId = workspaceId.replace("ws_", "");
workspaceId = normalizeWorkspaceId(workspaceId);

// get access token from bitly
const response = await fetch(
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/callback/plain/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { prefixWorkspaceId } from "@/lib/api/workspace-id";
import { plain } from "@/lib/plain";
import { prisma } from "@dub/prisma";
import { capitalize, formatDate } from "@dub/utils";
Expand Down Expand Up @@ -143,7 +144,7 @@ export async function POST(req: NextRequest) {
components: [
...plainCopySection({
label: "Workspace ID",
value: `ws_${id}`,
value: prefixWorkspaceId(id),
}),
...plainCopySection({
label: "Workspace Name",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/api/links/[linkId]/transfer/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getAnalytics } from "@/lib/analytics/get-analytics";
import { DubApiError } from "@/lib/api/errors";
import { linkCache } from "@/lib/api/links/cache";
import { getLinkOrThrow } from "@/lib/api/links/get-link-or-throw";
import { normalizeWorkspaceId } from "@/lib/api/workspace-id";
import { withWorkspace } from "@/lib/auth";
import { verifyFolderAccess } from "@/lib/folder/permissions";
import { recordLink } from "@/lib/tinybird";
Expand All @@ -14,8 +15,7 @@ const transferLinkBodySchema = z.object({
newWorkspaceId: z
.string()
.min(1, "Missing new workspace ID.")
// replace "ws_" with "" to get the workspace ID
.transform((v) => v.replace("ws_", "")),
.transform((v) => normalizeWorkspaceId(v)),
});

// POST /api/links/[linkId]/transfer – transfer a link to another workspace
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/oauth/userinfo/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DubApiError, handleAndReturnErrorResponse } from "@/lib/api/errors";
import { prefixWorkspaceId } from "@/lib/api/workspace-id";
import { getAuthTokenOrThrow, hashToken } from "@/lib/auth";
import { prisma } from "@dub/prisma";
import { NextRequest, NextResponse } from "next/server";
Expand Down Expand Up @@ -59,7 +60,7 @@ export async function GET(req: NextRequest) {
name: user.name,
image: user.image,
workspace: {
id: `ws_${tokenRecord.project.id}`,
id: prefixWorkspaceId(tokenRecord.project.id),
slug: tokenRecord.project.slug,
name: tokenRecord.project.name,
logo: tokenRecord.project.logo,
Expand Down
9 changes: 2 additions & 7 deletions apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { prefixWorkspaceId } from "@/lib/api/workspace-id";
import { withWorkspace } from "@/lib/auth";
import { tb } from "@/lib/tinybird";
import z from "@/lib/zod";
Expand All @@ -16,13 +17,7 @@ export const GET = withWorkspace(async ({ searchParams, workspace }) => {
workspaceId: z
.string()
.optional()
.transform((v) => {
if (v && !v.startsWith("ws_")) {
return `ws_${v}`;
} else {
return v;
}
}),
.transform((v) => (v ? prefixWorkspaceId(v) : undefined)),
start: z.string(),
end: z.string(),
}),
Expand Down
7 changes: 4 additions & 3 deletions apps/web/app/api/workspaces/[idOrSlug]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DubApiError } from "@/lib/api/errors";
import { parseRequestBody } from "@/lib/api/utils";
import { validateAllowedHostnames } from "@/lib/api/validate-allowed-hostnames";
import { prefixWorkspaceId } from "@/lib/api/workspace-id";
import { deleteWorkspace } from "@/lib/api/workspaces";
import { withWorkspace } from "@/lib/auth";
import { getFeatureFlags } from "@/lib/edge-config";
Expand Down Expand Up @@ -44,7 +45,7 @@ export const GET = withWorkspace(
{
...WorkspaceSchema.parse({
...workspace,
id: `ws_${workspace.id}`,
id: prefixWorkspaceId(workspace.id),
domains,
// TODO: Remove this once Folders goes GA
flags: {
Expand Down Expand Up @@ -81,7 +82,7 @@ export const PATCH = withWorkspace(

const logoUploaded = logo
? await storage.upload(
`workspaces/ws_${workspace.id}/logo_${nanoid(7)}`,
`workspaces/${prefixWorkspaceId(workspace.id)}/logo_${nanoid(7)}`,
logo,
)
: null;
Expand Down Expand Up @@ -128,7 +129,7 @@ export const PATCH = withWorkspace(
return NextResponse.json(
WorkspaceSchema.parse({
...response,
id: `ws_${response.id}`,
id: prefixWorkspaceId(response.id),
flags: await getFeatureFlags({
workspaceId: response.id,
}),
Expand Down
7 changes: 5 additions & 2 deletions apps/web/app/api/workspaces/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DubApiError } from "@/lib/api/errors";
import { createWorkspaceId, prefixWorkspaceId } from "@/lib/api/workspace-id";
import { withSession } from "@/lib/auth";
import { checkIfUserExists } from "@/lib/planetscale";
import {
Expand Down Expand Up @@ -45,11 +46,12 @@ export const GET = withSession(async ({ session }) => {
createdAt: "asc",
},
});

return NextResponse.json(
workspaces.map((project) =>
WorkspaceSchema.parse({
...project,
id: `ws_${project.id}`,
id: prefixWorkspaceId(project.id),
}),
),
);
Expand Down Expand Up @@ -91,6 +93,7 @@ export const POST = withSession(async ({ req, session }) => {
try {
const workspaceResponse = await prisma.project.create({
data: {
id: createWorkspaceId(),
name,
slug,
users: {
Expand Down Expand Up @@ -144,7 +147,7 @@ export const POST = withSession(async ({ req, session }) => {
return NextResponse.json(
WorkspaceSchema.parse({
...workspaceResponse,
id: `ws_${workspaceResponse.id}`,
id: prefixWorkspaceId(workspaceResponse.id),
}),
);
} catch (error) {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/actions/safe-action.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { prisma } from "@dub/prisma";
import { createSafeActionClient } from "next-safe-action";
import { normalizeWorkspaceId } from "../api/workspace-id";
import { getSession } from "../auth";

export const actionClient = createSafeActionClient({
Expand Down Expand Up @@ -43,7 +44,7 @@ export const authActionClient = actionClient.use(
throw new Error("WorkspaceId is required.");
}

workspaceId = workspaceId.replace("ws_", "");
workspaceId = normalizeWorkspaceId(workspaceId);

const workspace = await prisma.project.findUnique({
where: {
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/api/create-id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import baseX from "base-x";
import crypto from "crypto";

const prefixes = [
"ws_",
"user_",
"link_",
"tag_",
Expand Down
5 changes: 3 additions & 2 deletions apps/web/lib/api/domains/get-domain-or-throw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { prisma } from "@dub/prisma";
import { DUB_WORKSPACE_ID, isDubDomain } from "@dub/utils";
import { Project } from "@prisma/client";
import { DubApiError } from "../errors";
import { prefixWorkspaceId } from "../workspace-id";

export const getDomainOrThrow = async ({
workspace,
Expand Down Expand Up @@ -37,13 +38,13 @@ export const getDomainOrThrow = async ({
if (dubDomainChecks && workspace.id !== DUB_WORKSPACE_ID) {
throw new DubApiError({
code: "forbidden",
message: `Domain ${domain} does not belong to workspace ws_${workspace.id}.`,
message: `Domain ${domain} does not belong to workspace ${prefixWorkspaceId(workspace.id)}.`,
});
}
} else if (domainRecord.projectId !== workspace.id) {
throw new DubApiError({
code: "forbidden",
message: `Domain ${domain} does not belong to workspace ws_${workspace.id}.`,
message: `Domain ${domain} does not belong to workspace ${prefixWorkspaceId(workspace.id)}.`,
});
}

Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/api/links/get-link-or-throw.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { prisma } from "@dub/prisma";
import { Link } from "@dub/prisma/client";
import { DubApiError } from "../errors";
import { prefixWorkspaceId } from "../workspace-id";
import {
decodeLinkIfCaseSensitive,
encodeKeyIfCaseSensitive,
Expand Down Expand Up @@ -75,7 +76,7 @@ export const getLinkOrThrow = async (params: GetLinkParams) => {
if (link.projectId !== workspaceId) {
throw new DubApiError({
code: "unauthorized",
message: `Link does not belong to workspace ws_${workspaceId}.`,
message: `Link does not belong to workspace ${prefixWorkspaceId(workspaceId)}.`,
});
}

Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/api/links/utils/transform-link.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dashboard, Link, Tag } from "@dub/prisma/client";
import { prefixWorkspaceId } from "../../workspace-id";
import { decodeLinkIfCaseSensitive } from "../case-sensitivity";

// used in API (e.g. transformLink)
Expand Down Expand Up @@ -31,7 +32,7 @@ export const transformLink = (
tags,
webhookIds,
qrCode: `https://api.dub.co/qr?url=${link.shortLink}?qr=1`,
workspaceId: link.projectId ? `ws_${link.projectId}` : null,
workspaceId: link.projectId ? prefixWorkspaceId(link.projectId) : null,
...(dashboard && { dashboardId: dashboard.id || null }),
};
};
3 changes: 2 additions & 1 deletion apps/web/lib/api/tokens/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Role } from "@dub/prisma/client";
import { combineWords } from "@dub/utils";
import { DubApiError } from "../errors";
import { PermissionAction, ROLE_PERMISSIONS } from "../rbac/permissions";
import { prefixWorkspaceId } from "../workspace-id";

// Check if the required scope is in the list of user scopes
export const throwIfNoAccess = ({
Expand All @@ -28,7 +29,7 @@ export const throwIfNoAccess = ({
}

const message = externalRequest
? `The provided key does not have the required permissions for this endpoint on the workspace 'ws_${workspaceId}'. Having the '${missingPermissions.join(" ")}' permission would allow this request to continue.`
? `The provided key does not have the required permissions for this endpoint on the workspace '${prefixWorkspaceId(workspaceId)}'. Having the '${missingPermissions.join(" ")}' permission would allow this request to continue.`
: "You don't have the necessary permissions to complete this request.";

throw new DubApiError({
Expand Down
21 changes: 21 additions & 0 deletions apps/web/lib/api/workspace-id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createId } from "./create-id";

export const prefixWorkspaceId = (workspaceId: string) => {
return workspaceId.startsWith("ws_") ? workspaceId : `ws_${workspaceId}`;
};

export const normalizeWorkspaceId = (workspaceId: string) => {
return workspaceId.startsWith("ws_c")
? workspaceId.replace("ws_", "")
: workspaceId;
};

export const createWorkspaceId = () => {
const workspaceId = createId({ prefix: "ws_" });

if (workspaceId.toLowerCase().startsWith("ws_c")) {
return createWorkspaceId();
}

return workspaceId;
};
3 changes: 2 additions & 1 deletion apps/web/lib/auth/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "../api/rbac/permissions";
import { throwIfNoAccess } from "../api/tokens/permissions";
import { Scope, mapScopesToPermissions } from "../api/tokens/scopes";
import { normalizeWorkspaceId } from "../api/workspace-id";
import { getFeatureFlags } from "../edge-config";
import { hashToken } from "./hash-token";
import { Session, getSession } from "./utils";
Expand Down Expand Up @@ -131,7 +132,7 @@ export const withWorkspace = (

if (idOrSlug) {
if (idOrSlug.startsWith("ws_")) {
workspaceId = idOrSlug.replace("ws_", "");
workspaceId = normalizeWorkspaceId(idOrSlug);
} else {
workspaceSlug = idOrSlug;
}
Expand Down
5 changes: 2 additions & 3 deletions apps/web/lib/edge-config/get-feature-flags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { get } from "@vercel/edge-config";
import { prefixWorkspaceId } from "../api/workspace-id";
import { BetaFeatures } from "../types";

type BetaFeaturesRecord = Record<BetaFeatures, string[]>;
Expand All @@ -11,9 +12,7 @@ export const getFeatureFlags = async ({
workspaceSlug?: string;
}) => {
if (workspaceId) {
workspaceId = workspaceId.startsWith("ws_")
? workspaceId
: `ws_${workspaceId}`;
workspaceId = prefixWorkspaceId(workspaceId);
}

const workspaceFeatures: Record<BetaFeatures, boolean> = {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/planetscale/get-workspace-via-edge.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { normalizeWorkspaceId } from "../api/workspace-id";
import { WorkspaceProps } from "../types";
import { conn } from "./connection";

export const getWorkspaceViaEdge = async (workspaceId: string) => {
const { rows } =
(await conn.execute<WorkspaceProps>(
"SELECT * FROM Project WHERE id = ? LIMIT 1",
[workspaceId.replace("ws_", "")],
[normalizeWorkspaceId(workspaceId)],
)) || {};

return rows && Array.isArray(rows) && rows.length > 0 ? rows[0] : null;
Expand Down
9 changes: 7 additions & 2 deletions apps/web/lib/swr/use-domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@dub/utils";
import { useMemo } from "react";
import useSWR from "swr";
import { prefixWorkspaceId } from "../api/workspace-id";
import useDefaultDomains from "./use-default-domains";
import useWorkspace from "./use-workspace";

Expand Down Expand Up @@ -62,14 +63,18 @@ export default function useDomains({
const allDomains = useMemo(
() => [
...allWorkspaceDomains,
...(workspaceId === `ws_${DUB_WORKSPACE_ID}` ? [] : DUB_DOMAINS),
...(workspaceId === prefixWorkspaceId(DUB_WORKSPACE_ID)
? []
: DUB_DOMAINS),
],
[allWorkspaceDomains, workspaceId],
);
const allActiveDomains = useMemo(
() => [
...(activeWorkspaceDomains || []),
...(workspaceId === `ws_${DUB_WORKSPACE_ID}` ? [] : activeDefaultDomains),
...(workspaceId === prefixWorkspaceId(DUB_WORKSPACE_ID)
? []
: activeDefaultDomains),
],
[activeWorkspaceDomains, activeDefaultDomains, workspaceId],
);
Expand Down
Loading