share api, queries, permissions, migration, entity lib
Some checks are pending
Node.js CI / build (push) Waiting to run

This commit is contained in:
Francis Cao 2026-01-15 16:25:56 -08:00
parent a270b0afea
commit 29f2c7b7d4
11 changed files with 256 additions and 23 deletions

View file

@ -3,7 +3,7 @@ CREATE TABLE "share" (
"share_id" UUID NOT NULL, "share_id" UUID NOT NULL,
"entity_id" UUID NOT NULL, "entity_id" UUID NOT NULL,
"share_type" INTEGER NOT NULL, "share_type" INTEGER NOT NULL,
"share_code" VARCHAR(50), "slug" VARCHAR(100) NOT NULL,
"parameters" JSONB NOT NULL, "parameters" JSONB NOT NULL,
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ(6), "updated_at" TIMESTAMPTZ(6),
@ -15,13 +15,13 @@ CREATE TABLE "share" (
CREATE UNIQUE INDEX "share_share_id_key" ON "share"("share_id"); CREATE UNIQUE INDEX "share_share_id_key" ON "share"("share_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "share_share_code_key" ON "share"("share_code"); CREATE UNIQUE INDEX "share_slug_key" ON "share"("slug");
-- CreateIndex -- CreateIndex
CREATE INDEX "share_entity_id_idx" ON "share"("entity_id"); CREATE INDEX "share_entity_id_idx" ON "share"("entity_id");
-- MigrateData -- MigrateData
INSERT INTO "share" (share_id, entity_id, share_type, share_code, parameters, created_at) INSERT INTO "share" (share_id, entity_id, share_type, slug, parameters, created_at)
SELECT gen_random_uuid(), SELECT gen_random_uuid(),
website_id, website_id,
1, 1,

View file

@ -319,7 +319,7 @@ model Share {
id String @id() @unique() @map("share_id") @db.Uuid id String @id() @unique() @map("share_id") @db.Uuid
entityId String @map("entity_id") @db.Uuid entityId String @map("entity_id") @db.Uuid
shareType Int @map("share_type") @db.Integer shareType Int @map("share_type") @db.Integer
shareCode String? @unique @map("share_code") @db.VarChar(50) slug String @unique() @db.VarChar(100)
parameters Json parameters Json
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6) updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)

View file

@ -1,19 +1,80 @@
import { secret } from '@/lib/crypto'; import z from 'zod';
import { createToken } from '@/lib/jwt'; import { parseRequest } from '@/lib/request';
import { json, notFound } from '@/lib/response'; import { json, notFound, ok, unauthorized } from '@/lib/response';
import { getSharedWebsite } from '@/queries/prisma'; import { anyObjectParam } from '@/lib/schema';
import { canDeleteEntity, canUpdateEntity, canViewEntity } from '@/permissions';
import { deleteShare, getShare, updateShare } from '@/queries/prisma';
export async function GET(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
const { auth, error } = await parseRequest(request);
if (error) {
return error();
}
export async function GET(_request: Request, { params }: { params: Promise<{ shareId: string }> }) {
const { shareId } = await params; const { shareId } = await params;
const website = await getSharedWebsite(shareId); const share = await getShare(shareId);
if (!website) { if (!(await canViewEntity(auth, share.entityId))) {
return unauthorized();
}
return json(share);
}
export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
const schema = z.object({
slug: z.string().max(100),
parameters: anyObjectParam,
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { shareId } = await params;
const { slug, parameters } = body;
const share = await getShare(shareId);
if (!share) {
return notFound(); return notFound();
} }
const data = { websiteId: website.id }; if (!(await canUpdateEntity(auth, share.entityId))) {
const token = createToken(data, secret()); return unauthorized();
}
return json({ ...data, token }); const result = await updateShare(shareId, {
slug,
parameters,
} as any);
return json(result);
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ shareId: string }> },
) {
const { auth, error } = await parseRequest(request);
if (error) {
return error();
}
const { shareId } = await params;
const share = await getShare(shareId);
if (!(await canDeleteEntity(auth, share.entityId))) {
return unauthorized();
}
await deleteShare(shareId);
return ok();
} }

View file

@ -0,0 +1,19 @@
import { secret } from '@/lib/crypto';
import { createToken } from '@/lib/jwt';
import { json, notFound } from '@/lib/response';
import { getShareByCode } from '@/queries/prisma';
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const share = await getShareByCode(slug);
if (!share) {
return notFound();
}
const data = { shareId: share.id };
const token = createToken(data, secret());
return json({ ...data, token });
}

