Appearance
Authentication - Autenticação OAuth
Fluxo de Login Social
O sistema utiliza autenticação OAuth 2.0 com popup para os provedores: Google, LinkedIn, Microsoft e GitHub.
IMPORTANTE: A URL OAuth é construída diretamente no frontend usando as credenciais de cada provedor. O backend NÃO fornece URLs de OAuth.
LoginPage
↓
Clica no botão social (ex: Google)
↓
useOAuthPopup.openPopup('google')
↓
Frontend constrói URL OAuth com buildOAuthUrl('google')
↓
Popup abre com URL OAuth do provedor
↓
Usuário autoriza no provedor
↓
Provedor redireciona para /auth/callback/:provider?code=...
↓
OAuthCallback.vue (no popup):
- Extrai o código de autorização da URL
- Envia postMessage({ type: 'oauth:success', provider, code })
- Fecha popup
↓
LoginPage recebe mensagem:
- Chama socialAuthService({ provider, code })
↓
Backend troca code por token com provedor e retorna:
{
user, token, companies,
action: 'login' | 'register',
needsProfileCompletion: boolean
}
↓
Se needsProfileCompletion:
→ Redireciona para /complete-profile
Senão:
→ Redireciona para /select-companyConstrução da URL OAuth (Frontend)
A URL OAuth é construída usando constantes definidas em src/domain/auth/constants.ts:
typescript
export const OAUTH_CONFIGS: Record<AuthProvider, OAuthConfig> = {
google: {
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,
scope: 'openid email profile',
responseType: 'code',
},
// ... outros provedores
}
export const buildOAuthUrl = (provider: AuthProvider): string => {
const config = OAUTH_CONFIGS[provider]
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: `${OAUTH_REDIRECT_URI}/${provider}`,
response_type: config.responseType,
scope: config.scope,
state: crypto.randomUUID(),
})
return `${config.authUrl}?${params.toString()}`
}Variáveis de Ambiente para OAuth
env
VITE_GOOGLE_CLIENT_ID="seu-google-client-id"
VITE_LINKEDIN_CLIENT_ID="seu-linkedin-client-id"
VITE_MICROSOFT_CLIENT_ID="seu-microsoft-client-id"
VITE_AUTH_GITHUB_CLIENT_ID="seu-github-client-id"
VITE_MOCK_MODE="false"Endpoints de Autenticação
| Endpoint | Método | Descrição |
|---|---|---|
/auth/social | POST | Autentica com código OAuth e retorna dados do usuário |
/auth/complete-profile | POST | Completa dados do perfil do usuário |
/auth/companies | GET | Lista empresas do usuário autenticado |
/auth/create-company | POST | Cadastra nova empresa |
/users/me | GET | Retorna dados do usuário autenticado |
/users/me | PUT | Atualiza perfil do usuário (nome, telefone, CPF) |
/users/me/password | PUT | Altera a senha do usuário |
/app/exchange-token | POST | Troca QR token ou master key por sessão autenticada (público, sem JWT) |
/app/mobile-logout | POST | Logout do app mobile; envia deviceId para revogar a sessão (autenticado) |
/app/devices | GET | Lista os aparelhos conectados da conta (autenticado) |
/app/devices/:id | DELETE | Desconecta/revoga um aparelho específico (autenticado) |
Login do app mobile
O app aceita duas variantes do mesmo input para autenticar:
- QR token: gerado no painel web, validade 2 min, uso único.
- Master key: token administrativo configurado via env var no backend, usado para QA interno e revisão da Google Play Store.
Ambos são trocados pela mesma rota POST /app/exchange-token com body { token, deviceId, deviceInfo }. O frontend não distingue um do outro: qualquer string aceita pelo backend libera a sessão. O backend valida o formato/origem e responde com AuthResponseDto (user + accessToken + companies + tenant).
Tenant no login do app (pular a tela de access)
Desde 2026-07-03, o accessToken do app carrega o claim tenant (a empresa do QR/pareamento) e a resposta AuthResponseDto inclui tenant: { companyId, slug } | null. O app não decodifica o JWT; ele lê o tenant do corpo da resposta. No AuthContext.login, quando há tenant.companyId, o app carrega o /me/access (já escopado pelo backend), resolve a experiência e seleciona automaticamente:
- contrato cujo
borrower.id === tenant.companyId→experience = Contract(entra direto no contrato); - senão, empresa cujo
id === tenant.companyId→experience = Borrower.
Com a experience já definida, o resolveRootRedirect leva direto a /contract ou /borrower, sem passar pela tela /(access). O claim assinado no token é o que o backend usa para o bloqueio; o tenant no corpo é só dica de roteamento.
Identificação do dispositivo e revogação
O app gera e persiste um deviceId estável (UUID) no SecureStore (contrasync_device_id) e o envia no exchange-token e no mobile-logout. O backend cria/atualiza uma sessão por (userId, deviceId) em app_device_sessions e devolve um accessToken com o claim sid. Vários aparelhos podem ficar conectados ao mesmo tempo (sem limite). Quando o painel desconecta um aparelho, o sid é revogado e a próxima requisição daquele app recebe 401 — o interceptor de resposta já trata 401 limpando o SecureStore e disparando o logout. O deviceId não é apagado no logout, para o mesmo aparelho reusar a sessão ao parear de novo. Detalhes em api/app-devices.md.
Headers de Requisição
Todas as requisições autenticadas incluem automaticamente:
| Header | Origem | Descrição |
|---|---|---|
Authorization | localStorage | Token Bearer do usuário |
x-company-id | sessionStorage | ID da empresa selecionada |
O header x-company-id é adicionado automaticamente pelo interceptor do Axios quando há uma empresa selecionada no sessionStorage.
Request do Endpoint /auth/social
typescript
type SocialAuthRequest = {
provider: AuthProvider // 'google' | 'linkedin' | 'microsoft' | 'github'
code: string // Código de autorização retornado pelo provedor
}Response do Endpoint /auth/social
typescript
type SocialAuthResponse = {
user: User
token: string
companies: Company[]
action: 'login' | 'register'
needsProfileCompletion: boolean
}Composable useOAuthPopup
Gerencia o fluxo de popup OAuth:
typescript
const { openPopup, isLoading, error } = useOAuthPopup()
const result = await openPopup('google')
// result: { provider: 'google', code: '...' }Tela de Completar Cadastro
Exibida quando needsProfileCompletion: true na resposta do /auth/social.
IMPORTANTE: Esta é uma página full-screen sem menu lateral (não usa ContainerApp).
Campos obrigatórios:
- Nome Completo
- Telefone
Campos opcionais:
- CPF
Páginas de Autenticação Full-Screen
As seguintes páginas são renderizadas em full-screen, sem o layout ContainerApp:
| Página | Rota | Descrição |
|---|---|---|
| LoginPage | /login | Tela de login social |
| OAuthCallback | /auth/callback/:provider | Callback do OAuth (popup) |
| CompleteProfilePage | /complete-profile | Completar cadastro |
| SelectCompanyPage | /select-company | Selecionar empresa |