Files
budget-manager/report/igor-barcik-131780-projekt.md
T
biggy 8681aa88af feat: Implement push notifications in Budget Manager PWA
- Added functionality for subscribing to push notifications in Settings.vue.
- Integrated service worker registration and notification permission requests.
- Enhanced Vite configuration for PWA with manifest and caching strategies.
- Created a script for generating PDF reports.
- Added project report markdown file detailing the PWA implementation and features.
2025-05-23 23:54:46 +02:00

940 lines
29 KiB
Markdown

---
title: "PWA z JavaScript"
date: "2025-05-23"
author: "Igor Barcik 131780"
subtitle: "Projekt"
papersize: a4
geometry: margin=2cm
header-includes:
- \usepackage{float}
- \usepackage{fontspec}
- \usepackage{unicode-math}
- \usepackage{xcolor}
- \usepackage{minted}
- \usepackage{mdframed}
- \floatplacement{figure}{H}
- \setmonofont{FiraCode Nerd Font Mono} # Set the font for code blocks
- \definecolor{light-gray}{gray}{0.95} # Define a custom color
- \BeforeBeginEnvironment{Highlighting}{\begin{mdframed}[backgroundcolor=light-gray,linewidth=0pt]} # Wrap Highlighting environments in a mdframed environment 1/2
- \AfterEndEnvironment{Highlighting}{\end{mdframed}} # Wrap Highlighting environments in a mdframed environment 2/2
- \DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines, commandchars=\\\{\}, breakanywhere=true, fontsize=\small, breakafter=d, breakaftersymbolpre=, breakaftersymbolpost=} # Define the Verbatim environment for code blocks
---
\newpage
# Budget Manager PWA - Raport projektu
## 1. Wprowadzenie
Aplikacja "Budget Manager PWA" *([link do git](https://gitea.bigoscloud.com/biggy/budget-manager))* to progresywna aplikacja internetowa (PWA) służąca do zarządzania budżetem osobistym. Umożliwia śledzenie wydatków i przychodów, kategoryzowanie transakcji, generowanie raportów oraz ustawianie powiadomień związanych z budżetem. Aplikacja działa zarówno online, jak i offline, dzięki wykorzystaniu nowoczesnych technologii webowych.
### 1.1 Cel projektu
Celem projektu było stworzenie aplikacji PWA, która:
- Umożliwia śledzenie wydatków i przychodów użytkownika
- Działa zarówno online, jak i offline
- Przechowuje dane w Local Storage oraz MongoDB
- Oferuje funkcje budżetowania, raportowania i analizy finansowej
### 1.2 Wykorzystane technologie
Frontend:
- Vue 3 z Composition API
- TypeScript
- Tailwind CSS
- Vite (bundler)
- Pinia (zarządzanie stanem)
- Vue Router
Backend:
- Koa.js
- TypeScript
- MongoDB
PWA:
- Service Worker
- Web App Manifest
- IndexedDB (poprzez abstrakcję)
## 2. Architektura aplikacji
### 2.1 Struktura projektu
Projekt jest podzielony na dwie główne części:
#### Frontend
```bash
frontend/
├── public/ # Statyczne pliki i manifest PWA
│ ├── manifest.json # Konfiguracja PWA
│ ├── sw.js # Service Worker
│ └── icons/ # Ikony dla PWA
├── src/
│ ├── components/ # Komponenty Vue
│ ├── pages/ # Widoki aplikacji
│ ├── stores/ # Pinia stores
│ ├── lib/ # Funkcje pomocnicze
│ │ └── pwa-utils.ts # Narzędzia związane z PWA
│ └── main.ts # Główny plik aplikacji
└── styles/ # Style CSS
```
#### Backend
```bash
backend/
├── src/
│ ├── app.ts # Główny plik aplikacji
│ ├── controllers/ # Kontrolery REST API
│ ├── middleware/ # Middleware (np. autoryzacja)
│ └── services/ # Serwisy (np. baza danych)
└── scripts/
└── generate-vapid-keys.js # Skrypt do generowania kluczy VAPID
```
### 2.2 Przepływ danych
1. Dane użytkownika są przechowywane lokalnie w Local Storage dla dostępu offline
2. Przy połączeniu z internetem dane są synchronizowane z bazą MongoDB na serwerze
3. Service Worker zapisuje dane do cache, umożliwiając działanie aplikacji offline
4. Background Sync API jest używane do synchronizacji danych, gdy połączenie internetowe zostanie przywrócone
## 3. Implementacja PWA
### 3.1 Manifest aplikacji
Manifest definiuje jak aplikacja ma się zachowywać po zainstalowaniu na urządzeniu użytkownika:
```json
{
"name": "Budget Manager PWA",
"short_name": "Budget",
"description": "Budget Manager application for tracking expenses and income",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"icons": [
{
"src": "/icons/icon-192px.jpg",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512px.jpg",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"shortcuts": [
{
"name": "Add Transaction",
"short_name": "Add",
"description": "Add a new transaction",
"url": "/add",
"icons": [{ "src": "/icons/add-icon-96x96.png", "sizes": "96x96" }]
},
{
"name": "View Reports",
"short_name": "Reports",
"description": "View budget reports",
"url": "/reports",
"icons": [{ "src": "/icons/report-icon-96x96.png", "sizes": "96x96" }]
}
]
}
```
### 3.2 Service Worker
Service Worker obsługuje:
- Cache'owanie zasobów dla dostępu offline
- Synchronizację w tle (background sync)
Kluczowe fragmenty implementacji:
```javascript
// Instalacja Service Worker'a
self.addEventListener("install", (event) => {
console.log("Service worker install event");
event.waitUntil(
caches
.open(CACHE_NAME)
.then((cache) => {
return cache.addAll(precacheResources);
})
.then(() => {
return self.skipWaiting();
}),
);
});
// Strategia fetch: cache-first, następnie sieć
self.addEventListener("fetch", (event) => {
// Pomijanie żądań cross-origin i API
if (
!event.request.url.startsWith(self.location.origin) ||
event.request.url.includes("/api/")
) {
return;
}
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request)
.then((response) => {
// Cache'owanie odpowiedzi
if (
response &&
response.status === 200 &&
response.type === "basic"
) {
const responseToCache = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch(() => {
// Fallback w przypadku braku sieci
if (event.request.mode === "navigate") {
return caches.match("/");
}
return null;
});
}),
);
});
// Obsługa background sync
self.addEventListener("sync", (event) => {
if (event.tag === "sync-transactions") {
event.waitUntil(syncTransactions());
} else if (event.tag === "sync-categories") {
event.waitUntil(syncCategories());
}
});
```
### 3.3 Rejestracja Service Worker
Rejestracja Service Worker'a w aplikacji:
```typescript
export async function registerServiceWorker() {
if ("serviceWorker" in navigator) {
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
if (registration.installing) {
console.log("Service worker installing");
} else if (registration.waiting) {
console.log("Service worker installed");
} else if (registration.active) {
console.log("Service worker active");
}
// Upewniamy się, że service worker jest gotowy przed próbą subskrypcji
await navigator.serviceWorker.ready;
return registration;
} catch (error) {
console.error("Registration failed with error:", error);
return null;
}
}
return null;
}
```
### 3.4 Powiadomienia Push
Powiadomienia Push są kluczowym elementem aplikacji PWA, umożliwiającym informowanie użytkownika o ważnych zdarzeniach, nawet gdy aplikacja nie jest aktywna w przeglądarce. W Budget Manager PWA, powiadomienia push są wykorzystywane do informowania użytkowników o nowych transakcjach dodanych do ich budżetu.
#### 3.4.1 Klucze VAPID
Do bezpiecznego wysyłania powiadomień push wykorzystywany jest protokół VAPID (Voluntary Application Server Identification). Klucze VAPID (publiczny i prywatny) są generowane za pomocą skryptu i przechowywane jako zmienne środowiskowe na serwerze.
Skrypt `backend/scripts/generate-vapid-keys.js` generuje te klucze:
```bash
# backend/scripts/generate-vapid-keys.js
const webpush = require('web-push');
const fs = require('fs');
const vapidKeys = webpush.generateVAPIDKeys();
console.log('Public Key: ', vapidKeys.publicKey);
console.log('Private Key: ', vapidKeys.privateKey);
// Zapisz klucze do pliku .env lub użyj bezpośrednio
fs.appendFileSync('.env.example', `\nVAPID_PUBLIC_KEY=${vapidKeys.publicKey}`);
fs.appendFileSync('.env.example', `VAPID_PRIVATE_KEY=${vapidKeys.privateKey}\n`);
console.log('VAPID keys appended to .env.example');
```
Klucz publiczny VAPID jest udostępniany frontendowi, aby mógł zasubskrybować usługę push. Klucze są ładowane na serwerze przy starcie aplikacji.
#### 3.4.2 Implementacja Frontend
**Subskrypcja powiadomień:**
Logika subskrypcji znajduje się w `frontend/src/lib/pwa-utils.ts`. Użytkownik może włączyć lub wyłączyć powiadomienia na stronie ustawień (`frontend/src/pages/Settings.vue`). Proces obejmuje:
1. Sprawdzenie wsparcia dla Service Worker i Push API.
2. Poproszenie użytkownika o zgodę na otrzymywanie powiadomień.
3. Pobranie klucza publicznego VAPID z backendu (`/api/push/vapid-public-key`).
4. Subskrypcja usługi push za pomocą `registration.pushManager.subscribe()`.
5. Wysłanie obiektu subskrypcji na backend (`/api/push/subscribe`) w celu zapisania.
Fragment kodu z `pwa-utils.ts` odpowiedzialny za subskrypcję:
```typescript
// frontend/src/lib/pwa-utils.ts
// ... (uproszczony przykład)
async function subscribeToPushNotifications(registration: ServiceWorkerRegistration) {
try {
const vapidPublicKeyResponse = await fetch('/api/push/vapid-public-key', { headers: { 'Accept': 'text/plain' } });
if (!vapidPublicKeyResponse.ok) {
throw new Error(`Failed to get VAPID public key: ${vapidPublicKeyResponse.statusText}`);
}
const vapidPublicKey = await vapidPublicKeyResponse.text();
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
});
// Odczyt tokena JWT z localStorage
const token = localStorage.getItem('authToken'); // Upewnij się, że klucz jest poprawny
if (!token) {
console.warn('No auth token found, cannot save subscription to backend.');
// Można rozważyć zapisanie subskrypcji lokalnie i wysłanie później
return false;
}
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
});
console.log('User is subscribed.');
return true;
} catch (error) {
console.error('Failed to subscribe the user: ', error);
return false;
}
}
function urlBase64ToUint8Array(base64String: string): ArrayBuffer {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray.buffer; // Zwraca ArrayBuffer
}
```
**Obsługa powiadomień w Service Worker (`frontend/public/sw.js`):**
Service Worker nasłuchuje na zdarzenie `push` i `notificationclick`.
- Po otrzymaniu zdarzenia `push`, Service Worker wyświetla powiadomienie systemowe z danymi otrzymanymi od serwera (np. tytuł, treść, ikonę). Wiadomość jest również przekazywana do aktywnych klientów aplikacji w celu wyświetlenia toastu.
- Zdarzenie `notificationclick` obsługuje interakcję użytkownika z powiadomieniem, np. otwarcie aplikacji lub konkretnej strony.
```javascript
// frontend/public/sw.js
self.addEventListener('push', event => {
let data = { title: 'Nowa wiadomość', body: 'Otrzymałeś nowe powiadomienie.', data: { url: '/' } };
if (event.data) {
try {
data = event.data.json();
} catch (e) {
console.error('Push event data is not valid JSON:', event.data.text());
data.body = event.data.text(); // Użyj tekstu jako body jeśli JSON się nie sparsuje
}
}
const title = data.title || 'Budget Manager';
const options = {
body: data.body || 'Nowa aktywność na Twoim koncie.',
icon: data.icon || '/icons/icon-192px.jpg',
badge: '/icons/icon-192px.jpg', // Użyj tej samej ikony dla badge
data: data.data || { url: '/' }
};
event.waitUntil(self.registration.showNotification(title, options));
// Wysyłanie wiadomości do aktywnych klientów (zakładek aplikacji)
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clients => {
if (clients && clients.length) {
clients.forEach(client => {
client.postMessage({
type: 'PUSH_NOTIFICATION_RECEIVED',
payload: data // Przekaż całe dane, w tym title, body, data.url
});
});
}
});
});
self.addEventListener('notificationclick', event => {
event.notification.close();
const urlToOpen = event.notification.data && event.notification.data.url
? event.notification.data.url
: '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(windowClients => {
for (let i = 0; i < windowClients.length; i++) {
const client = windowClients[i];
// Sprawdź, czy URL klienta (po usunięciu hasha) pasuje do urlToOpen
const clientURL = new URL(client.url);
const targetURL = new URL(urlToOpen, self.location.origin); // Upewnij się, że targetURL jest absolutny
if (clientURL.origin + clientURL.pathname === targetURL.origin + targetURL.pathname && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
```
#### 3.4.3 Implementacja Backend
Backend (`backend/src/controllers/push.ts`) odpowiada za:
1. Udostępnianie publicznego klucza VAPID (`/api/push/vapid-public-key`). Ten endpoint jest publicznie dostępny.
2. Rejestrowanie subskrypcji użytkowników (`/api/push/subscribe`). Subskrypcje są przechowywane w bazie danych, powiązane z użytkownikiem.
3. Wysłanie powiadomień do zasubskrybowanych użytkowników. Na przykład, po dodaniu nowej transakcji (`backend/src/controllers/protected.ts`), serwer wysyła powiadomienie.
Fragment kodu z `pushController.ts` (uproszczony):
```typescript
// backend/src/controllers/push.ts
import webpush from 'web-push';
import { Context } from 'koa';
import User from '../models'; // Załóżmy, że User model ma pole pushSubscriptions
// Konfiguracja VAPID keys przy starcie aplikacji (np. w app.ts lub dedykowanym pliku konfiguracyjnym)
// webpush.setVapidDetails(
// 'mailto:your-email@example.com',
// process.env.VAPID_PUBLIC_KEY!,
// process.env.VAPID_PRIVATE_KEY!
// );
export const getVapidPublicKey = async (ctx: Context) => {
if (!process.env.VAPID_PUBLIC_KEY) {
ctx.throw(500, 'VAPID public key not configured');
return;
}
ctx.body = process.env.VAPID_PUBLIC_KEY;
ctx.set('Content-Type', 'text/plain');
};
export const subscribe = async (ctx: Context) => {
const subscription = ctx.request.body as webpush.PushSubscription;
const userId = ctx.state.user.id; // ID zalogowanego użytkownika
try {
// Zapisz subskrypcję w dokumencie użytkownika
// Załóżmy, że model User ma pole pushSubscriptions: webpush.PushSubscription[]
await User.findByIdAndUpdate(userId, { $addToSet: { pushSubscriptions: subscription } });
ctx.status = 201;
ctx.body = { message: 'Subscribed successfully' };
} catch (error) {
console.error('Error saving subscription:', error);
ctx.throw(500, 'Failed to save subscription');
}
};
// Funkcja do wysyłania powiadomień (może być w serwisie)
export const sendPushNotification = async (userId: string, payload: object) => {
try {
const user = await User.findById(userId);
if (!user || !user.pushSubscriptions || user.pushSubscriptions.length === 0) {
console.log(`No push subscriptions found for user ${userId}`);
return;
}
const notificationPayload = JSON.stringify(payload);
for (const sub of user.pushSubscriptions) {
try {
await webpush.sendNotification(sub, notificationPayload);
} catch (error: any) {
console.error('Error sending notification to a subscription: ', error.body);
// Jeśli subskrypcja jest nieaktualna (np. status 410 Gone), usuń ją
if (error.statusCode === 410 || error.statusCode === 404) {
await User.findByIdAndUpdate(userId, { $pull: { pushSubscriptions: sub } });
console.log('Removed outdated subscription.');
}
}
}
} catch (error) {
console.error(`Failed to send push notifications for user ${userId}:`, error);
}
};
```
Integracja wysyłania powiadomień przy tworzeniu transakcji w `protectedController.ts`:
```typescript
// backend/src/controllers/protected.ts
// ...
import { sendPushNotification } from './push'; // Zaimportuj funkcję
export const addTransaction = async (ctx: Context) => {
// ... logika dodawania transakcji ...
// const newTransaction = ...; // Wynik dodania transakcji
// const userId = ctx.state.user.id;
// Po pomyślnym dodaniu transakcji:
if (newTransaction && userId) { // Upewnij się, że newTransaction i userId istnieją
const notificationPayload = {
title: 'Nowa transakcja',
body: `Dodano: ${newTransaction.description} (${newTransaction.amount} ${newTransaction.currency || 'PLN'})`,
data: { url: `/transactions` } // Ogólny link do transakcji lub ID: /transactions/${newTransaction._id}
};
// Wyślij asynchronicznie, aby nie blokować odpowiedzi
sendPushNotification(userId, notificationPayload).catch(console.error);
}
// ...
};
```
Powiadomienia push znacząco zwiększają zaangażowanie użytkowników i użyteczność aplikacji Budget Manager PWA, informując ich na bieżąco o zmianach w ich budżecie.
## 4. Zarządzanie stanem aplikacji
### 4.1 Stores w Pinia
Aplikacja używa Pinia do zarządzania stanem, co ułatwia komunikację między komponentami i zarządzanie danymi aplikacji.
Przykładowy store dla transakcji:
```typescript
export const useTransactionsStore = defineStore("transactions", () => {
// Stan
const transactions = ref<Transaction[]>([]);
const loading = ref(false);
const error = ref("");
// API URL
const apiBaseUrl = "http://localhost:3000/api";
// Store autoryzacji
const authStore = useAuthStore();
// Gettery
const incomes = computed(() =>
transactions.value.filter((t) => t.type === "income"),
);
const expenses = computed(() =>
transactions.value.filter((t) => t.type === "expense"),
);
const totalIncome = computed(() =>
incomes.value.reduce((sum, t) => sum + t.amount, 0),
);
const totalExpenses = computed(() =>
expenses.value.reduce((sum, t) => sum + t.amount, 0),
);
const balance = computed(() => totalIncome.value - totalExpenses.value);
// Akcje
async function fetchTransactions() {
if (!authStore.token) return;
loading.value = true;
error.value = "";
try {
const response = await fetch(`${apiBaseUrl}/transactions`, {
headers: {
Authorization: `Bearer ${authStore.token}`,
},
});
if (!response.ok) {
throw new Error("Failed to fetch transactions");
}
const data = await response.json();
transactions.value = data.data;
// Zapisywanie do localStorage dla dostępu offline
saveToLocalStorage();
} catch (err: any) {
error.value = err.message;
// Jeśli API call nie powiedzie się, próba załadowania z localStorage
loadFromLocalStorage();
} finally {
loading.value = false;
}
}
// ... inne metody
return {
transactions,
loading,
error,
incomes,
expenses,
totalIncome,
totalExpenses,
balance,
fetchTransactions,
// ... inne eksportowane metody
};
});
```
### 4.2 Synchronizacja offline/online
Aplikacja wykorzystuje strategię "offline-first", gdzie dane są najpierw zapisywane lokalnie, a następnie synchronizowane z serwerem gdy połączenie jest dostępne:
```typescript
// Dodawanie transakcji
async function addTransaction(transaction: Omit<Transaction, "_id">) {
try {
error.value = "";
// Generujemy tymczasowe ID dla transakcji
const tempId = `temp_${Date.now()}`;
const newTransaction = {
...transaction,
_id: tempId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
synced: false
};
// Dodajemy do listy lokalnie
transactions.value.push(newTransaction);
saveToLocalStorage();
// Próbujemy zapisać na serwerze
if (navigator.onLine && authStore.token) {
const response = await fetch(`${apiBaseUrl}/transactions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${authStore.token}`,
},
body: JSON.stringify(transaction),
});
if (response.ok) {
// Aktualizujemy transakcję z danymi z serwera
const data = await response.json();
const index = transactions.value.findIndex(t => t._id === tempId);
if (index !== -1) {
transactions.value[index] = { ...data.data, synced: true };
saveToLocalStorage();
}
} else {
// Oznaczamy transakcję do późniejszej synchronizacji
const syncStore = useSyncStore();
syncStore.addPendingTransaction(newTransaction);
throw new Error("Failed to save transaction to server");
}
} else {
// Jesteśmy offline, dodajemy do listy oczekujących na sync
const syncStore = useSyncStore();
syncStore.addPendingTransaction(newTransaction);
}
} catch (err: any) {
error.value = err.message;
throw err;
}
}
```
## 5. Interfejs użytkownika
### 5.1 Komponenty
Aplikacja wykorzystuje komponenty Vue do zbudowania interfejsu użytkownika. Przykładowy komponent Dashboard:
![Komponent Dashboard](dashboard-preview.png)
```vue
<template>
<PageLayout header="Dashboard">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Podsumowanie salda -->
<div class="lg:col-span-2">
<BalanceSummary />
</div>
<!-- Szybkie akcje -->
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<router-link to="/add" class="w-full block">
<Button class="w-full" variant="default">Add New Transaction</Button>
</router-link>
<router-link to="/reports" class="w-full block">
<Button class="w-full" variant="outline">View Reports</Button>
</router-link>
<!-- Inne przyciski akcji -->
</CardContent>
</Card>
<!-- Ostatnie transakcje -->
<Card class="lg:col-span-3">
<CardHeader>
<CardTitle>Recent Transactions</CardTitle>
</CardHeader>
<CardContent>
<!-- Zawartość sekcji transakcji -->
</CardContent>
</Card>
</div>
</PageLayout>
</template>
```
### 5.2 Responsywność
Aplikacja wykorzystuje Tailwind CSS do tworzenia responsywnego interfejsu, który działa dobrze na urządzeniach mobilnych i desktopowych:
![Dashboard mobilny](responsive-dashboard-image.png)
- Używanie systemu grid z breakpointami (md:, lg:)
- Elastyczne komponenty dopasowujące się do różnych rozmiarów ekranów
- Mobilne menu nawigacyjne dla małych ekranów
### 5.3 Dostępność offline
Interfejs uwzględnia stan połączenia internetowego:
![Tryb offline](no-internet-image.png)
- Wyświetlanie informacji o trybie offline
- Możliwość dodawania transakcji offline
- Automatyczna synchronizacja po ponownym połączeniu
## 6. Backend API
### 6.1 Struktura API
Backend implementuje RESTful API do komunikacji z frontendem:
- `POST /api/auth/login` - logowanie użytkownika
- `POST /api/auth/register` - rejestracja nowego użytkownika
- `GET /api/transactions` - pobieranie listy transakcji
- `POST /api/transactions` - dodawanie nowej transakcji
- `PUT /api/transactions/:id` - aktualizacja transakcji
- `DELETE /api/transactions/:id` - usunięcie transakcji
- `GET /api/categories` - pobieranie kategorii
- `PUT /api/categories` - dodawanie nowej kategorii
### 6.2 Bezpieczeństwo API
API zabezpieczone jest przez:
- Tokeny JWT do autoryzacji
- Middleware uwierzytelniające
- Zabezpieczenie przed CSRF
- CORS
```typescript
// Middleware autoryzacji
export const authenticate = async (ctx: Context, next: Next) => {
// Pomijamy middleware dla ścieżek publicznych
if (ctx.path.startsWith('/api/auth/login') ||
ctx.path.startsWith('/api/auth/register')) {
return next();
}
const token = ctx.headers.authorization?.split(' ')[1];
if (!token) {
ctx.status = 401;
ctx.body = {
success: false,
message: 'No token provided'
};
return;
}
try {
const decoded = jwt.verify(token, jwtConfig.secret);
ctx.state.user = decoded;
return next();
} catch (err) {
ctx.throw(401, 'Invalid or expired token');
}
};
```
## 7. Baza danych
### 7.1 Model danych
Aplikacja wykorzystuje MongoDB do przechowywania danych, z następującymi kolekcjami:
1. `users` - informacje o użytkownikach
2. `transactions` - transakcje finansowe
3. `categories` - kategorie transakcji
### 7.2 Usługa bazodanowa
Abstrakcja dostępu do bazy danych zapewniona jest przez klasę DatabaseService:
```typescript
export default class DatabaseService {
private static instance: DatabaseService;
private client: MongoClient | null = null;
private db: Db | null = null;
private constructor() {}
// Wzorzec Singleton
public static getInstance(): DatabaseService {
if (!DatabaseService.instance) {
DatabaseService.instance = new DatabaseService();
}
return DatabaseService.instance;
}
// Połączenie z bazą danych
public async connect() {
try {
this.client = await MongoClient.connect(dbConfig.url);
this.db = this.client.db(dbConfig.name);
console.log('Connected to MongoDB');
} catch (error) {
console.error('Error connecting to MongoDB:', error);
throw error;
}
}
// Pobranie kolekcji
public getCollection(collectionName: string): Collection {
if (!this.db) {
throw new Error('Database not connected');
}
return this.db.collection(collectionName);
}
// Zamknięcie połączenia
public async disconnect() {
if (this.client) {
await this.client.close();
this.client = null;
this.db = null;
console.log('Disconnected from MongoDB');
}
}
}
```
## 8. Testy i użycie
### 8.1 Testowanie offline
Aplikacja została przetestowana w różnych scenariuszach:
- Dodawanie transakcji w trybie offline
- Automatyczna synchronizacja po przywróceniu połączenia
- Działanie Service Worker'a i cache'owanie zasobów
### 8.2 Instalacja jako PWA
Aplikacja może być zainstalowana na urządzeniach:
- Poprawnie wyświetla prompt instalacyjny
- Działa jako samodzielna aplikacja po zainstalowaniu
- Wykorzystuje ikony i kolory zdefiniowane w manifeście
## 9. Wnioski i perspektywy rozwoju
### 9.1 Osiągnięte cele
Aplikacja spełnia wszystkie założenia projektowe:
- Funkcjonuje jako pełnoprawna PWA
- Umożliwia zarządzanie budżetem osobistym
- Działa offline z synchronizacją
- Ma responsywny i przyjazny interfejs
### 9.2 Możliwe rozszerzenia
Potencjalne kierunki rozwoju aplikacji:
- Dodanie eksportu danych do CSV/PDF
- Implementacja celów oszczędnościowych
- Rozbudowa funkcji raportowania o wykresy
- Dodanie powiadomień o zbliżających się regularnych płatnościach
- Integracja z bankami poprzez API
## 10. Zrzuty ekranu
### 10.1 Ekran logowania
![Ekran logowania](login_screen.png)
### 10.2 Ekran transakcji
![Ekran transakcji](transaction_screen.png)
### 10.3 Ekran kategorii
![Ekran kategorii](category_screen.png)
### 10.4 Ekran raportów
![Ekran raportów](report_screenshot_1.png)
### 10.5 Ekran ustawień
![Ekran ustawień](settings_screen.png)
## 11. Bibliografia
1. Vue.js Documentation: [https://vuejs.org/guide/introduction.html](https://vuejs.org/guide/introduction.html)
2. Progressive Web Apps: [https://web.dev/progressive-web-apps/](https://web.dev/progressive-web-apps/)
3. MDN Web Docs - Service Worker API: [https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
4. Tailwind CSS Documentation: [https://tailwindcss.com/docs](https://tailwindcss.com/docs)
5. Koa.js Documentation: [https://koajs.com/](https://koajs.com/)
6. MongoDB Documentation: [https://docs.mongodb.com/](https://docs.mongodb.com/)