Commit 72556b0da726f41b4d0bcc15c19e4e4179c28848
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>
Showing
13 changed files
with
391 additions
and
81 deletions
Show diff stats
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 | ... | ... |
| ... | ... | @@ -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__ . '/../src/bootstrap.php'; |
| 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['user'] ?? '')); |
| 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('Administradores'); |
| 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('Administradores'); |
| 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('Administradores'); |
| 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('Auditoria'); |
| 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('Auditoria'); |
| 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('Auditoria'); |
| 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('Alunos'); |
| 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['REQUEST_METHOD'] === 'POST') { |
| 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['REQUEST_METHOD'] === 'POST') { |
| 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
public_html/newpasswd.php
| ... | ... | @@ -52,7 +52,7 @@ View::header('Nova senha'); |
| 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('Nova senha'); |
| 62 | 62 | </form> |
| 63 | 63 | <?php endif; ?> |
| 64 | 64 | |
| 65 | - <p><a href="dashboard.php">← Voltar</a></p> | |
| 65 | + <p><a href="dashboard">← Voltar</a></p> | |
| 66 | 66 | </div> |
| 67 | 67 | <?php |
| 68 | 68 | View::footer(); | ... | ... |
public_html/settings.php
| ... | ... | @@ -29,14 +29,14 @@ View::header('Configurações'); |
| 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
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"> | ... | ... |