first commit
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
<?php
|
||||
namespace WorkbloomERP\Module\v0\Auth\Services;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use DateTimeImmutable;
|
||||
use KrothiumAPI\Utils\HttpUtil;
|
||||
use WorkbloomERP\Utils\JwtUtil;
|
||||
use WorkbloomERP\Utils\CacheUtil;
|
||||
use WorkbloomERP\Utils\CypherUtil;
|
||||
use WorkbloomERP\Services\DBService;
|
||||
use WorkbloomERP\Exceptions\AppException;
|
||||
use WorkbloomERP\Module\v0\Auth\Utils\AuthUtil;
|
||||
use WorkbloomERP\Module\v0\Empresa\Repos\EmpresaRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioSessionRepo;
|
||||
use WorkbloomERP\Module\v0\Usuario\Repos\UsuarioEmpresaRepo;
|
||||
use WorkbloomERP\Module\v0\Empresa\Models\EmpresaModel;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioModel;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioSessionModel;
|
||||
use WorkbloomERP\Module\v0\Usuario\Models\UsuarioEmpresaModel;
|
||||
|
||||
class AuthService {
|
||||
public function __construct(
|
||||
private DBService $db,
|
||||
private UsuarioRepo $usuarioRepo,
|
||||
private EmpresaRepo $empresaRepo,
|
||||
private UsuarioSessionRepo $usuarioSessionRepo,
|
||||
private UsuarioEmpresaRepo $usuarioEmpresaRepo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Realiza a autenticação de usuário e inicia uma sessão no sistema.
|
||||
*
|
||||
* Este método é o coração do processo de autenticação. Ele valida as credenciais
|
||||
* fornecidas, gera um token JWT (JSON Web Token) para comunicação segura, persiste
|
||||
* a sessão no banco de dados para fins de auditoria/rastreabilidade e armazena
|
||||
* o token no cache (Redis) para validação rápida de requests subsequentes.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Validação de Entrada:** Verifica se `login` e `senha` foram fornecidos.
|
||||
* 2. **Autenticação:** Busca o usuário pelo login e utiliza `password_verify` para validar a hash da senha.
|
||||
* 3. **Geração de Token:** Instancia o `JwtUtil` com configurações de ambiente e gera um novo token JWT.
|
||||
* 4. **Persistência de Sessão (Atomicidade):**
|
||||
* - Insere o registro de sessão na tabela `usuario_session` (contendo dados como IP e User-Agent).
|
||||
* - O hash do token é armazenado para permitir revogação futura.
|
||||
* 5. **Caching:** Armazena o token no Redis com a mesma TTL da expiração do token para otimizar a validação de acesso.
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - **Transacionalidade:** Toda a criação de sessão e persistência ocorre dentro de uma transação de banco de dados, garantindo que não existam sessões órfãs caso o cache ou a criação do token falhem.
|
||||
* - **Segurança:** O token JWT gerado é hasheado (`CypherUtil::hash`) antes de ser salvo no banco para evitar a exposição do token real em caso de vazamento de dados.
|
||||
* - **Infraestrutura:** Requer acesso funcional a: `JWT_SECRET_KEY`, `JWT_ALGORITHM`, `JWT_EXPIRATION_TIME`, `JWT_ISSUER`.
|
||||
*
|
||||
* @param string $login O nome de usuário ou e-mail.
|
||||
* @param string $senha A senha em texto puro fornecida pelo usuário.
|
||||
* @return array{response_code: int, message: string, output: array{token: string}} Resposta estruturada contendo o código de sucesso e o token JWT.
|
||||
* @throws AppException Caso as credenciais sejam inválidas (401), o payload esteja vazio (400) ou falhas críticas ocorram na geração de token/sessão (500).
|
||||
*/
|
||||
public function login(string $login, string $senha): array {
|
||||
try {
|
||||
// Validação básica de entrada
|
||||
if (empty($login) || empty($senha)) {
|
||||
throw new AppException(message: 'Login e senha são obrigatórios.', code: 400);
|
||||
}
|
||||
|
||||
// Inicia uma transação para garantir a atomicidade das operações de autenticação
|
||||
return $this->db->transaction(
|
||||
callback: function() use ($login, $senha) {
|
||||
$usuarioModel = $this->usuarioRepo->findByLogin(login: $login);
|
||||
if (!$usuarioModel || !password_verify(password: $senha, hash: $usuarioModel->getSenhaHash())) {
|
||||
throw new AppException(message: 'Login ou senha inválidos.', code: 401);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'user_data' => [
|
||||
'initial_login' => true,
|
||||
'uuid' => $usuarioModel->getUuid(),
|
||||
'nome_usuario' => $usuarioModel->getNomeCompleto(),
|
||||
'email' => $usuarioModel->getEmail(),
|
||||
],
|
||||
'exp' => (new DateTimeImmutable())->modify("+5 minutes")->getTimestamp(),
|
||||
];
|
||||
|
||||
$jwtUtil = new JwtUtil(
|
||||
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
|
||||
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
|
||||
);
|
||||
$jwtToken = $jwtUtil->generate(payload: $payload);
|
||||
if (!$jwtToken) {
|
||||
throw new AppException(message: 'Erro ao gerar token de acesso.', code: 500);
|
||||
}
|
||||
|
||||
// Armazena o token JWT no cache Redis com uma TTL de 10 minutos (600 segundos)
|
||||
CacheUtil::set(key: "login:init:{$jwtToken}", ttl: 600, value: $payload);
|
||||
|
||||
return [
|
||||
'response_code' => 200,
|
||||
'message' => 'Login realizado com sucesso.',
|
||||
'output' => [
|
||||
'tmp_token' => $jwtToken,
|
||||
'empresas' => $this->getEmpresasByUsuarioId($usuarioModel->getId())
|
||||
]
|
||||
];
|
||||
}
|
||||
);
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recupera e organiza a lista de empresas vinculadas a um determinado usuário.
|
||||
*
|
||||
* Este método realiza o mapeamento entre o ID do usuário e suas respectivas
|
||||
* empresas. Ele filtra as associações através do repositório `usuarioEmpresaRepo`,
|
||||
* hidrata os objetos `EmpresaModel` e organiza os dados em uma estrutura
|
||||
* associativa agrupada pelo nome empresarial.
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Busca:** Recupera todos os registros de associação `usuario_id` -> `empresa_id`.
|
||||
* 2. **Hidratação:** Itera sobre as associações, buscando o `EmpresaModel` completo para cada ID encontrado.
|
||||
* 3. **Estruturação:** Monta um array multidimensional onde as chaves são os nomes das empresas e os valores são arrays contendo metadados (UUID, nome, CNPJ, tipo formatado).
|
||||
* 4. **Tratamento de Dados:** Normaliza o campo `tipo` (MATRIZ/FILIAL) para o formato "Capitalizado" (ex: Matriz).
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - Caso o usuário não possua empresas vinculadas ou o repositório retorne vazio, o método retorna um array vazio.
|
||||
* - Ignora silenciosamente qualquer `empresa_id` que não resulte em um `EmpresaModel` válido (falha de integridade referencial).
|
||||
* - O agrupamento pelo nome empresarial facilita o consumo da estrutura pelo frontend.
|
||||
*
|
||||
* @param int $usuario_id O identificador numérico interno do usuário.
|
||||
* @return array<string, array<int, array{uuid: string, nome_empresarial: string, nome_fantasia: string, cnpj: string, tipo: string}>> Array estruturado de empresas.
|
||||
* @throws AppException Caso ocorra falha de acesso aos repositórios.
|
||||
*/
|
||||
private function getEmpresasByUsuarioId(int $usuario_id): array {
|
||||
try {
|
||||
$empresas = $this->usuarioEmpresaRepo->findAllByUsuarioId(usuario_id: $usuario_id);
|
||||
if (empty($empresas)) {
|
||||
return ['Nenhuma empresa vinculada ao usuário.'];
|
||||
}
|
||||
|
||||
$preparedEmpresas = [];
|
||||
foreach($empresas as $i => $empresa) {
|
||||
$empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'id', value: $empresa['empresa_id']);
|
||||
if (!$empresaModel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$preparedEmpresas[$empresaModel->getNomeEmpresarial()][] = [
|
||||
'uuid' => $empresaModel->getUuid(),
|
||||
'nome_empresarial' => $empresaModel->getNomeEmpresarial(),
|
||||
'nome_fantasia' => $empresaModel->getNomeFantasia(),
|
||||
'cnpj' => $empresaModel->getDocumentCnpj(),
|
||||
'tipo' => ucfirst(string: strtolower(string: $empresaModel->getTipo()))
|
||||
];
|
||||
}
|
||||
|
||||
return $preparedEmpresas;
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seleciona e define a empresa ativa para o contexto da sessão do usuário.
|
||||
*
|
||||
* Este método é utilizado em sistemas multi-empresa onde o usuário precisa alternar
|
||||
* entre diferentes entidades (empresas) sem realizar um novo login completo.
|
||||
* O método valida se o usuário possui vínculo com a empresa solicitada, gera
|
||||
* um novo token JWT com o contexto da empresa atualizado e registra essa nova
|
||||
* sessão no banco de dados e no cache (Redis).
|
||||
*
|
||||
* ---
|
||||
* ## Fluxo de Execução
|
||||
* 1. **Verificação de Sessão:** Garante que o usuário já possui um login ativo (leitura via `AuthUtil`).
|
||||
* 2. **Validação de Acesso:** Verifica se o usuário solicitado possui associação ativa com a empresa (`usuarioEmpresaRepo`).
|
||||
* 3. **Contextualização:** Monta um novo payload contendo os dados do usuário e os dados da empresa selecionada.
|
||||
* 4. **Token Rotation:** Gera um novo token JWT com os novos dados de contexto e define um tempo de expiração (60 minutos).
|
||||
* 5. **Persistência:** Registra a nova sessão no banco de dados (`usuario_session`) e atualiza o estado no cache (`CacheUtil`).
|
||||
*
|
||||
* ---
|
||||
* ## Observações Técnicas
|
||||
* - **Token Rotation:** O método invalida implicitamente o contexto anterior ao gerar um novo token baseado na nova seleção.
|
||||
* - **Atomicidade:** A operação ocorre dentro de uma transação, garantindo que se o token não for gerado ou a sessão não for criada, nada é alterado.
|
||||
* - **Segurança:** O hash do token gerado é armazenado para permitir rastreabilidade e segurança.
|
||||
*
|
||||
* @param string $empresa_uuid O Identificador Único Universal da empresa que o usuário deseja selecionar.
|
||||
* @return array{response_code: int, message: string, output: array{token: string}} Resposta estruturada contendo o novo token de acesso com o contexto atualizado.
|
||||
* @throws AppException Caso a sessão seja inválida, a empresa não exista, o usuário não tenha permissão de acesso à empresa ou falhas de persistência ocorram.
|
||||
*/
|
||||
public function selectCompany(string $empresa_uuid) {
|
||||
try {
|
||||
return $this->db->transaction(
|
||||
callback: function() use ($empresa_uuid) {
|
||||
if (empty($empresa_uuid)) {
|
||||
throw new AppException(message: 'UUID da empresa é obrigatório.', code: 400);
|
||||
}
|
||||
|
||||
// Lê os dados da sessão atual para validar a existência de um login prévio e obter o UUID do usuário
|
||||
$sessionData = AuthUtil::readInitSession();
|
||||
if (!$sessionData || !isset($sessionData['user_data']['uuid'])) {
|
||||
throw new AppException(message: 'Sessão de usuário não encontrada. Faça login novamente.', code: 401);
|
||||
}
|
||||
|
||||
// Valida se o usuário tem acesso à empresa selecionada
|
||||
$usuarioModel = $this->usuarioRepo->findByIdentifier(identifier: 'uuid', value: $sessionData['user_data']['uuid']);
|
||||
if (!$usuarioModel) {
|
||||
throw new AppException(message: 'Ocorreu um erro ao selecionar a empresa. Tente novamente mais tarde.', code: 404);
|
||||
}
|
||||
|
||||
// Valida se a empresa existe e se o usuário tem associação com ela
|
||||
$empresaModel = $this->empresaRepo->findByIdentifier(identifier: 'uuid', value: $empresa_uuid);
|
||||
if (!$empresaModel || !$this->usuarioEmpresaRepo->checkAssociationByUsuarioIdAndEmpresaId(usuario_id: $usuarioModel->getId(), empresa_id: $empresaModel->getId())) {
|
||||
throw new AppException(message: 'Empresa não encontrada.', code: 404);
|
||||
}
|
||||
|
||||
// Monta o payload da sessão principal, incluindo os dados do usuário e da empresa selecionada
|
||||
$payload = [
|
||||
'user_data' => [
|
||||
'initial_login' => false,
|
||||
'uuid' => $usuarioModel->getUuid(),
|
||||
'nome_completo' => $usuarioModel->getNomeCompleto(),
|
||||
'nome_usuario' => $usuarioModel->getNomeUsuario(),
|
||||
'email' => $usuarioModel->getEmail(),
|
||||
'is_root' => $usuarioModel->getIsRoot() ? true : false
|
||||
],
|
||||
'empresa_data' => [
|
||||
'uuid' => $empresaModel->getUuid(),
|
||||
'nome_empresarial' => $empresaModel->getNomeEmpresarial(),
|
||||
'nome_fantasia' => $empresaModel->getNomeFantasia(),
|
||||
'cnpj' => $empresaModel->getDocumentCnpj(),
|
||||
'tipo' => ucfirst(string: strtolower(string: $empresaModel->getTipo()))
|
||||
],
|
||||
'session_data' => [
|
||||
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
|
||||
],
|
||||
'iat' => (new DateTimeImmutable())->getTimestamp(),
|
||||
'exp' => (new DateTimeImmutable())->modify("+{$_ENV['JWT_EXPIRATION_TIME']} seconds")->getTimestamp(),
|
||||
];
|
||||
|
||||
$jwtUtil = new JwtUtil(
|
||||
secretKey: $_ENV['JWT_SECRET_KEY'], algorithm: $_ENV['JWT_ALGORITHM'],
|
||||
ttl: $_ENV['JWT_EXPIRATION_TIME'], issuer: $_ENV['JWT_ISSUER']
|
||||
);
|
||||
$jwtToken = $jwtUtil->generate(payload: $payload);
|
||||
if (!$jwtToken) {
|
||||
throw new AppException(message: 'Erro ao gerar token de acesso.', code: 500);
|
||||
}
|
||||
|
||||
$usuarioSessionModel = $this->usuarioSessionRepo->insert(
|
||||
new UsuarioSessionModel(
|
||||
uuid: Uuid::uuid7()->toString(),
|
||||
usuario_id: $usuarioModel->getId(),
|
||||
token_hash: CypherUtil::hash(data: $jwtToken),
|
||||
ip_address: $payload['session_data']['ip_address'],
|
||||
user_agent: $payload['session_data']['user_agent'],
|
||||
created_at: new DateTimeImmutable()
|
||||
)
|
||||
);
|
||||
if (!$usuarioSessionModel) {
|
||||
throw new AppException(message: 'Erro ao criar sessão de usuário.', code: 500);
|
||||
}
|
||||
|
||||
if (!CacheUtil::set(key: "login:main:{$jwtToken}", ttl: $_ENV['JWT_EXPIRATION_TIME'], value: $payload)) {
|
||||
throw new AppException(message: 'Erro ao armazenar sessão no cache.', code: 500);
|
||||
}
|
||||
|
||||
$beforeToken = HttpUtil::getBearerToken();
|
||||
if (!CacheUtil::delete(keys: ["login:init:{$beforeToken}"])) {
|
||||
throw new AppException(message: 'Erro ao limpar sessão temporária do cache.', code: 500);
|
||||
}
|
||||
|
||||
return [
|
||||
'response_code' => 200,
|
||||
'message' => 'Empresa selecionada com sucesso.',
|
||||
'output' => [
|
||||
'token' => $jwtToken
|
||||
]
|
||||
];
|
||||
}
|
||||
);
|
||||
} catch(AppException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user