Files
InsanityLab-web/.superpowers/sdd/review-6d1e260..c3267c9.diff
T
Adriano b913a2bf90 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>
2026-10-06 10:36:26 +02:00

135 lines
5.7 KiB
Diff

# 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);
+ });
+});