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
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
private baseUrl = '/api';
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
// Accounts
|
||||
getAccounts(): Observable<any[]> {
|
||||
return this.http.get<any[]>(`${this.baseUrl}/accounts/`);
|
||||
}
|
||||
|
||||
createAccount(account: {name: string, balance: number, account_type: string}): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/accounts/`, account);
|
||||
}
|
||||
|
||||
updateAccount(id: number, account: {name: string, balance: number, account_type: string}): Observable<any> {
|
||||
return this.http.put(`${this.baseUrl}/accounts/${id}/`, account);
|
||||
}
|
||||
|
||||
deleteAccount(id: number): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/accounts/${id}/`);
|
||||
}
|
||||
|
||||
// Budgets
|
||||
getBudgets(): Observable<any[]> {
|
||||
return this.http.get<any[]>(`${this.baseUrl}/budgets/`);
|
||||
}
|
||||
|
||||
createBudget(budget: any): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/budgets/`, budget);
|
||||
}
|
||||
|
||||
updateBudget(id: number, budget: any): Observable<any> {
|
||||
return this.http.put(`${this.baseUrl}/budgets/${id}/`, budget);
|
||||
}
|
||||
|
||||
deleteBudget(id: number): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/budgets/${id}/`);
|
||||
}
|
||||
|
||||
// Transactions
|
||||
getTransactions(): Observable<any[]> {
|
||||
return this.http.get<any[]>(`${this.baseUrl}/transactions/`);
|
||||
}
|
||||
|
||||
createTransaction(transaction: any): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/transactions/`, transaction);
|
||||
}
|
||||
|
||||
updateTransaction(id: number, transaction: any): Observable<any> {
|
||||
return this.http.put(`${this.baseUrl}/transactions/${id}/`, transaction);
|
||||
}
|
||||
|
||||
deleteTransaction(id: number): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/transactions/${id}/`);
|
||||
}
|
||||
|
||||
// Expenses
|
||||
getExpenses(): Observable<any[]> {
|
||||
return this.http.get<any[]>(`${this.baseUrl}/expenses/`);
|
||||
}
|
||||
|
||||
createExpense(expense: any): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/expenses/`, expense);
|
||||
}
|
||||
|
||||
updateExpense(id: number, expense: any): Observable<any> {
|
||||
return this.http.put(`${this.baseUrl}/expenses/${id}/`, expense);
|
||||
}
|
||||
|
||||
deleteExpense(id: number): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/expenses/${id}/`);
|
||||
}
|
||||
|
||||
// Profile
|
||||
getProfile(): Observable<any> {
|
||||
return this.http.get(`${this.baseUrl}/profile/`);
|
||||
}
|
||||
|
||||
updateProfile(data: any): Observable<any> {
|
||||
const formData = new FormData();
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key] !== null && data[key] !== undefined) {
|
||||
formData.append(key, data[key]);
|
||||
}
|
||||
});
|
||||
return this.http.put(`${this.baseUrl}/profile/`, formData);
|
||||
}
|
||||
|
||||
deleteProfile(password: string): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/profile/`, { body: { password } });
|
||||
}
|
||||
|
||||
changePassword(password: string): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/auth/password/`, { password });
|
||||
}
|
||||
|
||||
// Deadlines
|
||||
getDeadlines(): Observable<any[]> {
|
||||
return this.http.get<any[]>(`${this.baseUrl}/deadlines/`);
|
||||
}
|
||||
|
||||
createDeadline(d: any): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/deadlines/`, d);
|
||||
}
|
||||
|
||||
updateDeadline(id: number, d: any): Observable<any> {
|
||||
return this.http.put(`${this.baseUrl}/deadlines/${id}/`, d);
|
||||
}
|
||||
|
||||
deleteDeadline(id: number): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/deadlines/${id}/`);
|
||||
}
|
||||
|
||||
getICalUrl(): Observable<{ url: string }> {
|
||||
return this.http.get<{ url: string }>(`${this.baseUrl}/calendar/ical-url/`);
|
||||
}
|
||||
|
||||
search(q: string): Observable<Record<string, any[]>> {
|
||||
return this.http.get<Record<string, any[]>>(`${this.baseUrl}/search/?q=${encodeURIComponent(q)}`);
|
||||
}
|
||||
|
||||
getNotifications(): Observable<any[]> {
|
||||
return this.http.get<any[]>(`${this.baseUrl}/notifications/`);
|
||||
}
|
||||
|
||||
markNotificationRead(event_type: string, event_id: number): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/notifications/`, { event_type, event_id });
|
||||
}
|
||||
|
||||
// 2FA
|
||||
get2FASetup(): Observable<{ secret: string; uri: string }> {
|
||||
return this.http.get<{ secret: string; uri: string }>(`${this.baseUrl}/auth/2fa/setup/`);
|
||||
}
|
||||
|
||||
enable2FA(code: string): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/auth/2fa/enable/`, { code });
|
||||
}
|
||||
|
||||
disable2FA(code: string): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/auth/2fa/disable/`, { code });
|
||||
}
|
||||
|
||||
login2FA(temp_token: string, code: string): Observable<{ access: string; refresh: string; session_key?: string }> {
|
||||
return this.http.post<{ access: string; refresh: string; session_key?: string }>(`${this.baseUrl}/auth/2fa/login/`, { temp_token, code });
|
||||
}
|
||||
|
||||
request2FARecovery(temp_token: string): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/auth/2fa/recover/`, { temp_token });
|
||||
}
|
||||
|
||||
confirm2FARecovery(temp_token: string, recovery_code: string): Observable<{ access: string; refresh: string; session_key?: string }> {
|
||||
return this.http.post<{ access: string; refresh: string; session_key?: string }>(`${this.baseUrl}/auth/2fa/recover/confirm/`, { temp_token, recovery_code });
|
||||
}
|
||||
|
||||
// Sessions
|
||||
getSessions(): Observable<any[]> {
|
||||
return this.http.get<any[]>(`${this.baseUrl}/auth/sessions/`);
|
||||
}
|
||||
|
||||
revokeSession(sessionKey: string): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/auth/sessions/${sessionKey}/`);
|
||||
}
|
||||
|
||||
revokeAllOtherSessions(): Observable<any> {
|
||||
return this.http.delete(`${this.baseUrl}/auth/sessions/revoke-all/`);
|
||||
}
|
||||
|
||||
// Data export
|
||||
downloadExport(): Observable<Blob> {
|
||||
return this.http.get(`${this.baseUrl}/export/`, { responseType: 'blob' });
|
||||
}
|
||||
|
||||
// Notification preferences
|
||||
updateNotificationPrefs(prefs: { notif_deadlines?: boolean; notif_budget_alerts?: boolean; notif_monthly_summary?: boolean }): Observable<any> {
|
||||
return this.http.patch(`${this.baseUrl}/notifications/prefs/`, prefs);
|
||||
}
|
||||
|
||||
// Email verification & password reset
|
||||
verifyEmail(token: string): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/auth/verify-email/`, { token });
|
||||
}
|
||||
|
||||
requestPasswordReset(email: string): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/auth/password-reset/`, { email });
|
||||
}
|
||||
|
||||
confirmPasswordReset(token: string, password: string): Observable<any> {
|
||||
return this.http.post(`${this.baseUrl}/auth/password-reset/confirm/`, { token, password });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user