mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
share api, queries, permissions, migration, entity lib
Some checks are pending
Node.js CI / build (push) Waiting to run
Some checks are pending
Node.js CI / build (push) Waiting to run
This commit is contained in:
parent
a270b0afea
commit
29f2c7b7d4
11 changed files with 256 additions and 23 deletions
|
|
@ -3,7 +3,7 @@ CREATE TABLE "share" (
|
|||
"share_id" UUID NOT NULL,
|
||||
"entity_id" UUID NOT NULL,
|
||||
"share_type" INTEGER NOT NULL,
|
||||
"share_code" VARCHAR(50),
|
||||
"slug" VARCHAR(100) NOT NULL,
|
||||
"parameters" JSONB NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6),
|
||||
|
|
@ -15,13 +15,13 @@ CREATE TABLE "share" (
|
|||
CREATE UNIQUE INDEX "share_share_id_key" ON "share"("share_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "share_share_code_key" ON "share"("share_code");
|
||||
CREATE UNIQUE INDEX "share_slug_key" ON "share"("slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "share_entity_id_idx" ON "share"("entity_id");
|
||||
|
||||
-- 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(),
|
||||
website_id,
|
||||
1,
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ model Share {
|
|||
id String @id() @unique() @map("share_id") @db.Uuid
|
||||
entityId String @map("entity_id") @db.Uuid
|
||||
shareType Int @map("share_type") @db.Integer
|
||||
shareCode String? @unique @map("share_code") @db.VarChar(50)
|
||||
slug String @unique() @db.VarChar(100)
|
||||
parameters Json
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
||||
return json({ ...data, token });
|
||||
if (!(await canUpdateEntity(auth, share.entityId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
19
src/app/api/share/[slug]/route.ts
Normal file
19
src/app/api/share/[slug]/route.ts
Normal 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 });
|
||||
}
|
||||
38
src/app/api/share/route.ts
Normal file
38
src/app/api/share/route.ts
Normal 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
11
src/lib/entity.ts
Normal 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
65
src/permissions/entity.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
export * from './entity';
|
||||
export * from './link';
|
||||
export * from './pixel';
|
||||
export * from './report';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
46
src/queries/prisma/share.ts
Normal file
46
src/queries/prisma/share.ts
Normal 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 } });
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue