mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-13 08:12:40 +10:00
fix: decduck's code review
This commit is contained in:
52
prisma/migrations/20250309234300_news_articles/migration.sql
Normal file
52
prisma/migrations/20250309234300_news_articles/migration.sql
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `news` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "news" DROP CONSTRAINT "news_authorId_fkey";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "news";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Tag" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Article" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"image" TEXT,
|
||||||
|
"publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"authorId" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Article_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_ArticleToTag" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_ArticleToTag_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_ArticleToTag_B_index" ON "_ArticleToTag"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Article" ADD CONSTRAINT "Article_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArticleToTag" ADD CONSTRAINT "_ArticleToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_ArticleToTag" ADD CONSTRAINT "_ArticleToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[name]` on the table `Tag` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name");
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[token]` on the table `APIToken` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "APIToken_token_key" ON "APIToken"("token");
|
||||||
@ -29,7 +29,7 @@ enum APITokenMode {
|
|||||||
|
|
||||||
model APIToken {
|
model APIToken {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
token String @default(uuid())
|
token String @default(uuid()) @unique
|
||||||
mode APITokenMode
|
mode APITokenMode
|
||||||
name String
|
name String
|
||||||
|
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
model News {
|
model Tag {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
title String
|
name String @unique
|
||||||
content String @db.Text
|
|
||||||
excerpt String
|
|
||||||
tags String[]
|
|
||||||
image String?
|
|
||||||
publishedAt DateTime @default(now())
|
|
||||||
author User @relation(fields: [authorId], references: [id])
|
|
||||||
authorId String
|
|
||||||
|
|
||||||
@@map("news")
|
articles Article[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Article {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
title String
|
||||||
|
description String
|
||||||
|
content String @db.Text
|
||||||
|
|
||||||
|
tags Tag[]
|
||||||
|
|
||||||
|
image String? // Object ID
|
||||||
|
publishedAt DateTime @default(now())
|
||||||
|
|
||||||
|
author User? @relation(fields: [authorId], references: [id]) // Optional, if no user, it's a system post
|
||||||
|
authorId String?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
previewFeatures = ["prismaSchemaFolder", "omitApi"]
|
previewFeatures = ["prismaSchemaFolder", "omitApi", "fullTextSearchPostgres"]
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ model User {
|
|||||||
clients Client[]
|
clients Client[]
|
||||||
notifications Notification[]
|
notifications Notification[]
|
||||||
collections Collection[]
|
collections Collection[]
|
||||||
news News[]
|
articles Article[]
|
||||||
|
|
||||||
tokens APIToken[]
|
tokens APIToken[]
|
||||||
}
|
}
|
||||||
|
|||||||
27
server/api/v1/admin/news/[id]/index.get.ts
Normal file
27
server/api/v1/admin/news/[id]/index.get.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { defineEventHandler, createError } from "h3";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import newsManager from "~/server/internal/news";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, ["news:read"]);
|
||||||
|
if (!allowed)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = h3.context.params?.id;
|
||||||
|
if (!id)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Missing news ID",
|
||||||
|
});
|
||||||
|
|
||||||
|
const news = await newsManager.fetchById(id);
|
||||||
|
if (!news)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: "News article not found",
|
||||||
|
});
|
||||||
|
|
||||||
|
return news;
|
||||||
|
});
|
||||||
36
server/api/v1/admin/news/index.get.ts
Normal file
36
server/api/v1/admin/news/index.get.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { defineEventHandler, getQuery } from "h3";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
|
import newsManager from "~/server/internal/news";
|
||||||
|
|
||||||
|
export default defineEventHandler(async (h3) => {
|
||||||
|
const allowed = await aclManager.allowSystemACL(h3, ["news:read"]);
|
||||||
|
if (!allowed)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = getQuery(h3);
|
||||||
|
|
||||||
|
const orderBy = query.order as "asc" | "desc";
|
||||||
|
if (orderBy) {
|
||||||
|
if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy))
|
||||||
|
throw createError({ statusCode: 400, statusMessage: "Invalid order" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = query.tags as string[] | undefined;
|
||||||
|
if (tags) {
|
||||||
|
if (typeof tags !== "object" || !Array.isArray(tags))
|
||||||
|
throw createError({ statusCode: 400, statusMessage: "Invalid tags" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
take: parseInt(query.limit as string),
|
||||||
|
skip: parseInt(query.skip as string),
|
||||||
|
orderBy: orderBy,
|
||||||
|
tags: tags?.map((e) => e.toString()),
|
||||||
|
search: query.search as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const news = await newsManager.fetch(options);
|
||||||
|
return news;
|
||||||
|
});
|
||||||
@ -1,21 +1,20 @@
|
|||||||
import { defineEventHandler, createError, readBody } from "h3";
|
import { defineEventHandler, createError, readBody } from "h3";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
import newsManager from "~/server/internal/news";
|
import newsManager from "~/server/internal/news";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const body = await readBody(event);
|
const allowed = await aclManager.allowSystemACL(h3, ["news:create"]);
|
||||||
|
if (!allowed) throw createError({ statusCode: 403 });
|
||||||
|
|
||||||
if (!body.authorId) {
|
const body = await readBody(h3);
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
message: 'Author ID is required'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const article = await newsManager.create({
|
const article = await newsManager.create({
|
||||||
title: body.title,
|
title: body.title,
|
||||||
|
description: body.description,
|
||||||
content: body.content,
|
content: body.content,
|
||||||
excerpt: body.excerpt,
|
|
||||||
tags: body.tags,
|
tags: body.tags,
|
||||||
|
|
||||||
image: body.image,
|
image: body.image,
|
||||||
authorId: body.authorId,
|
authorId: body.authorId,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export default defineEventHandler(async (h3) => {
|
|||||||
profilePictureId,
|
profilePictureId,
|
||||||
async () => jdenticon.toPng(username, 256),
|
async () => jdenticon.toPng(username, 256),
|
||||||
{},
|
{},
|
||||||
[`anonymous:read`, `${userId}:write`]
|
[`internal:read`, `${userId}:write`]
|
||||||
);
|
);
|
||||||
const user = await prisma.user.create({
|
const user = await prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -1,22 +1,30 @@
|
|||||||
import { defineEventHandler, createError } from "h3";
|
import { defineEventHandler, createError } from "h3";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
import newsManager from "~/server/internal/news";
|
import newsManager from "~/server/internal/news";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const id = event.context.params?.id;
|
const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
|
||||||
if (!id) {
|
if (!userId)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: "Requires authentication",
|
||||||
|
});
|
||||||
|
|
||||||
|
const id = h3.context.params?.id;
|
||||||
|
if (!id)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: "Missing news ID",
|
message: "Missing news ID",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const news = await newsManager.getById(id);
|
|
||||||
if (!news) {
|
const news = await newsManager.fetchById(id);
|
||||||
|
if (!news)
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 404,
|
statusCode: 404,
|
||||||
message: "News article not found",
|
message: "News article not found",
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return news;
|
return news;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,17 +1,37 @@
|
|||||||
import { defineEventHandler, getQuery } from "h3";
|
import { defineEventHandler, getQuery } from "h3";
|
||||||
|
import aclManager from "~/server/internal/acls";
|
||||||
import newsManager from "~/server/internal/news";
|
import newsManager from "~/server/internal/news";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const query = getQuery(event);
|
const userId = await aclManager.getUserIdACL(h3, ["news:read"]);
|
||||||
|
if (!userId)
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
statusMessage: "Requires authentication",
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = getQuery(h3);
|
||||||
|
|
||||||
|
const orderBy = query.order as "asc" | "desc";
|
||||||
|
if (orderBy) {
|
||||||
|
if (typeof orderBy !== "string" || !["asc", "desc"].includes(orderBy))
|
||||||
|
throw createError({ statusCode: 400, statusMessage: "Invalid order" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = query.tags as string[] | undefined;
|
||||||
|
if (tags) {
|
||||||
|
if (typeof tags !== "object" || !Array.isArray(tags))
|
||||||
|
throw createError({ statusCode: 400, statusMessage: "Invalid tags" });
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
take: query.limit ? parseInt(query.limit as string) : undefined,
|
take: parseInt(query.limit as string),
|
||||||
skip: query.skip ? parseInt(query.skip as string) : undefined,
|
skip: parseInt(query.skip as string),
|
||||||
orderBy: query.order as 'asc' | 'desc',
|
orderBy: orderBy,
|
||||||
tags: query.tags ? (query.tags as string).split(',') : undefined,
|
tags: tags?.map((e) => e.toString()),
|
||||||
search: query.search as string,
|
search: query.search as string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const news = await newsManager.getAll(options);
|
const news = await newsManager.fetch(options);
|
||||||
return news;
|
return news;
|
||||||
});
|
});
|
||||||
|
|||||||
@ -30,6 +30,8 @@ export const userACLDescriptions: ObjectFromList<typeof userACLs> = {
|
|||||||
"Remove a game from any collection (excluding library).",
|
"Remove a game from any collection (excluding library).",
|
||||||
"library:add": "Add a game to your library.",
|
"library:add": "Add a game to your library.",
|
||||||
"library:remove": "Remove a game from your library.",
|
"library:remove": "Remove a game from your library.",
|
||||||
|
|
||||||
|
"news:read": "Read the server's news articles.",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||||
@ -55,4 +57,8 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
|||||||
"import:game:new": "Import a game.",
|
"import:game:new": "Import a game.",
|
||||||
|
|
||||||
"user:read": "Fetch any user's information.",
|
"user:read": "Fetch any user's information.",
|
||||||
|
|
||||||
|
"news:read": "Read news articles.",
|
||||||
|
"news:create": "Create a new news article.",
|
||||||
|
"news:delete": "Delete a news article."
|
||||||
};
|
};
|
||||||
|
|||||||
@ -53,6 +53,10 @@ export const systemACLs = [
|
|||||||
"import:game:new",
|
"import:game:new",
|
||||||
|
|
||||||
"user:read",
|
"user:read",
|
||||||
|
|
||||||
|
"news:read",
|
||||||
|
"news:create",
|
||||||
|
"news:delete",
|
||||||
] as const;
|
] as const;
|
||||||
const systemACLPrefix = "system:";
|
const systemACLPrefix = "system:";
|
||||||
|
|
||||||
|
|||||||
@ -1,70 +1,72 @@
|
|||||||
|
import { triggerAsyncId } from "async_hooks";
|
||||||
import prisma from "../db/database";
|
import prisma from "../db/database";
|
||||||
|
|
||||||
class NewsManager {
|
class NewsManager {
|
||||||
async create(data: {
|
async create(data: {
|
||||||
title: string;
|
title: string;
|
||||||
content: string;
|
content: string;
|
||||||
excerpt: string;
|
description: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
authorId: string;
|
authorId: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
}) {
|
}) {
|
||||||
return await prisma.news.create({
|
return await prisma.article.create({
|
||||||
data: {
|
data: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
content: data.content,
|
content: data.content,
|
||||||
excerpt: data.excerpt,
|
|
||||||
tags: data.tags,
|
tags: {
|
||||||
|
connectOrCreate: data.tags.map((e) => ({
|
||||||
|
where: { name: e },
|
||||||
|
create: { name: e },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
|
||||||
image: data.image,
|
image: data.image,
|
||||||
author: {
|
author: {
|
||||||
connect: {
|
connect: {
|
||||||
id: data.authorId,
|
id: data.authorId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
publishedAt: new Date(),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAll(options?: {
|
async fetch(
|
||||||
take?: number;
|
options: {
|
||||||
skip?: number;
|
take?: number;
|
||||||
orderBy?: 'asc' | 'desc';
|
skip?: number;
|
||||||
tags?: string[];
|
orderBy?: "asc" | "desc";
|
||||||
search?: string;
|
tags?: string[];
|
||||||
}) {
|
search?: string;
|
||||||
const where = {
|
} = {}
|
||||||
AND: [
|
) {
|
||||||
options?.tags?.length ? {
|
return await prisma.article.findMany({
|
||||||
tags: {
|
where: {
|
||||||
hasSome: options.tags,
|
AND: [
|
||||||
|
{
|
||||||
|
tags: {
|
||||||
|
some: { OR: options.tags?.map((e) => ({ name: e })) ?? [] },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} : {},
|
{
|
||||||
options?.search ? {
|
title: {
|
||||||
OR: [
|
search: options.search
|
||||||
{
|
|
||||||
title: {
|
|
||||||
contains: options.search,
|
|
||||||
mode: 'insensitive' as const,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
description: {
|
||||||
content: {
|
search: options.search
|
||||||
contains: options.search,
|
|
||||||
mode: 'insensitive' as const,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
content: {
|
||||||
} : {},
|
search: options.search
|
||||||
],
|
}
|
||||||
};
|
}
|
||||||
|
],
|
||||||
return await prisma.news.findMany({
|
},
|
||||||
where,
|
take: options?.take || 10,
|
||||||
take: options?.take,
|
skip: options?.skip || 0,
|
||||||
skip: options?.skip,
|
|
||||||
orderBy: {
|
orderBy: {
|
||||||
publishedAt: options?.orderBy || 'desc',
|
publishedAt: options?.orderBy || "desc",
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
author: {
|
author: {
|
||||||
@ -77,8 +79,8 @@ class NewsManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getById(id: string) {
|
async fetchById(id: string) {
|
||||||
return await prisma.news.findUnique({
|
return await prisma.article.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
author: {
|
author: {
|
||||||
@ -91,21 +93,23 @@ class NewsManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: string, data: {
|
async update(
|
||||||
title?: string;
|
id: string,
|
||||||
content?: string;
|
data: {
|
||||||
excerpt?: string;
|
title?: string;
|
||||||
tags?: string[];
|
content?: string;
|
||||||
image?: string;
|
excerpt?: string;
|
||||||
}) {
|
image?: string;
|
||||||
return await prisma.news.update({
|
}
|
||||||
|
) {
|
||||||
|
return await prisma.article.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
return await prisma.news.delete({
|
return await prisma.article.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user