Commit 72556b0da726f41b4d0bcc15c19e4e4179c28848

Authored by Willen Borges Coelho
1 parent 09cef093
Exists in main

URLs amigáveis e redesign visual completo

- .htaccess: esconde a extensão .php (RewriteRule mapeando /rota pro
  arquivo .php correspondente, redirect 301 de .php -> sem extensão),
  bloqueia acesso a dotfiles e nega acesso via URL a src/config/storage/sql
- Todos os links, forms e redirects internos atualizados pras novas rotas
- CSS reescrito do zero: tema claro/escuro automático (prefers-color-scheme),
  tipografia Inter, cards com sombra e cantos arredondados, botões com
  gradiente, badges e alerts mais refinados
- Tabelas ficam responsivas: em telas estreitas (<720px) cada linha vira
  um cartão empilhado em vez de rolar horizontalmente
- Login com subtítulo e leve animação de entrada

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
README.md
... ... @@ -48,10 +48,17 @@ e resetar senha, com controle de acesso gerenciado pela própria aplicação.
48 48 - `sql/schema.sql` — schema do banco (admins, settings, audit_log, login_attempts).
49 49 - `storage/` — log de auditoria em arquivo (complementar ao banco).
50 50  
  51 +## URLs amigáveis
  52 +
  53 +O `public_html/.htaccess` esconde a extensão `.php` (requer `mod_rewrite` e
  54 +`AllowOverride FileInfo`/`Limit` no vhost, já presentes na config desse site):
  55 +`/dashboard`, `/admins`, `/auditoria`, `/settings`, `/logout` etc. em vez de
  56 +`dashboard.php`, `admins.php`... Acessar o `.php` direto redireciona pra URL limpa.
  57 +
51 58 ## Papéis
52 59  
53   -- **admin**: tudo que operador pode, mais gerenciar `admins.php` e `settings.php`.
54   -- **operador**: só habilita/desabilita aluno e reseta senha no `dashboard.php`.
  60 +- **admin**: tudo que operador pode, mais gerenciar administradores e configurações.
  61 +- **operador**: só habilita/desabilita aluno e reseta senha no dashboard.
55 62  
56 63 A autenticação de quem usa o sistema é sempre feita com login e senha do próprio AD
57 64 (bind LDAP) — o banco **não armazena nenhuma senha**. A tabela `admins` só guarda
... ...
public_html/.htaccess 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +RewriteEngine On
  2 +RewriteBase /
  3 +
  4 +# Esconde a extensão .php: se alguém acessar /dashboard.php direto, redireciona
  5 +# pra /dashboard (sem expor o nome do arquivo em links/histórico).
  6 +RewriteCond %{THE_REQUEST} \s/+([^.\s]+)\.php[\s?] [NC]
  7 +RewriteRule ^ /%1 [R=301,L]
  8 +
  9 +# Bloqueia acesso a qualquer coisa fora de public_html através de URL,
  10 +# mesmo sem rewrite (defesa extra, já que src/config/storage ficam um
  11 +# nível acima e não deveriam ser alcançáveis mesmo sem isso).
  12 +RewriteRule ^(src|config|storage|sql)(/|$) - [F,L]
  13 +
  14 +# Roteia /algumacoisa para algumacoisa.php quando o arquivo existir e
  15 +# nenhum arquivo/diretório real com esse nome exista.
  16 +RewriteCond %{REQUEST_FILENAME} !-f
  17 +RewriteCond %{REQUEST_FILENAME} !-d
  18 +RewriteCond %{DOCUMENT_ROOT}/$1.php -f
  19 +RewriteRule ^([a-zA-Z0-9_-]+)$ $1.php [L,QSA]
  20 +
  21 +# Nunca serve .htaccess/.env ou outro arquivo de config por engano.
  22 +# (Options -Indexes não entra aqui de propósito: o AllowOverride do vhost
  23 +# não libera o contexto "Options", só FileInfo/AuthConfig/Limit -- colocar
  24 +# a diretiva aqui derrubaria o site com "Options not allowed here".)
  25 +<FilesMatch "^\.">
  26 + Require all denied
  27 +</FilesMatch>
