Skip to content

Commit 0dc9df8

Browse files
Thales-Chagasclaude
andcommitted
Login: trava local (PIN/foto/nome) passa a ser por conta da nuvem
Antes a trava ficava numa caixa unica do navegador (financas_app_login), sem dono: uma conta herdava PIN/foto/nome de outra que ja tinha usado o aparelho (entrar com thalesregys mostrava a identidade e pedia o PIN da thalesregyss). Agora cada usuario da nuvem tem a propria trava (financas_app_login:<userId> via loginKey). resolverLogin(sess) resolve a trava da conta logada tanto na carga inicial quanto no observador aoMudarAuth (que antes usava o login velho). Nome exibido segue a nuvem; foto/PIN vem da trava da conta. A trava antiga sem dono so e adotada se o nome bater com o da conta (preserva o dono real, da caixa limpa p/ conta de nome diferente). Trocar de conta zera a janela de 1h. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2f9cbb8 commit 0dc9df8

1 file changed

Lines changed: 72 additions & 17 deletions

File tree

src/App.jsx

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ const SEED = seedFiles["./dadosPlanilha.json"]?.default ?? { months: {} };
9898

9999
const STORAGE_KEY = "financas_app_data";
100100
const LOGIN_KEY = "financas_app_login";
101+
// A trava local (PIN/foto/nome) é POR CONTA: cada usuário da nuvem tem a sua,
102+
// senão uma conta herda o PIN/foto de outra que já usou este aparelho.
103+
// Sem sessão (offline), cai na chave antiga (compatibilidade).
104+
const loginKey = (uid) => (uid ? `${LOGIN_KEY}:${uid}` : LOGIN_KEY);
101105
const TEMA_KEY = "financas_app_tema";
102106
const MIGR_KEY = "financas_app_migrado_"; // + userId: marca que já perguntamos da migração
103107

