Lekcja 27. Projekt 8 (końcowy): System rezerwacji wizyt — Angular + logowanie + zadanie kontrolne
Trudny / egzaminacyjnyPo co się tego uczymy?
Ostatni, czwarty projekt i jednocześnie podsumowanie całego działu. Łączymy WSZYSTKO: Angular (komponenty, formularze, routing, serwisy), ASP.NET Core Web API z bazą danych, oraz logowanie użytkowników (ASP.NET Core Identity, lekcja 14) — dokładnie ten typ pełnego, wielowarstwowego zadania, jaki może pojawić się w części praktycznej egzaminu zawodowego INF.04. Na końcu lekcji znajdziesz też krótkie zadanie kontrolne sprawdzające wiedzę z całego działu.
Teoria
Wymagania funkcjonalne systemu rezerwacji:
- Lista dostępnych terminów — widoczna dla WSZYSTKICH odwiedzających (komponent publiczny, bez logowania);
- Rezerwacja terminu — WYMAGA zalogowania; zalogowany użytkownik wybiera wolny termin i go rezerwuje;
- "Moje rezerwacje" — strona (chroniona) pokazująca WYŁĄCZNIE rezerwacje zalogowanego użytkownika;
- Anulowanie rezerwacji — użytkownik może odwołać WŁASNĄ rezerwację;
- Logowanie i rejestracja — ASP.NET Core (lekcja 14), z tokenem/ciasteczkiem uwierzytelniającym rozpoznawanym przez Angular.
Logowanie w architekturze Angular + Web API. W parze Razor Pages (jedna aplikacja), Identity działa "w tle" automatycznie przez ciasteczka. W architekturze Angular+API mamy DWIE osobne aplikacje (inny port!) — logowanie zwykle realizuje się przez token (najczęściej JWT — JSON Web Token): backend po poprawnym zalogowaniu zwraca token, Angular zapisuje go (np. w pamięci przeglądarki) i DOŁĄCZA do KAŻDEGO kolejnego żądania w nagłówku Authorization: Bearer <token>. Backend na podstawie tego nagłówka rozpoznaje, KTO wysyła żądanie, bez potrzeby przechowywania sesji po stronie serwera. To rozwiązanie technicznie różni się od ciasteczek sesyjnych z lekcji 13, ale odpowiada na TEN SAM problem — informator CKE wprost dopuszcza obie metody: "obsłużyć sesje LUB JWT".
Model danych. Termin (Data, CzyZarezerwowany), Rezerwacja (TerminId, UzytkownikId, DataUtworzenia) — relacja jeden-do-jednego między terminem a rezerwacją, jeden-do-wielu między użytkownikiem a jego rezerwacjami (EF Core, lekcje 14-16).
Kluczowy problem: unikanie podwójnej rezerwacji. Backend MUSI ponownie sprawdzić dostępność terminu TUŻ PRZED zapisem (nie tylko przy wyświetleniu listy) — jeśli dwóch użytkowników niemal jednocześnie próbuje zarezerwować ten sam termin, backend jako "źródło prawdy" musi to wykryć i jednego z nich odrzucić z czytelnym komunikatem błędu (kod 409 Conflict).
Struktura komponentów Angular: TerminyComponent (publiczny, lista + przycisk "Zarezerwuj", przekierowujący na logowanie jeśli niezalogowany), LogowanieComponent/RejestracjaComponent, MojeRezerwacjeComponent (chroniony przez Angular Route Guard — mechanizm blokujący dostęp do trasy bez ważnego tokenu, analogiczny do [Authorize] z ASP.NET Core).
Schemat
Gość (niezalogowany)
│
▼
Pages/Terminy/Lista.cshtml (publiczna) - widzi dostępne terminy
│
│ klika "Zarezerwuj" → przekierowanie na logowanie (jeśli niezalogowany)
▼
Pages/Konto/Logowanie.cshtml → po zalogowaniu wraca (ReturnUrl)
│
▼
Pages/Rezerwacje/Zarezerwuj.cshtml [Authorize]
│
│ OnPostAsync: PONOWNE sprawdzenie czy termin nadal wolny
│ (ochrona przed podwójną rezerwacją)
▼
Nowa Rezerwacja (TerminId, UzytkownikId) → SaveChangesAsync()
Termin.CzyZarezerwowany = true → SaveChangesAsync()
│
▼
Pages/Rezerwacje/Moje.cshtml [Authorize]
context.Rezerwacje.Include(r => r.Termin)
.Where(r => r.UzytkownikId == User Id zalogowanego)
Administrator: Pages/Admin/ZarzadzajTerminami.cshtml [Authorize(Roles="Admin")]
dodawanie/usuwanie dostępnych terminów
Przykład z życia
System rezerwacji wizyt u fryzjera, lekarza czy w gabinecie kosmetycznym — klient przegląda dostępne terminy, loguje się, rezerwuje wybrany termin i może go później odwołać. To ostatni, syntetyzujący projekt tego działu, łączący front-end (Angular), backend (ASP.NET Core Web API) i bazę danych (EF Core) w jedną, kompletną, praktyczną aplikację.
Controllers/RezerwacjeController.cs
// Controllers/RezerwacjeController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
namespace RezerwacjeApp.Server.Controllers;
[ApiController]
[Route("api/[controller]")]
public class RezerwacjeController : ControllerBase
{
private readonly RezerwacjeContext _context;
public RezerwacjeController(RezerwacjeContext context) => _context = context;
[HttpPost]
[Authorize]
public async Task<IActionResult> Zarezerwuj([FromBody] int terminId)
{
var termin = await _context.Terminy.FindAsync(terminId);
if (termin is null) return NotFound();
// KLUCZOWE: ponowne sprawdzenie tuż przed zapisem
if (termin.CzyZarezerwowany)
{
return Conflict("Ten termin został już zarezerwowany.");
}
string idUzytkownika = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
termin.CzyZarezerwowany = true;
_context.Rezerwacje.Add(new Rezerwacja
{
TerminId = terminId,
UzytkownikId = idUzytkownika
});
await _context.SaveChangesAsync();
return Ok();
}
[HttpGet("moje")]
[Authorize]
public async Task<ActionResult<List<Rezerwacja>>> MojeRezerwacje()
{
string idUzytkownika = User.FindFirstValue(ClaimTypes.NameIdentifier)!;
var rezerwacje = await _context.Rezerwacje
.Include(r => r.Termin)
.Where(r => r.UzytkownikId == idUzytkownika)
.ToListAsync();
return Ok(rezerwacje);
}
}
auth.interceptor.ts + auth.guard.ts + app.routes.ts
// auth.interceptor.ts - automatyczne dołączanie tokenu do KAŻDEGO żądania
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.pobierzToken();
if (token) {
const sklonowanyRequest = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
return next(sklonowanyRequest);
}
return next(req);
};
// ---------------------------------------------------------------------
// auth.guard.ts - Route Guard chroniący trasę przed niezalogowanymi
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.czyZalogowany()) {
return true;
}
router.navigate(['/logowanie']);
return false;
};
// ---------------------------------------------------------------------
// app.routes.ts - zastosowanie guarda
export const routes: Routes = [
{ path: 'terminy', component: TerminyComponent },
{ path: 'logowanie', component: LogowanieComponent },
{ path: 'moje-rezerwacje', component: MojeRezerwacjeComponent, canActivate: [authGuard] }
];
Komentarz i wyjaśnienie kodu
User.FindFirstValue(ClaimTypes.NameIdentifier) w kontrolerze odczytuje unikalny identyfikator użytkownika ZAKODOWANY w tokenie JWT — dostępny automatycznie dzięki atrybutowi [Authorize], który sprawdza poprawność tokenu z nagłówka Authorization ZANIM kod w ogóle wejdzie do metody kontrolera.
HttpInterceptorFn to mechanizm Angulara przechwytujący WSZYSTKIE wychodzące żądania HTTP w JEDNYM miejscu — zamiast ręcznie dopisywać nagłówek Authorization w KAŻDYM wywołaniu HttpClient osobno, interceptor robi to automatycznie dla całej aplikacji.
CanActivateFn (Route Guard) to Angularowy odpowiednik atrybutu [Authorize] z ASP.NET Core, tylko po stronie FRONTENDU — blokuje WEJŚCIE na daną trasę (np. /moje-rezerwacje), jeśli funkcja zwróci false, przekierowując zamiast tego na stronę logowania. Ważne: to zabezpieczenie WYŁĄCZNIE kosmetyczne/UX — prawdziwe bezpieczeństwo zapewnia [Authorize] na backendzie, bo Route Guard można teoretycznie ominąć, manipulując kodem w przeglądarce.
Ćwiczenie samodzielne
Zadanie kontrolne — 6 pytań powtórkowych (odpowiedz pisemnie, dokładnie jak w części pisemnej egzaminu):
- Czym różni się aplikacja typu SPA (Angular) od aplikacji z renderowaniem po stronie serwera? Podaj jedną zaletę i jedną wadę każdego podejścia. (2 pkt)
- Do czego służy mechanizm CORS i dlaczego jest konieczny w architekturze Angular + ASP.NET Core na dwóch różnych portach? (2 pkt)
- Wyjaśnij różnicę między
@Input()a@Output()w komponentach Angular — w którą stronę płyną dane w każdym przypadku? (2 pkt) - Co to jest
Observablei dlaczego samo wywołanie metody serwisu zwracającejObservable(bez.subscribe()) NIE wysyła żadnego żądania HTTP? (2 pkt) - Jaką rolę pełni
ThenIncludew zapytaniach Entity Framework Core? Podaj przykład sytuacji, w której jego brak spowoduje puste dane. (2 pkt) - Czym różni się uwierzytelnianie przez ciasteczka sesyjne od uwierzytelniania tokenem JWT? W jakiej architekturze (Razor Pages czy Angular+API) częściej stosuje się każde z nich? (2 pkt)
Maksymalna liczba punktów: 12. Sprawdź swoje odpowiedzi z lekcjami: architektura SPA (1), CORS (12), Angular Input/Output (10), Observable/HttpClient (12), relacje EF Core (17), sesje/logowanie (13-14, 27).
Następnie: zanim napiszesz kod tego projektu, narysuj na kartce: (1) diagram relacji Termin-Rezerwacja-Użytkownik, (2) mapę tras Angular z zaznaczeniem, które wymagają Route Guard, (3) sekwencję żądań między logowaniem a pierwszą chronioną akcją (jak token trafia z logowania do kolejnych żądań).
Zadania do pracy własnej
Zaimplementuj model
Termini publiczną listę terminów w Angularze (bez jeszcze logowania i rezerwacji) — na sztywno wpisane dane w backendzie, tylko GET.Dodaj rejestrację i logowanie (ASP.NET Core Identity zwracające JWT) oraz stronę "Moje rezerwacje" chronioną Route Guardem — na razie bez ochrony przed podwójną rezerwacją.
Zaimplementuj CAŁY projekt opisany w teorii: rejestrację, logowanie z JWT, listę terminów, rezerwację z ochroną przed podwójnym zapisem, "Moje rezerwacje" z możliwością anulowania (sprawdzającą, że anulowana rezerwacja NALEŻY do zalogowanego użytkownika — nie pozwalaj anulować cudzej rezerwacji nawet znając jej Id). Na koniec przygotuj projekt do wdrożenia zgodnie z lekcją 18 (ng build + dotnet publish, środowisko Production).
Typowe błędy
Sprawdzanie dostępności terminu TYLKO przy wyświetleniu listy, nie PONOWNIE przy faktycznej rezerwacji — otwiera drogę do podwójnej rezerwacji tego samego terminu.
Poleganie WYŁĄCZNIE na Route Guard (frontend) jako zabezpieczeniu bez [Authorize] na backendzie — Route Guard chroni tylko WYGLĄD aplikacji (UX), a nie same dane; bez zabezpieczenia na backendzie każdy mógłby wysłać żądanie bezpośrednio (np. przez Postmana), omijając Angular całkowicie.
Brak sprawdzenia właściciela rezerwacji przy anulowaniu — pozwoliłoby to dowolnemu zalogowanemu użytkownikowi anulować cudzą rezerwację, znając tylko jej Id (luka kontroli dostępu, IDOR).
Nawiązanie do egzaminu zawodowego
Ten projekt końcowy syntetyzuje WSZYSTKIE efekty INF.04.7 — środowisko (Visual Studio, lekcje 1-3), frameworki (Node.js, TypeScript, Bootstrap, jQuery, Angular, ASP.NET Core, lekcje 5-12), dynamiczne formularze i logowanie (lekcje 11, 14), bazę danych z relacjami (lekcje 15-17) i funkcjonalność serwisu rezerwacyjnego (wymienioną wprost w podstawie programowej). Razem z siedmioma poprzednimi projektami (BMI, kolorowy zgadywacz, sklep internetowy, zamówienia restauracji, portal społecznościowy, serwis ogłoszeniowy, ankiety) dotykasz WSZYSTKICH czterech "typowych funkcjonalności" wymienionych w INF.04.7.3: e-sklepu, portalu społecznościowego, serwisu ogłoszeniowego i serwisu rezerwacyjnego. Gratulacje z ukończenia wymaganej części działu "Aplikacje webowe" — jeśli chcesz pójść dalej, na końcu działu (po przerwie oznaczonej wyraźną linią) znajdziesz dodatkowy, nieobowiązkowy blok o Blazor.