shivao-projeto/server/src/auth.js
PontualTech / Karlão 85b60a800c feat(saas): multi-tenant com login/cadastro + JWT + planos free/pro/captain
BACKEND
- bcryptjs + jsonwebtoken adicionados (JS puro, sem build nativo)
- Schema users + licenses, migration adiciona user_id em todas tabelas (state, media, anchor_session, alarm_log, shares, audit_log)
- User default id=1 (karlao@outlook.com) com plano captain — preserva uso pessoal pré-multi-tenant
- Endpoints /api/auth/{signup,login,refresh,me} + /api/license
- Middleware requireAuth aceita JWT OU BOAT_TOKEN (fallback legado mapeia ao user 1)
- TODAS rotas autenticadas atualizadas pra usar req.user.id (state, media, anchor, share, alarm, audit)
- Dead-man switch agora itera todos anchor_sessions ativos (multi-user)
- 3 planos definidos em auth.js: free (Âncora), pro (R$19/mês), captain (R$39/mês)

FRONTEND
- state.auth + state.license persistidos em localStorage
- cloudFetch usa JWT preferencialmente, fallback BOAT_TOKEN; auto-refresh em 401
- Nova seção 'Conta' no painel Arquivo: tabs Entrar/Cadastrar + status de plano + Logout + botão upgrade
- Sincronizado em app/ e server/public/

Backward-compat 100% preservada: app legado com BOAT_TOKEN continua funcionando como user default.
Próximo: webhook Asaas pra ativar licenças após pagamento PIX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:37:15 -03:00

65 lines
2.4 KiB
JavaScript

// Auth — JWT + bcrypt pra Shivao SaaS multi-tenant.
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || (process.env.BOAT_TOKEN + '-jwt-derive-key');
const JWT_ACCESS_TTL = '7d'; // access token expira em 7 dias (menos fricção pra usuário móvel)
const JWT_REFRESH_TTL = '90d'; // refresh em 90 dias
if (!process.env.JWT_SECRET && !process.env.BOAT_TOKEN) {
console.warn('[auth] AVISO: JWT_SECRET não configurado. Use env JWT_SECRET em produção.');
}
export async function hashPassword(plain) {
if (!plain || plain.length < 8) throw new Error('Senha precisa ter no mínimo 8 caracteres');
return bcrypt.hash(plain, 10);
}
export async function verifyPassword(plain, hash) {
if (!plain || !hash) return false;
try { return await bcrypt.compare(plain, hash); } catch { return false; }
}
export function signAccessToken(user) {
return jwt.sign({ uid: user.id, email: user.email, type: 'access' }, JWT_SECRET, { expiresIn: JWT_ACCESS_TTL });
}
export function signRefreshToken(user) {
return jwt.sign({ uid: user.id, type: 'refresh' }, JWT_SECRET, { expiresIn: JWT_REFRESH_TTL });
}
export function verifyToken(token) {
try { return jwt.verify(token, JWT_SECRET); } catch { return null; }
}
// Plans → features matrix
// free: âncora local + diário básico (até 10 viagens)
// pro: tudo do free + sync nuvem ilimitada + GPS tracking + mídia + geofencing
// captain: tudo do pro + Windy premium + multi-barco + relatórios PDF + audit log
export const PLANS = {
free: {
name: 'Free (Âncora)',
price_brl: 0,
features: ['anchor_local', 'diary_limited_10', 'export_gpx_basic']
},
pro: {
name: 'Pro',
price_brl_monthly: 19,
price_brl_yearly: 149,
features: ['anchor_local', 'anchor_remote', 'diary_unlimited', 'cloud_sync', 'gps_tracking', 'media_cloud', 'geofencing', 'webhooks', 'export_all']
},
captain: {
name: 'Captain',
price_brl_monthly: 39,
price_brl_yearly: 299,
features: ['anchor_local', 'anchor_remote', 'diary_unlimited', 'cloud_sync', 'gps_tracking', 'media_cloud', 'geofencing', 'webhooks', 'export_all', 'windy_premium', 'multi_boat', 'pdf_reports', 'audit_log']
}
};
export function planFeatures(plan) {
return (PLANS[plan] || PLANS.free).features;
}
export function planHasFeature(plan, feature) {
return planFeatures(plan).includes(feature);
}