From 724a5054607aaad3ad427fc2ea7ba6e17a367f0e Mon Sep 17 00:00:00 2001 From: Willen Borges Coelho Date: Wed, 21 Oct 2026 15:41:40 +0000 Subject: [PATCH] Initial commit: sistema de gestão de alunos no AD --- .gitignore | 3 +++ README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ config/config.example.php | 35 +++++++++++++++++++++++++++++++++++ public_html/action_status.php | 47 +++++++++++++++++++++++++++++++++++++++++++++++ public_html/admins.php | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public_html/assets/style.css | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public_html/dashboard.php | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public_html/index.php | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public_html/logout.php | 9 +++++++++ public_html/newpasswd.php | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public_html/settings.php | 45 +++++++++++++++++++++++++++++++++++++++++++++ sql/schema.sql | 36 ++++++++++++++++++++++++++++++++++++ src/AdminRepository.php | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/AuditLog.php | 39 +++++++++++++++++++++++++++++++++++++++ src/Auth.php | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/Config.php | 40 ++++++++++++++++++++++++++++++++++++++++ src/Database.php | 25 +++++++++++++++++++++++++ src/Ldap.php | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/SettingsRepository.php | 38 ++++++++++++++++++++++++++++++++++++++ src/View.php | 44 ++++++++++++++++++++++++++++++++++++++++++++ src/bootstrap.php | 21 +++++++++++++++++++++ 21 files changed, 1210 insertions(+), 0 deletions(-) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/config.example.php create mode 100644 public_html/action_status.php create mode 100644 public_html/admins.php create mode 100644 public_html/assets/style.css create mode 100644 public_html/dashboard.php create mode 100644 public_html/index.php create mode 100644 public_html/logout.php create mode 100644 public_html/newpasswd.php create mode 100644 public_html/settings.php create mode 100644 sql/schema.sql create mode 100644 src/AdminRepository.php create mode 100644 src/AuditLog.php create mode 100644 src/Auth.php create mode 100644 src/Config.php create mode 100644 src/Database.php create mode 100644 src/Ldap.php create mode 100644 src/SettingsRepository.php create mode 100644 src/View.php create mode 100644 src/bootstrap.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e25dc9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +config/local.php +storage/*.sqlite +storage/*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..091e63f --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Gestão de Alunos (AD) - LabInfo + +Sistema independente (sem WordPress) para habilitar/desabilitar contas de aluno no AD +e resetar senha, com controle de acesso gerenciado pela própria aplicação. + +## Deploy no pandora + +1. No pandora, clonar o repositório (é privado — use uma chave SSH já autorizada + no GitLab, ou um deploy token) direto em `/home/labinfo/`, de forma que + `public_html/` deste projeto substitua (ou vire) o `/home/labinfo/public_html` do site, + e `src/`, `config/`, `storage/`, `sql/` fiquem um nível acima dele (fora da árvore + servida publicamente): + ```bash + git clone git@git.cachoeiro.ifes.edu.br:1652445/lab-info.git /home/labinfo/app + ``` + Depois ajuste os links/symlinks ou mova `public_html` conforme a estrutura real + do docroot do site. + +2. Criar as tabelas no banco `labinfo`: + ```bash + mysql -h -u 'labinfo@pandora%.cefetes.br' -p labinfo < sql/schema.sql + ``` + +3. Criar `config/local.php` a partir de `config/config.example.php` preenchendo: + - `db.pass` com a senha gerada pelo Puppet (me repassa por fora do catálogo). + - `ldap.service_user` / `ldap.service_pass` com uma conta de serviço (peça delegação + restrita à OU/grupo de alunos — não domain admin). + - `app.session_secret` com `bin2hex(random_bytes(32))`. + +4. Garantir que `storage/` tem permissão de escrita pelo usuário do PHP-FPM do site + (para `audit.log`), e que `config/local.php` está fora do alcance público + (confirmar que `.php` nunca é servido como texto puro — padrão do PHP já cobre isso). + +5. Cadastrar o primeiro administrador direto no banco (seed), já que a tela de + administradores exige estar logado como admin: + ```sql + INSERT INTO admins (login, papel, criado_por) VALUES ('willen', 'admin', 'seed'); + ``` + +6. Acessar `https://labinfo.cachoeiro.ifes.edu.br/`, logar com usuário/senha do AD + e confirmar que o dashboard carrega os alunos do grupo configurado. + +## Estrutura + +- `public_html/` — document root (único diretório exposto na web). +- `src/` — classes PHP (LDAP, Auth, repositórios, log). +- `config/` — config.example.php versionado; `local.php` (real) nunca versionado. +- `sql/schema.sql` — schema do banco (admins, settings, audit_log, login_attempts). +- `storage/` — log de auditoria em arquivo (complementar ao banco). + +## Papéis + +- **admin**: tudo que operador pode, mais gerenciar `admins.php` e `settings.php`. +- **operador**: só habilita/desabilita aluno e reseta senha no `dashboard.php`. + +A autenticação de quem usa o sistema é sempre feita com login e senha do próprio AD +(bind LDAP) — o banco **não armazena nenhuma senha**. A tabela `admins` só guarda +quem está autorizado a entrar e com qual papel (login, nome, papel), funcionando como +uma lista de permissão: se o login/senha do AD estiverem corretos mas o login não +estiver cadastrado (e ativo) em `admins`, o acesso ao sistema é negado mesmo assim. + +Não existe mais lista de SIAPE fixa no código — todo acesso é via tabela `admins`, +editável pela tela (exceto o primeiro admin, que precisa de uma inserção manual inicial). diff --git a/config/config.example.php b/config/config.example.php new file mode 100644 index 0000000..80b0160 --- /dev/null +++ b/config/config.example.php @@ -0,0 +1,35 @@ + [ + 'server' => 'ldap://ad.example.org', + 'base_dn' => 'OU=Example OU,DC=example,DC=org', + 'domain_suffix' => '@example.org', // usado para montar o login de bind: @example.org + 'group_prof' => 'CN=grp-example-prof,OU=Groups,OU=Example OU,DC=example,DC=org', + 'group_aluno' => 'cn=grp-example-aluno', + 'filter_prof' => 'cn=grp-example-prof', + // Conta de serviço usada para buscas e para as alterações (habilitar/desabilitar, + // resetar senha) na OU de alunos. Peça ao pessoal de AD uma conta com delegação + // restrita só a essa OU/grupo — nunca uma conta de domain admin. + 'service_user' => 'CHANGE_ME', + 'service_pass' => 'CHANGE_ME', + ], + + 'db' => [ + 'host' => 'CHANGE_ME', + 'name' => 'CHANGE_ME', + 'user' => 'CHANGE_ME', + 'pass' => 'CHANGE_ME', + ], + + 'app' => [ + // Gere com: bin2hex(random_bytes(32)) + 'session_name' => 'app_session', + 'session_secret' => 'CHANGE_ME', + 'log_file' => __DIR__ . '/../storage/audit.log', + ], +]; diff --git a/public_html/action_status.php b/public_html/action_status.php new file mode 100644 index 0000000..20b400d --- /dev/null +++ b/public_html/action_status.php @@ -0,0 +1,47 @@ +bindService(); + + $entry = $ldap->findUserByCn($user); + if ($entry === null) { + throw new RuntimeException('Aluno não encontrado no AD.'); + } + + $dn = $ldap->dnOf($entry); + $accountControl = (int) ($entry['useraccountcontrol'][0] ?? 0); + $new = $enable ? ($accountControl & ~2) : ($accountControl | 2); + + $quemFez = Auth::currentLogin() ?? 'desconhecido'; + $ldap->modify($dn, [ + 'useraccountcontrol' => $new, + 'description' => $quemFez, + ]); + + AuditLog::record($quemFez, $enable ? 'habilitou_aluno' : 'desabilitou_aluno', $user); + + header('Location: dashboard.php?ok=1'); + exit; +} catch (Throwable $e) { + header('Location: dashboard.php?erro=1&msg=' . urlencode($e->getMessage())); + exit; +} diff --git a/public_html/admins.php b/public_html/admins.php new file mode 100644 index 0000000..5f732a4 --- /dev/null +++ b/public_html/admins.php @@ -0,0 +1,122 @@ +bindService(); + $entry = $ldapObj->findUserByLogin($login); + $nome = $entry['displayname'][0] ?? null; + + if ($entry === null) { + $erro = 'Login não encontrado no AD. Confira e tente novamente.'; + } else { + AdminRepository::add($login, $nome, $papel, $quemFez); + AuditLog::record($quemFez, 'adicionou_admin', $login, "papel={$papel}"); + $ok = 'Administrador adicionado com sucesso.'; + } + } catch (Throwable $e) { + $erro = 'Erro ao consultar o AD: ' . $e->getMessage(); + } + } + } elseif ($acao === 'remover') { + $id = (int) ($_POST['id'] ?? 0); + $admin = AdminRepository::find($id); + + if (!$admin) { + $erro = 'Administrador não encontrado.'; + } elseif ($admin['papel'] === 'admin' && AdminRepository::countActiveAdmins($id) === 0) { + $erro = 'Não é possível remover o último administrador ativo do sistema.'; + } else { + AdminRepository::setActive($id, false); + AuditLog::record($quemFez, 'removeu_admin', $admin['login']); + $ok = 'Administrador removido (desativado) com sucesso.'; + } + } elseif ($acao === 'reativar') { + $id = (int) ($_POST['id'] ?? 0); + $admin = AdminRepository::find($id); + if ($admin) { + AdminRepository::setActive($id, true); + AuditLog::record($quemFez, 'reativou_admin', $admin['login']); + $ok = 'Administrador reativado.'; + } + } +} + +$admins = AdminRepository::all(); + +View::header('Administradores'); +?> +
+

Administradores do sistema

+ +
+
+ +
+ + + + + + +
+ + + + + + + + + + + + + + + + + + +
LoginNomePapelStatusAções
+ + Ativo + + Inativo + + +
+ + + + + + + + +
+
+
+bindService(); + + $groupFilter = SettingsRepository::get('group_aluno_filter', 'cn=grpCI-Aluno-Info'); + $logins = $ldap->groupMembers($groupFilter); + + $alunos = []; + foreach ($logins as $cn) { + $entry = $ldap->findUserByCn($cn); + if ($entry === null) { + continue; + } + $accountControl = (int) ($entry['useraccountcontrol'][0] ?? 0); + $alunos[] = [ + 'cn' => $cn, + 'nome' => $entry['sn'][0] ?? $cn, + 'habilitado' => ($accountControl & 2) !== 2, + 'descricao' => $entry['description'][0] ?? '', + ]; + } +} catch (Throwable $e) { + $erro = 'Não foi possível consultar o AD agora: ' . $e->getMessage(); + $alunos = []; +} + +if (($_GET['ok'] ?? '') === '1') { + $ok = 'Ação realizada com sucesso.'; +} elseif (($_GET['erro'] ?? '') === '1') { + $erro = $_GET['msg'] ?? 'Ocorreu um erro ao processar a ação.'; +} + +View::header('Alunos'); +?> +
+

Alunos do grupo

+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + +
AlunoStatusÚltima alteração porAções
+ + Habilitado + + Desabilitado + + +
+ + + +
+ Nova senha +
Nenhum aluno encontrado no grupo.
+
+ + + + + + Gestão de Alunos - LabInfo + + + + + + diff --git a/public_html/logout.php b/public_html/logout.php new file mode 100644 index 0000000..30c80cb --- /dev/null +++ b/public_html/logout.php @@ -0,0 +1,9 @@ +bindService(); + + $entry = $ldap->findUserByCn($user); + if ($entry === null) { + throw new RuntimeException('Aluno não encontrado no AD.'); + } + $dn = $ldap->dnOf($entry); + $ldap->modify($dn, [ + 'unicodePwd' => Ldap::encodePassword($pass1), + ]); + + $quemFez = Auth::currentLogin() ?? 'desconhecido'; + AuditLog::record($quemFez, 'alterou_senha_aluno', $user); + + $ok = 'Senha alterada com sucesso!'; + } catch (Throwable $e) { + $erro = 'Erro ao alterar a senha: ' . $e->getMessage(); + } + } +} + +View::header('Nova senha'); +?> +
+

Nova senha para

+ +
+
+ + +
+ + + + + + +
+ + +

← Voltar

+
+ +
+

Configurações

+ +
+
+ +
+ + + +
+ +

+ As demais configurações de conexão (servidor LDAP, conta de serviço, credenciais de banco) + ficam em config/local.php, fora do versionamento, por segurança. +

+
+prepare('SELECT * FROM admins WHERE login = :login AND ativo = 1'); + $stmt->execute(['login' => $login]); + $row = $stmt->fetch(); + return $row ?: null; + } + + public static function all(): array + { + return Database::pdo() + ->query('SELECT * FROM admins ORDER BY ativo DESC, login ASC') + ->fetchAll(); + } + + public static function countActiveAdmins(?int $excludeId = null): int + { + $sql = "SELECT COUNT(*) FROM admins WHERE ativo = 1 AND papel = 'admin'"; + $params = []; + if ($excludeId !== null) { + $sql .= ' AND id != :id'; + $params['id'] = $excludeId; + } + $stmt = Database::pdo()->prepare($sql); + $stmt->execute($params); + return (int) $stmt->fetchColumn(); + } + + public static function add(string $login, ?string $nome, string $papel, string $criadoPor): void + { + $stmt = Database::pdo()->prepare( + 'INSERT INTO admins (login, nome, papel, criado_por) VALUES (:login, :nome, :papel, :criado_por) + ON DUPLICATE KEY UPDATE ativo = 1, papel = VALUES(papel), nome = VALUES(nome)' + ); + $stmt->execute([ + 'login' => $login, + 'nome' => $nome, + 'papel' => $papel, + 'criado_por' => $criadoPor, + ]); + } + + public static function setActive(int $id, bool $active): void + { + $stmt = Database::pdo()->prepare('UPDATE admins SET ativo = :ativo WHERE id = :id'); + $stmt->execute(['ativo' => $active ? 1 : 0, 'id' => $id]); + } + + public static function find(int $id): ?array + { + $stmt = Database::pdo()->prepare('SELECT * FROM admins WHERE id = :id'); + $stmt->execute(['id' => $id]); + $row = $stmt->fetch(); + return $row ?: null; + } + + public static function touchLoginCache(string $login, ?string $nome): void + { + $stmt = Database::pdo()->prepare('UPDATE admins SET nome = :nome WHERE login = :login'); + $stmt->execute(['nome' => $nome, 'login' => $login]); + } +} diff --git a/src/AuditLog.php b/src/AuditLog.php new file mode 100644 index 0000000..b185e2a --- /dev/null +++ b/src/AuditLog.php @@ -0,0 +1,39 @@ +prepare( + 'INSERT INTO audit_log (usuario, acao, alvo, detalhe, ip) VALUES (:usuario, :acao, :alvo, :detalhe, :ip)' + ); + $stmt->execute([ + 'usuario' => $usuario, + 'acao' => $acao, + 'alvo' => $alvo, + 'detalhe' => $detalhe, + 'ip' => $ip, + ]); + } catch (Throwable $e) { + // Nunca deixa uma falha de log derrubar a ação principal. + error_log('AuditLog falhou: ' . $e->getMessage()); + } + + $logFile = Config::get('app.log_file'); + if ($logFile) { + $line = sprintf( + "%s|%s|%s|%s|%s|%s\n", + date('Y-m-d H:i:s'), + $ip, + $usuario, + $acao, + $alvo ?? '-', + $detalhe ?? '-' + ); + @file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX); + } + } +} diff --git a/src/Auth.php b/src/Auth.php new file mode 100644 index 0000000..4228959 --- /dev/null +++ b/src/Auth.php @@ -0,0 +1,134 @@ + 0, + 'path' => '/', + 'secure' => (!empty($_SERVER['HTTPS'])), + 'httponly' => true, + 'samesite' => 'Lax', + ]); + session_start(); + } + } + + public static function attempt(string $login, string $password): array + { + $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown'; + + if (self::isRateLimited($login, $ip)) { + return ['ok' => false, 'error' => 'Muitas tentativas. Aguarde alguns minutos e tente novamente.']; + } + + $ldap = new Ldap(); + $bindOk = $ldap->tryUserBind($login, $password); + + self::logAttempt($login, $ip, $bindOk); + + if (!$bindOk) { + return ['ok' => false, 'error' => 'Usuário ou senha incorretos.']; + } + + $admin = AdminRepository::findActiveByLogin($login); + if (!$admin) { + AuditLog::record($login, 'login_negado', null, 'usuário autenticado no AD mas sem permissão no sistema'); + return ['ok' => false, 'error' => 'Seu usuário não tem permissão para acessar este sistema.']; + } + + // Atualiza o nome em cache a partir do AD, sem depender da conta de serviço falhar. + try { + $ldap->bindService(); + $entry = $ldap->findUserByLogin($login); + $nome = $entry['displayname'][0] ?? null; + if ($nome) { + AdminRepository::touchLoginCache($login, $nome); + $admin['nome'] = $nome; + } + } catch (Throwable $e) { + // Não bloqueia o login por causa da conta de serviço. + } + + self::start(); + session_regenerate_id(true); + $_SESSION['login'] = $login; + $_SESSION['nome'] = $admin['nome'] ?? $login; + $_SESSION['papel'] = $admin['papel']; + + AuditLog::record($login, 'login', null, 'login efetuado'); + + return ['ok' => true]; + } + + public static function logout(): void + { + self::start(); + $login = $_SESSION['login'] ?? 'desconhecido'; + AuditLog::record($login, 'logout'); + $_SESSION = []; + session_destroy(); + } + + public static function requireLogin(): void + { + self::start(); + if (empty($_SESSION['login'])) { + header('Location: index.php?erro=sessao_expirada'); + exit; + } + } + + public static function requireAdmin(): void + { + self::requireLogin(); + if (($_SESSION['papel'] ?? null) !== 'admin') { + http_response_code(403); + echo 'Acesso restrito a administradores.'; + exit; + } + } + + public static function currentLogin(): ?string + { + return $_SESSION['login'] ?? null; + } + + public static function currentName(): ?string + { + return $_SESSION['nome'] ?? null; + } + + public static function isAdmin(): bool + { + return ($_SESSION['papel'] ?? null) === 'admin'; + } + + private static function isRateLimited(string $login, string $ip): bool + { + $stmt = Database::pdo()->prepare( + 'SELECT COUNT(*) FROM login_attempts + WHERE login = :login AND ip = :ip AND sucesso = 0 + AND criado_em >= (NOW() - INTERVAL :minutes MINUTE)' + ); + $stmt->bindValue('login', $login); + $stmt->bindValue('ip', $ip); + $stmt->bindValue('minutes', self::WINDOW_MINUTES, PDO::PARAM_INT); + $stmt->execute(); + return (int) $stmt->fetchColumn() >= self::MAX_ATTEMPTS; + } + + private static function logAttempt(string $login, string $ip, bool $success): void + { + $stmt = Database::pdo()->prepare( + 'INSERT INTO login_attempts (login, ip, sucesso) VALUES (:login, :ip, :sucesso)' + ); + $stmt->execute(['login' => $login, 'ip' => $ip, 'sucesso' => $success ? 1 : 0]); + } +} diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 0000000..7efda99 --- /dev/null +++ b/src/Config.php @@ -0,0 +1,40 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + } + + return self::$pdo; + } +} diff --git a/src/Ldap.php b/src/Ldap.php new file mode 100644 index 0000000..3dbe111 --- /dev/null +++ b/src/Ldap.php @@ -0,0 +1,125 @@ +connect = ldap_connect($server); + if (!$this->connect) { + throw new LdapException('Não foi possível conectar ao servidor LDAP.'); + } + ldap_set_option($this->connect, LDAP_OPT_PROTOCOL_VERSION, 3); + ldap_set_option($this->connect, LDAP_OPT_REFERRALS, 0); + ldap_set_option($this->connect, LDAP_OPT_NETWORK_TIMEOUT, 5); + } + + /** + * Tenta autenticar o usuário com a própria senha dele (bind direto). + * Retorna true/false, nunca lança por credencial errada. + */ + public function tryUserBind(string $login, string $password): bool + { + if ($login === '' || $password === '') { + return false; + } + $userPrincipal = $login . Config::get('ldap.domain_suffix', '@cefetes.br'); + set_error_handler(static function () { + return true; + }); // silencia warning nativo do ldap_bind em credencial inválida, sem usar @ + try { + return (bool) ldap_bind($this->connect, $userPrincipal, $password); + } finally { + restore_error_handler(); + } + } + + /** + * Faz bind com a conta de serviço (somente leitura), usada para todas as buscas. + */ + public function bindService(): void + { + $user = Config::get('ldap.service_user'); + $pass = Config::get('ldap.service_pass'); + $userPrincipal = $user . Config::get('ldap.domain_suffix', '@cefetes.br'); + $ok = @ldap_bind($this->connect, $userPrincipal, $pass); + if (!$ok) { + throw new LdapException('Falha ao autenticar a conta de serviço no LDAP.'); + } + } + + public function search(string $filter): array + { + $baseDn = Config::get('ldap.base_dn'); + $result = @ldap_search($this->connect, $baseDn, $filter); + if ($result === false) { + throw new LdapException('Erro na busca LDAP: ' . ldap_error($this->connect)); + } + $entries = @ldap_get_entries($this->connect, $result); + if ($entries === false) { + throw new LdapException('Erro ao ler entradas LDAP: ' . ldap_error($this->connect)); + } + return $entries; + } + + public function findUserByCn(string $cn): ?array + { + $filter = '(cn=' . ldap_escape($cn, '', LDAP_ESCAPE_FILTER) . ')'; + $entries = $this->search($filter); + return $entries['count'] > 0 ? $entries[0] : null; + } + + public function findUserByLogin(string $login): ?array + { + $filter = '(sAMAccountName=' . ldap_escape($login, '', LDAP_ESCAPE_FILTER) . ')'; + $entries = $this->search($filter); + return $entries['count'] > 0 ? $entries[0] : null; + } + + public function groupMembers(string $groupDnOrFilter): array + { + // aceita tanto um filtro pronto ("cn=grpX") quanto um CN cru + $filter = str_contains($groupDnOrFilter, '=') + ? $groupDnOrFilter + : '(cn=' . ldap_escape($groupDnOrFilter, '', LDAP_ESCAPE_FILTER) . ')'; + $entries = $this->search($filter); + if ($entries['count'] === 0) { + return []; + } + $members = []; + $count = (int) ($entries[0]['member']['count'] ?? 0); + for ($i = 0; $i < $count; $i++) { + $dn = $entries[0]['member'][$i]; + $parts = explode(',', $dn); + $cn = str_replace('CN=', '', $parts[0]); + $members[] = $cn; + } + return $members; + } + + public function modify(string $dn, array $attributes): void + { + $ok = @ldap_mod_replace($this->connect, $dn, $attributes); + if (!$ok) { + throw new LdapException('Erro ao modificar LDAP: ' . ldap_error($this->connect)); + } + } + + public function dnOf(array $entry): ?string + { + return $entry['dn'] ?? null; + } + + public static function encodePassword(string $plain): string + { + // AD exige a senha entre aspas e em UTF-16LE para unicodePwd. + return iconv('UTF-8', 'UTF-16LE', '"' . $plain . '"'); + } +} diff --git a/src/SettingsRepository.php b/src/SettingsRepository.php new file mode 100644 index 0000000..4882431 --- /dev/null +++ b/src/SettingsRepository.php @@ -0,0 +1,38 @@ + 'cn=grpCI-Aluno-Info', + ]; + + public static function get(string $key, $default = null) + { + $stmt = Database::pdo()->prepare('SELECT valor FROM settings WHERE chave = :chave'); + $stmt->execute(['chave' => $key]); + $value = $stmt->fetchColumn(); + if ($value !== false) { + return $value; + } + return self::DEFAULTS[$key] ?? $default; + } + + public static function set(string $key, string $value, string $usuario): void + { + $stmt = Database::pdo()->prepare( + 'INSERT INTO settings (chave, valor, atualizado_por) VALUES (:chave, :valor, :usuario) + ON DUPLICATE KEY UPDATE valor = VALUES(valor), atualizado_por = VALUES(atualizado_por)' + ); + $stmt->execute(['chave' => $key, 'valor' => $value, 'usuario' => $usuario]); + } + + public static function all(): array + { + $rows = Database::pdo()->query('SELECT chave, valor FROM settings')->fetchAll(); + $result = self::DEFAULTS; + foreach ($rows as $row) { + $result[$row['chave']] = $row['valor']; + } + return $result; + } +} diff --git a/src/View.php b/src/View.php new file mode 100644 index 0000000..230d7f4 --- /dev/null +++ b/src/View.php @@ -0,0 +1,44 @@ + + + + + <?= htmlspecialchars($title, ENT_QUOTES) ?> - LabInfo + + + +
+ Gestão de Alunos (AD) + +
+
+ +
+ + +getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); + http_response_code(500); + echo 'Ocorreu um erro inesperado. Tente novamente mais tarde.'; +}); + +Auth::start(); -- libgit2 0.21.2