View file

@ -0,0 +1,38 @@
import z from 'zod';
import { uuid } from '@/lib/crypto';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { anyObjectParam } from '@/lib/schema';
import { canUpdateEntity } from '@/permissions';
import { createShare } from '@/queries/prisma';
export async function POST(request: Request) {
const schema = z.object({
entityId: z.uuid(),
shareType: z.coerce.number().int(),
slug: z.string().max(100),
parameters: anyObjectParam,
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { entityId, shareType, slug, parameters } = body;
if (!(await canUpdateEntity(auth, entityId))) {
return unauthorized();
}
const share = await createShare({
id: uuid(),
entityId,
shareType,
slug,
parameters,
});
return json(share);
}

11
src/lib/entity.ts Normal file
View file

@ -0,0 +1,11 @@
import { getLink, getPixel, getWebsite } from '@/queries/prisma';
export async function getEntity(entityId: string) {
const website = await getWebsite(entityId);
const link = await getLink(entityId);
const pixel = await getPixel(entityId);
const entity = website || link || pixel;
return entity;
}

65
src/permissions/entity.ts Normal file
View file

@ -0,0 +1,65 @@
import { hasPermission } from '@/lib/auth';
import { PERMISSIONS } from '@/lib/constants';
import { getEntity } from '@/lib/entity';
import type { Auth } from '@/lib/types';
import { getTeamUser } from '@/queries/prisma';
export async function canViewEntity({ user }: Auth, entityId: string) {
if (user?.isAdmin) {
return true;
}
const entity = await getEntity(entityId);
if (entity.userId) {
return user.id === entity.userId;
}
if (entity.teamId) {
const teamUser = await getTeamUser(entity.teamId, user.id);
return !!teamUser;
}
return false;
}
export async function canUpdateEntity({ user }: Auth, entityId: string) {
if (user.isAdmin) {
return true;
}
const entity = await getEntity(entityId);
if (entity.userId) {
return user.id === entity.userId;
}
if (entity.teamId) {
const teamUser = await getTeamUser(entity.teamId, user.id);
return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteUpdate);
}
return false;
}
export async function canDeleteEntity({ user }: Auth, entityId: string) {
if (user.isAdmin) {
return true;
}
const entity = await getEntity(entityId);
if (entity.userId) {
return user.id === entity.userId;
}
if (entity.teamId) {
const teamUser = await getTeamUser(entity.teamId, user.id);
return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteDelete);
}
return false;
}

View file

@ -1,3 +1,4 @@
export * from './entity';
export * from './link'; export * from './link';
export * from './pixel'; export * from './pixel';
export * from './report'; export * from './report';

View file

@ -2,6 +2,7 @@ export * from './link';
export * from './pixel'; export * from './pixel';
export * from './report'; export * from './report';
export * from './segment'; export * from './segment';
export * from './share';
export * from './team'; export * from './team';
export * from './teamUser'; export * from './teamUser';
export * from './user'; export * from './user';

View file

@ -0,0 +1,46 @@
import type { Prisma } from '@/generated/prisma/client';
import prisma from '@/lib/prisma';
export async function findShare(criteria: Prisma.ShareFindUniqueArgs) {
return prisma.client.share.findUnique(criteria);
}
export async function getShare(entityId: string) {
return findShare({
where: {
id: entityId,
},
});
}
export async function getShareByCode(slug: string) {
return findShare({
where: {
slug,
},
});
}
export async function createShare(
data: Prisma.ShareCreateInput | Prisma.ShareUncheckedCreateInput,
) {
return prisma.client.share.create({
data,
});
}
export async function updateShare(
shareId: string,
data: Prisma.ShareUpdateInput | Prisma.ShareUncheckedUpdateInput,
) {
return prisma.client.share.update({
where: {
id: shareId,
},
data,
});
}
export async function deleteShare(shareId: string) {
return prisma.client.share.delete({ where: { id: shareId } });
}

View file

@ -16,15 +16,6 @@ export async function getWebsite(websiteId: string) {
}); });
} }
export async function getSharedWebsite(shareId: string) {
return findWebsite({
where: {
shareId,
deletedAt: null,
},
});
}
export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: QueryFilters) { export async function getWebsites(criteria: Prisma.WebsiteFindManyArgs, filters: QueryFilters) {
const { search } = filters; const { search } = filters;
const { getSearchParameters, pagedQuery } = prisma; const { getSearchParameters, pagedQuery } = prisma;