Dataczwartek, 13 sierpnia 2026 Czas19:06:18
← Aplikacje webowe

Lekcja 27. Projekt 8 (końcowy): System rezerwacji wizyt — Angular + logowanie + zadanie kontrolne

Trudny / egzaminacyjny

Po 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):

  1. 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)
  2. 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)
  3. 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)
  4. Co to jest Observable i dlaczego samo wywołanie metody serwisu zwracającej Observable (bez .subscribe()) NIE wysyła żadnego żądania HTTP? (2 pkt)
  5. Jaką rolę pełni ThenInclude w zapytaniach Entity Framework Core? Podaj przykład sytuacji, w której jego brak spowoduje puste dane. (2 pkt)
  6. 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

  1. Zaimplementuj model Termin i publiczną listę terminów w Angularze (bez jeszcze logowania i rezerwacji) — na sztywno wpisane dane w backendzie, tylko GET.

  2. 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ą.

  3. 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.