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>
65 lines
2.4 KiB
JavaScript
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);
|
|
}
|