From ec216528b5e46aa6fea656489bf774eb23b28e74 Mon Sep 17 00:00:00 2001 From: Claudecio Martins Date: Wed, 22 Oct 2025 13:15:43 +0200 Subject: [PATCH] =?UTF-8?q?feat(auth):=20implementar=20rotas=20e=20servi?= =?UTF-8?q?=C3=A7os=20de=20autentica=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adicionadas rotas de login, logout e seleção de contexto de trabalho em AuthRoutes.php. - AuthService aprimorado com login detalhado, validação de token, listagem de contexto de trabalho e métodos de seleção. - JWTService introduzido para lidar com a geração e validação de JWT. - Migração de banco de dados atualizada para incluir dados de amostra da empresa. - Arquivo de migração RBAC criado para futura implementação de controle de acesso baseado em função. --- .../Auth/v0/Controllers/AuthController.php | 134 +++++- .../Auth/v0/Middlewares/AuthMiddleware.php | 64 +++ .../Component/Auth/v0/Models/UsuarioModel.php | 290 ++++++++++++- .../Component/Auth/v0/Routes/AuthRoutes.php | 30 +- .../Auth/v0/Services/AuthService.php | 409 +++++++++++++++++- app/Common/Services/JWTService.php | 179 ++++++++ database/v0/migrations/002_public.empresa.sql | 4 + database/v0/migrations/004_RBAC.sql | 0 8 files changed, 1098 insertions(+), 12 deletions(-) create mode 100644 app/Common/Component/Auth/v0/Middlewares/AuthMiddleware.php create mode 100644 app/Common/Services/JWTService.php create mode 100644 database/v0/migrations/004_RBAC.sql diff --git a/app/Common/Component/Auth/v0/Controllers/AuthController.php b/app/Common/Component/Auth/v0/Controllers/AuthController.php index 6e93964..04bb5d6 100644 --- a/app/Common/Component/Auth/v0/Controllers/AuthController.php +++ b/app/Common/Component/Auth/v0/Controllers/AuthController.php @@ -1,12 +1,25 @@ $form - ] + response_code: $loginService['response_code'] ?? 500, + status: $loginService['status'] ?? 'error', + message: $loginService['message'] ?? 'Internal server error', + output: $loginService['output'] ?? [] + ); + } + + /** + * Lida com a requisição para listar os contextos de trabalho (empresas) disponíveis para o usuário autenticado. + * + * Este método atua como um **controlador** de API, extraindo o UUID e o status de root do usuário a partir do token JWT e delegando a lógica de busca de contextos para o `AuthService`. + * + * #### Fluxo de Operação: + * 1. **Obtenção e Decodificação do Token:** O token Bearer é obtido e decodificado via `JWTService`. Os dados essenciais do usuário, como `uuid` e `is_root`, são extraídos do payload. + * 2. **Delegação ao Serviço:** O método `listWorkContexts` do **`AuthService`** é chamado, passando o UUID do usuário e seu status de root. O serviço é responsável por consultar os contextos de trabalho (todas as empresas para usuários root ou apenas as associadas para usuários normais). + * 3. **Envio da Resposta JSON:** O resultado retornado pelo serviço (`workContexts`) é formatado para uma resposta JSON padronizada usando **`RequestHelper::sendJsonResponse`**. O código HTTP, status, mensagem e os dados dos contextos são comunicados ao cliente. + * + * @return void Este método não retorna um valor, pois seu objetivo é enviar uma resposta JSON e encerrar a execução do script. + */ + public function listWorkContexts(): void { + // Receve o token Bearer da requisição + $bearerToken = JWTService::decode(token: JWTService::getBearerToken()); + + // Consulta os contextos de trabalho associados ao token + $workContexts = (new AuthService())->listWorkContexts(usuario_uuid: $bearerToken['user_data']['uuid'], is_root: $bearerToken['user_data']['is_root']); + + // Resposta JSON + RequestHelper::sendJsonResponse( + response_code: $workContexts['response_code'] ?? 500, + status: $workContexts['status'] ?? 'error', + message: $workContexts['message'] ?? 'Internal server error', + output: $workContexts['output'] ?? [] + ); + } + + /** + * Lida com a requisição de seleção do contexto de trabalho (empresa) de um usuário. + * + * Este método atua como um **controlador** de API, responsável por: + * 1. **Extrair a Identidade:** Obtém e decodifica o token Bearer para identificar o usuário. + * 2. **Validação de Presença:** Verifica se o campo obrigatório `contexto_uuid` foi fornecido. Se estiver ausente, envia um erro **400 Bad Request** e encerra a execução. + * 3. **Delegação ao Serviço:** Delega a validação de acesso ao contexto, a revogação do token antigo e a geração do novo token atualizado (que inclui o contexto de trabalho) para o **`AuthService::selectWorkContext()`**. + * 4. **Envio da Resposta JSON:** O resultado retornado pelo serviço é formatado e enviado de volta ao cliente, comunicando o código HTTP, status, mensagem e o novo token JWT. + * + * @return void Este método não retorna um valor, pois seu objetivo é enviar uma resposta JSON e encerrar a execução do script. + */ + public function selectWorkContext(): void { + // Receve o token Bearer da requisição + $bearerToken = JWTService::decode(token: JWTService::getBearerToken()); + + // Recebe formulário de seleção de contexto + $form = RequestHelper::getFilteredInput(form_type: INPUT_POST); + + // Campos Obrigatórios + $required_fields = [ + 'contexto_uuid' + ]; + + // 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 seleção de contexto + $workContexts = (new AuthService())->selectWorkContext( + usuario_uuid: $bearerToken['user_data']['uuid'], + contexto_uuid: DataSanitizer::string(value: $form['contexto_uuid'] ?? '') + ); + + // Resposta JSON + RequestHelper::sendJsonResponse( + response_code: $workContexts['response_code'] ?? 500, + status: $workContexts['status'] ?? 'error', + message: $workContexts['message'] ?? 'Internal server error', + output: $workContexts['output'] ?? [] + ); + } + + /** + * Lida com a requisição de logout, revogando o token de autenticação do usuário. + * + * Este método atua como um **controlador** de API, responsável por extrair o token JWT do cabeçalho da requisição, delegar a lógica de revogação para o serviço e formatar a resposta JSON final. + * + * #### Fluxo de Operação: + * 1. **Obtenção do Token:** O token Bearer é extraído do cabeçalho da requisição. + * 2. **Delegação ao Serviço:** O método `logout` do **`AuthService`** é chamado com o token. O serviço é responsável por revogar o token no banco de dados e gerenciar a transação. + * 3. **Envio da Resposta JSON:** O resultado retornado pelo serviço (`logoutService`) é formatado para uma resposta JSON padronizada usando **`RequestHelper::sendJsonResponse`**. O código HTTP, status, mensagem e quaisquer dados de saída são comunicados ao cliente. + * + * @return void Este método não retorna um valor, pois seu objetivo é enviar uma resposta JSON e encerrar a execução do script. + */ + public function logout(): void { + // Chama o serviço de logout + $logoutService = (new AuthService())->logout( + token: JWTService::getBearerToken() + ); + + // Resposta JSON + RequestHelper::sendJsonResponse( + response_code: $logoutService['response_code'] ?? 500, + status: $logoutService['status'] ?? 'error', + message: $logoutService['message'] ?? 'Internal server error', + output: $logoutService['output'] ?? [] ); } } \ No newline at end of file diff --git a/app/Common/Component/Auth/v0/Middlewares/AuthMiddleware.php b/app/Common/Component/Auth/v0/Middlewares/AuthMiddleware.php new file mode 100644 index 0000000..7c391df --- /dev/null +++ b/app/Common/Component/Auth/v0/Middlewares/AuthMiddleware.php @@ -0,0 +1,64 @@ + 401, + 'status' => 'error', + 'message' => 'Unauthorized: Invalid or missing token.' + ]; + } + + // Chama serviço de login e valida token e permissões + $authService = (new AuthService())->validateAuthToken(token: $bearerToken); + + // Verifica se a validação foi bem-sucedida + if($authService['status'] !== 'success') { + return [ + 'response_code' => $authService['response_code'] ?? 500, + 'status' => $authService['status'] ?? 'error', + 'message' => $authService['message'] ?? 'Internal server error', + 'output' => $authService['output'] ?? [] + ]; + } + + return true; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode(), + 'status' => 'error', + 'message' => $e->getMessage() + ]; + } + } + } \ No newline at end of file diff --git a/app/Common/Component/Auth/v0/Models/UsuarioModel.php b/app/Common/Component/Auth/v0/Models/UsuarioModel.php index 9b49951..df2ac10 100644 --- a/app/Common/Component/Auth/v0/Models/UsuarioModel.php +++ b/app/Common/Component/Auth/v0/Models/UsuarioModel.php @@ -8,8 +8,10 @@ use Workbloom\Component\Auth\v0\DTO\UsuarioTokenDTO; class UsuarioModel { + protected string $empresaTable = 'empresa'; protected string $usuarioTable = 'usuario'; protected string $usuarioTokenTable = 'usuario_token'; + protected string $usuarioEmpresaTable = 'usuario_empresa'; /** * Armazena um novo usuário no banco de dados. @@ -475,7 +477,7 @@ * @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 { + public function revokeOldUsuarioTokens(string $usuario_uuid): array { try { $sql = "UPDATE {$this->usuarioTokenTable} AS ut SET @@ -507,4 +509,290 @@ ]; } } + + /** + * Busca um registro de token de usuário no banco de dados por um identificador específico. + * + * Este método recupera todos os detalhes de um token de autenticação (JWT) da tabela `{$this->usuarioTokenTable}` de forma flexível, permitindo a busca por qualquer coluna (`$identifier`) e o valor correspondente. + * + * #### Fluxo de Operação: + * 1. **Execução da Query:** 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. A consulta utiliza `DB::fetchOne`, indicando que apenas um registro é esperado. + * 2. **Verificação de Resultados:** + * - Se o token **não for encontrado** (`empty($result)`), retorna um array com **`status` 'error'** e código **404 Not Found**. + * - Se o token for encontrado, retorna um array com **`status` 'success'**, código **200 OK** e os dados do registro na chave `output`. + * + * #### 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: 'token', 'usuario_id', 'ip_address'). + * @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 token, se encontrado. + */ + public function findUsuarioTokenByIdentifier(string $identifier, mixed $value): array { + try { + $sql = + "SELECT + id, + usuario_id, + usuario_ip, + usuario_user_agent, + token, + is_revoked, + created_at, + expires_at, + revoked_at + FROM {$this->usuarioTokenTable} + WHERE {$identifier} = :value"; + + $params = [ + ":value" => $value, + ]; + + $result = DB::fetchOne(sql: $sql, params: $params); + if(empty($result)) { + return [ + 'response_code' => 404, + 'status' => 'error', + 'message' => 'Token de usuário não encontrado', + ]; + } + + return [ + 'response_code' => 200, + 'status' => 'success', + 'message' => 'Token de 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 e retorna todos os contextos de trabalho (empresas) associados a um usuário, identificando o usuário pelo seu UUID. + * + * Este método consulta a tabela de associação usuário-empresa para listar todas as empresas ativas (`status_id = 1`) às quais um usuário específico tem acesso. + * + * #### Fluxo de Operação: + * 1. **Execução da Query:** Executa uma consulta `SELECT` que une as tabelas de usuário, empresa e a tabela de associação (`{$this->usuarioEmpresaTable}`) para filtrar pelo `usuario_uuid`. A busca retorna múltiplos registros (empresas). + * 2. **Verificação de Resultados:** + * - Se **nenhum contexto de trabalho for encontrado** (`empty($result)`), retorna um array com **`status` 'error'** e código **404 Not Found**. + * - Se contextos forem encontrados, retorna um array com **`status` 'success'**, código **200 OK** e os dados das empresas encontradas na chave `output`. + * + * #### 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 contextos de trabalho serão buscados. + * @return array Um array associativo contendo o status da operação, o código de resposta HTTP e os dados dos contextos de trabalho, se encontrados. + */ + public function findWorkContextsByUsuarioUUID(string $usuario_uuid): array { + try { + $sql = + "SELECT + 'Empresa' AS contexto_tipo, + e.uuid AS contexto_uuid, + e.razao_social, + e.nome_fantasia, + e.document_inscricao + FROM {$this->usuarioEmpresaTable} ue + LEFT JOIN {$this->usuarioTable} u ON ue.usuario_id = u.id + LEFT JOIN {$this->empresaTable} e ON ue.empresa_id = e.id + WHERE u.uuid = :usuario_uuid + AND e.status_id = :status_id"; + + $params = [ + ":usuario_uuid"=> $usuario_uuid, + ":status_id" => 1 + ]; + + // Executa a query + $result = DB::fetchAll(sql: $sql, params: $params); + if(empty($result)) { + return [ + 'response_code' => 404, + 'status' => 'error', + 'message' => 'Contextos de trabalho não encontrados para o usuário', + ]; + } + + return [ + 'response_code' => 200, + 'status' => 'success', + 'message' => 'Contextos de trabalho encontrados com sucesso', + 'output' => [ + 'data' => ['contextos' => $result] + ] + ]; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode() ?? 500, + 'status' => 'error', + 'message' => $e->getMessage() ?? 'Internal Server Error' + ]; + } + } + + /** + * Busca e retorna todos os contextos de trabalho (empresas) ativos no sistema. + * + * Este método consulta a tabela `{$this->empresaTable}` para listar todas as empresas que estão marcadas como ativas (`status_id = 1`). É útil para a administração do sistema que precisa listar todas as entidades disponíveis. + * + * #### Fluxo de Operação: + * 1. **Execução da Query:** Executa uma consulta `SELECT` para buscar as informações essenciais de todas as empresas com `status_id = 1`. + * 2. **Verificação de Resultados:** + * * Se **nenhum contexto de trabalho for encontrado** (`empty($result)`), retorna um array com **`status` 'error'** e código **404 Not Found**, com uma mensagem indicando a ausência de contextos. + * * Se contextos forem encontrados, retorna um array com **`status` 'success'**, código **200 OK** e os dados das empresas na chave `output`. + * + * #### 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. + * + * @return array Um array associativo contendo o status da operação, um código de resposta HTTP e os dados dos contextos de trabalho, se encontrados. + */ + public function findAllWorkContexts(int $is_root = 0): array { + try { + $sql = + "SELECT + 'Empresa' AS contexto_tipo, + e.uuid AS contexto_uuid, + e.razao_social, + e.nome_fantasia, + e.document_inscricao + FROM {$this->empresaTable} e + WHERE e.status_id = :status_id"; + + $params = [ + ":status_id" => 1 + ]; + + // Executa a query + $result = DB::fetchAll(sql: $sql, params: $params); + if(empty($result)) { + return [ + 'response_code' => 404, + 'status' => 'error', + 'message' => 'Contextos de trabalho não encontrados para o usuário', + ]; + } + + // Adiciona o contexto da plataforma se o usuário for Administrador + if($is_root === 1) { + $adminContext = [ + 'contexto_tipo' => 'Sistema', + 'contexto_uuid' => '019a07ee-0c7c-7dd7-a447-802ede64dc98', + 'nome' => 'Área do Administrador' + ]; + + $result = array_merge([$adminContext], $result); + } + + return [ + 'response_code' => 200, + 'status' => 'success', + 'message' => 'Contextos de trabalho encontrados com sucesso', + 'output' => [ + 'data' => ['contextos' => $result] + ] + ]; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode() ?? 500, + 'status' => 'error', + 'message' => $e->getMessage() ?? 'Internal Server Error' + ]; + } + } + + /** + * Busca um contexto de trabalho (empresa) específico e verifica se o usuário tem permissão para acessá-lo. + * + * Este método é crucial para a validação do contexto de trabalho de um usuário (por exemplo, ao iniciar uma sessão em uma empresa). Ele usa uma lógica de consulta SQL para determinar se o `usuario_uuid` está associado à `contexto_uuid` fornecida ou se o usuário é um administrador de sistema (root). + * + * #### Fluxo de Permissão Lógica: + * A cláusula `WHERE` verifica DUAS condições de permissão: + * 1. **Associação Direta/Normal (EXISTS):** O usuário está diretamente associado à empresa (`ue.usuario_id = u.id AND ue.empresa_id = e.id`). + * 2. **Permissão de Root (OR EXISTS):** O usuário possui a flag `is_root = 1`. + * + * #### Fluxo de Operação: + * 1. **Execução da Query:** Executa uma consulta `SELECT` complexa que filtra as empresas por `contexto_uuid` e `status_id = 1`, além de verificar a permissão do usuário via `EXISTS`. + * 2. **Verificação de Resultados:** + * * Se o contexto for encontrado e o usuário tiver permissão, retorna um array com **`status` 'success'** e código **200 OK**, com os dados do contexto. + * * Se o contexto **não for encontrado** ou o usuário **não tiver permissão** (o que resulta em `empty($result)` devido ao `WHERE`), retorna um array com **`status` 'error'** e código **404 Not Found**. + * + * #### 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 cuja permissão de acesso está sendo verificada. + * @param string $contexto_uuid O UUID da empresa/contexto de trabalho que o usuário está tentando acessar. + * @return array Um array associativo contendo o status da operação, um código de resposta HTTP e os dados do contexto, se encontrado. + */ + public function findWorkContextByUsuarioAndContextoUUID(string $usuario_uuid, string $contexto_uuid): array { + try { + $sql = + "SELECT + 'Empresa' AS contexto_tipo, + e.uuid AS contexto_uuid, + e.razao_social, + e.nome_fantasia, + e.document_inscricao + FROM {$this->empresaTable} e + WHERE + ( + EXISTS ( + SELECT 1 + FROM {$this->usuarioEmpresaTable} ue + INNER JOIN {$this->usuarioTable} u + ON ue.usuario_id = u.id + WHERE + ue.empresa_id = e.id + AND u.uuid = :usuario_uuid + ) + OR EXISTS ( + SELECT 1 + FROM {$this->usuarioTable} u2 + WHERE + u2.uuid = :usuario_uuid + AND u2.is_root = 1 + ) + ) + AND e.uuid = :contexto_uuid + AND e.status_id = :status_id"; + + $params = [ + ":usuario_uuid"=> $usuario_uuid, + ":contexto_uuid"=> $contexto_uuid, + ":status_id" => 1 + ]; + + // Executa a query + $result = DB::fetchOne(sql: $sql, params: $params); + if(empty($result)) { + return [ + 'response_code' => 404, + 'status' => 'error', + 'message' => 'Contexto de trabalho não encontrado para o usuário', + ]; + } + + return [ + 'response_code' => 200, + 'status' => 'success', + 'message' => 'Contexto de trabalho encontrado com sucesso', + 'output' => [ + 'data' => $result + ] + ]; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode() ?? 500, + 'status' => 'error', + 'message' => $e->getMessage() ?? 'Internal Server Error' + ]; + } + } } \ No newline at end of file diff --git a/app/Common/Component/Auth/v0/Routes/AuthRoutes.php b/app/Common/Component/Auth/v0/Routes/AuthRoutes.php index 50a2e63..d5de0b6 100644 --- a/app/Common/Component/Auth/v0/Routes/AuthRoutes.php +++ b/app/Common/Component/Auth/v0/Routes/AuthRoutes.php @@ -1,9 +1,37 @@ $credentials]; + /** + * Realiza a autenticação de um usuário, verificando as credenciais, revogando sessões antigas e gerando um novo token JWT. + * + * Este método gerencia todo o fluxo de login e a criação da sessão de autenticação. Ele garante a atomicidade das operações de banco de dados (`revogação` e `registro do token`) utilizando transações. + * + * #### Fluxo de Operação: + * 1. **Início da Transação:** Inicia uma transação no banco de dados (`DB::beginTransaction()`). + * 2. **Verificação de Credenciais:** Busca o usuário pelo login (e-mail ou nome de usuário) e verifica a senha fornecida contra o hash armazenado. Se falhar, lança uma `Exception` (código 401). + * 3. **Verificação de Status:** Confere se o usuário está ativo (`status_id = 1`) e não excluído. Se não, lança uma `Exception` (código 403). + * 4. **Geração do JWT:** Monta o payload do token com os dados essenciais do usuário e gera o novo token JWT. + * 5. **Revogação de Tokens Antigos:** Revoga todos os tokens ativos anteriores do usuário no banco de dados. Se a revogação falhar, a transação é revertida, e uma `Exception` é lançada. + * 6. **Registro do Novo Token:** Armazena o novo token JWT e metadados de sessão (IP, User Agent, expiração) no banco de dados. Se o registro falhar, a transação é revertida, e uma `Exception` é lançada. + * 7. **Confirmação e Retorno:** Se todas as etapas forem bem-sucedidas, a transação é confirmada (`DB::commit()`), e o token gerado é retornado com `status` 'success' e código `200 OK`. + * + * #### Tratamento de Erros: + * - Qualquer `Exception` capturada reverte a transação e retorna um array de erro formatado. + * + * @param array $credentials Um array associativo contendo as credenciais do usuário, com as chaves 'login' e 'password'. + * @return array Um array associativo contendo o status da operação, o código de resposta HTTP e o token JWT em caso de sucesso. + */ + public function login(array $credentials): array { + try { + // Consulta o usuario com base no login + $usuarioLogin = (new UsuarioModel())->findUsuarioByLogin(login: $credentials['login']); + if($usuarioLogin['status'] !== 'success') { + throw new Exception(message: 'Usuário não encontrado', code: 404); + } + $usuarioData = $usuarioLogin['output']['data']; + + // Verifica a senha + if(!password_verify(password: $credentials['password'], hash: $usuarioData['senha_hash'])) { + throw new Exception(message: 'Senha inválida', code: 401); + } + + // Verifica se o usuário está ativo + if(($usuarioData['status_id'] != 1) || ($usuarioData['deleted_at'] !== null)) { + throw new Exception(message: 'Usuário inativo ou excluído', code: 403); + } + + // Monta o payload do token + $payload['user_data'] = [ + 'uuid' => $usuarioData['uuid'], + 'nome_completo' => $usuarioData['nome_completo'], + 'nome_usuario' => $usuarioData['nome_usuario'], + 'is_root' => $usuarioData['is_root'] + ]; + + // Gera o token JWT + $generateJWT = JWTService::generateToken(payload: $payload); + + // Inicia Transação no Banco de Dados + DB::beginTransaction(); + + // Revoga tokens de sessões anteriores + $revokeOldTokens = (new UsuarioModel())->revokeOldUsuarioTokens(usuario_uuid: $usuarioData['uuid']); + if($revokeOldTokens['status'] !== 'success') { + DB::rollBack(); + throw new Exception(message: 'Erro ao revogar tokens antigos', code: 500); + } + + // Registra o token da sessão + $registerToken = (new UsuarioModel())->storeUsuarioToken( + usuarioTokenDTO: new UsuarioTokenDTO(data: [ + 'usuario_id' => $usuarioData['id'], + 'usuario_ip' => $_SERVER['REMOTE_ADDR'] ?? null, + 'usuario_user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, + 'token' => $generateJWT['token'], + 'is_revoked' => 0, + 'expires_at' => $generateJWT['expires_at'] + ]) + ); + if($registerToken['status'] !== 'success') { + DB::rollBack(); + throw new Exception(message: 'Erro ao registrar token de sessão', code: 500); + } + + DB::commit(); + return [ + 'response_code' => 200, + 'status' => 'success', + 'message' => 'Login realizado com sucesso', + 'output' => [ + 'data' => $generateJWT + ] + ]; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode() ?? 500, + 'status' => 'error', + 'message' => $e->getMessage() ?? 'Internal server error' + ]; + } + } + + /** + * Valida um token de autenticação JWT contra o registro do banco de dados e verifica a expiração/revogação. + * + * Este método realiza uma série de verificações de segurança para garantir que o token seja válido e não tenha sido comprometido ou expirado. Ele gerencia o processo de revogação do token no banco de dados se o token for encontrado, mas estiver expirado. + * + * #### Fluxo de Operação: + * 1. **Busca e Validação no DB:** Consulta o token no banco de dados. Se não for encontrado, lança `Exception` (código 404/401). + * 2. **Verificação de Expiração e Revogação:** Checa se o token está revogado (`is_revoked === 1`) ou se o prazo de validade (`expires_at`) foi ultrapassado. + * 3. **Revogação Automática (se Expirado):** Se o token estiver expirado, e ainda não estiver revogado, o método inicia uma transação (`DB::beginTransaction()`) para revogá-lo no DB. Em seguida, lança uma `Exception` com código `401 Unauthorized` (Token revogado). + * 4. **Verificação de Dispositivo:** Compara o IP (`usuario_ip`) e o User Agent (`usuario_user_agent`) armazenados no token com os da requisição atual. Se não houver correspondência, lança uma `Exception` (código 401), invalidando o token para o dispositivo. + * 5. **Retorno de Sucesso:** Se todas as verificações passarem, retorna `status` 'success' com o código `200 OK` e os dados do token. + * + * #### Tratamento de Erros: + * - Qualquer `Exception` lançada durante o processo é capturada, e o método retorna um array de erro formatado. O `DB::rollBack()` é tratado nos sub-métodos de revogação, mas o fluxo garante que o erro seja propagado. + * + * @param string $token O token JWT de autenticação a ser validado. + * @return array Um array associativo contendo o status da operação, um código de resposta HTTP e os dados do token, se válido. + */ + public function validateAuthToken(string $token) { + try { + // Consulta o token no banco de dados + $findToken = (new UsuarioModel())->findUsuarioTokenByIdentifier(identifier: 'token', value: $token); + if($findToken['status'] !== 'success') { + throw new Exception(message: 'Token não encontrado', code: 404); + } + $tokenData = $findToken['output']['data']; + + // Verifica se o token está revogado ou expirado + if(($tokenData['is_revoked'] === 1) || (new DateTime() > new DateTime(datetime: $tokenData['expires_at']))) { + + // Revoga o token se ainda não estiver revogado + if($tokenData['is_revoked'] === 0) { + // Incia Transação no Banco de Dados + DB::beginTransaction(); + + // Atualiza o token para revogado + $revokeToken = (new UsuarioModel())->updateUsuarioToken(usuarioTokenDTO: new UsuarioTokenDTO(data: [ + 'is_revoked' => 1, + 'revoked_at' => (new DateTime())->format(format: 'Y-m-d H:i:s'), + 'token' => $tokenData['token'] + ])); + + // Verifica se a revogação foi bem-sucedida + if($revokeToken['status'] !== 'success') { + DB::rollBack(); + throw new Exception(message: 'Erro ao revogar token', code: 500); + } + + // Finaliza Transação no Banco de Dados + DB::commit(); + } + + // Lança exceção indicando que o token foi revogado + throw new Exception(message: 'Token revogado', code: 401); + } + + // Verifica se o agente do usuário e o IP correspondem ao da requisição atual + if(isset($tokenData['usuario_ip']) && !in_array(needle: $tokenData['usuario_ip'], haystack: [$_SERVER['REMOTE_ADDR'], null], strict: true)) { + throw new Exception(message: "Token inválido para este dispositivo ou IP", code: 401); + } + if(isset($tokenData['usuario_user_agent']) && !in_array(needle: $tokenData['usuario_user_agent'], haystack: [$_SERVER['HTTP_USER_AGENT'], null], strict: true)) { + throw new Exception(message: "Token inválido para este dispositivo ou IP", code: 401); + } + + return [ + 'response_code' => 200, + 'status' => 'success', + 'message' => 'Token válido', + 'output' => [ + 'data' => $tokenData + ] + ]; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode() ?? 500, + 'status' => 'error', + 'message' => $e->getMessage() ?? 'Internal server error' + ]; + } + } + + /** + * Lista os contextos de trabalho (empresas) disponíveis para um usuário. + * + * Este método determina quais empresas o usuário autenticado pode acessar. Se o usuário for marcado como 'root' (`$is_root` = 1), ele lista todos os contextos ativos no sistema. Caso contrário, lista apenas os contextos associados diretamente ao seu UUID. + * + * #### Fluxo de Operação: + * 1. **Verificação de Permissão Root:** + * - Se `$is_root` for `1`, chama `UsuarioModel::findAllWorkContexts()` para listar todos os contextos ativos do sistema. + * - Se a consulta falhar, lança uma `Exception` (código 500). + * 2. **Busca por Usuário Específico:** + * - Se o usuário não for root, chama `UsuarioModel::findWorkContextsByUsuarioUUID()` para listar apenas os contextos diretamente associados ao `usuario_uuid`. + * - Se a consulta falhar, lança uma `Exception` (código 500). + * 3. **Retorno:** Retorna o array de resultados de contextos (sucesso ou falha, conforme retornado pelo modelo). + * + * #### Tratamento de Erros: + * - Qualquer `Exception` capturada durante o processo (incluindo falhas nos métodos do modelo) retorna um array de erro formatado com o código HTTP e a mensagem da exceção. + * + * @param string $usuario_uuid O UUID do usuário autenticado. + * @param int $is_root Indica se o usuário possui permissão de root (1) ou não (0). + * @return array Um array associativo contendo o status da operação, código HTTP e os dados dos contextos de trabalho (`output` com `data`), ou um erro. + */ + public function listWorkContexts(string $usuario_uuid, int $is_root = 0): array { + try { + // Se for root, lista todos os contextos de trabalho + if($is_root) { + // Consulta os contextos de trabalho do usuário + $contextos = (new UsuarioModel())->findAllWorkContexts(is_root: $is_root); + if($contextos['status'] !== 'success') { + throw new Exception(message: 'Erro ao recuperar contextos de trabalho', code: 500); + } + + // Lógica para listar todos os contextos de trabalho + return $contextos; + } + + // Consulta os contextos de trabalho do usuário + $contextos = (new UsuarioModel())->findWorkContextsByUsuarioUUID(usuario_uuid: $usuario_uuid); + if($contextos['status'] !== 'success') { + throw new Exception(message: 'Erro ao recuperar contextos de trabalho', code: 500); + } + + // Lógica para listar os contextos de trabalho do usuário + return $contextos; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode() ?? 500, + 'status' => 'error', + 'message' => $e->getMessage() ?? 'Internal server error' + ]; + } + } + + /** + * Seleciona e ativa um contexto de trabalho (empresa) para o usuário autenticado. + * + * Este método é chamado após o login para reemitir um token JWT que inclui as informações do contexto de trabalho selecionado. A operação revoga o token antigo da sessão atual e registra um novo, garantindo a atomicidade das operações de banco de dados. + * + * #### Fluxo de Operação: + * 1. **Verificação de Permissão:** Se o usuário não for 'root' (`$is_root === 0`), verifica se o usuário tem permissão para acessar o `contexto_uuid` específico. Se a verificação falhar, lança uma `Exception` (código 404). + * 2. **Busca de Dados:** Busca os dados completos do usuário e os dados do contexto (se não for root). + * 3. **Montagem do Payload:** Monta o payload do novo token JWT, incluindo os dados básicos do usuário e o novo `context_data` (UUID e informações da empresa, se aplicável). + * 4. **Gerenciamento Transacional:** + * - Inicia uma transação no banco de dados (`DB::beginTransaction()`). + * - **Revoga Token Antigo:** Atualiza o token anterior para revogado. Se falhar, reverte a transação e lança `Exception` (código 500). + * - **Gera e Registra Novo Token:** Gera um novo token JWT com o payload atualizado e o registra no banco de dados. Se falhar, reverte a transação e lança `Exception` (código 500). + * 5. **Confirmação e Retorno:** A transação é confirmada (`DB::commit()`), e o novo token é retornado com `status` 'success' e código `200 OK`. + * + * #### Tratamento de Erros: + * - Qualquer `Exception` lançada é capturada, e o método retorna um array de erro formatado com o código HTTP e a mensagem da exceção. + * + * @param string $usuario_uuid O UUID do usuário autenticado. + * @param string $contexto_uuid O UUID do contexto de trabalho (empresa) selecionado. + * @param int $is_root Flag que indica se o usuário é root (0 ou 1). + * @return array Um array associativo contendo o status da operação, o código de resposta HTTP e o novo token JWT. + */ + public function selectWorkContext(string $usuario_uuid, string $contexto_uuid, int $is_root = 0): array { + try { + // Se não for root, verifica se o contexto de trabalho pertence ao usuário + if($is_root === 0) { + // Consulta se o contexto de trabalho pertence ao usuário + $workContext = (new UsuarioModel())->findWorkContextByUsuarioAndContextoUUID( + usuario_uuid: $usuario_uuid, + contexto_uuid: $contexto_uuid + ); + if($workContext['status'] !== 'success') { + throw new Exception(message: 'Contexto de trabalho não encontrado para o usuário', code: 404); + } + } + + // Consulta os dados do usuário + $usuarioLogin = (new UsuarioModel())->findUsuarioByIdentifier(identifier: 'uuid', value: $usuario_uuid); + if($usuarioLogin['status'] !== 'success') { + throw new Exception(message: 'Usuário não encontrado', code: 404); + } + $usuarioData = $usuarioLogin['output']['data']; + + // Monta novo payload do token com o contexto selecionado + $payload['user_data'] = [ + 'uuid' => $usuarioData['uuid'], + 'nome_completo' => $usuarioData['nome_completo'], + 'nome_usuario' => $usuarioData['nome_usuario'], + 'is_root' => $usuarioData['is_root'] + ]; + + // Monta novo payload do token com o contexto selecionado + if(isset($workContext)) { + $payload['context_data'] = [ + 'uuid' => $workContext['output']['data']['contexto_uuid'] + ]; + + if($workContext['output']['data']['razao_social'] !== null) { + $payload['context_data']['razao_social'] = $workContext['output']['data']['razao_social']; + } + + if($workContext['output']['data']['nome_fantasia'] !== null) { + $payload['context_data']['nome_fantasia'] = $workContext['output']['data']['nome_fantasia']; + } + } else { + $payload['context_data'] = [ + 'uuid' => $contexto_uuid + ]; + } + + // Inicia Transação no Banco de Dados + DB::beginTransaction(); + + // Gera o token JWT + $generateJWT = JWTService::generateToken(payload: $payload); + + // Revoga o token antigo + $revokeToken = (new UsuarioModel())->updateUsuarioToken(usuarioTokenDTO: new UsuarioTokenDTO(data: [ + 'is_revoked' => 1, + 'revoked_at' => (new DateTime())->format(format: 'Y-m-d H:i:s'), + 'token' => JWTService::getBearerToken() + ])); + if($revokeToken['status'] !== 'success') { + DB::rollBack(); + throw new Exception(message: 'Erro ao revogar token antigo', code: 500); + } + + // Registra o novo token da sessão + $registerToken = (new UsuarioModel())->storeUsuarioToken( + usuarioTokenDTO: new UsuarioTokenDTO(data: [ + 'usuario_id' => $usuarioData['id'], + 'usuario_ip' => $_SERVER['REMOTE_ADDR'] ?? null, + 'usuario_user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null, + 'token' => $generateJWT['token'], + 'is_revoked' => 0, + 'expires_at' => $generateJWT['expires_at'] + ]) + ); + if($registerToken['status'] !== 'success') { + DB::rollBack(); + throw new Exception(message: 'Erro ao registrar token de sessão', code: 500); + } + + // Confirma a transação no banco de dados + DB::commit(); + + return [ + 'response_code' => 200, + 'status' => 'success', + 'message' => 'Contexto de trabalho selecionado com sucesso', + 'output' => ['data' => $generateJWT] + ]; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode() ?? 500, + 'status' => 'error', + 'message' => $e->getMessage() ?? 'Internal server error' + ]; + } + } + + /** + * Finaliza a sessão do usuário revogando um token de autenticação JWT específico. + * + * Este método atua como um **serviço**, orquestrando a revogação do token no banco de dados e garantindo a atomicidade da operação. + * + * #### Fluxo de Operação: + * 1. **Início da Transação:** Inicia uma transação no banco de dados (`DB::beginTransaction()`). + * 2. **Preparação do DTO:** Cria um `UsuarioTokenDTO` para a revogação, definindo o `token`, o status `is_revoked = 1` e o `revoked_at` como o timestamp atual. + * 3. **Revogação no Modelo:** Chama `UsuarioModel::updateUsuarioToken()` para invalidar o token no banco de dados. + * 4. **Gerenciamento de Transação:** + * * Se a revogação falhar (status do modelo for `!== 'success'`), a transação é **revertida** (`DB::rollBack()`), e uma `Exception` é lançada. + * * Se a revogação for bem-sucedida, a transação é **confirmada** (`DB::commit()`). + * 5. **Retorno de Sucesso:** Retorna um array com **`status` 'success'** e código **200 OK**. + * + * #### Tratamento de Erros: + * - Qualquer `Exception` lançada durante o processo é capturada, a transação é revertida, e o método retorna um array de erro formatado com o código HTTP e a mensagem da exceção. + * + * @param string $token O token JWT a ser revogado. + * @return array Um array associativo contendo o status da operação, o código de resposta HTTP e uma mensagem. + */ + public function logout(string $token): array { + try { + // Inicia Transação no Banco de Dados + DB::beginTransaction(); + + // Atualiza o token para revogado + $revokeToken = (new UsuarioModel())->updateUsuarioToken(usuarioTokenDTO: new UsuarioTokenDTO(data: [ + 'is_revoked' => 1, + 'revoked_at' => (new DateTime())->format(format: 'Y-m-d H:i:s'), + 'token' => $token + ])); + if($revokeToken['status'] !== 'success') { + DB::rollBack(); + throw new Exception(message: 'Erro ao revogar token', code: 500); + } + + // Confirma a transação no banco de dados + DB::commit(); + return [ + 'response_code' => 200, + 'status' => 'success', + 'message' => 'Logout realizado com sucesso' + ]; + } catch(Exception $e) { + return [ + 'response_code' => $e->getCode() ?? 500, + 'status' => 'error', + 'message' => $e->getMessage() ?? 'Internal server error' + ]; + } } } \ No newline at end of file diff --git a/app/Common/Services/JWTService.php b/app/Common/Services/JWTService.php new file mode 100644 index 0000000..36955bb --- /dev/null +++ b/app/Common/Services/JWTService.php @@ -0,0 +1,179 @@ +get(key: 'JWT_ISSUER'); + self::$algorithm = $Config->get(key: 'JWT_ALGO'); + self::$secretKey = $Config->get(key: 'JWT_SECRET'); + self::$expiration = $Config->get(key: 'JWT_EXPIRATION'); + } + + /** + * Gera um token JWT com base em um payload e nas configurações da aplicação. + * + * Este método cria um token JWT válido a partir de um array de dados fornecido (`$payload`), + * adicionando automaticamente metadados essenciais como a data de emissão, a data de expiração e o emissor. + * + * O processo detalhado inclui: + * 1. **Inicialização:** Chama o método `init()` para garantir que todas as configurações do JWT + * (chave secreta, algoritmo, etc.) estejam carregadas. + * 2. **Definição dos Timestamps:** Calcula o timestamp de emissão (`iat`) e o de expiração (`exp`) + * com base no tempo de expiração definido nas configurações. + * 3. **Montagem do Payload:** Adiciona as chaves `iat`, `exp` e `iss` (emissor) ao array `$payload`. + * 4. **Codificação do Token:** Utiliza a biblioteca `JWT::encode()` para codificar o payload + * com a chave secreta e o algoritmo definidos nas configurações. + * + * @param array $payload Um array associativo contendo os dados personalizados que serão incluídos no token. + * @return array O token JWT gerado como um array. + */ + public static function generateToken(array $payload): array { + // Carrega as informações do arquivo de configuração + self::init(); + + // Define data de emissão e calcula a data de expiração do token + $expiration = self::$expiration; + $createdAt = (new DateTime())->getTimestamp(); + $expireAt = (new DateTime())->modify(modifier: "+{$expiration} seconds")->getTimestamp(); + + // Adiciona data de criação ao payload + $payload['iat'] = $createdAt; + // Adiciona a expiração ao payload + $payload['exp'] = $expireAt; + // Adiciona o emissor ao payload + $payload['iss'] = self::$issuer; + + return [ + 'token' => JWT::encode(payload: $payload, key: self::$secretKey, alg: self::$algorithm), + 'expires_at' => (new DateTime())->setTimestamp(timestamp: $expireAt)->format(format: 'Y-m-d H:i:s') + ]; + } + + /** + * Valida um token JWT e retorna o objeto de payload decodificado. + * + * Este método estático decodifica e valida um token JWT fornecido, utilizando + * a chave secreta e o algoritmo de criptografia configurados na classe. + * + * #### Como funciona: + * 1. Recebe o token (`$token`) que precisa ser validado. + * 2. Usa a biblioteca `JWT::decode()` para decodificar o token. + * 3. Para a decodificação, ele cria uma nova instância de `Key`, passando + * a chave secreta (`self::$secretKey`) e o algoritmo (`self::$algorithm`). + * Isso garante que o token só será decodificado se tiver sido assinado com a chave e algoritmo corretos. + * 4. Se o token for válido e a assinatura corresponder, a função retorna o payload do token + * como um objeto. + * 5. Se o token for inválido (por exemplo, assinatura incorreta, expirado ou formato incorreto), + * a biblioteca JWT lançará uma exceção, que a função que chama este método deve capturar. + * + * @param string $token O token JWT a ser validado e decodificado. + * @return object O objeto de payload decodificado do token. + */ + public static function validateToken(string $token): object { + // Inicializa as configurações se ainda não estiverem definidas + if (!isset(self::$secretKey)) { + self::init(); + } + + return JWT::decode(jwt: $token, keyOrKeyArray: new Key(keyMaterial: self::$secretKey, algorithm: self::$algorithm)); + } + + /** + * Decodifica um token JWT e retorna seu payload como um array associativo. + * + * Este método estático é responsável por decodificar e validar um token JWT. Antes de decodificar, ele garante que as configurações de chave secreta e algoritmo (`self::$secretKey` e `self::$algorithm`) já foram inicializadas chamando o método `init()` se necessário. + * + * #### Como Funciona: + * 1. **Inicialização:** Primeiro, verifica se a chave secreta (`self::$secretKey`) já foi carregada. Se não, ele chama `self::init()` para carregar as configurações de um arquivo externo. + * 2. **Decodificação:** Usa a biblioteca `Firebase\JWT\JWT::decode()` para decodificar o token fornecido. O método passa a chave secreta e o algoritmo para verificar a assinatura do token. + * 3. **Conversão:** O payload decodificado, que por padrão é um objeto, é convertido para uma string JSON e depois para um array associativo, o que facilita o acesso aos dados. + * 4. **Retorno:** Retorna o payload do token como um array associativo. Se o token for inválido (assinatura incorreta, expirado, etc.), a biblioteca JWT lançará uma exceção, que a função que chama este método deve capturar. + * + * @param string $token O token JWT a ser decodificado. + * @return array O payload do token decodificado como um array associativo. + */ + public static function decode(?string $token): array { + // Inicializa as configurações se ainda não estiverem definidas + if (!isset(self::$secretKey)) { + self::init(); + } + + if($token === null) { + return []; + } + + $decoded = JWT::decode(jwt: $token, keyOrKeyArray: new Key(keyMaterial: self::$secretKey, algorithm: self::$algorithm)); + return json_decode(json: json_encode(value: $decoded), associative: true); + } + + /** + * Extrai o token de autenticação do tipo 'Bearer' do cabeçalho da requisição HTTP. + * + * Este método estático é um utilitário projetado para buscar um token JWT (JSON Web Token) + * que é enviado no cabeçalho `Authorization`. Ele é robusto o suficiente para verificar + * o token em diferentes superglobais do PHP, dependendo da configuração do servidor web. + * + * #### Fluxo de Busca: + * 1. Primeiro, tenta obter o cabeçalho `Authorization` diretamente da superglobal `$_SERVER`. + * 2. Em seguida, tenta a chave `HTTP_AUTHORIZATION`, que é um nome de variável comum em alguns ambientes. + * 3. Por último, usa a função `apache_request_headers()` para tentar obter o cabeçalho, caso as + * superglobais não estejam preenchidas. Isso garante compatibilidade com servidores Apache onde o cabeçalho pode não ser exposto. + * 4. Se o cabeçalho for encontrado, o valor é sanitizado (removendo espaços em branco) e a busca continua. + * 5. Se nenhum cabeçalho de autenticação for encontrado, o método retorna `null`. + * + * #### Extração do Token: + * - Depois de encontrar o cabeçalho, o método usa uma expressão regular (`/Bearer\s(\S+)/`) + * para verificar se o valor do cabeçalho está no formato `Bearer `. + * - Se o padrão corresponder, ele extrai e retorna a string do token. + * - Se o padrão não corresponder, o método retorna `null`. + * + * @return string|null O token de autenticação do tipo 'Bearer' como uma string, ou `null` + * se o cabeçalho não for encontrado ou não estiver no formato esperado. + */ + public static function getBearerToken(): ?string { + if (!isset(self::$secretKey)) { + self::init(); + } + + // Tenta pegar o header de todas as fontes possíveis + $headers = $_SERVER['Authorization'] ?? $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? (function_exists(function: 'apache_request_headers') ? apache_request_headers() : []); + + // Se veio do apache_request_headers, normaliza chaves + if (is_array(value: $headers)) { + $headers = array_change_key_case(array: $headers, case: CASE_LOWER); + $headers = $headers['authorization'] ?? ''; + } + + $headers = trim(string: $headers); + + if ($headers && preg_match(pattern: '/Bearer\s(\S+)/', subject: $headers, matches: $matches)) { + return $matches[1]; + } + + return null; + } + } \ No newline at end of file diff --git a/database/v0/migrations/002_public.empresa.sql b/database/v0/migrations/002_public.empresa.sql index 852ef59..1452948 100644 --- a/database/v0/migrations/002_public.empresa.sql +++ b/database/v0/migrations/002_public.empresa.sql @@ -70,6 +70,10 @@ CREATE TABLE IF NOT EXISTS public.empresa ( deleted_at TIMESTAMP DEFAULT NULL ); +INSERT INTO public.empresa (uuid, razao_social, nome_fantasia, inscricao_tipo_id, document_inscricao, inscricao_raiz, endereco_codigo_ibge, endereco_cep, endereco_logradouro, endereco_numero, endereco_complemento, endereco_bairro, endereco_cidade, endereco_estado) VALUES +('019a07d3-9174-76b6-a9f0-9e0e189fdba8', 'Ativa Servicos em Refrigeracao e Servicos em Eletrica LTDA', 'Ativa Refrigeração e Elétrica', 1, '61365393000121', '61365393', '2304400', '60115191', 'Rua Monesenhor Bruno', '1153', 'Sala 1423', 'Aldeota', 'Fortaleza', 'CE'), +('019a07d3-9174-7879-93ae-4de7da3b5a93', 'A C C Andrade LTDA', 'A.c. Car', 1, '21899523000191', '21899523', '2307601', '62930000', 'Rua Leila Kristinna Lopes Maia', '58', NULL, 'Limoeirinho', 'Limoeiro do Norte', 'CE'); + -- =============================================================================================================== -- Empresa Certificado Digital -- =============================================================================================================== diff --git a/database/v0/migrations/004_RBAC.sql b/database/v0/migrations/004_RBAC.sql new file mode 100644 index 0000000..e69de29