Skip to content

Como Fazer: Sistema de Reserva de Recursos

Visão Geral

Este guia cobre a construção de uma API de reserva de recursos com o NENE2. As funcionalidades incluem imposição de capacidade, prevenção de reservas duplas, isolamento IDOR por usuário e cancelamento administrativo.

Implementação de referência: ../NENE2-FT/bookinglog/


Design do Schema

sql
CREATE TABLE IF NOT EXISTS resources (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    name       TEXT    NOT NULL UNIQUE,
    capacity   INTEGER NOT NULL DEFAULT 1,
    created_at TEXT    NOT NULL
);

CREATE TABLE IF NOT EXISTS bookings (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    resource_id INTEGER NOT NULL,
    user_id     INTEGER NOT NULL,
    slot_date   TEXT    NOT NULL,   -- 'YYYY-MM-DD'
    slot_hour   INTEGER NOT NULL,   -- 0-23
    created_at  TEXT    NOT NULL,
    cancelled   INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY (resource_id) REFERENCES resources(id) ON DELETE CASCADE,
    UNIQUE (resource_id, user_id, slot_date, slot_hour)
);

Restrições principais:

  • UNIQUE (resource_id, user_id, slot_date, slot_hour) — uma reserva por usuário por slot.
  • Flag de soft-delete cancelled — preserva o histórico enquanto permite novas reservas.
  • Capacidade verificada no momento da consulta (conta reservas ativas vs resource.capacity).

Tabela de Rotas

MétodoCaminhoAuthDescrição
GET/resourcesNenhumaListar todos os recursos
POST/resourcesAdminCriar um recurso
POST/bookingsUsuárioReservar um slot
GET/bookingsUsuárioListar próprias reservas
GET/bookings/{id}UsuárioObter uma reserva
DELETE/bookings/{id}Usuário/AdminCancelar uma reserva

Prevenção de Reservas Duplas

Primeiro, verificar se o usuário já tem este slot (nível de aplicação):

php
$stmt = $this->pdo->prepare(
    'SELECT id FROM bookings WHERE resource_id = :rid AND user_id = :uid
     AND slot_date = :d AND slot_hour = :h AND cancelled = 0'
);
$stmt->execute([...]);
if ($stmt->fetch() !== false) {
    return 'double_booking';
}

Em seguida, verificar capacidade:

php
$count = $this->countSlotBookings($resourceId, $date, $hour);
if ($count >= (int) $resource['capacity']) {
    return 'capacity_full';
}

Isolamento IDOR

Os usuários só podem ler/cancelar suas próprias reservas. Retorne 404 (não 403) para evitar revelar existência:

php
if (!$this->isAdmin($req) && (int) $booking['user_id'] !== $uid) {
    return $this->problem(404, 'not-found', 'Booking not found.');
}

Admin Cancela Sem X-User-Id

O admin pode cancelar qualquer reserva sem fornecer seu próprio ID de usuário:

php
$isAdmin = $this->isAdmin($req);
$uid     = $this->uid($req);
if ($uid === null && !$isAdmin) {
    return $this->problem(400, 'bad-request', 'X-User-Id required.');
}
$result = $this->repo->cancel($id, $uid ?? 0, $isAdmin);

Regras de Validação

CampoRegra
resource_idis_int() + positivo
slot_dateregex /\A\d{4}-\d{2}-\d{2}\z/
slot_houris_int() + 0–23
capacityis_int() + positivo
namestring não vazia

Códigos de Status HTTP

SituaçãoStatus
Recurso criado201
Reserva confirmada201
Reserva encontrada / lista200
Sem X-User-Id400
Tipo de campo inválido422
Formato de data inválido422
slot_hour fora de 0–23422
Recurso não encontrado404
Reserva não encontrada404
Sem chave admin403
Cancelar própria reserva200
Cancelar reserva de outro403
Reserva dupla409
Capacidade cheia409

Padrões VULN Cobertos

VULNPadrãoDefesa
AIDOR: usuário vê reserva de outroWHERE user_id = :uid + 404
Bresource_id negativoVerificação is_int() + > 0
Cslot_hour zero (meia-noite)Intervalo 0-23 permite 0
DInjeção SQL em slot_dateValidação regex + query parametrizada
EConfusão de tipo resource_id stringVerificação estrita is_int()
FReserva duplaVerificação de existência antes do INSERT
GOverflow de capacidadeVerificação de COUNT vs capacity
HSem X-User-Id400 com mensagem
ICancelar reserva de outro usuárioVerificação de propriedade user_id → 403
JLista vaza dados de outro usuárioWHERE user_id = :uid
KAdmin cancela qualquer reservaBypass de propriedade isAdmin
Lslot_hour = 24 (fora do intervalo)$hour > 23 → 422

Publicado sob a licença MIT.