Files
armarium-suite/frontend/src/app/services/notification.ts
T
Daniel Krähenbühl 1a7ef09805 feat: Armarium v1.1.0 — dashboard, auth, 2FA, SMTP, settings, deploy
Dashboard:
- ApexCharts bar chart (income vs fixed costs vs expenses) and donut chart
- KPI cards: income, fixed costs, savings rate with configurable goal
- Greeting with time-of-day and locale-aware date/time display

Authentication & security:
- Email-based login (no username), case-insensitive lookup
- JWT access/refresh tokens with rotation and blacklist
- TOTP 2FA with QR code, backup codes (copy + PDF export)
- 2FA recovery via email code
- Cloudflare Turnstile CAPTCHA on login and register

Email flows:
- Email verification on registration (24h token)
- Password reset flow (15min token, anti-enumeration)
- Brevo SMTP integration with HTML + plaintext email templates
- Notification emails: 2FA recovery, password changed, email changed

Settings page:
- 2FA management (enable/disable, QR, backup codes)
- Active sessions list with per-device revoke
- Data export: ZIP with 6 PDFs via fpdf2
- Notification preferences (3 toggles)
- Danger zone: account deletion with mandatory export + confirmation phrase

UI & layout:
- Sidebar with collapsible/flyout mode, Angular signal-based dropdowns
- Dark mode (class-based), language switcher (DE/FR/IT/EN)
- Mobile-responsive layout with touch-friendly targets
- Roboto font via @fontsource (GDPR-compliant, no Google CDN)
- Pure Tailwind CSS v3

Infrastructure:
- Forgejo Actions CI/CD pipeline (auto-deploy on push to main)
- Gunicorn + Nginx + PostgreSQL production setup
- Rate limiting, HSTS, secure cookies, CSRF protection
2026-05-25 22:46:30 +02:00

56 lines
1.3 KiB
TypeScript

import { Injectable, signal, OnDestroy } from '@angular/core';
import { ApiService } from './api';
export interface Notification {
event_type: 'deadline' | 'expense';
event_id: number;
title: string;
date: string;
}
@Injectable({ providedIn: 'root' })
export class NotificationService implements OnDestroy {
notifications = signal<Notification[]>([]);
private intervalId: any;
constructor(private api: ApiService) {}
start() {
this.load();
this.intervalId = setInterval(() => this.load(), 60_000);
}
stop() {
clearInterval(this.intervalId);
}
ngOnDestroy() {
this.stop();
}
private load() {
this.api.getNotifications().subscribe({
next: (data) => this.notifications.set(data),
error: () => {},
});
}
markRead(notification: Notification) {
this.api.markNotificationRead(notification.event_type, notification.event_id).subscribe({
next: () => {
this.notifications.update(list =>
list.filter(n => !(n.event_type === notification.event_type && n.event_id === notification.event_id))
);
},
});
}
markAllRead() {
const current = this.notifications();
current.forEach(n =>
this.api.markNotificationRead(n.event_type, n.event_id).subscribe()
);
this.notifications.set([]);
}
}