@@ -3043,6 +3047,7 @@ export default function App() {
30433047
const [nuvemPronta, setNuvemPronta] = useState(false);
30443048
const ultimoSyncRef = useRef(null); // último estado que já está na nuvem (base do diff)
30453049
const modoNuvemRef = useRef(false); // true quando os dados vêm/vão pra nuvem
3050+
const ultimoUidRef = useRef(null); // conta cuja trava local já foi resolvida (p/ detectar troca)
30463051
const [escuro, setEscuro] = useState(() => {
30473052
try {
30483053
return localStorage.getItem(TEMA_KEY) === "escuro";
@@ -3079,17 +3084,52 @@ export default function App() {
30793084
return "open";
30803085
}
30813086

3087+
// Resolve a trava local (PIN/foto/nome) DA CONTA logada na nuvem e atualiza o
3088+
// estado `login`. Cada conta tem sua trava (chave por uid). Se a única trava
3089+
// for a antiga (device-global, sem dono) e o nome bater com o da conta, ela é
3090+
// adotada por essa conta — preserva o PIN/foto de quem já usava o aparelho,
3091+
// sem entregá-los a uma conta diferente. O nome exibido segue a nuvem (fonte
3092+
// da verdade). Troca de conta zera a janela de 1h (re-pede a tranca da nova).
3093+
async function resolverLogin(sess) {
3094+
const uid = sess?.user?.id ?? null;
3095+
const nomeNuvem = (sess?.user?.user_metadata?.nome || "").trim();
3096+
3097+
let raw = await storageGet(loginKey(uid));
3098+
let conf = raw ? JSON.parse(raw) : null;
3099+
3100+
if (!conf && uid) {
3101+
const rawLegado = await storageGet(LOGIN_KEY);
3102+
const legado = rawLegado ? JSON.parse(rawLegado) : null;
3103+
const mesmaPessoa =
3104+
legado?.nome && nomeNuvem &&
3105+
legado.nome.trim().toLowerCase() === nomeNuvem.toLowerCase();
3106+
if (legado && mesmaPessoa) {
3107+
conf = { ...legado, uid };
3108+
await storageSet(loginKey(uid), JSON.stringify(conf)); // migra p/ a conta
3109+
await storageSet(LOGIN_KEY, ""); // consome a antiga (não vaza p/ outra conta)
3110+
}
3111+
}
3112+
3113+
// Trocou de conta neste aparelho → não reaproveita a janela de 1h.
3114+
if (uid && ultimoUidRef.current && uid !== ultimoUidRef.current) limparDesbloqueio();
3115+
ultimoUidRef.current = uid;
3116+
3117+
// Identidade exibida: PIN/foto vêm da trava desta conta; nome prioriza a nuvem.
3118+
if (conf?.pinHash) setLogin(nomeNuvem ? { ...conf, nome: nomeNuvem } : conf);
3119+
else if (nomeNuvem) setLogin({ nome: nomeNuvem, foto: conf?.foto ?? null });
3120+
else setLogin(conf);
3121+
3122+
return conf;
3123+
}
3124+
30823125
// Carrega dados salvos ao abrir
30833126
useEffect(() => {
30843127
(async () => {
30853128
try {
30863129
const sess = await sessaoAtual();
30873130
setSessao(sess);
30883131
setUserId(sess?.user?.id ?? null);
3089-
const rawLogin = await storageGet(LOGIN_KEY);
3090-
const conf = rawLogin ? JSON.parse(rawLogin) : null;
3091-
if (conf && conf.pinHash) setLogin(conf);
3092-
else if (sess?.user?.user_metadata?.nome) setLogin({ nome: sess.user.user_metadata.nome });
3132+
const conf = await resolverLogin(sess); // trava local DA conta logada
30933133
// Aberto por link de "Esqueci a senha" → tela de senha nova (mesmo
30943134
// que já exista sessão de recuperação, NÃO entrar direto).
30953135
// Sem sessão na nuvem → portão externo (entrar/criar conta).
@@ -3228,8 +3268,9 @@ export default function App() {
32283268
/* ---- login / foto / sair ---- */
32293269
async function criarLogin(nome, pin, pedirSempre, foto) {
32303270
const salt = Math.random().toString(36).slice(2, 12);
3231-
const conf = { nome, salt, pinHash: await hashPin(pin, salt), pedirSempre, foto: foto || null };
3232-
await storageSet(LOGIN_KEY, JSON.stringify(conf));
3271+
const conf = { nome, salt, pinHash: await hashPin(pin, salt), pedirSempre, foto: foto || null, uid: userId };
3272+
await storageSet(loginKey(userId), JSON.stringify(conf));
3273+
ultimoUidRef.current = userId;
32333274
setLogin(conf);
32343275
setAuth("open");
32353276
showToast(`Tudo pronto, ${nome}! ✓`);
@@ -3244,7 +3285,7 @@ export default function App() {
32443285

32453286
async function esqueciPin() {
32463287
if (!window.confirm("Redefinir o PIN? Seus dados financeiros NÃO serão apagados — você só vai criar um novo PIN.")) return;
3247-
await storageSet(LOGIN_KEY, "");
3288+
await storageSet(loginKey(userId), "");
32483289
setLogin(null);
32493290
setAuth("setup");
32503291
}
@@ -3276,6 +3317,13 @@ export default function App() {
32763317
if (auth === "open") marcarDesbloqueio();
32773318
}, [auth]);
32783319

3320+
// Espelha `auth` num ref, pra o observador da nuvem (deps []) saber se
3321+
// estamos no portão de login sem virar dependência do efeito.
3322+
const authRef = useRef(auth);
3323+
useEffect(() => {
3324+
authRef.current = auth;
3325+
}, [auth]);
3326+
32793327
// Observa login/logout na nuvem (ex.: logo após entrar pela TelaAuth,
32803328
// ou quando a sessão expira/é encerrada em outra aba).
32813329
useEffect(() => {
@@ -3287,14 +3335,21 @@ export default function App() {
32873335
// A restauração da sessão ao abrir (INITIAL_SESSION) e o refresh de token
32883336
// NÃO decidem a tela: quem decide o estado inicial é a carga (com a regra
32893337
// das 24h). Aqui só reagimos a um login FEITO na tela de entrada
3290-
// (prev === "auth") ou a um logout — assim nada fura a checagem de 24h.
3338+
// (auth === "auth") ou a um logout — assim nada fura a checagem de 24h.
32913339
if (evento === "INITIAL_SESSION") return;
3292-
if (!sess) return setAuth("auth");
3293-
setAuth((prev) => (prev === "auth" ? estadoComSessao(login) : prev));
3340+
if (!sess) {
3341+
setLogin(null); // logout → limpa nome/foto na saudação
3342+
return setAuth("auth");
3343+
}
3344+
// Login feito no portão: resolve a trava DESTA conta (nome/foto/PIN certos)
3345+
// e só então decide entre pedir o PIN (lock) ou abrir.
3346+
if (authRef.current === "auth") {
3347+
resolverLogin(sess).then((conf) => setAuth(estadoComSessao(conf)));
3348+
}
32943349
});
32953350
return parar;
32963351
// eslint-disable-next-line react-hooks/exhaustive-deps
3297-
}, [login]);
3352+
}, []);
32983353

32993354
// Ao abrir logado, carrega os dados da NUVEM. No 1º acesso deste aparelho,
33003355
// se houver dados locais ainda não enviados, pergunta se quer subir (migração).
@@ -3356,9 +3411,9 @@ export default function App() {
33563411
if (!login) return;
33573412
try {
33583413
const credId = await registrarBiometria(login.nome);
3359-
const conf = { ...login, bioCredId: credId };
3414+
const conf = { ...login, bioCredId: credId, uid: userId };
33603415
setLogin(conf);
3361-
await storageSet(LOGIN_KEY, JSON.stringify(conf));
3416+
await storageSet(loginKey(userId), JSON.stringify(conf));
33623417
showToast("Face ID / digital ativado ✓");
33633418
} catch {
33643419
showToast("Não consegui ativar a biometria neste aparelho.", true);
@@ -3367,10 +3422,10 @@ export default function App() {
33673422

33683423
async function desativarBiometria() {
33693424
if (!login) return;
3370-
const conf = { ...login };
3425+
const conf = { ...login, uid: userId };
33713426
delete conf.bioCredId;
33723427
setLogin(conf);
3373-
await storageSet(LOGIN_KEY, JSON.stringify(conf));
3428+
await storageSet(loginKey(userId), JSON.stringify(conf));
33743429
showToast("Biometria desativada.");
33753430
}
33763431

@@ -3387,10 +3442,10 @@ export default function App() {
33873442

33883443
async function salvarFoto(dataUrl) {
33893444
if (!login) return;
3390-
const conf = { ...login, foto: dataUrl };
3445+
const conf = { ...login, foto: dataUrl, uid: userId };
33913446
setLogin(conf);
33923447
setArquivoFoto(null);
3393-
await storageSet(LOGIN_KEY, JSON.stringify(conf));
3448+
await storageSet(loginKey(userId), JSON.stringify(conf));
33943449
showToast("Foto atualizada ✓");
33953450
}
33963451

0 commit comments

Comments
 (0)