feat(posts-api): assegna autore in POST, ownership su PUT/DELETE
This commit is contained in:
@@ -76,3 +76,8 @@ export function listArchiveMonths(db: Database.Database): { month: string; count
|
|||||||
export function listByAuthor(db: Database.Database, authorId: number): Post[] {
|
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[];
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { getDb } from '../../../../lib/db';
|
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';
|
import { parsePostBody } from '../../../../lib/post-body';
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const json = (status: number, body: object) =>
|
const json = (status: number, body: object) =>
|
||||||
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
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);
|
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>;
|
let data: Record<string, unknown>;
|
||||||
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||||||
const parsed = parsePostBody(data);
|
const parsed = parsePostBody(data);
|
||||||
@@ -23,7 +25,11 @@ export const PUT: APIRoute = async ({ params, request }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DELETE: APIRoute = ({ params }) => {
|
export const DELETE: APIRoute = ({ params, locals }) => {
|
||||||
deletePost(getDb(), Number(params.id));
|
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 });
|
return json(200, { ok: true });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ export const prerender = false;
|
|||||||
const json = (status: number, body: object) =>
|
const json = (status: number, body: object) =>
|
||||||
new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } });
|
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>;
|
let data: Record<string, unknown>;
|
||||||
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
try { data = await request.json(); } catch { return json(400, { error: 'Dati non validi.' }); }
|
||||||
const parsed = parsePostBody(data);
|
const parsed = parsePostBody(data);
|
||||||
if ('error' in parsed) return json(400, { error: parsed.error });
|
if ('error' in parsed) return json(400, { error: parsed.error });
|
||||||
try {
|
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 });
|
return json(201, { id, slug: parsed.value.slug });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
|
if (String(err?.message).includes('UNIQUE')) return json(400, { error: 'Slug già esistente.' });
|
||||||
|
|||||||
@@ -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