first commit

This commit is contained in:
Claudecio Martins
2025-10-21 16:51:43 +02:00
commit 044c6bd587
4215 changed files with 422857 additions and 0 deletions

View File

@@ -0,0 +1,73 @@
<?php
namespace Workbloom\Component\Auth;
use DateTime;
use Exception;
use AxiumPHP\Core\Router;
// Caminho do manifest.json
$manifestPath = realpath(path: __DIR__ . "/manifest.json");
if (!$manifestPath || !file_exists(filename: $manifestPath)) {
throw new Exception(message: "Arquivo 'manifest.json' não encontrado.");
}
// Lê e decodifica
$manifest = json_decode(json: file_get_contents(filename: $manifestPath), associative: true);
// Verifica estrutura
if (!isset($manifest['apis']) || !is_array(value: $manifest['apis'])) {
throw new Exception(message: "Estrutura de 'apis' inválida no manifest.");
}
$now = (new DateTime())->format(format: 'Y-m-d');
$newApis = [];
$updated = false;
// Checa se tem versão esperando que começa hoje
foreach ($manifest['apis'] as $api) {
if ($api['status'] === 'planned' && $api['start_date'] === $now) {
// Promote a nova
$api['status'] = 'active';
$newApis[] = $api;
$updated = true;
} elseif ($api['status'] === 'active' && $api['start_date'] !== $now) {
// Ignora versão antiga ativa
continue;
} else {
$newApis[] = $api;
}
}
// Atualiza manifest se tiver mudança
if ($updated) {
$manifest['apis'] = $newApis;
file_put_contents(filename: $manifestPath, data: json_encode(value: $manifest, flags: JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
// Carrega a versão ativa
$loaded = false;
foreach ($manifest['apis'] as $api) {
$startDate = new DateTime(datetime: $api['start_date']);
if ($api['status'] === 'active' && $startDate <= new DateTime()) {
$routeFile = COMPONENT_PATH . "/{$manifest['dirName']}/{$api['version']}/Routes/Routes.php";
if (file_exists(filename: $routeFile)) {
Router::group(
prefix: "{$api['version']}/{$manifest['slug']}",
callback: function() use ($routeFile) {
require_once realpath(path: $routeFile);
},
middlewares: []
);
$loaded = true;
break;
} else {
throw new Exception(message: "Arquivo de rotas não encontrado para a versão '{$api['version']}'.");
}
}
}
// Verifica se carregou
if (!$loaded) {
throw new Exception(message: "Nenhuma versão ativa da API foi carregada.");
}

View File

@@ -0,0 +1,16 @@
{
"name": "Autenticação",
"dirName": "Auth",
"slug": "auth",
"description": "Gerencia login, logout, sessão e permissões de acesso.",
"uuid": "0199abc8-026a-75b0-9b25-0f9456a44c70",
"version": "1.0",
"dependencies": [],
"apis": [
{
"version": "v0",
"status": "active",
"start_date": "2025-08-01"
}
]
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Workbloom\Component\Auth\v0\Controllers;
use Workbloom\Helpers\DataSanitizer;
use AxiumPHP\Helpers\RequestHelper;
use Workbloom\Component\Auth\v0\Services\AuthService;
class AuthController {
public function login() {
// Recebe formulário de login
$form = RequestHelper::getFilteredInput(form_type: INPUT_POST);
// Define campos obrigatórios
$required_fields = [
'login',
'password'
];
// Validação dos campos obrigatórios
foreach ($required_fields as $field) {
if (empty($form[$field])) {
// Resposta JSON de erro
RequestHelper::sendJsonResponse(
response_code: 400,
status: 'error',
message: "O campo '{$field}' é obrigatório",
output: []
);
return;
}
}
// Chama o serviço de autenticação
$loginService = (new AuthService())->login(
credentials: [
'login' => DataSanitizer::string(value: $form['login']),
'password' => DataSanitizer::string(value: $form['password'])
]
);
// Resposta JSON
RequestHelper::sendJsonResponse(
response_code: 200,
status: 'success',
message: 'Login successful',
output: [
'data' => $form
]
);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Workbloom\Component\Auth\v0\DTO;
class UsuarioDTO {
private ?int $id = null;
private ?string $uuid = null;
private ?int $status_id = null;
private ?int $is_root = 0;
private ?string $nome_completo = null;
private ?string $nome_usuario = null;
private ?string $email = null;
private ?string $senha_hash = null;
private ?int $need_update_password = 1;
private ?int $days_to_expire_password = 60;
private ?string $last_password_update = null;
private ?string $created_at = null;
private ?string $updated_at = null;
private ?string $deleted_at = null;
/**
* Construtor do DTO.
*
* Permite inicializar os atributos do objeto a partir de um array associativo.
*
* @param array<string, mixed> $data Array associativo opcional para popular os atributos.
*/
public function __construct(array $data = []) {
if (!empty($data)) {
$this->fromArray(data: $data);
}
}
/**
* Popula o DTO a partir de um array.
*
* @param array<string, mixed> $data
* @return void
*/
public function fromArray(array $data): void {
$this->id = $data['id'] ?? $this->id;
$this->uuid = $data['uuid'] ?? $this->uuid;
$this->status_id = $data['status_id'] ?? $this->status_id;
$this->is_root = $data['is_root'] ?? $this->is_root;
$this->nome_completo = $data['nome_completo'] ?? $this->nome_completo;
$this->nome_usuario = $data['nome_usuario'] ?? $this->nome_usuario;
$this->email = $data['email'] ?? $this->email;
$this->senha_hash = $data['senha_hash'] ?? $this->senha_hash;
$this->need_update_password = $data['need_update_password'] ?? $this->need_update_password;
$this->days_to_expire_password = $data['days_to_expire_password'] ?? $this->days_to_expire_password;
$this->last_password_update = $data['last_password_update'] ?? $this->last_password_update;
$this->created_at = $data['created_at'] ?? $this->created_at;
$this->updated_at = $data['updated_at'] ?? $this->updated_at;
$this->deleted_at = $data['deleted_at'] ?? $this->deleted_at;
}
/**
* Converte o DTO para um array associativo.
*
* @return array<string, mixed>
*/
public function toArray(): array {
return [
'id' => $this->id,
'uuid' => $this->uuid,
'status_id' => $this->status_id,
'is_root' => $this->is_root,
'nome_completo' => $this->nome_completo,
'nome_usuario' => $this->nome_usuario,
'email' => $this->email,
'senha_hash' => $this->senha_hash,
'need_update_password' => $this->need_update_password,
'days_to_expire_password' => $this->days_to_expire_password,
'last_password_update' => $this->last_password_update,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'deleted_at' => $this->deleted_at,
];
}
public function getId(): ?int {
return $this->id;
}
public function setId(?int $id): void {
$this->id = $id;
}
public function getUuid(): ?string {
return $this->uuid;
}
public function setUuid(?string $uuid): void {
$this->uuid = $uuid;
}
public function getStatusId(): ?int {
return $this->status_id;
}
public function setStatusId(?int $status_id): void {
$this->status_id = $status_id;
}
public function getIsRoot(): ?int {
return $this->is_root;
}
public function setIsRoot(?int $is_root): void {
$this->is_root = $is_root;
}
public function getNomeCompleto(): ?string {
return $this->nome_completo;
}
public function setNomeCompleto(?string $nome_completo): void {
$this->nome_completo = $nome_completo;
}
public function getNomeUsuario(): ?string {
return $this->nome_usuario;
}
public function setNomeUsuario(?string $nome_usuario): void {
$this->nome_usuario = $nome_usuario;
}
public function getEmail(): ?string {
return $this->email;
}
public function setEmail(?string $email): void {
$this->email = $email;
}
public function getSenhaHash(): ?string {
return $this->senha_hash;
}
public function setSenhaHash(?string $senha_hash): void {
$this->senha_hash = $senha_hash;
}
public function getNeedUpdatePassword(): ?int {
return $this->need_update_password;
}
public function setNeedUpdatePassword(?int $need_update_password): void {
$this->need_update_password = $need_update_password;
}
public function getDaysToExpirePassword(): ?int {
return $this->days_to_expire_password;
}
public function setDaysToExpirePassword(?int $days_to_expire_password): void {
$this->days_to_expire_password = $days_to_expire_password;
}
public function getLastPasswordUpdate(): ?string {
return $this->last_password_update;
}
public function setLastPasswordUpdate(?string $last_password_update): void {
$this->last_password_update = $last_password_update;
}
public function getCreatedAt(): ?string {
return $this->created_at;
}
public function setCreatedAt(?string $created_at): void {
$this->created_at = $created_at;
}
public function getUpdatedAt(): ?string {
return $this->updated_at;
}
public function setUpdatedAt(?string $updated_at): void {
$this->updated_at = $updated_at;
}
public function getDeletedAt(): ?string {
return $this->deleted_at;
}
public function setDeletedAt(?string $deleted_at): void {
$this->deleted_at = $deleted_at;
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Workbloom\Component\Auth\v0\DTO;
class UsuarioTokenDTO {
private ?int $id = null;
private ?int $usuario_id = null;
private ?string $usuario_ip = null;
private ?string $usuario_user_agent = null;
private ?string $token = null;
private ?int $is_revoked = 0;
private ?string $created_at = null;
private ?string $revoked_at = null;
private ?string $expires_at = null;
/**
* Construtor do DTO.
*
* Permite inicializar os atributos do objeto a partir de um array associativo.
*
* @param array<string, mixed> $data Array associativo opcional para popular os atributos.
*/
public function __construct(array $data = []) {
if (!empty($data)) {
$this->fromArray(data: $data);
}
}
/**
* Popula o DTO a partir de um array.
*
* @param array<string, mixed> $data
* @return void
*/
public function fromArray(array $data): void {
$this->id = $data['id'] ?? $this->id;
$this->usuario_id = $data['usuario_id'] ?? $this->usuario_id;
$this->usuario_ip = $data['usuario_ip'] ?? $this->usuario_ip;
$this->usuario_user_agent = $data['usuario_user_agent'] ?? $this->usuario_user_agent;
$this->token = $data['token'] ?? $this->token;
$this->is_revoked = $data['is_revoked'] ?? $this->is_revoked;
$this->created_at = $data['created_at'] ?? $this->created_at;
$this->revoked_at = $data['revoked_at'] ?? $this->revoked_at;
$this->expires_at = $data['expires_at'] ?? $this->expires_at;
}
/**
* Converte o DTO para um array associativo.
*
* @return array<string, mixed>
*/
public function toArray(): array {
return [
'id' => $this->id,
'usuario_id' => $this->usuario_id,
'usuario_ip' => $this->usuario_ip,
'usuario_user_agent' => $this->usuario_user_agent,
'token' => $this->token,
'is_revoked' => $this->is_revoked,
'created_at' => $this->created_at,
'revoked_at' => $this->revoked_at,
'expires_at' => $this->expires_at,
];
}
public function getId(): ?int {
return $this->id;
}
public function setId(?int $id): void {
$this->id = $id;
}
public function getUsuarioId(): ?int {
return $this->usuario_id;
}
public function setUsuarioId(?int $usuario_id): void {
$this->usuario_id = $usuario_id;
}
public function getUsuarioIp(): ?string {
return $this->usuario_ip;
}
public function setUsuarioIp(?string $usuario_ip): void {
$this->usuario_ip = $usuario_ip;
}
public function getUsuarioUserAgent(): ?string {
return $this->usuario_user_agent;
}
public function setUsuarioUserAgent(?string $usuario_user_agent): void {
$this->usuario_user_agent = $usuario_user_agent;
}
public function getToken(): ?string {
return $this->token;
}
public function setToken(?string $token): void {
$this->token = $token;
}
public function getIsRevoked(): ?int {
return $this->is_revoked;
}
public function setIsRevoked(?int $is_revoked): void {
$this->is_revoked = $is_revoked;
}
public function getCreatedAt(): ?string {
return $this->created_at;
}
public function setCreatedAt(?string $created_at): void {
$this->created_at = $created_at;
}
public function getRevokedAt(): ?string {
return $this->revoked_at;
}
public function setRevokedAt(?string $revoked_at): void {
$this->revoked_at = $revoked_at;
}
public function getExpiresAt(): ?string {
return $this->expires_at;
}
public function setExpiresAt(?string $expires_at): void {
$this->expires_at = $expires_at;
}
}

View File

@@ -0,0 +1,510 @@
<?php
namespace Workbloom\Component\Auth\v0\Models;
use DateTime;
use Exception;
use Workbloom\Services\DB;
use Workbloom\Component\Auth\v0\DTO\UsuarioDTO;
use Workbloom\Component\Auth\v0\DTO\UsuarioTokenDTO;
class UsuarioModel {
protected string $usuarioTable = 'usuario';
protected string $usuarioTokenTable = 'usuario_token';
/**
* Armazena um novo usuário no banco de dados.
*
* Este método insere os dados de um novo usuário na tabela `{$this->usuarioTable}`, incluindo informações de segurança como hash de senha, status e configurações de expiração de senha. A operação é protegida por um bloco `try-catch` para garantir o tratamento de erros.
*
* #### Fluxo de Operação:
* 1. **Execução da Query:** Prepara e executa a instrução `INSERT` com todos os campos preenchidos a partir do `UsuarioDTO`.
* 2. **Confirmação e Retorno de Sucesso:** Se a inserção for bem-sucedida, retorna um array com **`status` 'success'**, código **201 Created** e os dados do usuário criado (ID e UUID).
*
* #### Tratamento de Erros:
* - Qualquer `Exception` capturada durante o processo retorna um array com **`status` 'error'**, código **500 Internal Server Error** (ou o código da exceção) e a mensagem detalhada de erro.
*
* @param UsuarioDTO $usuarioDTO O objeto DTO contendo os dados do usuário a ser criado.
* @return array Um array associativo contendo o status da operação, um código de resposta HTTP e os dados do usuário criado.
*/
public function storeUsuario(UsuarioDTO $usuarioDTO): array {
try {
$sql =
"INSERT INTO {$this->usuarioTable} (
uuid,
status_id,
is_root,
nome_completo,
nome_usuario,
email,
senha_hash,
need_update_password,
days_to_expire_password,
last_password_update,
created_at,
updated_at,
deleted_at
) VALUES (
:uuid,
:status_id,
:is_root,
:nome_completo,
:nome_usuario,
:email,
:senha_hash,
:need_update_password,
:days_to_expire_password,
:last_password_update,
:created_at,
:updated_at,
:deleted_at
)";
$params = [
":uuid"=> $usuarioDTO->getUuid(),
":status_id"=> $usuarioDTO->getStatusId(),
":is_root"=> $usuarioDTO->getIsRoot(),
":nome_completo"=> $usuarioDTO->getNomeCompleto(),
":nome_usuario"=> $usuarioDTO->getNomeUsuario(),
":email"=> $usuarioDTO->getEmail(),
":senha_hash"=> $usuarioDTO->getSenhaHash(),
":need_update_password"=> $usuarioDTO->getNeedUpdatePassword(),
":days_to_expire_password"=> $usuarioDTO->getDaysToExpirePassword(),
":last_password_update"=> $usuarioDTO->getLastPasswordUpdate(),
":created_at"=> $usuarioDTO->getCreatedAt(),
":updated_at"=> $usuarioDTO->getUpdatedAt(),
":deleted_at"=> $usuarioDTO->getDeletedAt(),
];
// Executa a query
DB::execute(sql: $sql, params: $params);
return [
'response_code' => 201,
'status' => 'success',
'message' => 'Usuário criado com sucesso',
'output' => [
'data' => [
'id' => DB::lastInsertId(),
'uuid' => $usuarioDTO->getUuid()
]
]
];
} catch (Exception $e) {
return [
'response_code' => $e->getCode() ?? 500,
'status' => 'error',
'message' => $e->getMessage() ?? 'Internal Server Error'
];
}
}
/**
* Atualiza um usuário existente no banco de dados.
*
* Este método modifica os dados de um usuário na tabela `{$this->usuarioTable}` com base no **UUID** fornecido no objeto `UsuarioDTO`. Ele atualiza informações de segurança e perfil, como hash de senha, status, permissão de root, nome e configurações de expiração de senha. A operação utiliza um bloco `try-catch` para garantir o tratamento de erros.
*
* #### Fluxo de Operação:
* 1. **Execução da Query:** Prepara e executa a instrução `UPDATE` com todos os campos preenchidos a partir do `UsuarioDTO`, modificando o registro correspondente ao UUID.
* 2. **Confirmação e Retorno de Sucesso:** Se a atualização for bem-sucedida, retorna um array com **`status` 'success'**, código **200 OK** e uma mensagem de êxito.
*
* #### Tratamento de Erros:
* - Qualquer `Exception` capturada durante o processo retorna um array com **`status` 'error'**, o código de erro da exceção (ou `500` como fallback) e a mensagem detalhada de erro.
*
* @param UsuarioDTO $usuarioDTO O objeto DTO contendo os dados atualizados do usuário, incluindo o UUID para identificação.
* @return array Um array associativo contendo o status da operação, um código de resposta HTTP e uma mensagem.
*/
public function updateUsuario(UsuarioDTO $usuarioDTO): array {
try {
$sql =
"UPDATE {$this->usuarioTable} SET
status_id = :status_id,
is_root = :is_root,
nome_completo = :nome_completo,
nome_usuario = :nome_usuario,
email = :email,
senha_hash = :senha_hash,
need_update_password = :need_update_password,
days_to_expire_password = :days_to_expire_password,
last_password_update = :last_password_update,
updated_at = :updated_at
WHERE uuid = :uuid";
$params = [
":status_id" => $usuarioDTO->getStatusId(),
":is_root" => $usuarioDTO->getIsRoot(),
":nome_completo" => $usuarioDTO->getNomeCompleto(),
":nome_usuario" => $usuarioDTO->getNomeUsuario(),
":email" => $usuarioDTO->getEmail(),
":senha_hash" => $usuarioDTO->getSenhaHash(),
":need_update_password" => $usuarioDTO->getNeedUpdatePassword(),
":days_to_expire_password" => $usuarioDTO->getDaysToExpirePassword(),
":last_password_update" => $usuarioDTO->getLastPasswordUpdate(),
":updated_at" => $usuarioDTO->getUpdatedAt(),
":uuid" => $usuarioDTO->getUuid(),
];
// Executa a query
DB::execute(sql: $sql, params: $params);
return [
'response_code' => 200,
'status' => 'success',
'message' => 'Usuário atualizado com sucesso',
];
} catch(Exception $e) {
return [
'response_code' => $e->getCode() ?? 500,
'status' => 'error',
'message' => $e->getMessage() ?? 'Internal Server Error'
];
}
}
/**
* Realiza a exclusão lógica de um usuário no banco de dados.
*
* Este método atualiza a coluna `deleted_at` do registro do usuário na tabela `{$this->usuarioTable}` com o timestamp fornecido no objeto `UsuarioDTO`. Isso garante uma **exclusão lógica**, mantendo o registro para fins de auditoria, mas marcando-o como inativo.
*
* #### Fluxo de Operação:
* 1. **Execução da Query:** Prepara e executa a instrução `UPDATE` que define a data de exclusão para o registro correspondente ao UUID do usuário.
* 2. **Retorno de Sucesso:** Se a atualização for bem-sucedida, retorna um array com **`status` 'success'**, código **200 OK** e uma mensagem de êxito.
*
* #### Tratamento de Erros:
* - Qualquer `Exception` capturada durante o processo retorna um array com **`status` 'error'**, o código de erro da exceção (ou `500` como fallback) e a mensagem detalhada da exceção.
*
* @param UsuarioDTO $usuarioDTO O objeto DTO contendo o UUID do usuário a ser excluído e o timestamp a ser definido em `deleted_at`.
* @return array Um array associativo contendo o status da operação, um código de resposta HTTP e uma mensagem.
*/
public function deleteUsuario(UsuarioDTO $usuarioDTO): array {
try {
$sql =
"UPDATE {$this->usuarioTable} SET
deleted_at = :deleted_at
WHERE uuid = :uuid";
$params = [
":deleted_at" => $usuarioDTO->getDeletedAt(),
":uuid" => $usuarioDTO->getUuid(),
];
// Executa a query
DB::execute(sql: $sql, params: $params);
return [
'response_code' => 200,
'status' => 'success',
'message' => 'Usuário excluído com sucesso',
];
} catch(Exception $e) {
return [
'response_code' => $e->getCode() ?? 500,
'status' => 'error',
'message' => $e->getMessage() ?? 'Internal Server Error'
];
}
}
/**
* Busca um único usuário no banco de dados por um identificador específico.
*
* Este método recupera todos os detalhes de um usuário da tabela `{$this->usuarioTable}`. A busca é flexível e permite usar qualquer coluna como identificador (`$identifier`) e o valor correspondente.
*
* #### Fluxo de Operação:
* 1. **Construção da Query Dinâmica:** O método monta e executa uma consulta `SELECT` que usa o `$identifier` de forma dinâmica na cláusula `WHERE` e um parâmetro nomeado seguro (`:value`) para o valor.
* 2. **Execução da Consulta:** A consulta usa `DB::fetchOne`, indicando que apenas um registro é esperado.
* 3. **Verificação de Resultados:**
* * Se o usuário for encontrado, retorna um array com **`status` 'success'**, código **200 OK** e os dados do usuário na chave `output`.
* * Se o usuário **não for encontrado** (`empty($result)`), retorna um array com **`status` 'error'** e código **404 Not Found**, com uma mensagem indicando que o usuário não foi encontrado.
*
* #### Tratamento de Erros:
* - Qualquer `Exception` capturada durante o processo retorna um array com **`status` 'error'**, o código de erro da exceção (ou `500` como fallback) e uma mensagem detalhada de erro.
*
* @param string $identifier A coluna da tabela a ser usada como critério de busca (ex: 'uuid', 'email', 'nome_usuario').
* @param mixed $value O valor correspondente ao identificador que está sendo buscado.
* @return array Um array associativo contendo o status da operação, um código de resposta HTTP e os dados do usuário, se encontrado.
*/
public function findUsuarioByIdentifier(string $identifier, mixed $value): array {
try {
$sql =
"SELECT
id,
uuid,
status_id,
is_root,
nome_completo,
nome_usuario,
email,
senha_hash,
need_update_password,
days_to_expire_password,
last_password_update,
created_at,
updated_at,
deleted_at
FROM {$this->usuarioTable}
WHERE {$identifier} = :value";
$params = [
":value" => $value,
];
$result = DB::fetchOne(sql: $sql, params: $params);
if(empty($result)) {
return [
'response_code' => 404,
'status' => 'error',
'message' => 'Usuário não encontrado',
];
}
return [
'response_code' => 200,
'status' => 'success',
'message' => 'Usuário encontrado com sucesso',
'output' => [
'data' => $result
]
];
} catch(Exception $e) {
return [
'response_code' => $e->getCode() ?? 500,
'status' => 'error',
'message' => $e->getMessage() ?? 'Internal Server Error'
];
}
}
/**
* Busca um único usuário no banco de dados usando seu e-mail ou nome de usuário como critério de login.
*
* Este método é utilizado para o processo de autenticação, tentando encontrar um usuário na tabela `{$this->usuarioTable}` por meio de uma correspondência na coluna `nome_usuario` ou `email`.
*
* #### Fluxo de Operação:
* 1. **Execução da Query:** Monta e executa uma consulta `SELECT` que usa o valor `$login` para tentar encontrar um registro no campo `email` ou `nome_usuario`.
* 2. **Verificação de Resultados:**
* * Se o usuário for encontrado, retorna um array com **`status` 'success'**, código **200 OK** e os dados do usuário na chave `output`.
* * Se o usuário **não for encontrado** (`empty($result)`), retorna um array com **`status` 'error'** e código **404 Not Found**, com uma mensagem indicando que o usuário não foi encontrado.
*
* #### Tratamento de Erros:
* - Qualquer `Exception` capturada durante o processo retorna um array com **`status` 'error'**, o código de erro da exceção (ou `500` como fallback) e uma mensagem detalhada de erro.
*
* @param string $login O valor do e-mail ou nome de usuário a ser pesquisado.
* @return array Um array associativo contendo o status da operação, um código de resposta HTTP e os dados do usuário, se encontrado.
*/
public function findUsuarioByLogin(string $login): array {
try {
$sql =
"SELECT
id,
uuid,
status_id,
is_root,
nome_completo,
nome_usuario,
email,
senha_hash,
need_update_password,
days_to_expire_password,
last_password_update,
created_at,
updated_at,
deleted_at
FROM {$this->usuarioTable}
WHERE (nome_usuario = :value OR email = :value)";
$params = [
":value" => $login,
];
$result = DB::fetchOne(sql: $sql, params: $params);
if(empty($result)) {
return [
'response_code' => 404,
'status' => 'error',
'message' => 'Usuário não encontrado',
];
}
return [
'response_code' => 200,
'status' => 'success',
'message' => 'Usuário encontrado com sucesso',
'output' => [
'data' => $result
]
];
} catch(Exception $e) {
return [
'response_code' => $e->getCode() ?? 500,
'status' => 'error',
'message' => $e->getMessage() ?? 'Internal Server Error'
];
}
}
/**
* Armazena um novo token de autenticação de usuário no banco de dados.
*
* Este método é responsável por registrar o token na tabela `{$this->usuarioTokenTable}`, associando-o ao usuário, IP, User Agent e definindo seu status de revogação e tempo de expiração.
*
* #### Fluxo de Operação:
* 1. **Execução da Query:** Prepara e executa a instrução `INSERT` para inserir o registro do token com todos os detalhes fornecidos pelo `UsuarioTokenDTO`.
* 2. **Confirmação e Retorno de Sucesso:** Se a inserção for bem-sucedida, retorna um array com **`status` 'success'**, código **201 Created** e os dados do token (ID gerado e o token).
*
* #### Tratamento de Erros:
* - Qualquer `Exception` capturada durante o processo retorna um array com **`status` 'error'**, o código de erro da exceção (ou `500` como fallback) e a mensagem detalhada de erro.
*
* @param UsuarioTokenDTO $usuarioTokenDTO O objeto DTO contendo os dados do token a ser armazenado.
* @return array Um array associativo contendo o status da operação, um código de resposta HTTP e os dados do token.
*/
public function storeUsuarioToken(UsuarioTokenDTO $usuarioTokenDTO): array {
try {
$sql =
"INSERT INTO {$this->usuarioTokenTable} (
usuario_id,
usuario_ip,
usuario_user_agent,
token,
is_revoked,
created_at,
expires_at
) VALUES (
:usuario_id,
:usuario_ip,
:usuario_user_agent,
:token,
:is_revoked,
:created_at,
:expires_at
)";
$params = [
":usuario_id"=> $usuarioTokenDTO->getUsuarioId(),
":usuario_ip"=> $usuarioTokenDTO->getUsuarioIp(),
":usuario_user_agent"=> $usuarioTokenDTO->getUsuarioUserAgent(),
":token"=> $usuarioTokenDTO->getToken(),
":is_revoked"=> $usuarioTokenDTO->getIsRevoked(),
":created_at"=> $usuarioTokenDTO->getCreatedAt(),
":expires_at"=> $usuarioTokenDTO->getExpiresAt(),
];
// Executa a query
DB::execute(sql: $sql, params: $params);
return [
'response_code' => 201,
'status' => 'success',
'message' => 'Token de usuário criado com sucesso',
'output' => [
'data' => [
'id' => DB::lastInsertId(),
'token' => $usuarioTokenDTO->getToken()
]
]
];
} catch (Exception $e) {
return [
'response_code' => $e->getCode() ?? 500,
'status' => 'error',
'message' => $e->getMessage() ?? 'Internal Server Error'
];
}
}
/**
* Atualiza o status de revogação de um token de usuário no banco de dados.
*
* Este método modifica o registro de um token na tabela `{$this->usuarioTokenTable}`, alterando o status de revogação (`is_revoked`) e o timestamp de revogação (`revoked_at`), com base na string do token fornecida no DTO.
*
* #### Fluxo de Operação:
* 1. **Execução da Query:** Prepara e executa a instrução `UPDATE` para modificar o status de revogação do token.
* 2. **Retorno de Sucesso:** Se a atualização for bem-sucedida, retorna um array com **`status` 'success'**, código **200 OK** e uma mensagem de êxito.
*
* #### Tratamento de Erros:
* - Qualquer `Exception` capturada durante o processo retorna um array com **`status` 'error'**, o código de erro da exceção (ou `500` como fallback) e a mensagem detalhada de erro.
*
* @param UsuarioTokenDTO $usuarioTokenDTO O objeto DTO contendo o token a ser atualizado, o novo status de revogação (`is_revoked`) e a data de revogação (`revoked_at`).
* @return array Um array associativo contendo o status da operação, um código de resposta HTTP e uma mensagem.
*/
public function updateUsuarioToken(UsuarioTokenDTO $usuarioTokenDTO): array {
try {
$sql =
"UPDATE {$this->usuarioTokenTable} SET
is_revoked = :is_revoked,
revoked_at = :revoked_at
WHERE token = :token";
$params = [
":is_revoked"=> $usuarioTokenDTO->getIsRevoked(),
":revoked_at"=> $usuarioTokenDTO->getRevokedAt(),
":token"=> $usuarioTokenDTO->getToken(),
];
// Executa a query
DB::execute(sql: $sql, params: $params);
return [
'response_code' => 200,
'status' => 'success',
'message' => 'Token de usuário atualizado com sucesso',
];
} catch(Exception $e) {
return [
'response_code' => $e->getCode() ?? 500,
'status' => 'error',
'message' => $e->getMessage() ?? 'Internal Server Error'
];
}
}
/**
* Revoga (invalida) todos os tokens de autenticação ativos de um usuário no banco de dados, buscando o usuário pelo UUID.
*
* Este método executa uma exclusão lógica em todos os tokens não revogados (`is_revoked = 0`) associados ao ID interno de um usuário, que é determinado pelo `usuario_uuid` fornecido.
*
* #### Atenção à Sintaxe SQL (PostgreSQL/SQL Server):
* A sintaxe `UPDATE ... FROM` é comum em PostgreSQL ou SQL Server e está sendo usada para realizar um JOIN implícito na instrução UPDATE, o que não é sintaxe padrão do MySQL. Assumindo que o ambiente de banco de dados suporte essa sintaxe (ou que o código seja adaptado para o SGBD correto).
*
* #### Fluxo de Operação:
* 1. **Execução da Query:** Prepara e executa a instrução `UPDATE` para definir `is_revoked = 1` e `revoked_at` para o timestamp atual para todos os tokens ativos do usuário.
* 2. **Confirmação e Retorno de Sucesso:** Se a atualização for bem-sucedida, retorna um array com **`status` 'success'**, código **200 OK** e uma mensagem de êxito.
*
* #### Tratamento de Erros:
* - Qualquer `Exception` capturada durante o processo retorna um array com **`status` 'error'**, o código de erro da exceção (ou `500` como fallback) e uma mensagem detalhada de erro.
*
* @param string $usuario_uuid O UUID do usuário para o qual os tokens serão revogados.
* @return array Um array associativo contendo o status da operação ("success" ou "error"), um código de resposta HTTP e uma mensagem.
*/
public function revolkOldUsuarioTokens(string $usuario_uuid): array {
try {
$sql =
"UPDATE {$this->usuarioTokenTable} AS ut SET
is_revoked = 1,
revoked_at = :revoked_at
FROM {$this->usuarioTable} AS u
WHERE ut.usuario_id = u.id
AND u.uuid = :usuario_uuid
AND ut.is_revoked = 0";
$params = [
":revoked_at"=> (new DateTime())->format(format: 'Y-m-d H:i:s'),
":usuario_uuid"=> $usuario_uuid
];
// Executa a query
DB::execute(sql: $sql, params: $params);
return [
'response_code' => 200,
'status' => 'success',
'message' => 'Tokens antigos revogados com sucesso',
];
} catch(Exception $e) {
return [
'response_code' => $e->getCode() ?? 500,
'status' => 'error',
'message' => $e->getMessage() ?? 'Internal Server Error'
];
}
}
}

View File

@@ -0,0 +1,9 @@
<?php
use AxiumPHP\Core\Router;
use Workbloom\Component\Auth\v0\Controllers\AuthController;
// Rota de login
Router::POST(
uri: '/login',
handler: [AuthController::class, 'login']
);

View File

@@ -0,0 +1,3 @@
<?php
// Importa rotas de autenticação
require_once __DIR__ . '/AuthRoutes.php';

View File

@@ -0,0 +1,8 @@
<?php
namespace Workbloom\Component\Auth\v0\Services;
class AuthService {
public function login(array $credentials) {
return ['data' => $credentials];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Workbloom\Config;
use Dotenv\Dotenv;
class Config {
private $config = [];
private static $instance = null;
/**
* Método construtor
*/
public function __construct() {
// Carrega apenas para $_ENV, não para $_SERVER
$dotEnv = Dotenv::createUnsafeImmutable(
paths: realpath(path: __DIR__ . "/../../../"),
names: '.env'
);
$dotEnv->safeLoad(); // não dá erro se .env não existir
// Carrega as configurações
$this->config = require_once realpath(path: __DIR__ . "/Consts.php");
}
/**
* Retorna a instância única da classe
*/
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new Config();
}
return self::$instance;
}
/**
* Retorna uma configuração
*/
public function get(string $key) {
return $this->config[$key] ?? null;
}
}

View File

@@ -0,0 +1,21 @@
<?php
return [
'SYSTEM_URL' => (string) getenv(name: 'SYSTEM_URL'),
'SYSTEM_VERSION' => (string) getenv(name: 'SYSTEM_VERSION'),
'SYSTEM_TIMEZONE' => (string) getenv(name: 'SYSTEM_TIMEZONE'),
'SYSTEM_ENVIRONMENT_ID' => (int) getenv(name: 'SYSTEM_ENVIRONMENT_ID'),
'SYSTEM_FRONTEND_URL' => (string) getenv(name: 'SYSTEM_FRONTEND_URL'),
'JWT_ALGO' => getenv(name: 'JWT_ALGO'),
'JWT_ISSUER' => getenv(name: 'JWT_ISSUER'),
'JWT_SECRET' => getenv(name: 'JWT_SECRET'),
'JWT_EXPIRATION' => (int) getenv(name: 'JWT_EXPIRATION'),
'DEFAULT_DATABASE_HOST' => getenv(name: 'DEFAULT_DATABASE_HOST'),
'DEFAULT_DATABASE_PORT' => getenv(name: 'DEFAULT_DATABASE_PORT'),
'DEFAULT_DATABASE_DRIVER' => getenv(name: 'DEFAULT_DATABASE_DRIVER'),
'DEFAULT_DATABASE_SCHEMA' => getenv(name: 'DEFAULT_DATABASE_SCHEMA'),
'DEFAULT_DATABASE_CHARSET' => getenv(name: 'DEFAULT_DATABASE_CHARSET'),
'DEFAULT_DATABASE_USERNAME' => getenv(name: 'DEFAULT_DATABASE_USERNAME'),
'DEFAULT_DATABASE_PASSWORD' => getenv(name: 'DEFAULT_DATABASE_PASSWORD'),
];

View File

@@ -0,0 +1,69 @@
<?php
namespace Workbloom\Helpers;
use DateTime;
class DataSanitizer {
public static function string(mixed $value): string|null {
if($value === null) {
return null;
}
return trim(string: strip_tags(string: $value));
}
public static function int(mixed $value): int {
return filter_var(value: $value, filter: FILTER_VALIDATE_INT) ?? 0;
}
public static function float(mixed $value): float {
return filter_var(value: $value, filter: FILTER_VALIDATE_FLOAT) ?? 0.0;
}
public static function email(string $value): string {
$value = trim(string: strtolower(string: $value));
return filter_var(value: $value, filter: FILTER_VALIDATE_EMAIL) ?: '';
}
public static function phone(string $value): string {
// remove tudo que não for número
return preg_replace(pattern: '/\D+/', replacement: '', subject: $value) ?? '';
}
public static function document(string $value): string {
// CPF, CNPJ, RG etc. só números
return preg_replace(pattern: '/\D+/', replacement: '', subject: $value) ?? '';
}
public static function date(string $value, string $format = 'Y-m-d'): string {
$value = trim(string: $value);
if ($value === '') {
return '';
}
$date = DateTime::createFromFormat(format: "!{$format}", datetime: $value);
if (!$date) {
// tenta converter qualquer formato válido
$date = date_create(datetime: $value);
}
return $date ? $date->format(format: $format) : '';
}
public static function datetime(string $value, string $format = 'Y-m-d H:i:s'): string {
$value = trim(string: $value);
if ($value === '') {
return '';
}
$date = DateTime::createFromFormat(format: "!{$format}", datetime: $value);
if (!$date) {
$date = date_create(datetime: $value);
}
return $date ? $date->format(format: $format) : '';
}
}

View File

@@ -0,0 +1,222 @@
<?php
namespace Workbloom\Helpers;
class DataValidator {
private array $inputs;
private array $errors = [];
public function __construct(array $inputs) {
$this->inputs = $inputs;
}
/**
* Valida um campo usando uma regra específica.
*/
public function validate(string $field, callable|string $rule, string $message = '', array $additionalParams = []): static {
$value = $this->inputs[$field] ?? null;
// Se for string, monta callable usando a própria classe
if (is_string(value: $rule) && method_exists(object_or_class: self::class, method: $rule)) {
$rule = [self::class, $rule];
}
$params = array_merge([$value], $additionalParams);
$result = call_user_func_array(callback: $rule, args: $params);
// Caso seja validação de senha (retorna array)
if (is_array(value: $result) && isset($result['valid'])) {
if (!$result['valid']) {
$this->errors[$field] = [
'status' => 'is_invalid',
'message' => $result['errors']
];
}
}
// Para outras validações booleanas
elseif ($result === false) {
$this->errors[$field] = [
'status' => 'is_invalid',
'message' => $message
];
}
return $this;
}
public function getErrors(): array {
return $this->errors;
}
public function passes(): bool {
return empty($this->errors);
}
// ---------- Regras comuns ----------
public static function validateEmail(string $value): bool {
return (bool) filter_var(value: $value, filter: FILTER_VALIDATE_EMAIL);
}
public static function validatePhone(string $value, int $minLength = 10, int $maxLength = 13): bool {
$digits = preg_replace(pattern: '/\D+/', replacement: '', subject: $value);
$len = strlen(string: $digits);
return $len >= $minLength && $len <= $maxLength;
}
public static function validateCpf(string $value): bool {
$cpf = preg_replace(pattern: '/\D+/', replacement: '', subject: $value);
if (strlen(string: $cpf) !== 11 || preg_match(pattern: '/(\d)\1{10}/', subject: $cpf)) {
return false;
}
for ($t = 9; $t < 11; $t++) {
$d = 0;
for ($c = 0; $c < $t; $c++) {
$d += $cpf[$c] * (($t + 1) - $c);
}
$d = ((10 * $d) % 11) % 10;
if ($cpf[$c] != $d) {
return false;
}
}
return true;
}
public static function validateCnpj(string $value): bool {
// Remove qualquer máscara (pontos, barras, hífens)
$cleanedValue = preg_replace(pattern: '/[.\-\/]/', replacement: '', subject: $value);
// O CNPJ sem máscara deve ter 14 caracteres (12 alfanuméricos + 2 dígitos)
if (strlen(string: $cleanedValue) !== 14) {
return false;
}
$cnpjBase = strtoupper(string: substr(string: $cleanedValue, offset: 0, length: 12)); // Os 12 caracteres alfanuméricos
$dvRecebidos = substr(string: $cleanedValue, offset: 12, length: 2); // Os 2 dígitos verificadores
// Verifica se os dois últimos caracteres são realmente dígitos
if (!ctype_digit(text: $dvRecebidos)) {
return false;
}
// Mapeamento de Valor para cálculo do DV (do documento) [cite: 19]
$mapaValores = [
'0' => 0, '1' => 1, '2' => 2, '3' => 3, '4' => 4, '5' => 5, '6' => 6, '7' => 7, '8' => 8, '9' => 9,
'A' => 17, 'B' => 18, 'C' => 19, 'D' => 20, 'E' => 21, 'F' => 22, 'G' => 23, 'H' => 24, 'I' => 25,
'J' => 26, 'K' => 27, 'L' => 28, 'M' => 29, 'N' => 30, 'O' => 31, 'P' => 32, 'Q' => 33, 'R' => 34,
'S' => 35, 'T' => 36, 'U' => 37, 'V' => 38, 'W' => 39, 'X' => 40, 'Y' => 41, 'Z' => 42
];
// Array de pesos de 2 a 9 (repetidos da direita para a esquerda, recomeçando depois do 8º caracter) [cite: 26, 27]
// O documento usa [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] para o 1º DV no exemplo de 12 caracteres [cite: 27]
$pesos1Dv = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
// Para o 2º DV, são 13 caracteres (os 12 de base + o 1º DV), e a contagem de pesos recomeça (2 a 9)
// O documento usa [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2] para 13 caracteres no exemplo [cite: 40]
$pesos2Dv = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
$cnpjParaCalculo = $cnpjBase;
for ($i = 0; $i < 2; $i++) {
$soma = 0;
$pesos = ($i === 0) ? $pesos1Dv : $pesos2Dv;
$tamanho = strlen(string: $cnpjParaCalculo);
// O documento usa os pesos na ordem da esquerda para a direita no cálculo [cite: 27, 40]
for ($j = 0; $j < $tamanho; $j++) {
$caractere = $cnpjParaCalculo[$j];
$valor = $mapaValores[$caractere] ?? null;
// Se o caractere não for um dos mapeados (ex: um caractere especial ou minúsculo que não foi maiusculizado)
if ($valor === null) {
return false;
}
$peso = $pesos[$j];
$soma += $valor * $peso;
}
// Obter o resto da divisão do somatório por 11 [cite: 31]
$resto = $soma % 11;
// Se o resto da divisão for igual a 1 ou 0, o primeiro dígito será igual a 0 (zero)[cite: 32].
// Senão, o primeiro dígito será igual ao resultado de 11 - resto[cite: 33].
$digitoCalculado = ($resto < 2) ? 0 : 11 - $resto;
$dvRecebido = (int) $dvRecebidos[$i];
if ($dvRecebido !== $digitoCalculado) {
return false;
}
// Para o cálculo do segundo DV, acrescentar o primeiro DV (calculado) ao final do CNPJ base [cite: 38]
if ($i === 0) {
$cnpjParaCalculo .= $digitoCalculado;
}
}
return true;
}
public static function notEmpty(mixed $value): bool {
if (is_array(value: $value)) {
return !empty(array_filter(array: $value, callback: fn($v) => $v !== null && $v !== ''));
}
return !empty(trim(string: (string) $value));
}
public static function validatePassword(string $password, array $rules = []): array {
$errors = [];
$minLength = $rules['minLength'] ?? 8;
$maxLength = $rules['maxLength'] ?? 64;
$requireUpper = $rules['upper'] ?? true;
$requireLower = $rules['lower'] ?? true;
$requireDigit = $rules['digit'] ?? true;
$requireSymbol = $rules['symbol'] ?? true;
if (strlen(string: $password) < $minLength) {
$errors[] = "Senha deve ter no mínimo {$minLength} caracteres.";
}
if (strlen(string: $password) > $maxLength) {
$errors[] = "Senha deve ter no máximo {$maxLength} caracteres.";
}
if ($requireUpper && !preg_match(pattern: '/[A-Z]/', subject: $password)) {
$errors[] = "Senha deve conter pelo menos uma letra maiúscula.";
}
if ($requireLower && !preg_match(pattern: '/[a-z]/', subject: $password)) {
$errors[] = "Senha deve conter pelo menos uma letra minúscula.";
}
if ($requireDigit && !preg_match(pattern: '/\d/', subject: $password)) {
$errors[] = "Senha deve conter pelo menos um número.";
}
if ($requireSymbol && !preg_match(pattern: '/[\W_]/', subject: $password)) {
$errors[] = "Senha deve conter pelo menos um símbolo.";
}
return [
'valid' => empty($errors),
'errors' => $errors
];
}
public static function validateList(mixed $value, array $validOptions): bool {
if (is_array(value: $value)) {
foreach ($value as $item) {
if (!in_array(needle: $item, haystack: $validOptions, strict: true)) {
return false;
}
}
return true;
}
return in_array(needle: $value, haystack: $validOptions, strict: true);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Workbloom\Services;
use AxiumPHP\Core\Database;
class DB extends Database {
}