| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570 |
- <script>
- import { goto } from '$app/navigation';
- import { browser } from '$app/environment';
- import { onMount, tick } from 'svelte';
- import { authToken, companyId as companyIdStore } from '$lib/utils/stores';
- const apiUrl = import.meta.env.VITE_API_URL;
- let email = '';
- let password = '';
- let remember = true;
- let loading = false;
- let error = '';
- let success = '';
- let successTimeout;
- const kycStatusEndpoint = `${apiUrl}/user/kyc/status`;
- const KYC_POLL_INTERVAL_MS = 10_000;
- const LOGIN_KYC_PENDING_KEY = 'login_kyc_pending_number_token';
- let kycMode = null;
- let kycLink = '';
- let kycNumberToken = '';
- let kycStatusMessage = '';
- let kycStatusCode = null;
- let kycError = '';
- let qrCodeContainer;
- let qrCodeInstance = null;
- let kycPollingId = null;
- let isCheckingKyc = false;
- function showTimedSuccess(message) {
- success = message;
- if (successTimeout) {
- clearTimeout(successTimeout);
- }
- successTimeout = setTimeout(() => {
- success = '';
- }, 4000);
- }
- function savePendingNumberToken(token) {
- if (!browser) return;
- try {
- if (!token) {
- localStorage.removeItem(LOGIN_KYC_PENDING_KEY);
- return;
- }
- localStorage.setItem(LOGIN_KYC_PENDING_KEY, token);
- } catch (err) {
- console.warn('Não foi possível salvar o estado de pendência de KYC:', err);
- }
- }
- function loadPendingNumberToken() {
- if (!browser) return '';
- try {
- return localStorage.getItem(LOGIN_KYC_PENDING_KEY) ?? '';
- } catch (err) {
- console.warn('Não foi possível restaurar o estado de pendência de KYC:', err);
- return '';
- }
- }
- function clearPendingState() {
- if (!browser) return;
- try {
- localStorage.removeItem(LOGIN_KYC_PENDING_KEY);
- } catch (err) {
- console.warn('Não foi possível limpar o estado de pendência de KYC:', err);
- }
- }
- function extractKycLinkData(data = {}) {
- return {
- link: data?.link ?? data?.tshield?.link,
- numberToken: data?.numberToken ?? data?.tshield?.numberToken ?? data?.tshield?.number
- };
- }
- async function handleLoginErrorResponse(status, payload) {
- const code = payload?.code;
- const message = payload?.msg ?? payload?.message;
- if (status === 401 || code === 'E_VALIDATE') {
- error = message ?? 'Credenciais inválidas.';
- return;
- }
- if (status === 403 && code === 'E_KYC') {
- const reason = payload?.data?.reason;
- if (reason === 'KYC_PJ_PENDING') {
- enterPjPending(message ?? 'Necessário finalizar análise PJ ou contatar o suporte.');
- return;
- }
- const { link, numberToken } = extractKycLinkData(payload?.data ?? {});
- if (link && numberToken) {
- await enterPfPending(link, numberToken, message ?? 'KYC pendente. Conclua pelo link disponibilizado.');
- return;
- }
- if (reason === 'KYC_PF_MISSING_DOCUMENT') {
- error = message ?? 'CPF não cadastrado. Contate o suporte para concluir a verificação.';
- return;
- }
- error = message ?? 'KYC pendente. Entre em contato com o suporte.';
- return;
- }
- if (status === 502 || code === 'E_EXTERNAL') {
- error = message ?? 'Não foi possível gerar o link de verificação. Tente novamente.';
- return;
- }
- if (code === 'E_VALIDATE') {
- error = message ?? 'Credenciais inválidas.';
- return;
- }
- error = message ?? 'Falha ao autenticar. Tente novamente.';
- }
- function stopKycPolling() {
- if (kycPollingId) {
- clearInterval(kycPollingId);
- kycPollingId = null;
- }
- isCheckingKyc = false;
- }
- function resetKycState() {
- stopKycPolling();
- kycMode = null;
- kycLink = '';
- kycNumberToken = '';
- kycStatusMessage = '';
- kycStatusCode = null;
- kycError = '';
- if (qrCodeContainer) {
- qrCodeContainer.innerHTML = '';
- }
- }
- async function renderQrCode(link) {
- if (!browser || !link) return;
- if (!qrCodeInstance) {
- const module = await import('qr-code-styling');
- const QRCodeStyles = module.default ?? module;
- qrCodeInstance = new QRCodeStyles({
- width: 240,
- height: 240,
- type: 'svg',
- data: link,
- dotsOptions: {
- color: '#0f172a',
- type: 'rounded'
- },
- backgroundOptions: {
- color: '#ffffff'
- }
- });
- } else {
- qrCodeInstance.update({ data: link });
- }
- await tick();
- if (qrCodeContainer) {
- qrCodeContainer.innerHTML = '';
- qrCodeInstance.append(qrCodeContainer);
- }
- }
- function startKycPolling() {
- stopKycPolling();
- checkKycStatus();
- kycPollingId = setInterval(checkKycStatus, KYC_POLL_INTERVAL_MS);
- }
- async function checkKycStatus() {
- if (!kycNumberToken || isCheckingKyc) return;
- isCheckingKyc = true;
- try {
- const res = await fetch(kycStatusEndpoint, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ numberToken: kycNumberToken })
- });
- const raw = await res.text();
- let body = null;
- if (raw) {
- try {
- body = JSON.parse(raw);
- } catch (err) {
- console.error('Resposta inválida do endpoint de status do KYC:', err);
- }
- }
- //console.log('[login] status KYC retornado pelo backend', body ?? raw ?? null);
- if (!res.ok) {
- const message = body?.message ?? body?.msg ?? 'Falha ao consultar status do KYC.';
- throw new Error(message);
- }
- const resolvedStatus = Number(body?.status);
- if (!Number.isNaN(resolvedStatus)) {
- kycStatusCode = resolvedStatus;
- if (resolvedStatus === 1) {
- kycStatusMessage = 'KYC validado com sucesso.';
- kycError = '';
- clearPendingState();
- stopKycPolling();
- handleBackToLogin('KYC validado com sucesso. Faça login novamente.');
- } else if (resolvedStatus === 2) {
- kycStatusMessage = 'Falha na validação do KYC. Entre em contato com o suporte.';
- clearPendingState();
- stopKycPolling();
- } else {
- kycStatusMessage = 'Aguardando validação do KYC...';
- }
- } else {
- kycStatusMessage = 'Aguardando validação do KYC...';
- }
- } catch (err) {
- console.error('Erro ao verificar status do KYC:', err);
- kycError = err?.message ?? 'Não foi possível verificar o status do KYC.';
- } finally {
- isCheckingKyc = false;
- }
- }
- async function enterPfPending(link, numberToken, message, options = {}) {
- const { persist = true } = options;
- resetKycState();
- kycMode = 'PF_PENDING';
- kycLink = link;
- kycNumberToken = numberToken;
- kycStatusMessage = message ?? 'KYC pendente. Conclua pelo QR Code.';
- kycStatusCode = null;
- kycError = '';
- error = '';
- success = '';
- if (persist) {
- savePendingNumberToken(numberToken);
- }
- await renderQrCode(link);
- startKycPolling();
- }
- function enterPjPending(message, options = {}) {
- const { persist = true } = options;
- resetKycState();
- kycMode = 'PJ_PENDING';
- kycStatusMessage = message ?? 'Necessário finalizar análise PJ ou contatar o suporte.';
- error = '';
- success = '';
- if (persist) {
- savePendingState({
- mode: 'PJ_PENDING',
- message: kycStatusMessage
- });
- }
- }
- function handleBackToLogin(message = '') {
- resetKycState();
- clearPendingState();
- if (message) {
- showTimedSuccess(message);
- }
- }
- function handleCancelKycFlow() {
- handleBackToLogin('');
- }
- function setDark(enabled) {
- if (enabled) document.documentElement.classList.add('dark');
- else document.documentElement.classList.remove('dark');
- }
- // Aplica tema conforme localStorage: 'darkmode' = 'true' | 'false'
- function applyDarkModeFromStorage() {
- if (!browser) return;
- try {
- const saved = localStorage.getItem('darkmode');
- if (saved === 'true') setDark(true);
- else if (saved === 'false') setDark(false);
- else setDark(window.matchMedia('(prefers-color-scheme: dark)').matches);
- } catch (e) {}
- }
- onMount(() => {
- applyDarkModeFromStorage();
- const onStorage = (e) => {
- if (e.key === 'darkmode') applyDarkModeFromStorage();
- };
- if (browser) window.addEventListener('storage', onStorage);
- try {
- const stored = localStorage.getItem('registrationSuccessMessage');
- if (stored) {
- localStorage.removeItem('registrationSuccessMessage');
- showTimedSuccess(stored);
- }
- } catch (err) {
- console.warn('Falha ao recuperar mensagem de sucesso:', err);
- }
- const pendingToken = loadPendingNumberToken();
- if (pendingToken) {
- kycNumberToken = pendingToken;
- kycStatusMessage = 'KYC pendente. Aguarde a validação ou refaça o login para gerar um novo link.';
- startKycPolling();
- }
- return () => {
- if (browser) window.removeEventListener('storage', onStorage);
- if (successTimeout) {
- clearTimeout(successTimeout);
- }
- };
- });
- async function onSubmit(e) {
- e.preventDefault();
- error = '';
- success = '';
- loading = true;
- resetKycState();
- try {
- if (!email || !password) {
- throw new Error('Preencha e-mail e senha.');
- }
- if (!browser) {
- throw new Error('Ação disponível apenas no navegador.');
- }
- const res = await fetch(`${apiUrl}/login`, {
- method: 'POST',
- headers: { 'content-type': 'application/json' },
- body: JSON.stringify({ email, password })
- });
- const raw = await res.text();
- let payload = null;
- if (raw) {
- try {
- payload = JSON.parse(raw);
- } catch (err) {
- console.error('Resposta inválida do login:', err);
- }
- }
- //console.log('[login] resposta do backend', {
- status: res.status,
- payload,
- raw
- });
- if (!res.ok) {
- await handleLoginErrorResponse(res.status, payload);
- return;
- }
- const statusOk = payload?.status === 'ok' && payload?.code === 'S_OK';
- if (!statusOk) {
- await handleLoginErrorResponse(res.status, payload);
- return;
- }
- const token = payload?.data?.token;
- const companyIdFromApi = payload?.data?.company_id ?? payload?.data?.companyId;
- if (!token) {
- throw new Error('Resposta inválida do servidor.');
- }
- const attrs = [
- 'Path=/',
- 'SameSite=Lax',
- (typeof location !== 'undefined' && location.protocol === 'https:') ? 'Secure' : null,
- remember ? `Max-Age=${60 * 60 * 24 * 7}` : null
- ].filter(Boolean).join('; ');
- document.cookie = `auth_token=${encodeURIComponent(token)}; ${attrs}`;
- if (companyIdFromApi != null) {
- document.cookie = `company_id=${encodeURIComponent(companyIdFromApi)}; ${attrs}`;
- companyIdStore.set(companyIdFromApi);
- }
- authToken.set(token);
- clearPendingState();
- if (browser) {
- try {
- localStorage.removeItem('registrationSuccessMessage');
- } catch (err) {
- console.warn('Falha ao apagar mensagem de sucesso:', err);
- }
- }
- await goto('/dashboard');
- } catch (err) {
- error = err?.message ?? 'Falha ao autenticar, tente novamente.';
- } finally {
- loading = false;
- }
- }
- </script>
- <div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 px-4">
- <div class="w-full max-w-md">
- {#if kycMode === 'PF_PENDING'}
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-8 space-y-6">
- <div class="text-center">
- <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Validação necessária</h1>
- <p class="text-sm text-gray-600 dark:text-gray-400 mt-2">
- Escaneie o QR Code com o celular e conclua a validação facial no link da TShield.
- </p>
- </div>
- <div class="space-y-3 text-sm text-gray-600 dark:text-gray-300">
- <p>1. Abra a câmera do celular e aponte para o QR Code.</p>
- <p>2. Siga as instruções para a verificação facial.</p>
- <p>3. Aguarde alguns minutos — verificamos o status automaticamente.</p>
- </div>
- <div class="flex justify-center">
- <div class="p-4 border border-dashed border-gray-300 dark:border-gray-600 rounded-xl bg-gray-50 dark:bg-gray-800/50">
- <div
- class="w-56 h-56 flex items-center justify-center text-gray-500 dark:text-gray-400"
- bind:this={qrCodeContainer}
- >
- {#if !kycLink}
- Gerando QR Code...
- {/if}
- </div>
- </div>
- </div>
- <div class="rounded border border-blue-100 dark:border-blue-900/40 bg-blue-50 dark:bg-blue-900/20 px-3 py-2 text-sm text-blue-800 dark:text-blue-100">
- {kycStatusMessage || 'Aguardando validação do KYC...'}
- </div>
- {#if kycError}
- <div class="rounded border border-red-200 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-2 text-sm">{kycError}</div>
- {/if}
- <div class="space-y-2 text-sm text-gray-600 dark:text-gray-300">
- <p>Se preferir, abra o link diretamente:</p>
- <a
- class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-500 text-sm"
- href={kycLink}
- target="_blank"
- rel="noreferrer"
- >
- <span>📱 Abrir link de verificação</span>
- </a>
- </div>
- <div class="flex flex-col sm:flex-row gap-3">
- <button
- type="button"
- class="w-full inline-flex items-center justify-center rounded border border-gray-300 dark:border-gray-600 text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800 font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
- on:click={handleCancelKycFlow}
- >
- Voltar ao login
- </button>
- {#if kycStatusCode === 1}
- <button
- type="button"
- class="w-full inline-flex items-center justify-center rounded bg-green-600 hover:bg-green-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-green-500"
- on:click={() => handleBackToLogin('KYC concluído com sucesso. Faça login novamente.')}
- >
- Concluir
- </button>
- {/if}
- </div>
- </div>
- {:else if kycMode === 'PJ_PENDING'}
- <div class="bg-white dark:bg-gray-800 border border-amber-300 dark:border-amber-500/50 rounded-lg shadow-sm p-8 space-y-6">
- <div class="text-center space-y-2">
- <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Análise PJ em andamento</h1>
- <p class="text-sm text-gray-700 dark:text-gray-300">
- {kycStatusMessage || 'Necessário finalizar análise PJ ou contatar o suporte.'}
- </p>
- </div>
- <div class="rounded border border-amber-200 bg-amber-50 dark:border-amber-500/40 dark:bg-amber-500/10 px-3 py-2 text-sm text-amber-900 dark:text-amber-200">
- Nossa equipe está validando os documentos. Assim que concluir, tente acessar novamente ou fale com o suporte.
- </div>
- <div class="space-y-3">
- <a href="mailto:suporte@toeasy.com" class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-500 text-sm">
- <span>💬 Contactar suporte</span>
- </a>
- <button
- type="button"
- class="w-full inline-flex items-center justify-center rounded bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
- on:click={handleCancelKycFlow}
- >
- Voltar ao login
- </button>
- </div>
- </div>
- {:else}
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm p-8">
- <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">Entrar</h1>
- <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Acesse sua conta para continuar.</p>
- {#if error}
- <div class="mb-4 rounded border border-red-300 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 px-3 py-2 text-sm">{error}</div>
- {/if}
- {#if success}
- <div class="mb-4 rounded border border-yellow-300 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 px-3 py-2 text-sm">
- {success}
- </div>
- {/if}
- <form on:submit|preventDefault={onSubmit} class="space-y-4">
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="email">E-mail</label>
- <input
- id="email"
- name="email"
- type="email"
- bind:value={email}
- class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
- placeholder="seu@email.com"
- autocomplete="email"
- required
- />
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1" for="password">Senha</label>
- <input
- id="password"
- name="password"
- type="password"
- bind:value={password}
- class="w-full rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
- placeholder="••••••••"
- autocomplete="current-password"
- required
- />
- </div>
- <button
- class="w-full inline-flex items-center justify-center rounded bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-60"
- disabled={loading}
- type="submit"
- >
- {#if loading}
- Entrando...
- {:else}
- Entrar
- {/if}
- </button>
- </form>
- </div>
- <p class="mt-6 text-center text-sm text-gray-600 dark:text-gray-400">
- Esqueceu a senha? <a href="https://wa.me/+19997048082" class="text-blue-600 hover:text-blue-500">Fale com o suporte</a>
- </p>
- <div class="mt-3">
- <a href="/register" class="block w-full text-center rounded border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-300 hover:bg-blue-50 dark:hover:bg-blue-900/20 font-medium px-4 py-2">
- Ainda não tem conta? Crie agora
- </a>
- </div>
- {/if}
- </div>
-
- </div>
|