1<!DOCTYPE html> 2<html lang="pt"> 3<head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <title>Socem Client â Acesso</title> 7 <link rel="stylesheet" href="/static/css/app.css?v=1791382322"> 8
8<script> 9 // Apply theme & lang BEFORE paint to avoid flash 10 (function() { 11 // Idioma vindo do site socem.pt: /login?lang=en (ou pt) tem prioridade 12 // sobre a preferência guardada e fica gravado para o resto da aplicação. 13 try { 14 const q = (new URLSearchParams(location.search).get('lang') || '').toLowerCase(); 15 const fromLink = q.startsWith('en') ? 'en' : q.startsWith('pt') ? 'pt' : null; 16 if (fromLink) localStorage.setItem('sc_lang', fromLink); 17 // Sem link nem preferência guardada (1.ª visita neste dispositivo): usa o 18 // idioma do browser â português â PT, qualquer outro â EN. O site socem.pt 19 // não nos transmite o idioma escolhido lá (o link é igual nos dois). 20 if (!localStorage.getItem('sc_lang')) { 21 const nav = (navigator.languages && navigator.languages[0]) || navigator.language || 'pt'; 22 localStorage.setItem('sc_lang', nav.toLowerCase().startsWith('pt') ? 'pt' : 'en'); 23 } 24 } catch (e) { /* localStorage indisponÃvel: segue o idioma por defeito */ } 25 const t = localStorage.getItem('sc_theme') || 'light'; 26 const l = localStorage.getItem('sc_lang') || 'pt'; 27 document.documentElement.setAttribute('data-theme', t); 28 document.documentElement.setAttribute('lang', l); 29 })(); 30 </script>
30 31 <style> 32 .login-toggles { 33 position: fixed; 34 top: 16px; right: 16px; 35 display: flex; gap: 6px; 36 z-index: 10; 37 } 38 .login-toggle-btn { 39 background: rgba(255,255,255,.15); 40 border: 1px solid rgba(255,255,255,.25); 41 color: #fff; 42 border-radius: 20px; 43 padding: 4px 11px; 44 cursor: pointer; 45 font-size: .78rem; 46 font-weight: 600; 47 transition: all .2s; 48 display: flex; align-items: center; gap: 5px; 49 } 50 .login-toggle-btn:hover { background: rgba(255,255,255,.28); } 51 </style> 52</head> 53<body> 54 55<!-- Theme & Language toggles --> 56<div class="login-toggles"> 57 <button class="login-toggle-btn" id="themeToggle" onclick="toggleTheme()"> 58 <span id="themeIcon">ð</span> 59 </button> 60 <button class="login-toggle-btn" id="langToggle" onclick="toggleLang()"> 61 <span id="langIcon">ð¬ð§</span> 62 <span id="langLabel">EN</span> 63 </button> 64</div> 65 66<div class="login-bg"> 67 <div class="login-card"> 68 <div class="login-logo"> 69 <img src="/static/img/logo_socem_cor1.svg" alt="SOCEM" style="height:44px;display:block;margin:0 auto 12px;"> 70 <h1>Socem Client</h1> 71 <p id="loginSubtitle">Portal de Acompanhamento de Projetos</p> 72 </div> 73 <div id="err" class="alert" style="display:none;"></div> 74 <form id="loginForm" autocomplete="on"> 75 <div class="form-group"> 76 <label id="labelUser">Utilizador</label> 77 <input id="username" type="text" autocomplete="username" required 78 placeholder="Nome de utilizador"> 79 </div> 80 <div class="form-group"> 81 <label id="labelPw">Password</label> 82 <div style="position:relative"> 83 <input id="password" type="password" autocomplete="current-password" required 84 placeholder="â¢â¢â¢â¢â¢â¢â¢â¢" style="width:100%;padding-right:38px;box-sizing:border-box" 85 onkeydown="if(event.key==='Enter')document.getElementById('loginBtn').click()"> 86 <button type="button" id="togglePw" onclick="togglePasswordVisibility()" 87 title="Mostrar/ocultar password" 88 style="position:absolute;right:6px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;line-height:0;padding:6px;color:#6b7280;display:flex;align-items:center"> 89 <svg id="eyeIconOpen" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> 90 <path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"></path> 91 <circle cx="12" cy="12" r="3"></circle> 92 </svg> 93 <svg id="eyeIconClosed" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" style="display:none"> 94 <path d="M17.94 17.94A10.94 10.94 0 0 1 12 20c-7 0-11-7-11-7a20.3 20.3 0 0 1 4.22-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 7 11 7a20.3 20.3 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path> 95 <line x1="1" y1="1" x2="23" y2="23"></line> 96 </svg> 97 </button> 98 </div> 99 </div> 100 <button type="submit" id="loginBtn" class="btn-primary btn-full"> 101 Entrar 102 </button> 103 </form> 104 105 <!-- Passo 2FA: código por email --> 106 <form id="codeForm" autocomplete="off" style="display:none;"> 107 <p id="codeInfo" style="font-size:.82rem;color:#6b7280;margin:0 0 14px;text-align:center;"></p> 108 <div class="form-group"> 109 <label id="labelCode">Código de acesso</label> 110 <input id="codeInput" type="text" inputmode="numeric" maxlength="5" required 111 autocomplete="one-time-code" placeholder="_ _ _ _ _" 112 style="text-align:center;letter-spacing:.5em;font-size:1.3rem;" 113 onkeydown="if(event.key==='Enter')document.getElementById('verifyBtn').click()"> 114 </div> 115 <button type="submit" id="verifyBtn" class="btn-primary btn-full">Confirmar código</button> 116 <p style="text-align:center;margin-top:14px;font-size:.8rem;"> 117 <span id="codeTimer" style="color:#6b7280;"></span> 118 <a href="#" id="resendLink" style="color:#c0121c;margin-left:8px;">Reenviar código</a> 119 </p> 120 </form> 121 122 <p style="text-align:center;margin-top:20px;font-size:.75rem;color:#9ca3af;" id="loginFooter"> 123 © 2026 Socem â Portal Exclusivo para Clientes 124 </p> 125 </div> 126</div> 127
128<script> 129 if (localStorage.getItem('sc_token')) window.location.href = '/'; 130 131 function togglePasswordVisibility() { 132 const pw = document.getElementById('password'); 133 const show = pw.type === 'password'; 134 pw.type = show ? 'text' : 'password'; 135 document.getElementById('eyeIconOpen').style.display = show ? 'none' : ''; 136 document.getElementById('eyeIconClosed').style.display = show ? '' : 'none'; 137 } 138 139 const STRINGS = { 140 pt: { 141 subtitle: 'Portal de Acompanhamento de Projetos', 142 user: 'Utilizador', userPh: 'Nome de utilizador', 143 pw: 'Password', pwPh: 'â¢â¢â¢â¢â¢â¢â¢â¢', 144 submit: 'Entrar', submitting: 'A entrarâ¦', 145 footer: '© 2026 Socem â Portal Exclusivo para Clientes', 146 errLogin: 'Erro ao iniciar sessão', errConn: 'Erro de ligação ao servidor', 147 langIcon: 'ð¬ð§', langLabel: 'EN', 148 codeLabel: 'Código de acesso', 149 codeInfo: e => `Enviámos um código de 5 dÃgitos para ${e}. Introduza-o abaixo (válido 5 min).`, 150 verify: 'Confirmar código', verifying: 'A confirmarâ¦', 151 resend: 'Reenviar código', resent: 'Novo código enviado', 152 expired: 'Código expirado â peça um novo.', validFor: s => `Válido por ${s}`, 153 }, 154 en: { 155 subtitle: 'Project Tracking Portal', 156 user: 'Username', userPh: 'Enter username', 157 pw: 'Password', pwPh: 'â¢â¢â¢â¢â¢â¢â¢â¢', 158 submit: 'Sign in', submitting: 'Signing inâ¦', 159 footer: '© 2026 Socem â Exclusive Client Portal', 160 errLogin: 'Login error', errConn: 'Connection error', 161 langIcon: 'ðµð¹', langLabel: 'PT', 162 codeLabel: 'Access code', 163 codeInfo: e => `We sent a 5-digit code to ${e}. Enter it below (valid 5 min).`, 164 verify: 'Confirm code', verifying: 'Confirmingâ¦', 165 resend: 'Resend code', resent: 'New code sent', 166 expired: 'Code expired â request a new one.', validFor: s => `Valid for ${s}`, 167 }, 168 }; 169 170 let _lang = localStorage.getItem('sc_lang') || 'pt'; 171 let _theme = localStorage.getItem('sc_theme') || 'light'; 172 173 function applyLang(l) { 174 _lang = l; 175 const s = STRINGS[l]; 176 document.documentElement.setAttribute('lang', l); 177 document.getElementById('loginSubtitle').textContent = s.subtitle; 178 document.getElementById('labelUser').textContent = s.user; 179 document.getElementById('username').placeholder = s.userPh; 180 document.getElementById('labelPw').textContent = s.pw; 181 document.getElementById('password').placeholder = s.pwPh; 182 const btn = document.getElementById('loginBtn'); 183 if (!btn.disabled) btn.textContent = s.submit; 184 document.getElementById('labelCode').textContent = s.codeLabel; 185 const vb = document.getElementById('verifyBtn'); 186 if (!vb.disabled) vb.textContent = s.verify; 187 document.getElementById('resendLink').textContent = s.resend; 188 document.getElementById('loginFooter').textContent = s.footer; 189 document.getElementById('langIcon').textContent = s.langIcon; 190 document.getElementById('langLabel').textContent = s.langLabel; 191 localStorage.setItem('sc_lang', l); 192 } 193 194 function applyTheme(t) { 195 _theme = t; 196 document.documentElement.setAttribute('data-theme', t); 197 document.getElementById('themeIcon').textContent = t === 'dark' ? 'âï¸' : 'ð'; 198 localStorage.setItem('sc_theme', t); 199 } 200 201 function toggleTheme() { applyTheme(_theme === 'dark' ? 'light' : 'dark'); } 202 function toggleLang() { applyLang(_lang === 'pt' ? 'en' : 'pt'); } 203 204 // Apply on load 205 applyTheme(_theme); 206 applyLang(_lang); 207 208 document.getElementById('loginForm').addEventListener('submit', async e => { 209 e.preventDefault(); 210 const s = STRINGS[_lang]; 211 const btn = document.getElementById('loginBtn'); 212 const err = document.getElementById('err'); 213 btn.disabled = true; btn.textContent = s.submitting; 214 err.style.display = 'none'; 215 try { 216 const res = await fetch('/auth/login', { 217 method: 'POST', 218 headers: { 'Content-Type': 'application/json' }, 219 body: JSON.stringify({ 220 username: document.getElementById('username').value.trim(), 221 password: document.getElementById('password').value, 222 }), 223 }); 224 const data = await res.json(); 225 if (!res.ok) { 226 err.textContent = data.detail || s.errLogin; 227 err.style.display = 'block'; 228 return; 229 } 230 if (data.step === '2fa') { 231 startCodeStep(document.getElementById('username').value.trim(), data.email_hint); 232 return; 233 } 234 localStorage.setItem('sc_token', data.access_token); 235 localStorage.setItem('sc_user', JSON.stringify(data.user)); 236 window.location.href = '/'; 237 } catch { 238 err.textContent = s.errConn; 239 err.style.display = 'block'; 240 } finally { 241 btn.disabled = false; btn.textContent = STRINGS[_lang].submit; 242 } 243 }); 244 245 /* ââ 2FA: passo do código âââââââââââââââââââââââââââââââââââââââââââ */ 246 let _pendingUser = null, _timerInt = null; 247 248 function startCodeStep(username, emailHint) { 249 _pendingUser = username; 250 const s = STRINGS[_lang]; 251 document.getElementById('err').style.display = 'none'; 252 document.getElementById('loginForm').style.display = 'none'; 253 document.getElementById('codeForm').style.display = 'block'; 254 document.getElementById('codeInfo').textContent = s.codeInfo(emailHint || ''); 255 const inp = document.getElementById('codeInput'); 256 inp.value = ''; inp.focus(); 257 startTimer(5 * 60); 258 } 259 260 function startTimer(seconds) { 261 const el = document.getElementById('codeTimer'); 262 clearInterval(_timerInt); 263 let left = seconds; 264 const tick = () => { 265 const s = STRINGS[_lang]; 266 if (left <= 0) { clearInterval(_timerInt); el.textContent = s.expired; return; } 267 const m = Math.floor(left / 60), sec = String(left % 60).padStart(2, '0'); 268 el.textContent = s.validFor(`${m}:${sec}`); 269 left--; 270 }; 271 tick(); 272 _timerInt = setInterval(tick, 1000); 273 } 274 275 document.getElementById('codeForm').addEventListener('submit', async e => { 276 e.preventDefault(); 277 const s = STRINGS[_lang]; 278 const btn = document.getElementById('verifyBtn'); 279 const err = document.getElementById('err'); 280 const code = document.getElementById('codeInput').value.trim(); 281 btn.disabled = true; btn.textContent = s.verifying; err.style.display = 'none'; 282 try { 283 const res = await fetch('/auth/verify-code', { 284 method: 'POST', headers: { 'Content-Type': 'application/json' }, 285 body: JSON.stringify({ username: _pendingUser, code }), 286 }); 287 const data = await res.json(); 288 if (!res.ok) { err.textContent = data.detail || s.errLogin; err.style.display = 'block'; return; } 289 localStorage.setItem('sc_token', data.access_token); 290 localStorage.setItem('sc_user', JSON.stringify(data.user)); 291 window.location.href = '/'; 292 } catch { err.textContent = s.errConn; err.style.display = 'block'; } 293 finally { btn.disabled = false; btn.textContent = STRINGS[_lang].verify; } 294 }); 295 296 document.getElementById('resendLink').addEventListener('click', async e => { 297 e.preventDefault(); 298 const s = STRINGS[_lang]; 299 const err = document.getElementById('err'); 300 err.style.display = 'none'; 301 try { 302 const res = await fetch('/auth/resend-code', { 303 method: 'POST', headers: { 'Content-Type': 'application/json' }, 304 body: JSON.stringify({ username: _pendingUser }), 305 }); 306 const data = await res.json(); 307 if (!res.ok) { err.textContent = data.detail || s.errLogin; err.style.display = 'block'; return; } 308 document.getElementById('codeInfo').textContent = s.resent; 309 startTimer(5 * 60); 310 } catch { err.textContent = s.errConn; err.style.display = 'block'; } 311 }); 312</script>
312 313</body> 314</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.