05 — Curio: conexão e sessão
05 — Curio: conexão e sessão
Como o front autentica, mantém e encerra a sessão com o backend Curio. Toda a integração com
@curio/clientestá confinada emsrc/lib/curio/esrc/api/session.ts. Nenhuma página importa@curio/clientpara configurar transporte.
Camadas
Página
└─ useAuth() src/context/AuthProvider.tsx ← o que a página usa
└─ useAuthQuery() src/hooks/useAuthQuery.ts estado via React Query
└─ login/connect src/api/session.ts operações de sessão
└─ getSessionManager() src/lib/curio/index.ts transporte
└─ @curio/client
Componentes usam useAuth(). Nunca useAuthQuery diretamente, nunca getSessionManager().
src/lib/curio/
Quatro arquivos.
index.ts — fábrica do transporte
// src/lib/curio/index.ts
import { V3RequestParser } from "@curio/client/parsers";
import { Session } from "./Session";
import { SessionManager } from "./SessionManager";
import { createV3Validator } from "@curio/client/validators";
import { FetchLink } from "@curio/client/links";
export interface Config {
service: { url: string; server: string; system: string; port: string };
accessToken: string;
resources: { get: string; put: string; viewer: string };
logs: boolean;
}
const createSessionManager = (configuration: Config) => {
const link = new FetchLink(configuration.service);
const requestParser = new V3RequestParser(configuration.service);
// pipeline: request -> parse -> json -> post -> valida
return new SessionManager(configuration.service, (request) =>
Promise.resolve(request)
.then(requestParser.parse)
.then(JSON.stringify)
.then(link.parse)
.then(link.post)
.then((response) => response.json())
.then(createV3Validator(request).validate)
);
};
export const getSessionManager = async () => {
const CONFIG_PATH = "./config.json";
const cnfg = await (await fetch(CONFIG_PATH)).json();
return createSessionManager(cnfg);
};
export type { Service as SV } from "./SessionManager";
export { Session, SessionManager };
export { isAuthError } from "./isAuthError";
export * from "./SessionManager";
getSessionManager()cria uma instância nova a cada chamada — não é singleton, apesar do nome. Isso é aceitável porque só é chamado emlogin,connecte em requests anônimos. Não chame dentro de componente ou hook de página. Para falar com o backend a partir de uma tela, use a sessão douseAuth()— ver 06.
Session.ts — a sessão do app
// src/lib/curio/Session.ts
import { MainUseCase } from "@curio/client";
export class Session extends MainUseCase {
module: number | undefined;
storageToken: string | undefined;
}
Estende MainUseCase só para carregar module e storageToken. Se o projeto precisar de mais estado
por sessão, é aqui.
SessionManager.ts — detecção de sessão morta
// src/lib/curio/SessionManager.ts
import { MainUseCase, RequestDriver, SecurityManager, UseCaseMessageType } from "@curio/client";
import { Session } from "./Session";
import { isAuthError } from "./isAuthError";
import { ABORT } from "@curio/client/utils/constants";
export interface ConfigService {
url: string;
server: string;
system: number;
port: number;
module: number;
version: number;
}
// chave unica por app — nunca reaproveitar de outro projeto
export const STORAGEKEY = "br.com.nomedoprojeto";
export class SessionManager extends SecurityManager {
public session: MainUseCase | undefined;
private _service: ConfigService;
private _driver: RequestDriver;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- @curio nao tipa o service
constructor(service: any, driver: RequestDriver) {
super(service, driver);
this._service = service;
this._driver = driver;
}
public async openMainUseCase<U extends typeof MainUseCase>(username: string, password: string, constructor?: U) {
this.session = await super.openMainUseCase(username, password, constructor);
this._attachListeners();
return this.session! as InstanceType<U>;
}
public connectMainUseCase(mainUseCase: MainUseCase) {
this.session = mainUseCase;
this._attachListeners();
return super.connectMainUseCase(mainUseCase);
}
// sessao anonima: pre-login (ex. obter versao)
public anonymousSession() {
return new Session(0, this._service, this._driver);
}
private _attachListeners() {
this.session!.addListener(ABORT, () => this._unauthenticate(false));
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- @curio nao tipa a mensagem
this.session!.addListener(UseCaseMessageType.RESPONSE, (message: any) => {
if (isAuthError(message.error)) this._unauthenticate(true);
});
}
private _unauthenticate(expired: boolean) {
this.session = undefined;
if (expired) localStorage.removeItem(STORAGEKEY);
}
}
STORAGEKEY identifica a sessão no storage. Troque por um valor próprio do projeto novo.
Cuidado com storage inconsistente. Se o token é escrito em
sessionStoragemas a limpeza remove delocalStorage(ou vice-versa), a limpeza não limpa nada — são storages diferentes. Escolha um e use o mesmo nos dois lugares. O snippet acima e o desession.tsabaixo usamsessionStoragede forma consistente — token de sessão não deve sobreviver ao fechamento da aba.
isAuthError.ts — o que conta como falha de autenticação
// src/lib/curio/isAuthError.ts
import { DestinataryNotFoundError } from "@curio/client/errors";
// code -1 = sessao inexistente no servidor
export const isAuthError = (error: unknown): boolean =>
error instanceof DestinataryNotFoundError || String((error as { code?: unknown } | null | undefined)?.code) === "-1";
Ponto único de decisão. Se o backend introduzir outro código de sessão inválida, muda só aqui.
src/api/session.ts
// src/api/session.ts
import { ConnectionError, DestinataryNotFoundError } from "@curio/client/errors";
import { Session, getSessionManager } from "../lib/curio";
import { STORAGEKEY } from "../lib/curio/SessionManager";
export interface LoginParams {
email: string;
password?: string;
}
export async function login(params: LoginParams) {
const { email, password } = params;
try {
const sessionManager = await getSessionManager();
const session = await sessionManager.openMainUseCase(email, password ?? "", Session);
session.module = 0;
session.storageToken = STORAGEKEY;
sessionStorage.setItem(STORAGEKEY, session.token.toString());
return session;
} catch (error) {
return error as Error;
}
}
// reconecta a partir do token guardado (refresh de pagina)
export async function connect(token: string) {
try {
const sessionManager = await getSessionManager();
sessionManager.connectMainUseCase(new Session(token, sessionManager.service, sessionManager.driver));
await sessionManager.session!.timeoutCheck();
const session = sessionManager.session! as Session;
session.module = 0;
session.storageToken = STORAGEKEY;
sessionStorage.setItem(STORAGEKEY, session.token.toString());
return session;
} catch (error) {
if (error instanceof DestinataryNotFoundError) return error;
if (error instanceof ConnectionError) return error;
return error as Error;
}
}
export async function logoutSession(session?: Session) {
if (session) session.abort();
sessionStorage.removeItem(STORAGEKEY);
}
logineconnectretornam o erro em vez de lançar. Quem chama precisa testarresult instanceof Error. É contraintuitivo e fácil de errar — se o projeto novo puder, prefira deixar lançar e tratar no hook.Cuidado com
localStorage.clear()no logout — ele apaga preferências de UI não relacionadas à sessão. O snippet acima remove só a chave da sessão.
Requests anônimos (pré-login)
Para chamar o backend antes de autenticar — por exemplo, exibir a versão do servidor na tela de login:
// src/api/session.ts
export const VERSION = { useCase: "3916", getVersion: "RM_OBTER_VERSAO" };
export async function obterVersaoRequest() {
const sessionManager = await getSessionManager();
const anonymousSession = sessionManager.anonymousSession();
const uc = await anonymousSession?.openUseCase(VERSION.useCase);
const response = await uc?.sendRequest(VERSION.getVersion);
await uc?.abort(); // sempre fechar — sessao anonima nao tem dono
return response.Versao._ as string;
}
AuthProvider
// src/context/AuthProvider.tsx (trecho essencial)
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { notification } = useNotification();
const navigate = useNavigate();
const { session, isLoading, isAuth, login: authLogin, logout: authLogout, refetchConnection } = useAuthQuery();
// reconecta no boot se ha token guardado
useEffect(() => {
if (sessionStorage.getItem(STORAGEKEY)) refetchConnection();
}, []);
const logout = useCallback(() => {
authLogout();
navigate("/", { replace: true });
}, [authLogout, navigate]);
// fonte unica de logout: reage a notificacao de erro de auth.
// ref evita disparar 2x — logout muda de identidade a cada render.
const handledNotificationRef = useRef<NotificationType | null>(null);
useEffect(() => {
if (!notification) return;
const expiredSession = notification.message === "Sessão expirada!";
if (!notification.isAuthError && !expiredSession) return;
if (handledNotificationRef.current === notification) return;
handledNotificationRef.current = notification;
logout();
}, [notification, logout]);
const login = async (params: LoginCredentials) => {
await authLogin(params);
navigate("/dashboard");
};
return (
<AuthContext.Provider value={{ isAuth, session, isLoading, login, logout }}>{children}</AuthContext.Provider>
);
};
Ciclo de vida
| Evento | O que acontece |
|---|---|
| Boot com token |
refetchConnection() → connect(token) → sessão restaurada |
| Login |
login() → token no sessionStorage → navega para /dashboard
|
| Request com sessão morta |
isAuthError → notificação com isAuthError: true → AuthProvider desloga |
ABORT do servidor |
Listener no SessionManager limpa a sessão |
| Logout manual |
session.abort() → limpa storage → navega para /
|
Detecção de expiração por string
const expiredSession = notification.message === "Sessão expirada!";
Comparar mensagem literal é frágil: muda a tradução no backend, o auto-logout para de funcionar sem
erro visível. Preferível é o backend sinalizar com um código que isAuthError reconheça. Se
precisar manter, isole a string numa constante e documente a dependência.