registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
# Review package: 6d1e260..c3267c9
|
||||
|
||||
## Commits
|
||||
c3267c9 feat(posts-api): assegna autore in POST, ownership su PUT/DELETE
|
||||
|
||||
## Files changed
|
||||
src/lib/posts.ts | 5 +++++
|
||||
src/pages/api/admin/posts/[id].ts | 16 +++++++++++-----
|
||||
src/pages/api/admin/posts/index.ts | 4 ++--
|
||||
tests/posts-ownership.test.ts | 21 +++++++++++++++++++++
|
||||
4 files changed, 39 insertions(+), 7 deletions(-)
|
||||
|
||||
## Diff
|
||||
diff --git a/src/lib/posts.ts b/src/lib/posts.ts
|
||||
index 1c8e44a..2b346dd 100644
|
||||
--- a/src/lib/posts.ts
|
||||
+++ b/src/lib/posts.ts
|
||||
@@ -69,10 +69,15 @@ export function listRecent(db: Database.Database, limit = 4): Post[] {
|
||||
export function listArchiveMonths(db: Database.Database): { month: string; count: number }[] {
|
||||
return db.prepare(
|
||||
`SELECT strftime('%Y-%m', published_at) AS month, COUNT(*) AS count
|
||||
FROM posts WHERE draft = 0 GROUP BY month ORDER BY month DESC`
|
||||
).all() as { month: string; count: number }[];
|
||||
}
|
||||
|
||||
export function listByAuthor(db: Database.Database, authorId: number): Post[] {
|
||||
return db.prepare('SELECT * FROM posts WHERE author_id = ? ORDER BY updated_at DESC').all(authorId) as Post[];
|
||||
}
|
||||
+
|
||||
+export function canModifyPost(role: string, userId: number, post: { author_id: number | null }): boolean {
|
||||
+ if (role === 'admin' || role === 'superuser') return true;
|
||||
+ return post.author_id === userId;
|
||||
+}
|
||||
diff --git a/src/pages/api/admin/posts/[id].ts b/src/pages/api/admin/posts/[id].ts
|
||||
index c7e8a33..8c7ef21 100644
|
||||
--- a/src/pages/api/admin/posts/[id].ts
|
||||
+++ b/src/pages/api/admin/posts/[id].ts
|
||||
@@ -1,29 +1,35 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getDb } from '../../../../lib/db';
|
||||
-import { getPostById, updatePost, deletePost } from '../../../../lib/posts';
|
||||
+import { getPostById, updatePost, deletePost, canModifyPost } from '../../../../lib/posts';
|
||||
import { parsePostBody } from '../../../../lib/post-body';
|
||||
export const prerender = false;
|
||||
|
||||
const json = (status: number, body: object) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
-export const PUT: APIRoute = async ({ params, request }) => {
|
||||
+export const PUT: APIRoute = async ({ params, request, locals }) => {
|
||||
const id = Number(params.id);
|
||||
- if (!getPostById(getDb(), id)) return json(404, { error: 'Articolo non trovato.' });
|
||||
+ const existing = getPostById(getDb(), id);
|
||||
+ if (!existing) return json(404, { error: 'Articolo non trovato.' });
|
||||
+ if (!canModifyPost(locals.user!.role, locals.user!.id, existing)) return json(403, { error: 'Non puoi modificare questo articolo.' });
|
||||
let data: Record<string, unknown>;
|
||||
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||||
const parsed = parsePostBody(data);
|
||||
if ('error' in parsed) return json(400, { error: parsed.error });
|
||||
try {
|
||||
updatePost(getDb(), id, parsed.value);
|
||||
return json(200, { ok: true });
|
||||
} catch (err: any) {
|
||||
if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
-export const DELETE: APIRoute = ({ params }) => {
|
||||
- deletePost(getDb(), Number(params.id));
|
||||
+export const DELETE: APIRoute = ({ params, locals }) => {
|
||||
+ const id = Number(params.id);
|
||||
+ const existing = getPostById(getDb(), id);
|
||||
+ if (!existing) return json(404, { error: 'Articolo non trovato.' });
|
||||
+ if (!canModifyPost(locals.user!.role, locals.user!.id, existing)) return json(403, { error: 'Non puoi eliminare questo articolo.' });
|
||||
+ deletePost(getDb(), id);
|
||||
return json(200, { ok: true });
|
||||
};
|
||||
diff --git a/src/pages/api/admin/posts/index.ts b/src/pages/api/admin/posts/index.ts
|
||||
index 16305d7..6d95c5d 100644
|
||||
--- a/src/pages/api/admin/posts/index.ts
|
||||
+++ b/src/pages/api/admin/posts/index.ts
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { getDb } from '../../../../lib/db';
|
||||
import { createPost } from '../../../../lib/posts';
|
||||
import { parsePostBody } from '../../../../lib/post-body';
|
||||
export const prerender = false;
|
||||
|
||||
const json = (status: number, body: object) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
-export const POST: APIRoute = async ({ request }) => {
|
||||
+export const POST: APIRoute = async ({ request, locals }) => {
|
||||
let data: Record<string, unknown>;
|
||||
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||||
const parsed = parsePostBody(data);
|
||||
if ('error' in parsed) return json(400, { error: parsed.error });
|
||||
try {
|
||||
- const id = createPost(getDb(), parsed.value);
|
||||
+ const id = createPost(getDb(), { ...parsed.value, author_id: locals.user!.id });
|
||||
return json(201, { id, slug: parsed.value.slug });
|
||||
} catch (err: any) {
|
||||
if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
diff --git a/tests/posts-ownership.test.ts b/tests/posts-ownership.test.ts
|
||||
new file mode 100644
|
||||
index 0000000..88f4a40
|
||||
--- /dev/null
|
||||
+++ b/tests/posts-ownership.test.ts
|
||||
@@ -0,0 +1,21 @@
|
||||
+import { describe, it, expect } from 'vitest';
|
||||
+import { canModifyPost } from '../src/lib/posts';
|
||||
+
|
||||
+describe('canModifyPost', () => {
|
||||
+ const own = { author_id: 7 };
|
||||
+ const other = { author_id: 8 };
|
||||
+ const orphan = { author_id: null };
|
||||
+
|
||||
+ it('admin e superuser modificano qualsiasi post', () => {
|
||||
+ for (const role of ['admin', 'superuser']) {
|
||||
+ expect(canModifyPost(role, 7, other)).toBe(true);
|
||||
+ expect(canModifyPost(role, 7, orphan)).toBe(true);
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ it('user modifica solo i propri', () => {
|
||||
+ expect(canModifyPost('user', 7, own)).toBe(true);
|
||||
+ expect(canModifyPost('user', 7, other)).toBe(false);
|
||||
+ expect(canModifyPost('user', 7, orphan)).toBe(false);
|
||||
+ });
|
||||
+});
|
||||
Reference in New Issue
Block a user