... ...
public_html/action_status.php
... ... @@ -6,7 +6,7 @@ require __DIR__ . &#39;/../src/bootstrap.php&#39;;
6 6 Auth::requireLogin();
7 7  
8 8 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
9   - header('Location: dashboard.php');
  9 + header('Location: dashboard');
10 10 exit;
11 11 }
12 12  
... ... @@ -14,7 +14,7 @@ $user = trim((string) ($_POST[&#39;user&#39;] ?? &#39;&#39;));
14 14 $enable = ($_POST['enable'] ?? '') === '1';
15 15  
16 16 if ($user === '') {
17   - header('Location: dashboard.php?erro=1&msg=' . urlencode('Usuário inválido.'));
  17 + header('Location: dashboard?erro=1&msg=' . urlencode('Usuário inválido.'));
18 18 exit;
19 19 }
20 20  
... ... @@ -39,9 +39,9 @@ try {
39 39  
40 40 AuditLog::record($quemFez, $enable ? 'habilitou_aluno' : 'desabilitou_aluno', $user);
41 41  
42   - header('Location: dashboard.php?ok=1');
  42 + header('Location: dashboard?ok=1');
43 43 exit;
44 44 } catch (Throwable $e) {
45   - header('Location: dashboard.php?erro=1&msg=' . urlencode($e->getMessage()));
  45 + header('Location: dashboard?erro=1&msg=' . urlencode($e->getMessage()));
46 46 exit;
47 47 }
... ...
public_html/admins.php
... ... @@ -86,7 +86,7 @@ View::header(&#39;Administradores&#39;);
86 86 <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?>
87 87 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?>
88 88  
89   - <form method="post" action="admins.php">
  89 + <form method="post" action="admins">
90 90 <input type="hidden" name="acao" value="adicionar">
91 91 <label for="login">Login do AD (sem o domínio <?= View::escape(Config::get('ldap.domain_suffix', '')) ?>)</label>
92 92 <input type="text" id="login" name="login" required>
... ... @@ -108,11 +108,11 @@ View::header(&#39;Administradores&#39;);
108 108 </tr>
109 109 <?php foreach ($admins as $a): ?>
110 110 <tr>
111   - <td><?= View::escape($a['login']) ?></td>
112   - <td><?= View::escape($a['nome']) ?></td>
113   - <td>
  111 + <td data-label="Login"><?= View::escape($a['login']) ?></td>
  112 + <td data-label="Nome"><?= View::escape($a['nome']) ?></td>
  113 + <td data-label="Papel">
114 114 <?php if ($a['ativo']): ?>
115   - <form method="post" action="admins.php" style="display:flex;gap:6px;align-items:center;margin:0">
  115 + <form method="post" action="admins" style="display:flex;gap:6px;align-items:center;margin:0;justify-content:flex-end">
116 116 <input type="hidden" name="acao" value="atualizar_papel">
117 117 <input type="hidden" name="id" value="<?= (int) $a['id'] ?>">
118 118 <select name="papel" style="margin:0">
... ... @@ -125,15 +125,15 @@ View::header(&#39;Administradores&#39;);
125 125 <?= View::escape($a['papel']) ?>
126 126 <?php endif; ?>
127 127 </td>
128   - <td>
  128 + <td data-label="Status">
129 129 <?php if ($a['ativo']): ?>
130 130 <span class="badge ok">Ativo</span>
131 131 <?php else: ?>
132 132 <span class="badge off">Inativo</span>
133 133 <?php endif; ?>
134 134 </td>
135   - <td>
136   - <form method="post" action="admins.php" style="display:inline">
  135 + <td data-label="Ações">
  136 + <form method="post" action="admins" style="display:inline">
137 137 <input type="hidden" name="id" value="<?= (int) $a['id'] ?>">
138 138 <?php if ($a['ativo']): ?>
139 139 <input type="hidden" name="acao" value="desativar">
... ...
public_html/assets/style.css
1 1 :root {
2 2 color-scheme: light;
3   - --bg: #f4f5f7;
  3 + --bg: #f1f3f8;
  4 + --bg-pattern: radial-gradient(circle at 10% -10%, #dbe4ff 0%, transparent 45%),
  5 + radial-gradient(circle at 110% 10%, #e3f3ff 0%, transparent 40%);
4 6 --card: #ffffff;
5   - --text: #1f2430;
6   - --muted: #6b7280;
7   - --border: #e2e4e9;
8   - --accent: #2563eb;
  7 + --card-border: #e7e9f0;
  8 + --text: #1a1d29;
  9 + --text-muted: #6b7280;
  10 + --border: #e7e9f0;
  11 + --accent: #4f46e5;
  12 + --accent-2: #4338ca;
  13 + --accent-soft: #eef0ff;
9 14 --accent-contrast: #ffffff;
10 15 --danger: #dc2626;
  16 + --danger-soft: #fee2e2;
11 17 --success: #16a34a;
  18 + --success-soft: #dcfce7;
  19 + --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  20 + --shadow-md: 0 8px 24px -8px rgba(16, 24, 40, 0.18), 0 2px 6px rgba(16, 24, 40, 0.06);
  21 + --radius: 14px;
  22 + --radius-sm: 8px;
  23 +}
  24 +
  25 +@media (prefers-color-scheme: dark) {
  26 + :root:not([data-theme="light"]) {
  27 + color-scheme: dark;
  28 + --bg: #0f1117;
  29 + --bg-pattern: radial-gradient(circle at 10% -10%, #1e2140 0%, transparent 45%),
  30 + radial-gradient(circle at 110% 10%, #10233a 0%, transparent 40%);
  31 + --card: #171923;
  32 + --card-border: #272b3a;
  33 + --text: #eef0f5;
  34 + --text-muted: #9aa0b2;
  35 + --border: #272b3a;
  36 + --accent: #6d66f0;
  37 + --accent-2: #857ef5;
  38 + --accent-soft: #211f3d;
  39 + --accent-contrast: #ffffff;
  40 + --danger: #f87171;
  41 + --danger-soft: #3a1a1c;
  42 + --success: #4ade80;
  43 + --success-soft: #11301f;
  44 + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  45 + --shadow-md: 0 12px 32px -12px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35);
  46 + }
  47 +}
  48 +
  49 +:root[data-theme="dark"] {
  50 + color-scheme: dark;
  51 + --bg: #0f1117;
  52 + --bg-pattern: radial-gradient(circle at 10% -10%, #1e2140 0%, transparent 45%),
  53 + radial-gradient(circle at 110% 10%, #10233a 0%, transparent 40%);
  54 + --card: #171923;
  55 + --card-border: #272b3a;
  56 + --text: #eef0f5;
  57 + --text-muted: #9aa0b2;
  58 + --border: #272b3a;
  59 + --accent: #6d66f0;
  60 + --accent-2: #857ef5;
  61 + --accent-soft: #211f3d;
  62 + --accent-contrast: #ffffff;
  63 + --danger: #f87171;
  64 + --danger-soft: #3a1a1c;
  65 + --success: #4ade80;
  66 + --success-soft: #11301f;
  67 + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
  68 + --shadow-md: 0 12px 32px -12px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35);
12 69 }
13 70  
14 71 * { box-sizing: border-box; }
15 72  
  73 +html { -webkit-text-size-adjust: 100%; }
  74 +
16 75 body {
17 76 margin: 0;
18   - font-family: -apple-system, Segoe UI, Roboto, Arial, sans-serif;
19   - background: var(--bg);
  77 + min-height: 100vh;
  78 + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  79 + background-color: var(--bg);
  80 + background-image: var(--bg-pattern);
  81 + background-attachment: fixed;
20 82 color: var(--text);
  83 + -webkit-font-smoothing: antialiased;
21 84 }
22 85  
  86 +/* Topbar */
  87 +
23 88 .topbar {
24 89 display: flex;
25 90 justify-content: space-between;
26 91 align-items: center;
  92 + gap: 16px;
27 93 padding: 14px 24px;
28   - background: var(--card);
  94 + background: color-mix(in srgb, var(--card) 88%, transparent);
  95 + backdrop-filter: blur(10px);
29 96 border-bottom: 1px solid var(--border);
  97 + position: sticky;
  98 + top: 0;
  99 + z-index: 20;
  100 + flex-wrap: wrap;
  101 +}
  102 +
  103 +.topbar strong {
  104 + font-weight: 700;
  105 + letter-spacing: -0.01em;
  106 + font-size: 15px;
  107 + white-space: nowrap;
  108 +}
  109 +
  110 +.topbar nav {
  111 + display: flex;
  112 + align-items: center;
  113 + gap: 4px;
  114 + flex-wrap: wrap;
  115 +}
  116 +
  117 +.topbar nav span {
  118 + color: var(--text-muted);
  119 + font-size: 13px;
  120 + margin-right: 10px;
  121 + padding-right: 10px;
  122 + border-right: 1px solid var(--border);
30 123 }
31 124  
32 125 .topbar nav a {
33   - margin-left: 16px;
34   - color: var(--text);
  126 + color: var(--text-muted);
35 127 text-decoration: none;
36 128 font-size: 14px;
  129 + font-weight: 500;
  130 + padding: 7px 12px;
  131 + border-radius: 999px;
  132 + transition: background-color 0.15s ease, color 0.15s ease;
  133 + white-space: nowrap;
37 134 }
38 135  
39   -.topbar nav a:hover { color: var(--accent); }
  136 +.topbar nav a:hover {
  137 + color: var(--accent);
  138 + background: var(--accent-soft);
  139 +}
  140 +
  141 +/* Layout */
40 142  
41 143 .container {
42   - max-width: 960px;
43   - margin: 32px auto;
44   - padding: 0 16px;
  144 + max-width: 1000px;
  145 + margin: 0 auto;
  146 + padding: 28px 16px 48px;
45 147 }
46 148  
47 149 .card {
48 150 background: var(--card);
49   - border: 1px solid var(--border);
50   - border-radius: 10px;
51   - padding: 24px;
  151 + border: 1px solid var(--card-border);
  152 + border-radius: var(--radius);
  153 + padding: 28px;
  154 + box-shadow: var(--shadow-sm);
52 155 }
53 156  
54 157 .login-wrap {
... ... @@ -56,46 +159,210 @@ body {
56 159 min-height: 100vh;
57 160 align-items: center;
58 161 justify-content: center;
  162 + padding: 16px;
  163 +}
  164 +
  165 +.login-wrap .card {
  166 + width: 100%;
  167 + max-width: 380px;
  168 + box-shadow: var(--shadow-md);
  169 + animation: rise 0.35s ease;
59 170 }
60 171  
61   -.login-wrap .card { width: 100%; max-width: 360px; }
  172 +.login-wrap .card h1 {
  173 + text-align: center;
  174 + margin-bottom: 4px;
  175 +}
62 176  
63   -h1 { font-size: 20px; margin: 0 0 16px; }
  177 +.login-sub {
  178 + text-align: center;
  179 + color: var(--text-muted);
  180 + font-size: 13px;
  181 + margin: 0 0 22px;
  182 +}
64 183  
65   -label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; }
  184 +@keyframes rise {
  185 + from { opacity: 0; transform: translateY(8px); }
  186 + to { opacity: 1; transform: translateY(0); }
  187 +}
66 188  
67   -input[type=text], input[type=password], select {
  189 +h1 {
  190 + font-size: 21px;
  191 + font-weight: 700;
  192 + letter-spacing: -0.01em;
  193 + margin: 0 0 18px;
  194 +}
  195 +
  196 +label {
  197 + display: block;
  198 + font-size: 13px;
  199 + font-weight: 500;
  200 + color: var(--text-muted);
  201 + margin-bottom: 6px;
  202 +}
  203 +
  204 +input[type=text],
  205 +input[type=password],
  206 +select {
68 207 width: 100%;
69   - padding: 8px 10px;
70   - margin-bottom: 14px;
  208 + padding: 10px 12px;
  209 + margin-bottom: 16px;
71 210 border: 1px solid var(--border);
72   - border-radius: 6px;
  211 + border-radius: var(--radius-sm);
73 212 font-size: 14px;
  213 + background: var(--bg);
  214 + color: var(--text);
  215 + transition: border-color 0.15s ease, box-shadow 0.15s ease;
  216 +}
  217 +
  218 +input[type=text]:focus,
  219 +input[type=password]:focus,
  220 +select:focus {
  221 + outline: none;
  222 + border-color: var(--accent);
  223 + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent);
74 224 }
75 225  
76 226 button, .btn {
77   - display: inline-block;
  227 + display: inline-flex;
  228 + align-items: center;
  229 + justify-content: center;
  230 + gap: 6px;
78 231 background: var(--accent);
  232 + background-image: linear-gradient(135deg, var(--accent), var(--accent-2));
79 233 color: var(--accent-contrast);
80 234 border: none;
81   - padding: 8px 14px;
82   - border-radius: 6px;
  235 + padding: 9px 16px;
  236 + border-radius: var(--radius-sm);
83 237 font-size: 14px;
  238 + font-weight: 600;
84 239 cursor: pointer;
85 240 text-decoration: none;
  241 + box-shadow: var(--shadow-sm);
  242 + transition: transform 0.1s ease, box-shadow 0.15s ease, opacity 0.15s ease;
  243 +}
  244 +
  245 +button:hover, .btn:hover { opacity: 0.92; }
  246 +button:active, .btn:active { transform: translateY(1px); }
  247 +
  248 +button.danger, .btn.danger {
  249 + background-image: none;
  250 + background: var(--danger);
  251 +}
  252 +
  253 +button.secondary, .btn.secondary {
  254 + background-image: none;
  255 + background: transparent;
  256 + color: var(--text-muted);
  257 + border: 1px solid var(--border);
  258 + box-shadow: none;
  259 +}
  260 +
  261 +button.secondary:hover, .btn.secondary:hover {
  262 + color: var(--text);
  263 + border-color: var(--text-muted);
  264 + opacity: 1;
  265 +}
  266 +
  267 +/* Tabela / responsiva */
  268 +
  269 +table {
  270 + width: 100%;
  271 + border-collapse: collapse;
  272 + margin-top: 16px;
  273 +}
  274 +
  275 +th, td {
  276 + text-align: left;
  277 + padding: 12px 10px;
  278 + border-bottom: 1px solid var(--border);
  279 + font-size: 14px;
  280 + vertical-align: middle;
  281 +}
  282 +
  283 +th {
  284 + color: var(--text-muted);
  285 + font-weight: 600;
  286 + font-size: 12px;
  287 + text-transform: uppercase;
  288 + letter-spacing: 0.03em;
  289 +}
  290 +
  291 +tr:last-child td { border-bottom: none; }
  292 +
  293 +.badge {
  294 + display: inline-block;
  295 + padding: 3px 10px;
  296 + border-radius: 999px;
  297 + font-size: 12px;
  298 + font-weight: 600;
86 299 }
87 300  
88   -button.danger, .btn.danger { background: var(--danger); }
89   -button.secondary, .btn.secondary { background: var(--muted); }
  301 +.badge.ok { background: var(--success-soft); color: var(--success); }
  302 +.badge.off { background: var(--danger-soft); color: var(--danger); }
90 303  
91   -table { width: 100%; border-collapse: collapse; margin-top: 12px; }
92   -th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); font-size: 14px; }
93   -th { color: var(--muted); font-weight: 600; }
  304 +.alert {
  305 + padding: 11px 16px;
  306 + border-radius: var(--radius-sm);
  307 + margin-bottom: 18px;
  308 + font-size: 14px;
  309 + border: 1px solid transparent;
  310 +}
94 311  
95   -.badge { padding: 2px 8px; border-radius: 999px; font-size: 12px; }
96   -.badge.ok { background: #dcfce7; color: var(--success); }
97   -.badge.off { background: #fee2e2; color: var(--danger); }
  312 +.alert.error { background: var(--danger-soft); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, transparent); }
  313 +.alert.success { background: var(--success-soft); color: var(--success); border-color: color-mix(in srgb, var(--success) 30%, transparent); }
98 314  
99   -.alert { padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 14px; }
100   -.alert.error { background: #fee2e2; color: var(--danger); }
101   -.alert.success { background: #dcfce7; color: var(--success); }
  315 +.hint {
  316 + color: var(--text-muted);
  317 + font-size: 13px;
  318 +}
  319 +
  320 +code {
  321 + background: var(--accent-soft);
  322 + color: var(--accent);
  323 + padding: 2px 6px;
  324 + border-radius: 4px;
  325 + font-size: 12.5px;
  326 +}
  327 +
  328 +/* Responsivo: tabela vira "cards" empilhados no celular */
  329 +
  330 +@media (max-width: 720px) {
  331 + .container { padding: 20px 12px 40px; }
  332 + .card { padding: 18px; border-radius: 12px; }
  333 +
  334 + .topbar { padding: 12px 16px; }
  335 + .topbar nav span { border-right: none; padding-right: 0; margin-right: 0; width: 100%; order: -1; }
  336 +
  337 + table, thead, tbody, th, td, tr { display: block; }
  338 + thead { display: none; }
  339 +
  340 + tr {
  341 + border: 1px solid var(--border);
  342 + border-radius: var(--radius-sm);
  343 + margin-bottom: 12px;
  344 + padding: 10px 12px;
  345 + }
  346 +
  347 + td {
  348 + border-bottom: none;
  349 + padding: 6px 0;
  350 + display: flex;
  351 + justify-content: space-between;
  352 + align-items: center;
  353 + gap: 12px;
  354 + text-align: right;
  355 + }
  356 +
  357 + td::before {
  358 + content: attr(data-label);
  359 + font-size: 12px;
  360 + font-weight: 600;
  361 + color: var(--text-muted);
  362 + text-transform: uppercase;
  363 + letter-spacing: 0.03em;
  364 + text-align: left;
  365 + }
  366 +
  367 + td form { justify-content: flex-end; }
  368 +}
... ...
public_html/auditoria.php
... ... @@ -40,7 +40,7 @@ View::header(&#39;Auditoria&#39;);
40 40 ?>
41 41 <div class="card">
42 42 <h1>Auditoria</h1>
43   - <p style="color:#6b7280;font-size:13px">
  43 + <p class="hint">
44 44 Histórico de logins (inclusive tentativas com senha incorreta), alterações em
45 45 alunos, administradores e configurações. Mostrando <?= count($logs) ?> de <?= $total ?> registros.
46 46 </p>
... ... @@ -56,12 +56,12 @@ View::header(&#39;Auditoria&#39;);
56 56 </tr>
57 57 <?php foreach ($logs as $log): ?>
58 58 <tr>
59   - <td><?= View::escape($log['criado_em']) ?></td>
60   - <td><?= View::escape($log['usuario']) ?></td>
61   - <td><?= View::escape($acaoLabels[$log['acao']] ?? $log['acao']) ?></td>
62   - <td><?= View::escape($log['alvo'] ?? '') ?></td>
63   - <td><?= View::escape($log['detalhe'] ?? '') ?></td>
64   - <td><?= View::escape($log['ip'] ?? '') ?></td>
  59 + <td data-label="Data/hora"><?= View::escape($log['criado_em']) ?></td>
  60 + <td data-label="Usuário"><?= View::escape($log['usuario']) ?></td>
  61 + <td data-label="Ação"><?= View::escape($acaoLabels[$log['acao']] ?? $log['acao']) ?></td>
  62 + <td data-label="Alvo"><?= View::escape($log['alvo'] ?? '') ?></td>
  63 + <td data-label="Detalhe"><?= View::escape($log['detalhe'] ?? '') ?></td>
  64 + <td data-label="IP"><?= View::escape($log['ip'] ?? '') ?></td>
65 65 </tr>
66 66 <?php endforeach; ?>
67 67 <?php if (!$logs): ?>
... ... @@ -75,7 +75,7 @@ View::header(&#39;Auditoria&#39;);
75 75 <?php if ($p === $page): ?>
76 76 <strong><?= $p ?></strong>
77 77 <?php else: ?>
78   - <a href="auditoria.php?p=<?= $p ?>"><?= $p ?></a>
  78 + <a href="auditoria?p=<?= $p ?>"><?= $p ?></a>
79 79 <?php endif; ?>
80 80 <?php if ($p < $totalPages): ?> | <?php endif; ?>
81 81 <?php endfor; ?>
... ...
public_html/dashboard.php
... ... @@ -61,24 +61,24 @@ View::header(&#39;Alunos&#39;);
61 61 </tr>
62 62 <?php foreach ($alunos as $aluno): ?>
63 63 <tr>
64   - <td><?= View::escape($aluno['nome']) ?></td>
65   - <td>
  64 + <td data-label="Aluno"><?= View::escape($aluno['nome']) ?></td>
  65 + <td data-label="Status">
66 66 <?php if ($aluno['habilitado']): ?>
67 67 <span class="badge ok">Habilitado</span>
68 68 <?php else: ?>
69 69 <span class="badge off">Desabilitado</span>
70 70 <?php endif; ?>
71 71 </td>
72   - <td><?= View::escape($aluno['descricao']) ?></td>
73   - <td>
74   - <form method="post" action="action_status.php" style="display:inline">
  72 + <td data-label="Última alteração por"><?= View::escape($aluno['descricao']) ?></td>
  73 + <td data-label="Ações">
  74 + <form method="post" action="action_status" style="display:inline">
75 75 <input type="hidden" name="user" value="<?= View::escape($aluno['cn']) ?>">
76 76 <input type="hidden" name="enable" value="<?= $aluno['habilitado'] ? '0' : '1' ?>">
77 77 <button type="submit" class="<?= $aluno['habilitado'] ? 'danger' : '' ?>">
78 78 <?= $aluno['habilitado'] ? 'Desabilitar' : 'Habilitar' ?>
79 79 </button>
80 80 </form>
81   - <a class="btn secondary" href="newpasswd.php?user=<?= urlencode($aluno['cn']) ?>">Nova senha</a>
  81 + <a class="btn secondary" href="newpasswd?user=<?= urlencode($aluno['cn']) ?>">Nova senha</a>
82 82 </td>
83 83 </tr>
84 84 <?php endforeach; ?>
... ...
public_html/index.php
... ... @@ -5,7 +5,7 @@ declare(strict_types=1);
5 5 require __DIR__ . '/../src/bootstrap.php';
6 6  
7 7 if (!empty($_SESSION['login'])) {
8   - header('Location: dashboard.php');
  8 + header('Location: dashboard');
9 9 exit;
10 10 }
11 11  
... ... @@ -20,7 +20,7 @@ if ($_SERVER[&#39;REQUEST_METHOD&#39;] === &#39;POST&#39;) {
20 20 } else {
21 21 $result = Auth::attempt($login, $senha);
22 22 if ($result['ok']) {
23   - header('Location: dashboard.php');
  23 + header('Location: dashboard');
24 24 exit;
25 25 }
26 26 $erro = $result['error'];
... ... @@ -33,17 +33,22 @@ if ($_SERVER[&#39;REQUEST_METHOD&#39;] === &#39;POST&#39;) {
33 33 <html lang="pt-br">
34 34 <head>
35 35 <meta charset="utf-8">
  36 + <meta name="viewport" content="width=device-width, initial-scale=1">
36 37 <title>Gestão de Alunos Lab. Informática</title>
  38 + <link rel="preconnect" href="https://fonts.googleapis.com">
  39 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  40 + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
37 41 <link rel="stylesheet" href="assets/style.css">
38 42 </head>
39 43 <body>
40 44 <div class="login-wrap">
41 45 <div class="card">
42   - <h1>Gestão de Alunos Lab. Informática</h1>
  46 + <h1>Gestão de Alunos</h1>
  47 + <p class="login-sub">Lab. Informática</p>
43 48 <?php if ($erro): ?>
44 49 <div class="alert error"><?= htmlspecialchars($erro, ENT_QUOTES) ?></div>
45 50 <?php endif; ?>
46   - <form method="post" action="index.php">
  51 + <form method="post" action="">
47 52 <label for="login">Login</label>
48 53 <input type="text" id="login" name="login" autocomplete="username" required autofocus>
49 54  
... ...
public_html/logout.php
... ... @@ -5,5 +5,5 @@ declare(strict_types=1);
5 5 require __DIR__ . '/../src/bootstrap.php';
6 6  
7 7 Auth::logout();
8   -header('Location: index.php');
  8 +header('Location: /');
9 9 exit;
... ...
public_html/newpasswd.php
... ... @@ -52,7 +52,7 @@ View::header(&#39;Nova senha&#39;);
52 52 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?>
53 53  
54 54 <?php if (!$ok): ?>
55   - <form method="post" action="newpasswd.php">
  55 + <form method="post" action="newpasswd">
56 56 <input type="hidden" name="user" value="<?= View::escape($user) ?>">
57 57 <label for="passwd1">Nova senha</label>
58 58 <input type="password" id="passwd1" name="passwd1" required minlength="8">
... ... @@ -62,7 +62,7 @@ View::header(&#39;Nova senha&#39;);
62 62 </form>
63 63 <?php endif; ?>
64 64  
65   - <p><a href="dashboard.php">&larr; Voltar</a></p>
  65 + <p><a href="dashboard">&larr; Voltar</a></p>
66 66 </div>
67 67 <?php
68 68 View::footer();
... ...
public_html/settings.php
... ... @@ -29,14 +29,14 @@ View::header(&#39;Configurações&#39;);
29 29 <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?>
30 30 <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?>
31 31  
32   - <form method="post" action="settings.php">
  32 + <form method="post" action="settings">
33 33 <label for="group_aluno_filter">Grupo (CN) de alunos monitorado no dashboard</label>
34 34 <input type="text" id="group_aluno_filter" name="group_aluno_filter"
35 35 value="<?= View::escape($settings['group_aluno_filter'] ?? '') ?>" required>
36 36 <button type="submit">Salvar</button>
37 37 </form>
38 38  
39   - <p style="color:#6b7280;font-size:13px">
  39 + <p class="hint">
40 40 As demais configurações de conexão (servidor LDAP, conta de serviço, credenciais de banco)
41 41 ficam em <code>config/local.php</code>, fora do versionamento, por segurança.
42 42 </p>
... ...
src/Auth.php
... ... @@ -81,7 +81,7 @@ final class Auth
81 81 {
82 82 self::start();
83 83 if (empty($_SESSION['login'])) {
84   - header('Location: index.php?erro=sessao_expirada');
  84 + header('Location: /?erro=sessao_expirada');
85 85 exit;
86 86 }
87 87 }
... ...
src/View.php
... ... @@ -8,7 +8,11 @@ final class View
8 8 <html lang="pt-br">
9 9 <head>
10 10 <meta charset="utf-8">
  11 + <meta name="viewport" content="width=device-width, initial-scale=1">
11 12 <title><?= htmlspecialchars($title, ENT_QUOTES) ?> - Gestão de Alunos Lab. Informática</title>
  13 + <link rel="preconnect" href="https://fonts.googleapis.com">
  14 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  15 + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
12 16 <link rel="stylesheet" href="assets/style.css">
13 17 </head>
14 18 <body>
... ... @@ -16,13 +20,13 @@ final class View
16 20 <strong>Gestão de Alunos Lab. Informática</strong>
17 21 <nav>
18 22 <span><?= htmlspecialchars(Auth::currentName() ?? '', ENT_QUOTES) ?></span>
19   - <a href="dashboard.php">Alunos</a>
  23 + <a href="dashboard">Alunos</a>
20 24 <?php if (Auth::isAdmin()): ?>
21   - <a href="admins.php">Administradores</a>
22   - <a href="auditoria.php">Auditoria</a>
23   - <a href="settings.php">Configurações</a>
  25 + <a href="admins">Administradores</a>
  26 + <a href="auditoria">Auditoria</a>
  27 + <a href="settings">Configurações</a>
24 28 <?php endif; ?>
25   - <a href="logout.php">Sair</a>
  29 + <a href="logout">Sair</a>
26 30 </nav>
27 31 </div>
28 32 <div class="container">
... ...