From b48afaa84f17c1803970c85f0bf9a3720689554f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PontualTech=20/=20Karl=C3=A3o?= Date: Tue, 28 Apr 2026 08:00:54 -0300 Subject: [PATCH] feat(welcome): tela de boas-vindas com login Google/Email + URL hardcoded v1.6.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX simplificada drasticamente — usuário não precisa mais saber URL/token: - Tela de boas-vindas full-screen quando não logado - 3 botões grandes: Google, Email, Servidor próprio (avançado) - URL hardcoded https://shivao.pontualtech.work como padrão - Auto-conecta WebSocket + Google Calendar status após login - Pull inicial automático pra puxar dados existentes da conta Backend (server/src/index.js): - Endpoint POST /api/auth/google: recebe credential (Google ID token), valida via tokeninfo do Google, confere aud == GOOGLE_CLIENT_ID, cria user automático com email do Google se não existe, retorna JWT access+refresh tokens - Reusa GOOGLE_CLIENT_ID/SECRET já configurados no Coolify Frontend (app/diario-bordo.html): - Modal welcome com Google Sign-In via @google/gsi/client (script async) - Tabs Login/Signup pro fluxo email - Form avançado pra power users self-hosters - Skip pra modo offline - Once dismissed, fica oculto (localStorage flag) Service Worker bumped pra v1.6.0 (invalida cache antigo). Co-Authored-By: Claude Opus 4.7 (1M context) --- app/diario-bordo.html | 246 ++++++++++++++++++++++++++++++++ mobile/android/app/build.gradle | 4 +- mobile/package.json | 2 +- server/public/index.html | 246 ++++++++++++++++++++++++++++++++ server/public/sw.js | 2 +- server/src/index.js | 44 +++++- 6 files changed, 539 insertions(+), 5 deletions(-) diff --git a/app/diario-bordo.html b/app/diario-bordo.html index 923f690..c0f9a3d 100644 --- a/app/diario-bordo.html +++ b/app/diario-bordo.html @@ -11,6 +11,7 @@ + Diário de Bordo @@ -528,6 +529,65 @@ header{ .fleet-units-toggle button.active{background:var(--brass);color:var(--bg-paper)} .fleet-units-toggle button:hover:not(.active){background:var(--bg-aged)} +/* === Welcome Screen === */ +.welcome-screen{ + position:fixed;inset:0;z-index:9999; + background:linear-gradient(135deg,#0e2a3d 0%,#1a3d54 50%,#0a1f2d 100%); + display:flex;align-items:center;justify-content:center; + padding:20px;overflow-y:auto; +} +.welcome-card{ + background:var(--bg-paper); + border:1px solid rgba(212,160,74,.4); + padding:32px 28px;max-width:380px;width:100%; + box-shadow:0 30px 80px rgba(0,0,0,.5); +} +.welcome-logo{ + font-size:64px;text-align:center;margin-bottom:8px; + filter:drop-shadow(0 4px 8px rgba(212,160,74,.3)); +} +.welcome-title{ + font-family:var(--f-display);font-style:italic;font-weight:500; + font-size:28px;color:var(--ink-deep); + text-align:center;margin:0 0 8px;letter-spacing:-.01em; +} +.welcome-tagline{ + font-family:var(--f-display);font-style:italic; + color:var(--sepia);text-align:center; + font-size:14px;line-height:1.45; + margin:0 0 24px; +} +.welcome-buttons{display:flex;flex-direction:column;gap:8px} +.welcome-btn{ + display:flex;align-items:center;justify-content:center;gap:10px; + padding:13px 16px;border:1px solid var(--rule); + background:var(--bg-canvas);color:var(--ink-deep); + font-family:var(--f-body);font-size:14.5px;font-weight:500; + cursor:pointer;width:100%; + transition:all .15s;text-align:center; +} +.welcome-btn:hover{background:var(--bg-aged);border-color:var(--brass)} +.welcome-btn:active{transform:scale(.98)} +.welcome-btn-google{background:#fff;border-color:#dadce0} +.welcome-btn-google:hover{background:#f8f9fa;box-shadow:0 1px 3px rgba(0,0,0,.1)} +.welcome-btn-email{} +.welcome-btn-primary{background:var(--brass);color:var(--bg-paper);border-color:var(--brass-deep);font-weight:600} +.welcome-btn-primary:hover{background:var(--brass-deep)} +.welcome-btn-text{ + background:transparent;border:none;color:var(--sepia); + font-size:12.5px;text-decoration:underline; +} +.welcome-btn-text:hover{color:var(--ink-deep);background:transparent} +.welcome-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--rule)} +.welcome-tab{ + flex:1;background:transparent;border:none; + padding:10px;font-family:var(--f-mono);font-size:11px; + letter-spacing:.12em;text-transform:uppercase;color:var(--sepia); + cursor:pointer;border-bottom:2px solid transparent; + margin-bottom:-1px; +} +.welcome-tab.active{color:var(--brass);border-bottom-color:var(--brass)} + /* === Sync Indicator === */ .sync-indicator{ display:inline-block;font-size:10px; @@ -2007,6 +2067,55 @@ Hora: {HORA} + + +
@@ -2220,6 +2329,8 @@ function dbDelete(id){return new Promise((r,j)=>{const q=db.transaction('media', function dbAll(){return new Promise((r,j)=>{const q=db.transaction('media','readonly').objectStore('media').getAll();q.onsuccess=()=>r(q.result);q.onerror=()=>j(q.error)})} function loadState(){try{const raw=localStorage.getItem(STORAGE_KEY);if(raw){const d=JSON.parse(raw);Object.assign(state,d);state.pending=d.pending||[];state.passengers=d.passengers||[];state.contacts=d.contacts||[];if(d.alarmTemplate)state.alarmTemplate=d.alarmTemplate;state.cloud=d.cloud||{url:'',token:'',lastSync:0};state.webhooks=d.webhooks||{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}};state.anchorHistory=d.anchorHistory||[];state.zones=d.zones||[];state.checklists=d.checklists||[];state.weatherCfg=d.weatherCfg||{windyKey:'',model:'gfs'};state.boats=d.boats||[];state.activeBoatId=d.activeBoatId||null;state.units=d.units||'metric'}}catch(e){console.warn(e)} + // Default cloud URL pra usuários social/email (não precisam digitar nada) + if(!state.cloud.url&&typeof DEFAULT_CLOUD_URL!=='undefined')state.cloud.url=DEFAULT_CLOUD_URL; migrateBoatsSchema(); // popular checklists padrão se vazio if(!state.checklists.length){ @@ -3024,7 +3135,11 @@ async function updateStorageInfo(){ if(cloudConfigured()){rtConnect();refreshGoogleStatus()} // tenta auto-fetch do tempo após pequeno delay setTimeout(maybeAutoFetchWeather,3000); + // Welcome screen — só pra usuários sem login + setTimeout(maybeShowWelcome,300); })(); +// Re-tenta init Google Sign-In quando o script async carrega +window.addEventListener('load',()=>setTimeout(()=>{if(document.getElementById('welcome-screen').style.display==='flex')initGoogleSignIn()},500)); document.addEventListener('visibilitychange',async()=>{ if(document.visibilityState==='visible'){ if(tracking.active&&!tracking.wakeLock)await requestWakeLock(); @@ -3679,6 +3794,137 @@ async function cloudFetch(path,opts={}){ return r; } +// ============ WELCOME / LOGIN SCREEN ============ +// Default cloud URL — usuários não-avançados não precisam configurar nada +const DEFAULT_CLOUD_URL='https://shivao.pontualtech.work'; +const GOOGLE_CLIENT_ID_FRONTEND='989184529532-uceun7l7a12e63fdrkilnh8vml0v0lv4.apps.googleusercontent.com'; + +function maybeShowWelcome(){ + const ws=document.getElementById('welcome-screen'); + if(!ws)return; + // Mostra se: nuvem não configurada E user não está logado E nunca dispensou + const dismissed=localStorage.getItem('shivao_welcome_dismissed')==='1'; + const needsSetup=(!cloudConfigured()||!state.auth)&&!dismissed; + ws.style.display=needsSetup?'flex':'none'; + // Se mostrar, prepara o Google Sign-In quando carregar + if(needsSetup&&window.google?.accounts?.id){ + initGoogleSignIn(); + } +} + +function welcomeShowEmail(){ + document.getElementById('welcome-email-form').style.display='block'; + document.querySelector('.welcome-buttons').style.display='none'; + document.getElementById('welcome-advanced-form').style.display='none'; +} +function welcomeShowAdvanced(){ + document.getElementById('welcome-advanced-form').style.display='block'; + document.querySelector('.welcome-buttons').style.display='none'; + document.getElementById('welcome-email-form').style.display='none'; +} +function welcomeBack(){ + document.querySelector('.welcome-buttons').style.display='flex'; + document.getElementById('welcome-email-form').style.display='none'; + document.getElementById('welcome-advanced-form').style.display='none'; +} +function welcomeSwitchTab(t){ + document.getElementById('we-tab-login').classList.toggle('active',t==='login'); + document.getElementById('we-tab-signup').classList.toggle('active',t==='signup'); + document.getElementById('we-name-field').style.display=t==='signup'?'block':'none'; + document.getElementById('we-submit').textContent=t==='login'?'Entrar':'Criar conta'; +} +function welcomeSkip(){ + if(!confirm('Continuar sem sincronização? Seus dados ficam só neste dispositivo.'))return; + localStorage.setItem('shivao_welcome_dismissed','1'); + document.getElementById('welcome-screen').style.display='none'; +} +async function welcomeEmailSubmit(){ + const email=document.getElementById('we-email').value.trim(); + const pwd=document.getElementById('we-pwd').value; + const name=document.getElementById('we-name').value.trim(); + const isSignup=document.getElementById('we-tab-signup').classList.contains('active'); + const msg=document.getElementById('we-msg'); + msg.style.color='var(--storm)';msg.textContent=''; + if(!email||!pwd){msg.textContent='Preencha email e senha';return} + if(isSignup&&pwd.length<8){msg.textContent='Senha mínima: 8 caracteres';return} + // Garante URL hardcoded + state.cloud.url=DEFAULT_CLOUD_URL; + saveState(); + try{ + if(isSignup)await authSignup(email,pwd,name); + else await authLogin(email,pwd); + msg.style.color='var(--algae)';msg.textContent='Logado! Sincronizando...'; + welcomeFinish(); + }catch(e){msg.textContent=e.message} +} +async function welcomeAdvancedSubmit(){ + const url=document.getElementById('we-srv-url').value.trim(); + const tok=document.getElementById('we-srv-token').value.trim(); + const msg=document.getElementById('we-srv-msg'); + if(!url||!tok){msg.textContent='Preencha URL e token';return} + state.cloud={url:url.replace(/\/$/,''),token:tok,lastSync:0}; + saveState(); + msg.style.color='var(--algae)';msg.textContent='Conectando...'; + welcomeFinish(); +} +function welcomeFinish(){ + document.getElementById('welcome-screen').style.display='none'; + // Conecta sync automaticamente + rtConnect(); + refreshGoogleStatus(); + // Pull inicial pra puxar dados existentes da conta + setTimeout(()=>autoPullNow().catch(()=>{}),500); +} + +function initGoogleSignIn(){ + if(!window.google?.accounts?.id||window._gsiInited)return; + try{ + window.google.accounts.id.initialize({ + client_id:GOOGLE_CLIENT_ID_FRONTEND, + callback:onGoogleCredential, + auto_select:false, + ux_mode:'popup', + }); + window._gsiInited=true; + }catch(e){console.warn('[gsi] init failed',e)} +} +function welcomeGoogleClick(){ + if(!window.google?.accounts?.id){ + toast('Google Sign-In ainda carregando, aguarde 2s'); + return; + } + initGoogleSignIn(); + try{ + window.google.accounts.id.prompt(()=>{}); + // Fallback: render botão visível ao lado caso prompt seja bloqueado + }catch(e){ + console.warn('[gsi] prompt failed',e); + toast('Erro: '+e.message); + } +} +async function onGoogleCredential(resp){ + if(!resp?.credential){toast('Sem credential do Google');return} + state.cloud.url=DEFAULT_CLOUD_URL; + saveState(); + try{ + const r=await fetch(cloudUrl('/api/auth/google'),{ + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({credential:resp.credential}), + }); + const j=await r.json(); + if(!r.ok)throw new Error(j.error||'login failed'); + state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user}; + saveState(); + toast('Bem-vindo, '+(j.user.name||j.user.email)); + welcomeFinish(); + if(typeof renderAuthBox==='function')renderAuthBox(); + }catch(e){ + console.warn('[google login]',e); + toast('Falha login Google: '+e.message); + } +} + // ============ REALTIME SYNC (WebSocket + auto push/pull) ============ // Conecta ao /ws do servidor, escuta notificações de mudança de outros devices // e dispara pull/push automáticos. Reconnect com backoff exponencial. diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 48d02ea..3e93e45 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "br.com.pontualtech.shivao" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 6 - versionName "1.5.0" + versionCode 7 + versionName "1.6.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/mobile/package.json b/mobile/package.json index 5b29636..593e9ff 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -1,6 +1,6 @@ { "name": "shivao-mobile", - "version": "1.5.0", + "version": "1.6.0", "description": "Shivao app nativo (Capacitor wrapper Android/iOS)", "main": "index.js", "type": "module", diff --git a/server/public/index.html b/server/public/index.html index 923f690..c0f9a3d 100644 --- a/server/public/index.html +++ b/server/public/index.html @@ -11,6 +11,7 @@ + Diário de Bordo @@ -528,6 +529,65 @@ header{ .fleet-units-toggle button.active{background:var(--brass);color:var(--bg-paper)} .fleet-units-toggle button:hover:not(.active){background:var(--bg-aged)} +/* === Welcome Screen === */ +.welcome-screen{ + position:fixed;inset:0;z-index:9999; + background:linear-gradient(135deg,#0e2a3d 0%,#1a3d54 50%,#0a1f2d 100%); + display:flex;align-items:center;justify-content:center; + padding:20px;overflow-y:auto; +} +.welcome-card{ + background:var(--bg-paper); + border:1px solid rgba(212,160,74,.4); + padding:32px 28px;max-width:380px;width:100%; + box-shadow:0 30px 80px rgba(0,0,0,.5); +} +.welcome-logo{ + font-size:64px;text-align:center;margin-bottom:8px; + filter:drop-shadow(0 4px 8px rgba(212,160,74,.3)); +} +.welcome-title{ + font-family:var(--f-display);font-style:italic;font-weight:500; + font-size:28px;color:var(--ink-deep); + text-align:center;margin:0 0 8px;letter-spacing:-.01em; +} +.welcome-tagline{ + font-family:var(--f-display);font-style:italic; + color:var(--sepia);text-align:center; + font-size:14px;line-height:1.45; + margin:0 0 24px; +} +.welcome-buttons{display:flex;flex-direction:column;gap:8px} +.welcome-btn{ + display:flex;align-items:center;justify-content:center;gap:10px; + padding:13px 16px;border:1px solid var(--rule); + background:var(--bg-canvas);color:var(--ink-deep); + font-family:var(--f-body);font-size:14.5px;font-weight:500; + cursor:pointer;width:100%; + transition:all .15s;text-align:center; +} +.welcome-btn:hover{background:var(--bg-aged);border-color:var(--brass)} +.welcome-btn:active{transform:scale(.98)} +.welcome-btn-google{background:#fff;border-color:#dadce0} +.welcome-btn-google:hover{background:#f8f9fa;box-shadow:0 1px 3px rgba(0,0,0,.1)} +.welcome-btn-email{} +.welcome-btn-primary{background:var(--brass);color:var(--bg-paper);border-color:var(--brass-deep);font-weight:600} +.welcome-btn-primary:hover{background:var(--brass-deep)} +.welcome-btn-text{ + background:transparent;border:none;color:var(--sepia); + font-size:12.5px;text-decoration:underline; +} +.welcome-btn-text:hover{color:var(--ink-deep);background:transparent} +.welcome-tabs{display:flex;gap:0;margin-bottom:14px;border-bottom:1px solid var(--rule)} +.welcome-tab{ + flex:1;background:transparent;border:none; + padding:10px;font-family:var(--f-mono);font-size:11px; + letter-spacing:.12em;text-transform:uppercase;color:var(--sepia); + cursor:pointer;border-bottom:2px solid transparent; + margin-bottom:-1px; +} +.welcome-tab.active{color:var(--brass);border-bottom-color:var(--brass)} + /* === Sync Indicator === */ .sync-indicator{ display:inline-block;font-size:10px; @@ -2007,6 +2067,55 @@ Hora: {HORA} + + +
@@ -2220,6 +2329,8 @@ function dbDelete(id){return new Promise((r,j)=>{const q=db.transaction('media', function dbAll(){return new Promise((r,j)=>{const q=db.transaction('media','readonly').objectStore('media').getAll();q.onsuccess=()=>r(q.result);q.onerror=()=>j(q.error)})} function loadState(){try{const raw=localStorage.getItem(STORAGE_KEY);if(raw){const d=JSON.parse(raw);Object.assign(state,d);state.pending=d.pending||[];state.passengers=d.passengers||[];state.contacts=d.contacts||[];if(d.alarmTemplate)state.alarmTemplate=d.alarmTemplate;state.cloud=d.cloud||{url:'',token:'',lastSync:0};state.webhooks=d.webhooks||{telegram:{token:'',chatIds:''},discord:{url:''},generic:{url:''}};state.anchorHistory=d.anchorHistory||[];state.zones=d.zones||[];state.checklists=d.checklists||[];state.weatherCfg=d.weatherCfg||{windyKey:'',model:'gfs'};state.boats=d.boats||[];state.activeBoatId=d.activeBoatId||null;state.units=d.units||'metric'}}catch(e){console.warn(e)} + // Default cloud URL pra usuários social/email (não precisam digitar nada) + if(!state.cloud.url&&typeof DEFAULT_CLOUD_URL!=='undefined')state.cloud.url=DEFAULT_CLOUD_URL; migrateBoatsSchema(); // popular checklists padrão se vazio if(!state.checklists.length){ @@ -3024,7 +3135,11 @@ async function updateStorageInfo(){ if(cloudConfigured()){rtConnect();refreshGoogleStatus()} // tenta auto-fetch do tempo após pequeno delay setTimeout(maybeAutoFetchWeather,3000); + // Welcome screen — só pra usuários sem login + setTimeout(maybeShowWelcome,300); })(); +// Re-tenta init Google Sign-In quando o script async carrega +window.addEventListener('load',()=>setTimeout(()=>{if(document.getElementById('welcome-screen').style.display==='flex')initGoogleSignIn()},500)); document.addEventListener('visibilitychange',async()=>{ if(document.visibilityState==='visible'){ if(tracking.active&&!tracking.wakeLock)await requestWakeLock(); @@ -3679,6 +3794,137 @@ async function cloudFetch(path,opts={}){ return r; } +// ============ WELCOME / LOGIN SCREEN ============ +// Default cloud URL — usuários não-avançados não precisam configurar nada +const DEFAULT_CLOUD_URL='https://shivao.pontualtech.work'; +const GOOGLE_CLIENT_ID_FRONTEND='989184529532-uceun7l7a12e63fdrkilnh8vml0v0lv4.apps.googleusercontent.com'; + +function maybeShowWelcome(){ + const ws=document.getElementById('welcome-screen'); + if(!ws)return; + // Mostra se: nuvem não configurada E user não está logado E nunca dispensou + const dismissed=localStorage.getItem('shivao_welcome_dismissed')==='1'; + const needsSetup=(!cloudConfigured()||!state.auth)&&!dismissed; + ws.style.display=needsSetup?'flex':'none'; + // Se mostrar, prepara o Google Sign-In quando carregar + if(needsSetup&&window.google?.accounts?.id){ + initGoogleSignIn(); + } +} + +function welcomeShowEmail(){ + document.getElementById('welcome-email-form').style.display='block'; + document.querySelector('.welcome-buttons').style.display='none'; + document.getElementById('welcome-advanced-form').style.display='none'; +} +function welcomeShowAdvanced(){ + document.getElementById('welcome-advanced-form').style.display='block'; + document.querySelector('.welcome-buttons').style.display='none'; + document.getElementById('welcome-email-form').style.display='none'; +} +function welcomeBack(){ + document.querySelector('.welcome-buttons').style.display='flex'; + document.getElementById('welcome-email-form').style.display='none'; + document.getElementById('welcome-advanced-form').style.display='none'; +} +function welcomeSwitchTab(t){ + document.getElementById('we-tab-login').classList.toggle('active',t==='login'); + document.getElementById('we-tab-signup').classList.toggle('active',t==='signup'); + document.getElementById('we-name-field').style.display=t==='signup'?'block':'none'; + document.getElementById('we-submit').textContent=t==='login'?'Entrar':'Criar conta'; +} +function welcomeSkip(){ + if(!confirm('Continuar sem sincronização? Seus dados ficam só neste dispositivo.'))return; + localStorage.setItem('shivao_welcome_dismissed','1'); + document.getElementById('welcome-screen').style.display='none'; +} +async function welcomeEmailSubmit(){ + const email=document.getElementById('we-email').value.trim(); + const pwd=document.getElementById('we-pwd').value; + const name=document.getElementById('we-name').value.trim(); + const isSignup=document.getElementById('we-tab-signup').classList.contains('active'); + const msg=document.getElementById('we-msg'); + msg.style.color='var(--storm)';msg.textContent=''; + if(!email||!pwd){msg.textContent='Preencha email e senha';return} + if(isSignup&&pwd.length<8){msg.textContent='Senha mínima: 8 caracteres';return} + // Garante URL hardcoded + state.cloud.url=DEFAULT_CLOUD_URL; + saveState(); + try{ + if(isSignup)await authSignup(email,pwd,name); + else await authLogin(email,pwd); + msg.style.color='var(--algae)';msg.textContent='Logado! Sincronizando...'; + welcomeFinish(); + }catch(e){msg.textContent=e.message} +} +async function welcomeAdvancedSubmit(){ + const url=document.getElementById('we-srv-url').value.trim(); + const tok=document.getElementById('we-srv-token').value.trim(); + const msg=document.getElementById('we-srv-msg'); + if(!url||!tok){msg.textContent='Preencha URL e token';return} + state.cloud={url:url.replace(/\/$/,''),token:tok,lastSync:0}; + saveState(); + msg.style.color='var(--algae)';msg.textContent='Conectando...'; + welcomeFinish(); +} +function welcomeFinish(){ + document.getElementById('welcome-screen').style.display='none'; + // Conecta sync automaticamente + rtConnect(); + refreshGoogleStatus(); + // Pull inicial pra puxar dados existentes da conta + setTimeout(()=>autoPullNow().catch(()=>{}),500); +} + +function initGoogleSignIn(){ + if(!window.google?.accounts?.id||window._gsiInited)return; + try{ + window.google.accounts.id.initialize({ + client_id:GOOGLE_CLIENT_ID_FRONTEND, + callback:onGoogleCredential, + auto_select:false, + ux_mode:'popup', + }); + window._gsiInited=true; + }catch(e){console.warn('[gsi] init failed',e)} +} +function welcomeGoogleClick(){ + if(!window.google?.accounts?.id){ + toast('Google Sign-In ainda carregando, aguarde 2s'); + return; + } + initGoogleSignIn(); + try{ + window.google.accounts.id.prompt(()=>{}); + // Fallback: render botão visível ao lado caso prompt seja bloqueado + }catch(e){ + console.warn('[gsi] prompt failed',e); + toast('Erro: '+e.message); + } +} +async function onGoogleCredential(resp){ + if(!resp?.credential){toast('Sem credential do Google');return} + state.cloud.url=DEFAULT_CLOUD_URL; + saveState(); + try{ + const r=await fetch(cloudUrl('/api/auth/google'),{ + method:'POST', + headers:{'Content-Type':'application/json'}, + body:JSON.stringify({credential:resp.credential}), + }); + const j=await r.json(); + if(!r.ok)throw new Error(j.error||'login failed'); + state.auth={accessToken:j.accessToken,refreshToken:j.refreshToken,user:j.user}; + saveState(); + toast('Bem-vindo, '+(j.user.name||j.user.email)); + welcomeFinish(); + if(typeof renderAuthBox==='function')renderAuthBox(); + }catch(e){ + console.warn('[google login]',e); + toast('Falha login Google: '+e.message); + } +} + // ============ REALTIME SYNC (WebSocket + auto push/pull) ============ // Conecta ao /ws do servidor, escuta notificações de mudança de outros devices // e dispara pull/push automáticos. Reconnect com backoff exponencial. diff --git a/server/public/sw.js b/server/public/sw.js index 5f22d64..33a5734 100644 --- a/server/public/sw.js +++ b/server/public/sw.js @@ -1,7 +1,7 @@ // Shivao Service Worker — offline real // Estratégia: shell precachado, tiles cache-first, windy network-first, /api passa direto. // Versão usada nos cache names — bumpa essa string pra invalidar caches antigos em deploys. -const VERSION = 'shivao-v1.5.0'; +const VERSION = 'shivao-v1.6.0'; const SHELL_CACHE = `shivao-shell-${VERSION}`; const TILES_CACHE = 'shivao-tiles-v1'; // separado pra não invalidar tiles em update do shell const WINDY_CACHE = `shivao-windy-${VERSION}`; diff --git a/server/src/index.js b/server/src/index.js index 10f7beb..1d9ca69 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -126,6 +126,48 @@ app.get('/api/auth/me', requireAuth, (req, res) => { res.json(req.user); }); +// Login com Google (Sign-In) — recebe ID token, valida no Google, cria/loga user +app.post('/api/auth/google', async (req, res) => { + const { credential } = req.body || {}; + if (!credential) return res.status(400).json({ error: 'credential (Google ID token) required' }); + try { + // Valida o ID token via tokeninfo endpoint do Google (sem dependência adicional) + const r = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(credential)}`); + if (!r.ok) return res.status(401).json({ error: 'invalid_google_token' }); + const info = await r.json(); + // Confere que aud = nosso CLIENT_ID + const expectedAud = process.env.GOOGLE_CLIENT_ID || ''; + if (expectedAud && info.aud !== expectedAud) { + return res.status(401).json({ error: 'token_audience_mismatch' }); + } + if (!info.email_verified || info.email_verified === 'false') { + return res.status(403).json({ error: 'email_not_verified' }); + } + const email = info.email; + const name = info.name || email.split('@')[0]; + let user = db.findUserByEmail(email); + if (!user) { + // Auto-cria com senha aleatória inutilizável (login só via Google daqui pra frente) + const randomPwd = 'google-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); + const hash = await hashPassword(randomPwd); + const id = db.createUser(email, hash, name); + db.audit(id, 'user_signup_google', 'user', String(id), { email }, req.ip); + user = db.findUserById(id); + } + db.updateLastLogin(user.id); + db.audit(user.id, 'user_login_google', 'user', String(user.id), {}, req.ip); + const safe = db.findUserById(user.id); + res.json({ + user: safe, + accessToken: signAccessToken(safe), + refreshToken: signRefreshToken(safe), + }); + } catch (e) { + console.warn('[auth/google]', e.message); + res.status(500).json({ error: e.message }); + } +}); + // Plans + license info app.get('/api/license', requireAuth, (req, res) => { const lic = db.getActiveLicense(req.user.id) || { plan: 'free', status: 'active', expires_at: null }; @@ -267,7 +309,7 @@ app.get('/.well-known/assetlinks.json', (req, res) => { }); // Atalho: /apk redireciona pra última APK release no Forgejo -const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.5.0/Shivao-v1.5.0.apk'; +const LATEST_APK_URL = 'https://git.pontualtech.work/karlao/shivao-projeto/releases/download/v1.6.0/Shivao-v1.6.0.apk'; app.get('/apk', (req, res) => res.redirect(302, LATEST_APK_URL)); // Página A4 imprimível com QR Code + instruções (cola no barco/marina)