Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14b85d28a1 | ||
|
|
f6d7d42f10 |
+115
-1
@@ -215,6 +215,114 @@ Ao implementar endpoints:
|
|||||||
|
|
||||||
Não altere contratos existentes sem avaliar seus consumidores.
|
Não altere contratos existentes sem avaliar seus consumidores.
|
||||||
|
|
||||||
|
## 9.1. Identificadores nos endpoints
|
||||||
|
|
||||||
|
**Endpoints devem utilizar exclusivamente UUID como identificador de recursos.**
|
||||||
|
|
||||||
|
É **proibido expor ou utilizar IDs numéricos (`id`) nos endpoints da API**, tanto em parâmetros de rota quanto em parâmetros destinados a identificar recursos.
|
||||||
|
|
||||||
|
Exemplos proibidos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /produtos/123
|
||||||
|
GET /clientes/456
|
||||||
|
GET /documentos-fiscais/789
|
||||||
|
DELETE /produtos/123
|
||||||
|
PUT /clientes/456
|
||||||
|
```
|
||||||
|
|
||||||
|
O padrão obrigatório é utilizar o `uuid`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /produtos/{uuid}
|
||||||
|
GET /clientes/{uuid}
|
||||||
|
GET /documentos-fiscais/{uuid}
|
||||||
|
DELETE /produtos/{uuid}
|
||||||
|
PUT /clientes/{uuid}
|
||||||
|
```
|
||||||
|
|
||||||
|
Também é proibido utilizar `id` numérico em:
|
||||||
|
|
||||||
|
* parâmetros de rota;
|
||||||
|
* query parameters quando o parâmetro representa a identificação de um recurso;
|
||||||
|
* payloads de entrada quando o objetivo for referenciar uma entidade externamente;
|
||||||
|
* respostas da API como identificador público;
|
||||||
|
* links e relacionamentos retornados pela API;
|
||||||
|
* filtros de endpoints que tenham como finalidade localizar uma entidade específica.
|
||||||
|
|
||||||
|
Exemplo proibido:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cliente_id": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Quando a referência for necessária através do contrato da API, utilize:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cliente_uuid": "019..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
ou, conforme o padrão já estabelecido no projeto:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cliente": "019..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A nomenclatura deve seguir o padrão existente na API. Não crie uma nova convenção sem antes verificar como os recursos relacionados já são representados.
|
||||||
|
|
||||||
|
### Uso interno do `id`
|
||||||
|
|
||||||
|
O `id` numérico pode continuar sendo utilizado **internamente pela aplicação e pelo banco de dados**, quando essa for a estrutura existente.
|
||||||
|
|
||||||
|
Por exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
API
|
||||||
|
↓
|
||||||
|
UUID
|
||||||
|
↓
|
||||||
|
Controller / DTO
|
||||||
|
↓
|
||||||
|
Service
|
||||||
|
↓
|
||||||
|
Repository
|
||||||
|
↓
|
||||||
|
id interno
|
||||||
|
↓
|
||||||
|
Banco de dados
|
||||||
|
```
|
||||||
|
|
||||||
|
Nesse cenário, o UUID é o identificador público e o `id` é apenas um identificador interno.
|
||||||
|
|
||||||
|
É permitido, por exemplo, utilizar:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
uuid,
|
||||||
|
nome
|
||||||
|
FROM cliente
|
||||||
|
WHERE uuid = :uuid;
|
||||||
|
```
|
||||||
|
|
||||||
|
e posteriormente utilizar o `id` internamente para relacionamentos, JOINs ou operações no banco, desde que o `id` **não seja exposto ao consumidor da API**.
|
||||||
|
|
||||||
|
### Regra absoluta
|
||||||
|
|
||||||
|
**`id` numérico é identificador interno. `uuid` é identificador público.**
|
||||||
|
|
||||||
|
Nenhum endpoint deve exigir que o consumidor conheça ou forneça o `id` numérico de uma entidade.
|
||||||
|
|
||||||
|
Antes de implementar ou alterar um endpoint, verifique se todos os identificadores expostos pelo contrato da API utilizam UUID.
|
||||||
|
|
||||||
|
Caso um endpoint existente utilize `id` numérico, não faça a alteração automaticamente. Analise os consumidores, apresente o impacto e solicite autorização antes de alterar o contrato.
|
||||||
|
|
||||||
## 10. Alterações mínimas
|
## 10. Alterações mínimas
|
||||||
|
|
||||||
Altere somente o necessário para atender à tarefa.
|
Altere somente o necessário para atender à tarefa.
|
||||||
@@ -374,9 +482,15 @@ Antes de finalizar uma implementação:
|
|||||||
* [ ] Nenhuma refatoração desnecessária.
|
* [ ] Nenhuma refatoração desnecessária.
|
||||||
* [ ] Nenhum segredo ou credencial exposto.
|
* [ ] Nenhum segredo ou credencial exposto.
|
||||||
* [ ] Resultado informado ao usuário.
|
* [ ] Resultado informado ao usuário.
|
||||||
|
* [ ] Endpoints utilizam exclusivamente UUID como identificador público.
|
||||||
|
* [ ] Nenhum `id` numérico é exposto ou exigido pelos endpoints.
|
||||||
|
* [ ] IDs numéricos são utilizados somente internamente, quando necessário.
|
||||||
|
* [ ] Alterações em endpoints existentes que utilizam IDs numéricos foram analisadas antes de modificar o contrato.
|
||||||
|
|
||||||
## Regra de ouro
|
## Regra de ouro
|
||||||
|
**Use exclusivamente UUID nos endpoints.
|
||||||
|
Nunca exponha ou exija IDs numéricos (`id`) na API.
|
||||||
|
`id` é identificador interno; `uuid` é identificador público.**
|
||||||
**Planeje antes de implementar.
|
**Planeje antes de implementar.
|
||||||
Peça autorização antes de alterar.
|
Peça autorização antes de alterar.
|
||||||
Nunca utilize `SELECT *`.
|
Nunca utilize `SELECT *`.
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
namespace WorkbloomERP\Module\Auth\Controller;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use KrothiumAPI\Utils\HttpUtil;
|
||||||
|
use WorkbloomERP\Module\Auth\DTO\LoginDTO;
|
||||||
|
use WorkbloomERP\Module\Auth\Service\AuthService;
|
||||||
|
use WorkbloomERP\Security\AuthContext;
|
||||||
|
|
||||||
|
class AuthController {
|
||||||
|
private AuthService $authService;
|
||||||
|
|
||||||
|
public function __construct(?AuthService $authService = null) {
|
||||||
|
$this->authService = $authService ?? new AuthService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint de login da API.
|
||||||
|
* POST /api/v1/auth/login
|
||||||
|
*/
|
||||||
|
public function login(): void {
|
||||||
|
$input = HttpUtil::getRequestBody('POST');
|
||||||
|
|
||||||
|
if (empty($input) || !is_array($input)) {
|
||||||
|
HttpUtil::error('Corpo da requisição inválido ou vazio.', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray($input);
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
$errors = json_decode($e->getMessage(), true) ?? ['login' => $e->getMessage()];
|
||||||
|
HttpUtil::error('Dados inválidos para autenticação.', 422, ['validation_errors' => $errors]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $this->authService->login($dto);
|
||||||
|
HttpUtil::success($result, 'Autenticação realizada com sucesso.');
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
$code = $e->getCode();
|
||||||
|
$statusCode = ($code >= 400 && $code <= 499) ? $code : 401;
|
||||||
|
HttpUtil::error($e->getMessage(), $statusCode);
|
||||||
|
} catch (Throwable) {
|
||||||
|
HttpUtil::error('Erro ao processar autenticação.', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint de consulta do perfil do usuário autenticado.
|
||||||
|
* GET /api/v1/auth/me
|
||||||
|
*/
|
||||||
|
public function me(): void {
|
||||||
|
$userUuid = AuthContext::getUserUuid();
|
||||||
|
|
||||||
|
if ($userUuid === null) {
|
||||||
|
HttpUtil::error('Usuário não autenticado.', 401);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$profile = $this->authService->getProfile($userUuid);
|
||||||
|
HttpUtil::success($profile, 'Perfil recuperado com sucesso.');
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
$code = $e->getCode();
|
||||||
|
$statusCode = ($code >= 400 && $code <= 499) ? $code : 404;
|
||||||
|
HttpUtil::error($e->getMessage(), $statusCode);
|
||||||
|
} catch (Throwable) {
|
||||||
|
HttpUtil::error('Erro ao buscar perfil do usuário.', 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
namespace WorkbloomERP\Module\Auth\DTO;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
class LoginDTO {
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $login,
|
||||||
|
public readonly string $senha
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instancia e valida a partir dos dados da requisição.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return self
|
||||||
|
* @throws InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public static function fromArray(array $data): self {
|
||||||
|
$login = trim((string)($data['login'] ?? $data['email'] ?? $data['nome_usuario'] ?? ''));
|
||||||
|
$senha = (string)($data['senha'] ?? $data['password'] ?? '');
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($login === '') {
|
||||||
|
$errors['login'] = 'O campo login (e-mail ou nome de usuário) é obrigatório.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($senha === '') {
|
||||||
|
$errors['senha'] = 'O campo senha é obrigatório.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
throw new InvalidArgumentException(json_encode($errors, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
login: $login,
|
||||||
|
senha: $senha
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
namespace WorkbloomERP\Module\Auth\Repository;
|
||||||
|
|
||||||
|
use KrothiumAPI\Database\DBManager;
|
||||||
|
use KrothiumAPI\Database\Contracts\DriverInterface;
|
||||||
|
|
||||||
|
class AuthRepository {
|
||||||
|
private DriverInterface $db;
|
||||||
|
|
||||||
|
public function __construct(?DriverInterface $db = null) {
|
||||||
|
$this->db = $db ?? DBManager::getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Localiza usuário ativo e não deletado por e-mail ou nome de usuário.
|
||||||
|
* Retorna apenas colunas estritamente necessárias. NUNCA SELECT *.
|
||||||
|
*
|
||||||
|
* @param string $login E-mail ou nome_usuario
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function findByLogin(string $login): ?array {
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
u.id,
|
||||||
|
u.uuid,
|
||||||
|
u.nome_completo,
|
||||||
|
u.cpf,
|
||||||
|
u.email,
|
||||||
|
u.nome_usuario,
|
||||||
|
u.senha_hash,
|
||||||
|
u.ativo
|
||||||
|
FROM usuario u
|
||||||
|
WHERE (u.email = :login OR u.nome_usuario = :login)
|
||||||
|
AND u.deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
";
|
||||||
|
|
||||||
|
return $this->db->fetchOne($sql, ['login' => $login]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Localiza dados cadastrais do usuário por UUID (sem expor id ou hash de senha).
|
||||||
|
*
|
||||||
|
* @param string $uuid
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function findByUuid(string $uuid): ?array {
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
u.uuid,
|
||||||
|
u.nome_completo,
|
||||||
|
u.cpf,
|
||||||
|
u.email,
|
||||||
|
u.nome_usuario,
|
||||||
|
u.ativo,
|
||||||
|
u.created_at
|
||||||
|
FROM usuario u
|
||||||
|
WHERE u.uuid = :uuid
|
||||||
|
AND u.deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
";
|
||||||
|
|
||||||
|
return $this->db->fetchOne($sql, ['uuid' => $uuid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Localiza dados cadastrais do usuário por ID (compatibilidade interna).
|
||||||
|
*
|
||||||
|
* @param int $id
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
public function findById(int $id): ?array {
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
u.uuid,
|
||||||
|
u.nome_completo,
|
||||||
|
u.cpf,
|
||||||
|
u.email,
|
||||||
|
u.nome_usuario,
|
||||||
|
u.ativo,
|
||||||
|
u.created_at
|
||||||
|
FROM usuario u
|
||||||
|
WHERE u.id = :id
|
||||||
|
AND u.deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
";
|
||||||
|
|
||||||
|
return $this->db->fetchOne($sql, ['id' => $id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista empresas vinculadas ao usuário por UUID (retornando apenas e.uuid como identificador).
|
||||||
|
* NUNCA SELECT *.
|
||||||
|
*
|
||||||
|
* @param string $usuarioUuid
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function findEmpresasByUsuarioUuid(string $usuarioUuid): array {
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
e.uuid,
|
||||||
|
e.nome_empresarial,
|
||||||
|
e.nome_fantasia,
|
||||||
|
e.cnpj,
|
||||||
|
e.tipo_estabelecimento,
|
||||||
|
e.status,
|
||||||
|
ue.perfil,
|
||||||
|
ue.acesso_todas_filiais
|
||||||
|
FROM usuario_empresa ue
|
||||||
|
INNER JOIN empresa e
|
||||||
|
ON e.id = ue.empresa_id
|
||||||
|
INNER JOIN usuario u
|
||||||
|
ON u.id = ue.usuario_id
|
||||||
|
WHERE u.uuid = :usuario_uuid
|
||||||
|
AND e.deleted_at IS NULL
|
||||||
|
AND u.deleted_at IS NULL
|
||||||
|
ORDER BY e.tipo_estabelecimento ASC, e.nome_empresarial ASC
|
||||||
|
";
|
||||||
|
|
||||||
|
return $this->db->fetchAll($sql, ['usuario_uuid' => $usuarioUuid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lista empresas vinculadas ao usuário por ID interno.
|
||||||
|
* Retorna exclusivamente e.uuid como identificador da empresa.
|
||||||
|
*
|
||||||
|
* @param int $usuarioId
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function findEmpresasByUsuarioId(int $usuarioId): array {
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
e.uuid,
|
||||||
|
e.nome_empresarial,
|
||||||
|
e.nome_fantasia,
|
||||||
|
e.cnpj,
|
||||||
|
e.tipo_estabelecimento,
|
||||||
|
e.status,
|
||||||
|
ue.perfil,
|
||||||
|
ue.acesso_todas_filiais
|
||||||
|
FROM usuario_empresa ue
|
||||||
|
INNER JOIN empresa e
|
||||||
|
ON e.id = ue.empresa_id
|
||||||
|
WHERE ue.usuario_id = :usuario_id
|
||||||
|
AND e.deleted_at IS NULL
|
||||||
|
ORDER BY e.tipo_estabelecimento ASC, e.nome_empresarial ASC
|
||||||
|
";
|
||||||
|
|
||||||
|
return $this->db->fetchAll($sql, ['usuario_id' => $usuarioId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
namespace WorkbloomERP\Module\Auth;
|
||||||
|
|
||||||
|
use KrothiumAPI\Http\Router;
|
||||||
|
use WorkbloomERP\Module\Auth\Controller\AuthController;
|
||||||
|
|
||||||
|
class Routes {
|
||||||
|
public static function register(): void {
|
||||||
|
Router::group('/api/v1/auth', function () {
|
||||||
|
Router::post('/login', [AuthController::class, 'login']);
|
||||||
|
Router::get('/me', [AuthController::class, 'me'], ['auth']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
namespace WorkbloomERP\Module\Auth\Service;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use WorkbloomERP\Module\Auth\DTO\LoginDTO;
|
||||||
|
use WorkbloomERP\Module\Auth\Repository\AuthRepository;
|
||||||
|
use WorkbloomERP\Security\JwtService;
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
private AuthRepository $repository;
|
||||||
|
private JwtService $jwtService;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
?AuthRepository $repository = null,
|
||||||
|
?JwtService $jwtService = null
|
||||||
|
) {
|
||||||
|
$this->repository = $repository ?? new AuthRepository();
|
||||||
|
$this->jwtService = $jwtService ?? new JwtService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Realiza a autenticação do usuário com validação de credenciais e status.
|
||||||
|
*
|
||||||
|
* @param LoginDTO $dto
|
||||||
|
* @return array
|
||||||
|
* @throws RuntimeException
|
||||||
|
*/
|
||||||
|
public function login(LoginDTO $dto): array {
|
||||||
|
$user = $this->repository->findByLogin($dto->login);
|
||||||
|
|
||||||
|
// Prevenção contra enumeração de usuários: mensagem genérica para usuário inexistente
|
||||||
|
if ($user === null) {
|
||||||
|
throw new RuntimeException('Credenciais inválidas.', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação de conta ativa
|
||||||
|
if (empty($user['ativo'])) {
|
||||||
|
throw new RuntimeException('Usuário inativo no sistema.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação segura de senha com hash
|
||||||
|
if (!password_verify($dto->senha, $user['senha_hash'])) {
|
||||||
|
throw new RuntimeException('Credenciais inválidas.', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userUuid = (string)$user['uuid'];
|
||||||
|
$empresas = $this->repository->findEmpresasByUsuarioUuid($userUuid);
|
||||||
|
|
||||||
|
$tokenPayload = [
|
||||||
|
'sub' => $userUuid,
|
||||||
|
'email' => $user['email'],
|
||||||
|
'nome_usuario' => $user['nome_usuario'],
|
||||||
|
'nome_completo' => $user['nome_completo'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$tokenData = $this->jwtService->generateToken($tokenPayload);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => $tokenData['token'],
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
'expires_in' => $tokenData['expires_in'],
|
||||||
|
'expires_at' => $tokenData['expires_at'],
|
||||||
|
'usuario' => [
|
||||||
|
'uuid' => $user['uuid'],
|
||||||
|
'nome_completo' => $user['nome_completo'],
|
||||||
|
'cpf' => $user['cpf'],
|
||||||
|
'email' => $user['email'],
|
||||||
|
'nome_usuario' => $user['nome_usuario'],
|
||||||
|
],
|
||||||
|
'empresas' => $empresas,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna os dados do usuário autenticado a partir do UUID (sem expor credenciais nem ID inteiro).
|
||||||
|
*
|
||||||
|
* @param string $userUuid
|
||||||
|
* @return array
|
||||||
|
* @throws RuntimeException
|
||||||
|
*/
|
||||||
|
public function getProfile(string $userUuid): array {
|
||||||
|
$user = $this->repository->findByUuid($userUuid);
|
||||||
|
|
||||||
|
if ($user === null) {
|
||||||
|
throw new RuntimeException('Usuário não encontrado.', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($user['ativo'])) {
|
||||||
|
throw new RuntimeException('Usuário inativo no sistema.', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$empresas = $this->repository->findEmpresasByUsuarioUuid($userUuid);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'usuario' => [
|
||||||
|
'uuid' => $user['uuid'],
|
||||||
|
'nome_completo' => $user['nome_completo'],
|
||||||
|
'cpf' => $user['cpf'],
|
||||||
|
'email' => $user['email'],
|
||||||
|
'nome_usuario' => $user['nome_usuario'],
|
||||||
|
'ativo' => (bool)$user['ativo'],
|
||||||
|
'created_at' => $user['created_at'],
|
||||||
|
],
|
||||||
|
'empresas' => $empresas,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
namespace WorkbloomERP\Middleware;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
use KrothiumAPI\Utils\HttpUtil;
|
||||||
|
use WorkbloomERP\Security\JwtService;
|
||||||
|
use WorkbloomERP\Security\AuthContext;
|
||||||
|
|
||||||
|
class AuthMiddleware {
|
||||||
|
private JwtService $jwtService;
|
||||||
|
|
||||||
|
public function __construct(?JwtService $jwtService = null) {
|
||||||
|
$this->jwtService = $jwtService ?? new JwtService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executa a validação de autenticação Bearer Token.
|
||||||
|
* Retorna true em caso de sucesso ou array estruturado de bloqueio (401).
|
||||||
|
*/
|
||||||
|
public function handle(): array|bool {
|
||||||
|
$token = HttpUtil::getBearerToken();
|
||||||
|
|
||||||
|
if (empty($token)) {
|
||||||
|
return [
|
||||||
|
'block' => true,
|
||||||
|
'response_code' => 401,
|
||||||
|
'message' => 'Token de autenticação não fornecido.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$payload = $this->jwtService->validateToken($token);
|
||||||
|
AuthContext::setUser($payload);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable) {
|
||||||
|
return [
|
||||||
|
'block' => true,
|
||||||
|
'response_code' => 401,
|
||||||
|
'message' => 'Token inválido ou expirado.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
namespace WorkbloomERP\Security;
|
||||||
|
|
||||||
|
class AuthContext {
|
||||||
|
private static ?array $user = null;
|
||||||
|
|
||||||
|
public static function setUser(array $user): void {
|
||||||
|
self::$user = $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getUser(): ?array {
|
||||||
|
return self::$user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getUserId(): ?int {
|
||||||
|
return isset(self::$user['id']) ? (int)self::$user['id'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getUserUuid(): ?string {
|
||||||
|
return self::$user['sub'] ?? self::$user['uuid'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clear(): void {
|
||||||
|
self::$user = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
namespace WorkbloomERP\Security;
|
||||||
|
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Firebase\JWT\Key;
|
||||||
|
use Throwable;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class JwtService {
|
||||||
|
private string $secret;
|
||||||
|
private int $expiresIn;
|
||||||
|
|
||||||
|
public function __construct(?string $secret = null, ?int $expiresIn = null) {
|
||||||
|
$this->secret = $secret ?? $_ENV['JWT_SECRET'] ?? getenv('JWT_SECRET') ?: '';
|
||||||
|
if (empty($this->secret)) {
|
||||||
|
throw new RuntimeException('JWT secret is not configured.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->expiresIn = $expiresIn ?? (int)($_ENV['JWT_EXPIRES_IN'] ?? getenv('JWT_EXPIRES_IN') ?: 86400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gera um novo token JWT para o payload fornecido.
|
||||||
|
*
|
||||||
|
* @param array $userData Dados do usuário e contexto
|
||||||
|
* @return array{token: string, expires_in: int, expires_at: int}
|
||||||
|
*/
|
||||||
|
public function generateToken(array $userData): array {
|
||||||
|
$now = time();
|
||||||
|
$expiresAt = $now + $this->expiresIn;
|
||||||
|
|
||||||
|
$payload = array_merge($userData, [
|
||||||
|
'iat' => $now,
|
||||||
|
'nbf' => $now,
|
||||||
|
'exp' => $expiresAt,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = JWT::encode($payload, $this->secret, 'HS256');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'token' => $token,
|
||||||
|
'expires_in' => $this->expiresIn,
|
||||||
|
'expires_at' => $expiresAt,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodifica e valida o token JWT.
|
||||||
|
* Retorna o payload em array ou lança uma exceção caso inválido ou expirado.
|
||||||
|
*
|
||||||
|
* @param string $token
|
||||||
|
* @return array
|
||||||
|
* @throws RuntimeException
|
||||||
|
*/
|
||||||
|
public function validateToken(string $token): array {
|
||||||
|
try {
|
||||||
|
$decoded = JWT::decode($token, new Key($this->secret, 'HS256'));
|
||||||
|
return (array)$decoded;
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
throw new RuntimeException('Token inválido ou expirado: ' . $e->getMessage(), 401, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use KrothiumAPI\Database\DBManager;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$db = DBManager::getConnection();
|
||||||
|
|
||||||
|
$nome = $argv[1] ?? 'Administrador Workbloom';
|
||||||
|
$email = $argv[2] ?? 'admin@workbloom.com.br';
|
||||||
|
$usuario = $argv[3] ?? 'admin';
|
||||||
|
$senha = $argv[4] ?? 'Admin@123456';
|
||||||
|
|
||||||
|
echo "=== Criação de Usuário Administrador ===\n";
|
||||||
|
echo "Nome: {$nome}\n";
|
||||||
|
echo "Email: {$email}\n";
|
||||||
|
echo "Usuário: {$usuario}\n";
|
||||||
|
|
||||||
|
// Verifica se já existe por e-mail ou nome_usuario
|
||||||
|
$existente = $db->fetchOne(
|
||||||
|
"SELECT id, uuid, email, nome_usuario FROM usuario WHERE email = :email OR nome_usuario = :usuario LIMIT 1",
|
||||||
|
['email' => $email, 'usuario' => $usuario]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($existente !== null) {
|
||||||
|
echo "Erro: Usuário já cadastrado com este e-mail ou nome de usuário (ID: {$existente['id']}).\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userUuid = Uuid::uuid7()->toString();
|
||||||
|
$senhaHash = password_hash($senha, PASSWORD_ARGON2ID);
|
||||||
|
|
||||||
|
// Cria o usuário
|
||||||
|
$db->insert('usuario', [
|
||||||
|
'uuid' => $userUuid,
|
||||||
|
'nome_completo' => $nome,
|
||||||
|
'email' => $email,
|
||||||
|
'nome_usuario' => $usuario,
|
||||||
|
'senha_hash' => $senhaHash,
|
||||||
|
'ativo' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$userId = (int)$db->fetchColumn(
|
||||||
|
"SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1",
|
||||||
|
['uuid' => $userUuid]
|
||||||
|
);
|
||||||
|
|
||||||
|
echo "Usuário criado com sucesso! ID: {$userId}, UUID: {$userUuid}\n";
|
||||||
|
|
||||||
|
// Verifica se existe ao menos uma empresa
|
||||||
|
$empresaId = $db->fetchColumn("SELECT id FROM empresa WHERE deleted_at IS NULL LIMIT 1");
|
||||||
|
|
||||||
|
if (!$empresaId) {
|
||||||
|
echo "Nenhuma empresa cadastrada. Criando Empresa Matriz de teste...\n";
|
||||||
|
$empresaUuid = Uuid::uuid7()->toString();
|
||||||
|
$db->insert('empresa', [
|
||||||
|
'uuid' => $empresaUuid,
|
||||||
|
'tipo_estabelecimento' => 'MATRIZ',
|
||||||
|
'status' => 'ATIVO',
|
||||||
|
'nome_empresarial' => 'WORKBLOOM SISTEMAS LTDA',
|
||||||
|
'nome_fantasia' => 'Workbloom Matriz',
|
||||||
|
'cnpj' => '12345678000195',
|
||||||
|
'regime_tributario' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$empresaId = (int)$db->fetchColumn(
|
||||||
|
"SELECT id FROM empresa WHERE uuid = :uuid LIMIT 1",
|
||||||
|
['uuid' => $empresaUuid]
|
||||||
|
);
|
||||||
|
echo "Empresa criada! ID: {$empresaId}, UUID: {$empresaUuid}\n";
|
||||||
|
} else {
|
||||||
|
$empresaId = (int)$empresaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vincula usuário à empresa como ADMIN
|
||||||
|
$db->insert('usuario_empresa', [
|
||||||
|
'usuario_id' => $userId,
|
||||||
|
'empresa_id' => $empresaId,
|
||||||
|
'perfil' => 'ADMIN',
|
||||||
|
'acesso_todas_filiais' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "Vínculo criado na empresa ID: {$empresaId} como ADMIN!\n";
|
||||||
|
echo "Concluído com sucesso!\n";
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Suíte de testes automatizados do fluxo de autenticação (Workbloom ERP).
|
||||||
|
* Executa todos os cenários requeridos com validação rigorosa de regras.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
use Dotenv\Dotenv;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
use KrothiumAPI\Database\DBManager;
|
||||||
|
use WorkbloomERP\Security\JwtService;
|
||||||
|
use WorkbloomERP\Security\AuthContext;
|
||||||
|
use WorkbloomERP\Middleware\AuthMiddleware;
|
||||||
|
use WorkbloomERP\Module\Auth\DTO\LoginDTO;
|
||||||
|
use WorkbloomERP\Module\Auth\Repository\AuthRepository;
|
||||||
|
use WorkbloomERP\Module\Auth\Service\AuthService;
|
||||||
|
|
||||||
|
$dotenv = Dotenv::createImmutable(__DIR__ . '/..');
|
||||||
|
$dotenv->load();
|
||||||
|
|
||||||
|
$db = DBManager::getConnection();
|
||||||
|
$repo = new AuthRepository($db);
|
||||||
|
$jwt = new JwtService();
|
||||||
|
$service = new AuthService($repo, $jwt);
|
||||||
|
|
||||||
|
$passed = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
function assertTest(string $title, bool $condition, ?string $detail = null): void {
|
||||||
|
global $passed, $failed;
|
||||||
|
if ($condition) {
|
||||||
|
echo " [PASS] {$title}\n";
|
||||||
|
$passed++;
|
||||||
|
} else {
|
||||||
|
echo " [FAIL] {$title}" . ($detail ? " - {$detail}" : "") . "\n";
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=======================================================\n";
|
||||||
|
echo " INICIANDO BATERIA DE TESTES DE AUTENTICAÇÃO\n";
|
||||||
|
echo "=======================================================\n\n";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 1. Login com e-mail válido
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "Cenário 1: Login com e-mail válido\n";
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray(['login' => 'admin@workbloom.com.br', 'senha' => 'Admin@123456']);
|
||||||
|
$res = $service->login($dto);
|
||||||
|
|
||||||
|
assertTest("Geração do token JWT", !empty($res['token']));
|
||||||
|
assertTest("Tipo do token é Bearer", $res['token_type'] === 'Bearer');
|
||||||
|
assertTest("Tempo de expiração retornado", $res['expires_in'] > 0);
|
||||||
|
assertTest("Dados do usuário presentes", isset($res['usuario']['email']) && $res['usuario']['email'] === 'admin@workbloom.com.br');
|
||||||
|
assertTest("UUID presente no usuário", !empty($res['usuario']['uuid']));
|
||||||
|
assertTest("ID numérico NÃO exposto no usuário", !isset($res['usuario']['id']));
|
||||||
|
assertTest("Senha hash NÃO exposta no retorno", !isset($res['usuario']['senha_hash']));
|
||||||
|
assertTest("Empresas vinculadas presentes", is_array($res['empresas']) && count($res['empresas']) >= 1);
|
||||||
|
assertTest("UUID presente na empresa", !empty($res['empresas'][0]['uuid']));
|
||||||
|
assertTest("ID numérico NÃO exposto na empresa", !isset($res['empresas'][0]['id']));
|
||||||
|
assertTest("Perfil ADMIN identificado na empresa", $res['empresas'][0]['perfil'] === 'ADMIN');
|
||||||
|
assertTest("Flag de acesso a todas as filiais presente", isset($res['empresas'][0]['acesso_todas_filiais']));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
assertTest("Execução do login por e-mail", false, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 2. Login com nome de usuário válido
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 2: Login com nome de usuário válido\n";
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray(['login' => 'admin', 'senha' => 'Admin@123456']);
|
||||||
|
$res = $service->login($dto);
|
||||||
|
assertTest("Login por username autenticado com sucesso", !empty($res['token']));
|
||||||
|
assertTest("ID do usuário consistente", $res['usuario']['nome_usuario'] === 'admin');
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
assertTest("Execução do login por username", false, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 3. Login com senha inválida
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 3: Login com senha inválida\n";
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray(['login' => 'admin', 'senha' => 'SenhaIncorreta@999']);
|
||||||
|
$service->login($dto);
|
||||||
|
assertTest("Bloqueio de senha incorreta", false, "Deveria ter lançado RuntimeException");
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
assertTest("Rejeitado com código 401", $e->getCode() === 401);
|
||||||
|
assertTest("Mensagem genérica de credenciais inválidas", $e->getMessage() === 'Credenciais inválidas.');
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
assertTest("Bloqueio de senha incorreta", false, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 4. Login com usuário inexistente (prevenção contra enumeração)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 4: Tentativa com usuário inexistente\n";
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray(['login' => 'usuario_fantasma_xyz@workbloom.com.br', 'senha' => 'Admin@123456']);
|
||||||
|
$service->login($dto);
|
||||||
|
assertTest("Bloqueio de usuário inexistente", false, "Deveria ter lançado RuntimeException");
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
assertTest("Rejeitado com código 401", $e->getCode() === 401);
|
||||||
|
assertTest("Mesma mensagem genérica do erro de senha (anti-enumeração)", $e->getMessage() === 'Credenciais inválidas.');
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
assertTest("Bloqueio de usuário inexistente", false, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Criação temporária de usuários de teste para cenários avançados
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
$testUserUuidInativo = Uuid::uuid7()->toString();
|
||||||
|
$testUserUuidDeletado = Uuid::uuid7()->toString();
|
||||||
|
$testUserUuidSemEmpresa = Uuid::uuid7()->toString();
|
||||||
|
$testUserUuidMultiEmpresa = Uuid::uuid7()->toString();
|
||||||
|
|
||||||
|
$senhaHashPadrao = password_hash('Teste@123456', PASSWORD_ARGON2ID);
|
||||||
|
|
||||||
|
// 5. Usuário inativo
|
||||||
|
$db->insert('usuario', [
|
||||||
|
'uuid' => $testUserUuidInativo,
|
||||||
|
'nome_completo' => 'Usuário Inativo Teste',
|
||||||
|
'email' => 'inativo@workbloom.com.br',
|
||||||
|
'nome_usuario' => 'user_inativo',
|
||||||
|
'senha_hash' => $senhaHashPadrao,
|
||||||
|
'ativo' => 0,
|
||||||
|
]);
|
||||||
|
$idInativo = (int)$db->fetchColumn("SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1", ['uuid' => $testUserUuidInativo]);
|
||||||
|
|
||||||
|
// 6. Usuário logicamente excluído (deleted_at)
|
||||||
|
$db->insert('usuario', [
|
||||||
|
'uuid' => $testUserUuidDeletado,
|
||||||
|
'nome_completo' => 'Usuário Deletado Teste',
|
||||||
|
'email' => 'deletado@workbloom.com.br',
|
||||||
|
'nome_usuario' => 'user_deletado',
|
||||||
|
'senha_hash' => $senhaHashPadrao,
|
||||||
|
'ativo' => 1,
|
||||||
|
]);
|
||||||
|
$idDeletado = (int)$db->fetchColumn("SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1", ['uuid' => $testUserUuidDeletado]);
|
||||||
|
$db->execute("UPDATE usuario SET deleted_at = CURRENT_TIMESTAMP WHERE id = :id", ['id' => $idDeletado]);
|
||||||
|
|
||||||
|
// 7. Usuário sem vínculo com empresa
|
||||||
|
$db->insert('usuario', [
|
||||||
|
'uuid' => $testUserUuidSemEmpresa,
|
||||||
|
'nome_completo' => 'Usuário Sem Empresa',
|
||||||
|
'email' => 'semempresa@workbloom.com.br',
|
||||||
|
'nome_usuario' => 'user_semempresa',
|
||||||
|
'senha_hash' => $senhaHashPadrao,
|
||||||
|
'ativo' => 1,
|
||||||
|
]);
|
||||||
|
$idSemEmpresa = (int)$db->fetchColumn("SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1", ['uuid' => $testUserUuidSemEmpresa]);
|
||||||
|
|
||||||
|
// 8. Usuário multiempresa com perfis distintos
|
||||||
|
$db->insert('usuario', [
|
||||||
|
'uuid' => $testUserUuidMultiEmpresa,
|
||||||
|
'nome_completo' => 'Usuário Multiempresa',
|
||||||
|
'email' => 'multiempresa@workbloom.com.br',
|
||||||
|
'nome_usuario' => 'user_multiempresa',
|
||||||
|
'senha_hash' => $senhaHashPadrao,
|
||||||
|
'ativo' => 1,
|
||||||
|
]);
|
||||||
|
$idMultiEmpresa = (int)$db->fetchColumn("SELECT id FROM usuario WHERE uuid = :uuid LIMIT 1", ['uuid' => $testUserUuidMultiEmpresa]);
|
||||||
|
|
||||||
|
// Cria 2 empresas adicionais de teste
|
||||||
|
$empresaUuid1 = Uuid::uuid7()->toString();
|
||||||
|
$empresaUuid2 = Uuid::uuid7()->toString();
|
||||||
|
$db->insert('empresa', [
|
||||||
|
'uuid' => $empresaUuid1,
|
||||||
|
'tipo_estabelecimento' => 'MATRIZ',
|
||||||
|
'status' => 'ATIVO',
|
||||||
|
'nome_empresarial' => 'EMPRESA ALFA LTDA',
|
||||||
|
'nome_fantasia' => 'Alfa Matriz',
|
||||||
|
'cnpj' => '11111111000101',
|
||||||
|
'regime_tributario' => 1,
|
||||||
|
]);
|
||||||
|
$idEmpresaAlfa = (int)$db->fetchColumn("SELECT id FROM empresa WHERE uuid = :uuid LIMIT 1", ['uuid' => $empresaUuid1]);
|
||||||
|
|
||||||
|
$db->insert('empresa', [
|
||||||
|
'uuid' => $empresaUuid2,
|
||||||
|
'tipo_estabelecimento' => 'FILIAL',
|
||||||
|
'status' => 'ATIVO',
|
||||||
|
'nome_empresarial' => 'EMPRESA BETA FILIAL LTDA',
|
||||||
|
'nome_fantasia' => 'Beta Filial',
|
||||||
|
'cnpj' => '11111111000202',
|
||||||
|
'regime_tributario' => 1,
|
||||||
|
]);
|
||||||
|
$idEmpresaBeta = (int)$db->fetchColumn("SELECT id FROM empresa WHERE uuid = :uuid LIMIT 1", ['uuid' => $empresaUuid2]);
|
||||||
|
|
||||||
|
// Vincula usuário multiempresa às 2 empresas com perfis diferentes
|
||||||
|
$db->insert('usuario_empresa', [
|
||||||
|
'usuario_id' => $idMultiEmpresa,
|
||||||
|
'empresa_id' => $idEmpresaAlfa,
|
||||||
|
'perfil' => 'ADMIN',
|
||||||
|
'acesso_todas_filiais' => 1,
|
||||||
|
]);
|
||||||
|
$db->insert('usuario_empresa', [
|
||||||
|
'usuario_id' => $idMultiEmpresa,
|
||||||
|
'empresa_id' => $idEmpresaBeta,
|
||||||
|
'perfil' => 'EMISSOR',
|
||||||
|
'acesso_todas_filiais' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Cenário 5: Tentativa com usuário inativo
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 5: Tentativa com usuário inativo (ativo = false)\n";
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray(['login' => 'inativo@workbloom.com.br', 'senha' => 'Teste@123456']);
|
||||||
|
$service->login($dto);
|
||||||
|
assertTest("Bloqueio de usuário inativo", false, "Deveria ter lançado RuntimeException 403");
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
assertTest("Rejeitado com código 403", $e->getCode() === 403);
|
||||||
|
assertTest("Mensagem indica usuário inativo", str_contains($e->getMessage(), 'inativo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Cenário 6: Tentativa com usuário com deleted_at preenchido
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 6: Tentativa com usuário excluído (deleted_at preenchido)\n";
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray(['login' => 'deletado@workbloom.com.br', 'senha' => 'Teste@123456']);
|
||||||
|
$service->login($dto);
|
||||||
|
assertTest("Bloqueio de usuário excluído", false, "Deveria ter lançado RuntimeException 401");
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
assertTest("Rejeitado como não encontrado (401)", $e->getCode() === 401);
|
||||||
|
assertTest("Mensagem genérica de credenciais inválidas", $e->getMessage() === 'Credenciais inválidas.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Cenário 7: Usuário sem vínculo com empresa
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 7: Usuário sem vínculo com empresa\n";
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray(['login' => 'semempresa@workbloom.com.br', 'senha' => 'Teste@123456']);
|
||||||
|
$res = $service->login($dto);
|
||||||
|
assertTest("Login efetuado mesmo sem empresa vinculada", !empty($res['token']));
|
||||||
|
assertTest("Lista de empresas vazia", is_array($res['empresas']) && count($res['empresas']) === 0);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
assertTest("Login sem empresa", false, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Cenário 8: Usuário multiempresa com múltiplos perfis
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 8: Usuário com múltiplas empresas e perfis distintos\n";
|
||||||
|
try {
|
||||||
|
$dto = LoginDTO::fromArray(['login' => 'multiempresa@workbloom.com.br', 'senha' => 'Teste@123456']);
|
||||||
|
$res = $service->login($dto);
|
||||||
|
assertTest("Autenticação com sucesso", !empty($res['token']));
|
||||||
|
assertTest("Exatamente 2 empresas vinculadas", count($res['empresas']) === 2);
|
||||||
|
|
||||||
|
$perfilMap = [];
|
||||||
|
$filiaisMap = [];
|
||||||
|
foreach ($res['empresas'] as $emp) {
|
||||||
|
$perfilMap[$emp['nome_fantasia']] = $emp['perfil'];
|
||||||
|
$filiaisMap[$emp['nome_fantasia']] = (bool)$emp['acesso_todas_filiais'];
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTest("Perfil ADMIN na Empresa Alfa", ($perfilMap['Alfa Matriz'] ?? null) === 'ADMIN');
|
||||||
|
assertTest("Flag todas as filiais = true na Empresa Alfa", ($filiaisMap['Alfa Matriz'] ?? null) === true);
|
||||||
|
assertTest("Perfil EMISSOR na Empresa Beta", ($perfilMap['Beta Filial'] ?? null) === 'EMISSOR');
|
||||||
|
assertTest("Flag todas as filiais = false na Empresa Beta", ($filiaisMap['Beta Filial'] ?? null) === false);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
assertTest("Multiempresa", false, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Cenário 9: Validação do Token JWT e Middleware
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 9: Validação do Token JWT e Middleware de Proteção\n";
|
||||||
|
// Token forjado / adulterado
|
||||||
|
try {
|
||||||
|
$jwt->validateToken('token.invalido.falso');
|
||||||
|
assertTest("Rejeição de token adulterado", false, "Deveria ter lançado RuntimeException");
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
assertTest("Token forjado rejeitado com código 401", $e->getCode() === 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token expirado
|
||||||
|
try {
|
||||||
|
$expiredJwt = new JwtService(expiresIn: -10);
|
||||||
|
$expiredData = $expiredJwt->generateToken(['id' => 1, 'sub' => 'uuid-teste']);
|
||||||
|
$jwt->validateToken($expiredData['token']);
|
||||||
|
assertTest("Rejeição de token expirado", false, "Deveria ter lançado RuntimeException");
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
assertTest("Token expirado rejeitado com código 401", $e->getCode() === 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware sem header
|
||||||
|
unset($_SERVER['HTTP_AUTHORIZATION']);
|
||||||
|
$middleware = new AuthMiddleware($jwt);
|
||||||
|
$midRes = $middleware->handle();
|
||||||
|
assertTest("Middleware bloqueia requisição sem token", is_array($midRes) && ($midRes['block'] ?? false) === true);
|
||||||
|
assertTest("Código retornado pelo middleware é 401", ($midRes['response_code'] ?? null) === 401);
|
||||||
|
|
||||||
|
// Middleware com token válido
|
||||||
|
$tokenValido = $res['token'];
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = "Bearer {$tokenValido}";
|
||||||
|
AuthContext::clear();
|
||||||
|
$midResValido = $middleware->handle();
|
||||||
|
assertTest("Middleware aceita requisição com Bearer Token válido", $midResValido === true);
|
||||||
|
assertTest("AuthContext populado com o UUID do usuário", AuthContext::getUserUuid() === $testUserUuidMultiEmpresa);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Cenário 10: Consulta ao Perfil (endpoint me / getProfile)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
echo "\nCenário 10: Consulta ao Perfil do usuário autenticado (GET /api/v1/auth/me)\n";
|
||||||
|
try {
|
||||||
|
$profile = $service->getProfile(AuthContext::getUserUuid());
|
||||||
|
assertTest("Perfil recuperado com sucesso via UUID", !empty($profile['usuario']['uuid']));
|
||||||
|
assertTest("ID numérico NÃO exposto no perfil do usuário", !isset($profile['usuario']['id']));
|
||||||
|
assertTest("Nome completo presente", $profile['usuario']['nome_completo'] === 'Usuário Multiempresa');
|
||||||
|
assertTest("Senha hash NÃO exposta no perfil", !isset($profile['usuario']['senha_hash']));
|
||||||
|
assertTest("Lista de empresas presente no perfil", count($profile['empresas']) === 2);
|
||||||
|
assertTest("ID numérico NÃO exposto nas empresas do perfil", !isset($profile['empresas'][0]['id']));
|
||||||
|
assertTest("UUID presente nas empresas do perfil", !empty($profile['empresas'][0]['uuid']));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
assertTest("Consulta de perfil", false, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Limpeza dos dados temporários criados nos testes
|
||||||
|
$db->execute("DELETE FROM usuario_empresa WHERE usuario_id IN (:u1, :u2, :u3, :u4)", [
|
||||||
|
'u1' => $idInativo,
|
||||||
|
'u2' => $idDeletado,
|
||||||
|
'u3' => $idSemEmpresa,
|
||||||
|
'u4' => $idMultiEmpresa,
|
||||||
|
]);
|
||||||
|
$db->execute("DELETE FROM usuario WHERE id IN (:u1, :u2, :u3, :u4)", [
|
||||||
|
'u1' => $idInativo,
|
||||||
|
'u2' => $idDeletado,
|
||||||
|
'u3' => $idSemEmpresa,
|
||||||
|
'u4' => $idMultiEmpresa,
|
||||||
|
]);
|
||||||
|
$db->execute("DELETE FROM empresa WHERE id IN (:e1, :e2)", [
|
||||||
|
'e1' => $idEmpresaAlfa,
|
||||||
|
'e2' => $idEmpresaBeta,
|
||||||
|
]);
|
||||||
|
unset($_SERVER['HTTP_AUTHORIZATION']);
|
||||||
|
AuthContext::clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n=======================================================\n";
|
||||||
|
echo " RESULTADO DOS TESTES: {$passed} passaram, {$failed} falharam\n";
|
||||||
|
echo "=======================================================\n\n";
|
||||||
|
|
||||||
|
if ($failed > 0) {
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
+14
-2
@@ -20,7 +20,7 @@ KrothiumAPI::init(config: [
|
|||||||
'root' => __DIR__ . '/..',
|
'root' => __DIR__ . '/..',
|
||||||
'src' => __DIR__ . '/../app',
|
'src' => __DIR__ . '/../app',
|
||||||
'module' => __DIR__ . '/../app/Module',
|
'module' => __DIR__ . '/../app/Module',
|
||||||
'storage' => __DIR__ . '/var/www/workbloomerp-api/storage',
|
'storage' => __DIR__ . '/../storage',
|
||||||
'component' => __DIR__ . '/../app/Common/Component',
|
'component' => __DIR__ . '/../app/Common/Component',
|
||||||
],
|
],
|
||||||
'router' => [
|
'router' => [
|
||||||
@@ -43,7 +43,19 @@ KrothiumAPI::init(config: [
|
|||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Importa rotas da API v0
|
// ======================================
|
||||||
|
// Registra Middlewares
|
||||||
|
// ======================================
|
||||||
|
use KrothiumAPI\Http\Router;
|
||||||
|
use WorkbloomERP\Middleware\AuthMiddleware;
|
||||||
|
use WorkbloomERP\Module\Auth\Routes as AuthRoutes;
|
||||||
|
|
||||||
|
Router::defineMiddleware('auth', [AuthMiddleware::class, 'handle']);
|
||||||
|
|
||||||
|
// ======================================
|
||||||
|
// Importa rotas dos módulos
|
||||||
|
// ======================================
|
||||||
|
AuthRoutes::register();
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Dispara o roteador
|
// Dispara o roteador
|
||||||
|
|||||||
Reference in New Issue
Block a user