PageSourceSearch

https://socemclient.socem.pt/login?lang=pt

html socem.pt collected 2026-10-08 06:46:19 UTC 13,905 bytes, 314 lines download raw bytes

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.