2026-06-28 13:52:54 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Project Shares Routes – List, create, delete project shares
|
|
|
|
|
|
*/
|
|
|
|
|
|
import type { FastifyInstance } from 'fastify';
|
|
|
|
|
|
import type { DatabaseInterface } from '../database/DatabaseInterface.js';
|
|
|
|
|
|
import type { AuthService } from '../auth/AuthService.js';
|
|
|
|
|
|
|
|
|
|
|
|
function extractToken(request: any): string | null {
|
|
|
|
|
|
const auth = request.headers?.authorization;
|
|
|
|
|
|
if (auth && auth.startsWith('Bearer ')) return auth.slice(7);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-28 14:16:50 +02:00
|
|
|
|
const VALID_PERMISSIONS = ['view', 'edit', 'admin'];
|
|
|
|
|
|
|
2026-06-28 13:52:54 +02:00
|
|
|
|
export function registerShareRoutes(fastify: FastifyInstance, db: DatabaseInterface, authService: AuthService) {
|
2026-06-28 14:16:50 +02:00
|
|
|
|
// List shares for a project (owner only)
|
2026-06-28 13:52:54 +02:00
|
|
|
|
fastify.get('/api/projects/:projectId/shares', async (request, reply) => {
|
|
|
|
|
|
const token = extractToken(request);
|
|
|
|
|
|
if (!token) return reply.code(401).send({ error: 'Authentication required' });
|
|
|
|
|
|
const user = authService.getUserFromSession(token);
|
|
|
|
|
|
if (!user) return reply.code(401).send({ error: 'Invalid or expired session' });
|
|
|
|
|
|
const { projectId } = request.params as { projectId: string };
|
2026-06-28 14:16:50 +02:00
|
|
|
|
const project = db.getProject(projectId);
|
|
|
|
|
|
if (!project) return reply.code(404).send({ error: 'Project not found' });
|
|
|
|
|
|
if (project.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
2026-06-28 13:52:54 +02:00
|
|
|
|
return db.listProjectShares(projectId);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-28 14:16:50 +02:00
|
|
|
|
// Create a project share (owner only)
|
2026-06-28 13:52:54 +02:00
|
|
|
|
fastify.post('/api/projects/:projectId/shares', async (request, reply) => {
|
|
|
|
|
|
const token = extractToken(request);
|
|
|
|
|
|
if (!token) return reply.code(401).send({ error: 'Authentication required' });
|
|
|
|
|
|
const user = authService.getUserFromSession(token);
|
|
|
|
|
|
if (!user) return reply.code(401).send({ error: 'Invalid or expired session' });
|
|
|
|
|
|
const { projectId } = request.params as { projectId: string };
|
2026-06-28 14:16:50 +02:00
|
|
|
|
const project = db.getProject(projectId);
|
|
|
|
|
|
if (!project) return reply.code(404).send({ error: 'Project not found' });
|
|
|
|
|
|
if (project.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
2026-06-28 13:52:54 +02:00
|
|
|
|
const body = request.body as { shared_with_email?: string; permission?: string };
|
|
|
|
|
|
if (!body.shared_with_email) return reply.code(400).send({ error: 'shared_with_email is required' });
|
2026-06-28 14:16:50 +02:00
|
|
|
|
const permission = VALID_PERMISSIONS.includes(body.permission ?? '') ? body.permission! : 'view';
|
2026-06-28 13:52:54 +02:00
|
|
|
|
|
|
|
|
|
|
const share = db.createProjectShare({
|
|
|
|
|
|
project_id: projectId,
|
|
|
|
|
|
shared_with_email: body.shared_with_email,
|
|
|
|
|
|
shared_by: user.id,
|
2026-06-28 14:16:50 +02:00
|
|
|
|
permission,
|
2026-06-28 13:52:54 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Create a notification for the shared user if they exist
|
|
|
|
|
|
const sharedUser = db.getUserByEmail(body.shared_with_email);
|
|
|
|
|
|
if (sharedUser) {
|
|
|
|
|
|
db.createNotification({
|
|
|
|
|
|
user_id: sharedUser.id,
|
|
|
|
|
|
type: 'share',
|
|
|
|
|
|
title: 'Projekt geteilt',
|
2026-06-28 14:16:50 +02:00
|
|
|
|
message: `${user.name} hat ein Projekt mit Ihnen geteilt: ${project.name}`,
|
2026-06-28 13:52:54 +02:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return reply.code(201).send(share);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-28 14:16:50 +02:00
|
|
|
|
// Delete a project share (owner only)
|
2026-06-28 13:52:54 +02:00
|
|
|
|
fastify.delete('/api/shares/:id', async (request, reply) => {
|
|
|
|
|
|
const token = extractToken(request);
|
|
|
|
|
|
if (!token) return reply.code(401).send({ error: 'Authentication required' });
|
|
|
|
|
|
const user = authService.getUserFromSession(token);
|
|
|
|
|
|
if (!user) return reply.code(401).send({ error: 'Invalid or expired session' });
|
|
|
|
|
|
const { id } = request.params as { id: string };
|
2026-06-28 14:16:50 +02:00
|
|
|
|
const share = db.getProjectShare(id);
|
|
|
|
|
|
if (!share) return reply.code(404).send({ error: 'Share not found' });
|
|
|
|
|
|
const project = db.getProject(share.project_id);
|
|
|
|
|
|
if (!project) return reply.code(404).send({ error: 'Project not found' });
|
|
|
|
|
|
if (project.owner_id !== user.id) return reply.code(403).send({ error: 'Forbidden' });
|
2026-06-28 13:52:54 +02:00
|
|
|
|
const ok = db.deleteProjectShare(id);
|
|
|
|
|
|
if (!ok) return reply.code(404).send({ error: 'Share not found' });
|
|
|
|
|
|
return reply.code(204).send();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|