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

@ -1,19 +1,80 @@
import { secret } from '@/lib/crypto';
import { createToken } from '@/lib/jwt';
import { json, notFound } from '@/lib/response';
import { getSharedWebsite } from '@/queries/prisma';
import z from 'zod';
import { parseRequest } from '@/lib/request';
import { json, notFound, ok, unauthorized } from '@/lib/response';
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 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();
}
const data = { websiteId: website.id };
const token = createToken(data, secret());
if (!(await canUpdateEntity(auth, share.entityId))) {
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 './pixel';
export * from './report';

View file

@ -2,6 +2,7 @@ export * from './link';
export * from './pixel';
export * from './report';
export * from './segment';
export * from './share';
export * from './team';
export * from './teamUser';
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) {
const { search } = filters;
const { getSearchParameters, pagedQuery } = prisma;