Files
2026-10-21 18:34:35 +02:00

800 lines
28 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: Test REST API
version: 1.0.0
description: |
Testowy serwis REST API (CakePHP 5): klienci (`customers`), ich auta (`cars`) i użytkownicy (`users`).
## Uwierzytelnianie
1. Wywołaj `POST /api/auth/login` z e-mailem i hasłem.
2. Skopiuj `data.token` i kliknij **Authorize** (u góry strony) – wklej sam token (bez `Bearer`).
3. Token wysyłany jest w nagłówku `Authorization: Bearer <token>` i jest ważny 8 godzin.
Konta testowe:
| Rola | E-mail | Hasło |
|-------|---------------------|------------|
| admin | admin@example.com | Admin123! |
| user | user@example.com | User1234! |
## Uprawnienia
* **admin** – pełny dostęp, w tym CRUD użytkowników (`/api/users`).
* **user** – logowanie, własny profil (`/api/auth/me`), CRUD klientów i aut.
## Format odpowiedzi
* Sukces: `{"success": true, "data": ...}` (listy dodatkowo mają `meta` z paginacją).
* Błąd: `{"success": false, "error": {"code": 422, "message": "...", "fields": {...}}}`.
* `DELETE` zwraca `204 No Content`.
## Listy
Wszystkie listy obsługują `page`, `limit` (domyślnie 20, max 100), `sort` i `direction` (`asc`/`desc`).
Łączna liczba rekordów jest również w nagłówku `X-Total-Count`.
servers:
- url: /
description: Ten serwer
security:
- bearerAuth: []
tags:
- name: Auth
description: Logowanie i profil zalogowanego użytkownika
- name: Customers
description: Klienci (wszyscy zalogowani)
- name: Cars
description: Auta klientów (wszyscy zalogowani)
- name: Users
description: Użytkownicy systemu (tylko admin)
paths:
/api/auth/login:
post:
tags: [Auth]
summary: Logowanie – zwraca token JWT
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password]
properties:
email: { type: string, format: email, example: admin@example.com }
password: { type: string, format: password, example: Admin123! }
responses:
'200':
description: Zalogowano
content:
application/json:
schema:
type: object
properties:
success: { type: boolean, example: true }
data:
type: object
properties:
token: { type: string, example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... }
token_type: { type: string, example: Bearer }
expires_in: { type: integer, description: Czas ważności w sekundach, example: 28800 }
user: { $ref: '#/components/schemas/User' }
'401': { $ref: '#/components/responses/Unauthorized' }
/api/auth/me:
get:
tags: [Auth]
summary: Profil zalogowanego użytkownika
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/UserResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
patch:
tags: [Auth]
summary: Edycja własnego profilu (imię, nazwisko, e-mail, hasło)
description: Pola `role` i `active` są ignorowane – może je zmienić tylko admin przez `/api/users/{id}`.
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ProfileInput' }
responses:
'200':
description: Zaktualizowano
content:
application/json:
schema: { $ref: '#/components/schemas/UserResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/ValidationError' }
put:
tags: [Auth]
summary: Edycja własnego profilu (alias PATCH)
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ProfileInput' }
responses:
'200':
description: Zaktualizowano
content:
application/json:
schema: { $ref: '#/components/schemas/UserResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/ValidationError' }
/api/customers:
get:
tags: [Customers]
summary: Lista klientów
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- name: sort
in: query
schema:
type: string
enum: [id, type, first_name, last_name, email, company_name, city, country, created, modified]
- $ref: '#/components/parameters/direction'
- name: q
in: query
description: Szukaj w imieniu, nazwisku, e-mailu, telefonie, nazwie firmy, mieście i NIP
schema: { type: string }
- name: type
in: query
schema: { type: string, enum: [individual, company] }
- name: city
in: query
schema: { type: string }
- name: country
in: query
schema: { type: string, example: PL }
- name: with_cars
in: query
description: Dołącz listę aut każdego klienta
schema: { type: boolean }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
success: { type: boolean, example: true }
data:
type: array
items: { $ref: '#/components/schemas/Customer' }
meta: { $ref: '#/components/schemas/PaginationMeta' }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [Customers]
summary: Dodaj klienta
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CustomerCreate' }
responses:
'201':
description: Utworzono
content:
application/json:
schema: { $ref: '#/components/schemas/CustomerResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/ValidationError' }
/api/customers/{id}:
parameters:
- $ref: '#/components/parameters/id'
get:
tags: [Customers]
summary: Szczegóły klienta (z autami)
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
success: { type: boolean, example: true }
data: { $ref: '#/components/schemas/CustomerWithCars' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [Customers]
summary: Częściowa edycja klienta
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CustomerInput' }
responses:
'200':
description: Zaktualizowano
content:
application/json:
schema: { $ref: '#/components/schemas/CustomerResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
put:
tags: [Customers]
summary: Edycja klienta
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CustomerInput' }
responses:
'200':
description: Zaktualizowano
content:
application/json:
schema: { $ref: '#/components/schemas/CustomerResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
delete:
tags: [Customers]
summary: Usuń klienta (razem z jego autami)
responses:
'204': { description: Usunięto }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/customers/{customer_id}/cars:
parameters:
- name: customer_id
in: path
required: true
schema: { type: integer, example: 1 }
get:
tags: [Cars]
summary: Auta danego klienta
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/carSort'
- $ref: '#/components/parameters/direction'
responses:
'200': { $ref: '#/components/responses/CarList' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
post:
tags: [Cars]
summary: Dodaj auto do klienta
description: '`customer_id` jest brany ze ścieżki.'
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CarCreateNested' }
responses:
'201':
description: Utworzono
content:
application/json:
schema: { $ref: '#/components/schemas/CarResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
/api/cars:
get:
tags: [Cars]
summary: Lista aut
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- $ref: '#/components/parameters/carSort'
- $ref: '#/components/parameters/direction'
- name: q
in: query
description: Szukaj w marce, modelu, VIN i numerze rejestracyjnym
schema: { type: string }
- name: customer_id
in: query
schema: { type: integer }
- name: make
in: query
schema: { type: string, example: Toyota }
- name: fuel_type
in: query
schema: { $ref: '#/components/schemas/FuelType' }
- name: transmission
in: query
schema: { type: string, enum: [manual, automatic] }
- name: year_from
in: query
schema: { type: integer, example: 2015 }
- name: year_to
in: query
schema: { type: integer, example: 2022 }
- name: with_customer
in: query
description: Dołącz dane właściciela
schema: { type: boolean }
responses:
'200': { $ref: '#/components/responses/CarList' }
'401': { $ref: '#/components/responses/Unauthorized' }
post:
tags: [Cars]
summary: Dodaj auto
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CarCreate' }
responses:
'201':
description: Utworzono
content:
application/json:
schema: { $ref: '#/components/schemas/CarResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'422': { $ref: '#/components/responses/ValidationError' }
/api/cars/{id}:
parameters:
- $ref: '#/components/parameters/id'
get:
tags: [Cars]
summary: Szczegóły auta (z właścicielem)
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
success: { type: boolean, example: true }
data: { $ref: '#/components/schemas/CarWithCustomer' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [Cars]
summary: Częściowa edycja auta
description: Zmiana `customer_id` przenosi auto do innego klienta.
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CarInput' }
responses:
'200':
description: Zaktualizowano
content:
application/json:
schema: { $ref: '#/components/schemas/CarResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
put:
tags: [Cars]
summary: Edycja auta
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CarInput' }
responses:
'200':
description: Zaktualizowano
content:
application/json:
schema: { $ref: '#/components/schemas/CarResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
delete:
tags: [Cars]
summary: Usuń auto
responses:
'204': { description: Usunięto }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/users:
get:
tags: [Users]
summary: Lista użytkowników (admin)
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/limit'
- name: sort
in: query
schema:
type: string
enum: [id, email, first_name, last_name, role, active, last_login, created, modified]
- $ref: '#/components/parameters/direction'
- name: q
in: query
description: Szukaj w e-mailu, imieniu i nazwisku
schema: { type: string }
- name: role
in: query
schema: { $ref: '#/components/schemas/Role' }
- name: active
in: query
schema: { type: boolean }
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
success: { type: boolean, example: true }
data:
type: array
items: { $ref: '#/components/schemas/User' }
meta: { $ref: '#/components/schemas/PaginationMeta' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
post:
tags: [Users]
summary: Dodaj użytkownika (admin)
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UserCreate' }
responses:
'201':
description: Utworzono
content:
application/json:
schema: { $ref: '#/components/schemas/UserResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'422': { $ref: '#/components/responses/ValidationError' }
/api/users/{id}:
parameters:
- $ref: '#/components/parameters/id'
get:
tags: [Users]
summary: Szczegóły użytkownika (admin)
responses:
'200':
description: OK
content:
application/json:
schema: { $ref: '#/components/schemas/UserResponse' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
patch:
tags: [Users]
summary: Częściowa edycja użytkownika (admin)
description: Admin nie może zmienić własnej roli ani dezaktywować własnego konta.
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UserInput' }
responses:
'200':
description: Zaktualizowano
content:
application/json:
schema: { $ref: '#/components/schemas/UserResponse' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
put:
tags: [Users]
summary: Edycja użytkownika (admin)
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/UserInput' }
responses:
'200':
description: Zaktualizowano
content:
application/json:
schema: { $ref: '#/components/schemas/UserResponse' }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/ValidationError' }
delete:
tags: [Users]
summary: Usuń użytkownika (admin)
description: Nie można usunąć własnego konta.
responses:
'204': { description: Usunięto }
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
id:
name: id
in: path
required: true
schema: { type: integer, example: 1 }
page:
name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
limit:
name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
direction:
name: direction
in: query
schema: { type: string, enum: [asc, desc], default: asc }
carSort:
name: sort
in: query
schema:
type: string
enum: [id, customer_id, make, model, production_year, registration_number, fuel_type, mileage, power_hp, created, modified]
responses:
Unauthorized:
description: Brak lub nieprawidłowy token / błędne dane logowania
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
example: { success: false, error: { code: 401, message: Authentication is required to continue } }
Forbidden:
description: Brak uprawnień (wymagana rola admin)
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
example: { success: false, error: { code: 403, message: This action requires the admin role. } }
NotFound:
description: Nie znaleziono rekordu
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
example: { success: false, error: { code: 404, message: Record not found in table `customers`. } }
BadRequest:
description: Niedozwolona operacja
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
ValidationError:
description: Błędy walidacji (klucz = pole, wartość = reguła => komunikat)
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
example:
success: false
error:
code: 422
message: Validation failed.
fields:
email: { _isUnique: This email is already in use. }
phone: { _required: This field is required }
CarList:
description: OK
content:
application/json:
schema:
type: object
properties:
success: { type: boolean, example: true }
data:
type: array
items: { $ref: '#/components/schemas/Car' }
meta: { $ref: '#/components/schemas/PaginationMeta' }
schemas:
Error:
type: object
properties:
success: { type: boolean, example: false }
error:
type: object
properties:
code: { type: integer }
message: { type: string }
fields:
type: object
additionalProperties:
type: object
additionalProperties: { type: string }
PaginationMeta:
type: object
properties:
page: { type: integer, example: 1 }
limit: { type: integer, example: 20 }
total: { type: integer, example: 100 }
pages: { type: integer, example: 5 }
has_next_page: { type: boolean, example: true }
has_prev_page: { type: boolean, example: false }
sort: { type: string, nullable: true, example: id }
direction: { type: string, nullable: true, example: asc }
Role:
type: string
enum: [admin, user]
FuelType:
type: string
enum: [petrol, diesel, lpg, hybrid, electric]
# ---------- Users ----------
User:
type: object
properties:
id: { type: integer, readOnly: true, example: 1 }
email: { type: string, format: email, example: admin@example.com }
first_name: { type: string, example: Adam }
last_name: { type: string, example: Administrator }
role: { $ref: '#/components/schemas/Role' }
active: { type: boolean, example: true }
last_login: { type: string, format: date-time, nullable: true, readOnly: true }
created: { type: string, format: date-time, readOnly: true }
modified: { type: string, format: date-time, readOnly: true }
UserInput:
type: object
properties:
email: { type: string, format: email, maxLength: 255 }
password: { type: string, format: password, minLength: 8, maxLength: 72 }
first_name: { type: string, maxLength: 100 }
last_name: { type: string, maxLength: 100 }
role: { $ref: '#/components/schemas/Role' }
active: { type: boolean }
UserCreate:
allOf:
- $ref: '#/components/schemas/UserInput'
- type: object
required: [email, password, first_name, last_name]
example:
email: jan.kowalski@example.com
password: Haslo123!
first_name: Jan
last_name: Kowalski
role: user
active: true
ProfileInput:
type: object
properties:
email: { type: string, format: email }
password: { type: string, format: password, minLength: 8 }
first_name: { type: string }
last_name: { type: string }
example:
first_name: Jan
password: NoweHaslo123!
UserResponse:
type: object
properties:
success: { type: boolean, example: true }
data: { $ref: '#/components/schemas/User' }
# ---------- Customers ----------
CustomerInput:
type: object
properties:
type:
type: string
enum: [individual, company]
default: individual
description: Dla `company` wymagane jest `company_name`
first_name: { type: string, maxLength: 100, example: Anna }
last_name: { type: string, maxLength: 100, example: Nowak }
email: { type: string, format: email, maxLength: 255, description: Unikalny, example: anna.nowak@example.com }
phone: { type: string, pattern: '^\+?[0-9 ()-]{6,30}$', example: '+48 600 100 200' }
company_name: { type: string, nullable: true, maxLength: 255, example: null }
tax_id: { type: string, nullable: true, pattern: '^[0-9]{10}$', description: NIP (10 cyfr), example: null }
street: { type: string, nullable: true, maxLength: 255, example: ul. Długa 5/12 }
city: { type: string, nullable: true, maxLength: 100, example: Kraków }
postal_code: { type: string, nullable: true, pattern: '^[0-9]{2}-[0-9]{3}$', example: 30-001 }
country: { type: string, pattern: '^[A-Z]{2}$', default: PL, description: ISO 3166-1 alpha-2, example: PL }
birth_date: { type: string, format: date, nullable: true, example: '1985-04-12' }
notes: { type: string, nullable: true, maxLength: 5000, example: Preferuje kontakt telefoniczny. }
CustomerCreate:
allOf:
- $ref: '#/components/schemas/CustomerInput'
- type: object
required: [first_name, last_name, email, phone]
Customer:
allOf:
- type: object
properties:
id: { type: integer, readOnly: true, example: 1 }
- $ref: '#/components/schemas/CustomerInput'
- type: object
properties:
created_by: { type: integer, nullable: true, readOnly: true, description: ID użytkownika, który dodał klienta }
created: { type: string, format: date-time, readOnly: true }
modified: { type: string, format: date-time, readOnly: true }
CustomerWithCars:
allOf:
- $ref: '#/components/schemas/Customer'
- type: object
properties:
cars:
type: array
items: { $ref: '#/components/schemas/Car' }
CustomerResponse:
type: object
properties:
success: { type: boolean, example: true }
data: { $ref: '#/components/schemas/Customer' }
# ---------- Cars ----------
CarFields:
type: object
properties:
make: { type: string, maxLength: 50, example: Toyota }
model: { type: string, maxLength: 50, example: Corolla }
production_year: { type: integer, minimum: 1900, example: 2020 }
vin:
type: string
pattern: '^[A-HJ-NPR-Z0-9]{17}$'
description: 17 znaków, bez liter I, O, Q. Unikalny. Zapisywany wielkimi literami.
example: JTDBR32E720123456
registration_number:
type: string
description: 4-10 liter/cyfr. Unikalny. Spacje są usuwane, litery zamieniane na wielkie.
example: KR12345
fuel_type: { $ref: '#/components/schemas/FuelType' }
color: { type: string, nullable: true, maxLength: 30, example: srebrny }
engine_capacity: { type: integer, nullable: true, minimum: 0, maximum: 10000, description: Pojemność w cm³, example: 1798 }
power_hp: { type: integer, nullable: true, minimum: 1, maximum: 2000, description: Moc w KM, example: 122 }
transmission: { type: string, nullable: true, enum: [manual, automatic], example: automatic }
mileage: { type: integer, nullable: true, minimum: 0, description: Przebieg w km, example: 45000 }
notes: { type: string, nullable: true, maxLength: 5000, example: null }
CarInput:
allOf:
- type: object
properties:
customer_id: { type: integer, example: 1 }
- $ref: '#/components/schemas/CarFields'
CarCreate:
allOf:
- $ref: '#/components/schemas/CarInput'
- type: object
required: [customer_id, make, model, production_year, vin, registration_number, fuel_type]
CarCreateNested:
allOf:
- $ref: '#/components/schemas/CarFields'
- type: object
required: [make, model, production_year, vin, registration_number, fuel_type]
Car:
allOf:
- type: object
properties:
id: { type: integer, readOnly: true, example: 1 }
- $ref: '#/components/schemas/CarInput'
- type: object
properties:
created: { type: string, format: date-time, readOnly: true }
modified: { type: string, format: date-time, readOnly: true }
CarWithCustomer:
allOf:
- $ref: '#/components/schemas/Car'
- type: object
properties:
customer: { $ref: '#/components/schemas/Customer' }
CarResponse:
type: object
properties:
success: { type: boolean, example: true }
data: { $ref: '#/components/schemas/Car' }