add vikunja integration
This commit is contained in:
@@ -82,6 +82,7 @@ import eventsRoutes from './routes/events.routes';
|
||||
import bookingRoutes from './routes/booking.routes';
|
||||
import notificationRoutes from './routes/notification.routes';
|
||||
import bookstackRoutes from './routes/bookstack.routes';
|
||||
import vikunjaRoutes from './routes/vikunja.routes';
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/user', userRoutes);
|
||||
@@ -97,6 +98,7 @@ app.use('/api/events', eventsRoutes);
|
||||
app.use('/api/bookings', bookingRoutes);
|
||||
app.use('/api/notifications', notificationRoutes);
|
||||
app.use('/api/bookstack', bookstackRoutes);
|
||||
app.use('/api/vikunja', vikunjaRoutes);
|
||||
|
||||
// 404 handler
|
||||
app.use(notFoundHandler);
|
||||
|
||||
@@ -38,6 +38,10 @@ interface EnvironmentConfig {
|
||||
tokenId: string;
|
||||
tokenSecret: string;
|
||||
};
|
||||
vikunja: {
|
||||
url: string;
|
||||
apiToken: string;
|
||||
};
|
||||
}
|
||||
|
||||
const environment: EnvironmentConfig = {
|
||||
@@ -73,6 +77,10 @@ const environment: EnvironmentConfig = {
|
||||
tokenId: process.env.BOOKSTACK_TOKEN_ID || '',
|
||||
tokenSecret: process.env.BOOKSTACK_TOKEN_SECRET || '',
|
||||
},
|
||||
vikunja: {
|
||||
url: process.env.VIKUNJA_URL || '',
|
||||
apiToken: process.env.VIKUNJA_API_TOKEN || '',
|
||||
},
|
||||
};
|
||||
|
||||
export default environment;
|
||||
|
||||
94
backend/src/controllers/vikunja.controller.ts
Normal file
94
backend/src/controllers/vikunja.controller.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Request, Response } from 'express';
|
||||
import vikunjaService from '../services/vikunja.service';
|
||||
import notificationService from '../services/notification.service';
|
||||
import environment from '../config/environment';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
class VikunjaController {
|
||||
async getMyTasks(_req: Request, res: Response): Promise<void> {
|
||||
if (!environment.vikunja.url) {
|
||||
res.status(200).json({ success: true, data: [], configured: false });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const tasks = await vikunjaService.getMyTasks();
|
||||
res.status(200).json({ success: true, data: tasks, configured: true });
|
||||
} catch (error) {
|
||||
logger.error('VikunjaController.getMyTasks error', { error });
|
||||
res.status(500).json({ success: false, message: 'Vikunja konnte nicht abgefragt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getOverdueTasks(req: Request, res: Response): Promise<void> {
|
||||
if (!environment.vikunja.url) {
|
||||
res.status(200).json({ success: true, data: [], configured: false });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const tasks = await vikunjaService.getOverdueTasks();
|
||||
|
||||
// Side-effect: create notifications for each overdue task (dedup via DB constraint)
|
||||
if (req.user?.id && tasks.length > 0) {
|
||||
const userId = req.user.id;
|
||||
for (const task of tasks) {
|
||||
await notificationService.createNotification({
|
||||
user_id: userId,
|
||||
typ: 'vikunja_task',
|
||||
titel: 'Überfällige Aufgabe',
|
||||
nachricht: task.title,
|
||||
schwere: 'warnung',
|
||||
link: environment.vikunja.url ? `${environment.vikunja.url}/tasks/${task.id}` : undefined,
|
||||
quell_id: String(task.id),
|
||||
quell_typ: 'vikunja_task',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true, data: tasks, configured: true });
|
||||
} catch (error) {
|
||||
logger.error('VikunjaController.getOverdueTasks error', { error });
|
||||
res.status(500).json({ success: false, message: 'Vikunja konnte nicht abgefragt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getProjects(_req: Request, res: Response): Promise<void> {
|
||||
if (!environment.vikunja.url) {
|
||||
res.status(200).json({ success: true, data: [], configured: false });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const projects = await vikunjaService.getProjects();
|
||||
res.status(200).json({ success: true, data: projects, configured: true });
|
||||
} catch (error) {
|
||||
logger.error('VikunjaController.getProjects error', { error });
|
||||
res.status(500).json({ success: false, message: 'Vikunja-Projekte konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async createTask(req: Request, res: Response): Promise<void> {
|
||||
if (!environment.vikunja.url) {
|
||||
res.status(503).json({ success: false, message: 'Vikunja ist nicht eingerichtet' });
|
||||
return;
|
||||
}
|
||||
const { projectId, title, dueDate } = req.body as {
|
||||
projectId?: number;
|
||||
title?: string;
|
||||
dueDate?: string;
|
||||
};
|
||||
|
||||
if (!projectId || !title || title.trim().length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Projekt und Titel sind erforderlich' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const task = await vikunjaService.createTask(projectId, title.trim(), dueDate);
|
||||
res.status(201).json({ success: true, data: task });
|
||||
} catch (error) {
|
||||
logger.error('VikunjaController.createTask error', { error });
|
||||
res.status(500).json({ success: false, message: 'Aufgabe konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new VikunjaController();
|
||||
12
backend/src/routes/vikunja.routes.ts
Normal file
12
backend/src/routes/vikunja.routes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Router } from 'express';
|
||||
import vikunjaController from '../controllers/vikunja.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/tasks', authenticate, vikunjaController.getMyTasks.bind(vikunjaController));
|
||||
router.get('/overdue', authenticate, vikunjaController.getOverdueTasks.bind(vikunjaController));
|
||||
router.get('/projects', authenticate, vikunjaController.getProjects.bind(vikunjaController));
|
||||
router.post('/tasks', authenticate, vikunjaController.createTask.bind(vikunjaController));
|
||||
|
||||
export default router;
|
||||
112
backend/src/services/vikunja.service.ts
Normal file
112
backend/src/services/vikunja.service.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import axios from 'axios';
|
||||
import environment from '../config/environment';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
export interface VikunjaTask {
|
||||
id: number;
|
||||
title: string;
|
||||
done: boolean;
|
||||
due_date: string | null;
|
||||
priority: number;
|
||||
project_id: number;
|
||||
}
|
||||
|
||||
export interface VikunjaProject {
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function buildHeaders(): Record<string, string> {
|
||||
return {
|
||||
'Authorization': `Bearer ${environment.vikunja.apiToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
}
|
||||
|
||||
async function getMyTasks(): Promise<VikunjaTask[]> {
|
||||
const { vikunja } = environment;
|
||||
if (!vikunja.url) {
|
||||
throw new Error('VIKUNJA_URL is not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get<VikunjaTask[]>(
|
||||
`${vikunja.url}/api/v1/tasks/all`,
|
||||
{ headers: buildHeaders() },
|
||||
);
|
||||
return (response.data ?? []).filter((t) => !t.done);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error('Vikunja getMyTasks failed', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
});
|
||||
}
|
||||
logger.error('VikunjaService.getMyTasks failed', { error });
|
||||
throw new Error('Failed to fetch Vikunja tasks');
|
||||
}
|
||||
}
|
||||
|
||||
async function getOverdueTasks(): Promise<VikunjaTask[]> {
|
||||
const tasks = await getMyTasks();
|
||||
const now = new Date();
|
||||
return tasks.filter((t) => {
|
||||
if (!t.due_date) return false;
|
||||
return new Date(t.due_date) < now;
|
||||
});
|
||||
}
|
||||
|
||||
async function getProjects(): Promise<VikunjaProject[]> {
|
||||
const { vikunja } = environment;
|
||||
if (!vikunja.url) {
|
||||
throw new Error('VIKUNJA_URL is not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get<VikunjaProject[]>(
|
||||
`${vikunja.url}/api/v1/projects`,
|
||||
{ headers: buildHeaders() },
|
||||
);
|
||||
return response.data ?? [];
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error('Vikunja getProjects failed', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
});
|
||||
}
|
||||
logger.error('VikunjaService.getProjects failed', { error });
|
||||
throw new Error('Failed to fetch Vikunja projects');
|
||||
}
|
||||
}
|
||||
|
||||
async function createTask(projectId: number, title: string, dueDate?: string): Promise<VikunjaTask> {
|
||||
const { vikunja } = environment;
|
||||
if (!vikunja.url) {
|
||||
throw new Error('VIKUNJA_URL is not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
const body: Record<string, unknown> = { title };
|
||||
if (dueDate) {
|
||||
body.due_date = dueDate;
|
||||
}
|
||||
const response = await axios.put<VikunjaTask>(
|
||||
`${vikunja.url}/api/v1/projects/${projectId}/tasks`,
|
||||
body,
|
||||
{ headers: buildHeaders() },
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error('Vikunja createTask failed', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
});
|
||||
}
|
||||
logger.error('VikunjaService.createTask failed', { error });
|
||||
throw new Error('Failed to create Vikunja task');
|
||||
}
|
||||
}
|
||||
|
||||
export default { getMyTasks, getOverdueTasks, getProjects, createTask };
|
||||
Reference in New Issue
Block a user