@allanfsouza/aether-sdk 2.4.4 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +13 -2
- package/dist/auth.js +82 -34
- package/dist/http-client.d.ts +1 -0
- package/dist/http-client.js +91 -4
- package/dist/index.d.ts +45 -1
- package/dist/index.js +136 -1
- package/package.json +2 -2
- package/src/auth.ts +96 -35
- package/src/http-client.ts +117 -5
- package/src/index.ts +162 -6
package/dist/auth.d.ts
CHANGED
|
@@ -25,7 +25,6 @@ export interface Session {
|
|
|
25
25
|
export declare class AuthModule {
|
|
26
26
|
private client;
|
|
27
27
|
private http;
|
|
28
|
-
private refreshToken;
|
|
29
28
|
private currentUser;
|
|
30
29
|
constructor(client: PlataformaClient, http: AxiosInstance);
|
|
31
30
|
login(email: string, password: string): Promise<LoginResponse>;
|
|
@@ -43,7 +42,13 @@ export declare class AuthModule {
|
|
|
43
42
|
getSessions(): Promise<Session[]>;
|
|
44
43
|
forgotPassword(email: string): Promise<any>;
|
|
45
44
|
resetPassword(token: string, newPassword: string): Promise<any>;
|
|
45
|
+
/**
|
|
46
|
+
* @deprecated Use client.getRefreshToken() - mantido para compatibilidade
|
|
47
|
+
*/
|
|
46
48
|
getRefreshToken(): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* @deprecated Use client.setRefreshToken() - mantido para compatibilidade
|
|
51
|
+
*/
|
|
47
52
|
setRefreshToken(token: string | null): void;
|
|
48
53
|
/**
|
|
49
54
|
* Alias para login, retornando { user, error } padrão do Showcase
|
|
@@ -82,10 +87,16 @@ export declare class AuthModule {
|
|
|
82
87
|
}>;
|
|
83
88
|
/**
|
|
84
89
|
* Recupera a sessão atual (User + Token).
|
|
85
|
-
*
|
|
90
|
+
* Primeiro tenta memória, depois localStorage.
|
|
91
|
+
* Se não encontrar, retorna null.
|
|
86
92
|
*/
|
|
87
93
|
getSession(): Promise<{
|
|
88
94
|
user: User;
|
|
89
95
|
access_token: string;
|
|
90
96
|
} | null>;
|
|
97
|
+
/**
|
|
98
|
+
* Retorna o usuário atual (sem validar token).
|
|
99
|
+
* Útil para acesso síncrono aos dados do usuário.
|
|
100
|
+
*/
|
|
101
|
+
getCurrentUser(): User | null;
|
|
91
102
|
}
|
package/dist/auth.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
export class AuthModule {
|
|
2
2
|
constructor(client, http) {
|
|
3
|
-
|
|
4
|
-
//
|
|
3
|
+
// Armazena o user em memória para acesso rápido
|
|
4
|
+
// Persistência fica no PlataformaClient (localStorage)
|
|
5
5
|
this.currentUser = null;
|
|
6
6
|
this.client = client;
|
|
7
7
|
this.http = http;
|
|
8
|
+
// Restaura user do localStorage se existir
|
|
9
|
+
this.currentUser = this.client.getUser();
|
|
8
10
|
}
|
|
9
|
-
//
|
|
10
|
-
// MÉTODOS
|
|
11
|
-
//
|
|
11
|
+
// ==========================================================================
|
|
12
|
+
// MÉTODOS PRINCIPAIS
|
|
13
|
+
// ==========================================================================
|
|
12
14
|
async login(email, password) {
|
|
13
15
|
try {
|
|
14
16
|
const { data } = await this.http.post("/auth/login", {
|
|
@@ -16,15 +18,17 @@ export class AuthModule {
|
|
|
16
18
|
password,
|
|
17
19
|
});
|
|
18
20
|
if (data.accessToken) {
|
|
21
|
+
// Persiste automaticamente via PlataformaClient
|
|
19
22
|
this.client.setToken(data.accessToken);
|
|
20
|
-
this.
|
|
23
|
+
this.client.setRefreshToken(data.refreshToken);
|
|
24
|
+
this.client.setUser(data.user);
|
|
21
25
|
this.currentUser = data.user;
|
|
22
26
|
}
|
|
23
27
|
return data;
|
|
24
28
|
}
|
|
25
29
|
catch (e) {
|
|
26
|
-
|
|
27
|
-
this.
|
|
30
|
+
// Limpa tudo em caso de erro
|
|
31
|
+
this.client.clearSession();
|
|
28
32
|
this.currentUser = null;
|
|
29
33
|
throw e;
|
|
30
34
|
}
|
|
@@ -32,9 +36,11 @@ export class AuthModule {
|
|
|
32
36
|
async register(credentials) {
|
|
33
37
|
try {
|
|
34
38
|
const { data } = await this.http.post("/auth/register", credentials);
|
|
35
|
-
// Se o register
|
|
39
|
+
// Se o register retornar token (auto-login), configura sessão
|
|
36
40
|
if (data.accessToken) {
|
|
37
41
|
this.client.setToken(data.accessToken);
|
|
42
|
+
this.client.setRefreshToken(data.refreshToken);
|
|
43
|
+
this.client.setUser(data.user);
|
|
38
44
|
this.currentUser = data.user;
|
|
39
45
|
}
|
|
40
46
|
return data;
|
|
@@ -44,21 +50,28 @@ export class AuthModule {
|
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
async refresh() {
|
|
47
|
-
|
|
53
|
+
// Tenta pegar do localStorage primeiro, depois da memória
|
|
54
|
+
const refreshToken = this.client.getRefreshToken() || this.getRefreshToken();
|
|
55
|
+
if (!refreshToken) {
|
|
48
56
|
throw new Error("Nenhum refresh token disponível");
|
|
49
57
|
}
|
|
50
58
|
try {
|
|
51
59
|
const { data } = await this.http.post("/auth/refresh", {
|
|
52
|
-
refreshToken
|
|
60
|
+
refreshToken,
|
|
53
61
|
});
|
|
54
62
|
if (data.accessToken) {
|
|
55
63
|
this.client.setToken(data.accessToken);
|
|
64
|
+
// Se vier novo refresh token, atualiza
|
|
65
|
+
if (data.refreshToken) {
|
|
66
|
+
this.client.setRefreshToken(data.refreshToken);
|
|
67
|
+
}
|
|
56
68
|
}
|
|
57
69
|
return data;
|
|
58
70
|
}
|
|
59
71
|
catch (e) {
|
|
60
|
-
|
|
61
|
-
this.
|
|
72
|
+
// Token expirado - limpa sessão
|
|
73
|
+
this.client.clearSession();
|
|
74
|
+
this.currentUser = null;
|
|
62
75
|
throw e;
|
|
63
76
|
}
|
|
64
77
|
}
|
|
@@ -66,18 +79,17 @@ export class AuthModule {
|
|
|
66
79
|
return `${this.client.apiUrl}/v1/auth/google`;
|
|
67
80
|
}
|
|
68
81
|
async logout() {
|
|
69
|
-
|
|
82
|
+
const refreshToken = this.client.getRefreshToken();
|
|
83
|
+
if (refreshToken) {
|
|
70
84
|
try {
|
|
71
|
-
await this.http.post("/auth/logout", {
|
|
72
|
-
refreshToken: this.refreshToken,
|
|
73
|
-
});
|
|
85
|
+
await this.http.post("/auth/logout", { refreshToken });
|
|
74
86
|
}
|
|
75
|
-
catch
|
|
76
|
-
// Ignora erro de logout
|
|
87
|
+
catch {
|
|
88
|
+
// Ignora erro de logout no servidor
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
|
-
|
|
80
|
-
this.
|
|
91
|
+
// Limpa tudo (memória + localStorage)
|
|
92
|
+
this.client.clearSession();
|
|
81
93
|
this.currentUser = null;
|
|
82
94
|
}
|
|
83
95
|
async logoutAll() {
|
|
@@ -85,8 +97,7 @@ export class AuthModule {
|
|
|
85
97
|
await this.http.post("/auth/logout-all");
|
|
86
98
|
}
|
|
87
99
|
finally {
|
|
88
|
-
this.client.
|
|
89
|
-
this.refreshToken = null;
|
|
100
|
+
this.client.clearSession();
|
|
90
101
|
this.currentUser = null;
|
|
91
102
|
}
|
|
92
103
|
}
|
|
@@ -105,15 +116,21 @@ export class AuthModule {
|
|
|
105
116
|
});
|
|
106
117
|
return data;
|
|
107
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* @deprecated Use client.getRefreshToken() - mantido para compatibilidade
|
|
121
|
+
*/
|
|
108
122
|
getRefreshToken() {
|
|
109
|
-
return this.
|
|
123
|
+
return this.client.getRefreshToken();
|
|
110
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* @deprecated Use client.setRefreshToken() - mantido para compatibilidade
|
|
127
|
+
*/
|
|
111
128
|
setRefreshToken(token) {
|
|
112
|
-
this.
|
|
129
|
+
this.client.setRefreshToken(token);
|
|
113
130
|
}
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
131
|
+
// ==========================================================================
|
|
132
|
+
// MÉTODOS COMPATÍVEIS COM SUPABASE / SHOWCASE STYLE
|
|
133
|
+
// ==========================================================================
|
|
117
134
|
/**
|
|
118
135
|
* Alias para login, retornando { user, error } padrão do Showcase
|
|
119
136
|
*/
|
|
@@ -134,7 +151,7 @@ export class AuthModule {
|
|
|
134
151
|
const data = await this.register({
|
|
135
152
|
email: credentials.email,
|
|
136
153
|
password: credentials.password,
|
|
137
|
-
name: credentials.data?.name || credentials.email.split(
|
|
154
|
+
name: credentials.data?.name || credentials.email.split("@")[0],
|
|
138
155
|
});
|
|
139
156
|
return { user: data.user, error: null };
|
|
140
157
|
}
|
|
@@ -151,20 +168,51 @@ export class AuthModule {
|
|
|
151
168
|
}
|
|
152
169
|
/**
|
|
153
170
|
* Recupera a sessão atual (User + Token).
|
|
154
|
-
*
|
|
171
|
+
* Primeiro tenta memória, depois localStorage.
|
|
172
|
+
* Se não encontrar, retorna null.
|
|
155
173
|
*/
|
|
156
174
|
async getSession() {
|
|
157
175
|
const token = this.client.getToken();
|
|
158
176
|
if (!token)
|
|
159
177
|
return null;
|
|
160
|
-
//
|
|
161
|
-
// O ideal seria validar o token no backend (/auth/me), mas vamos simplificar.
|
|
178
|
+
// Tenta memória primeiro
|
|
162
179
|
if (this.currentUser) {
|
|
163
180
|
return {
|
|
164
181
|
user: this.currentUser,
|
|
165
|
-
access_token: token
|
|
182
|
+
access_token: token,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// Tenta localStorage
|
|
186
|
+
const savedUser = this.client.getUser();
|
|
187
|
+
if (savedUser) {
|
|
188
|
+
this.currentUser = savedUser;
|
|
189
|
+
return {
|
|
190
|
+
user: savedUser,
|
|
191
|
+
access_token: token,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Última opção: valida token no backend
|
|
195
|
+
try {
|
|
196
|
+
const { data } = await this.http.get("/auth/me");
|
|
197
|
+
this.currentUser = data.user;
|
|
198
|
+
this.client.setUser(data.user);
|
|
199
|
+
return {
|
|
200
|
+
user: data.user,
|
|
201
|
+
access_token: token,
|
|
166
202
|
};
|
|
167
203
|
}
|
|
168
|
-
|
|
204
|
+
catch {
|
|
205
|
+
// Token inválido/expirado - limpa sessão
|
|
206
|
+
this.client.clearSession();
|
|
207
|
+
this.currentUser = null;
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Retorna o usuário atual (sem validar token).
|
|
213
|
+
* Útil para acesso síncrono aos dados do usuário.
|
|
214
|
+
*/
|
|
215
|
+
getCurrentUser() {
|
|
216
|
+
return this.currentUser || this.client.getUser();
|
|
169
217
|
}
|
|
170
218
|
}
|
package/dist/http-client.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { PlataformaClient } from "./index.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Cria uma instância do Axios pré-configurada.
|
|
5
5
|
* - Injeta automaticamente headers de Auth e Projeto.
|
|
6
|
+
* - Renova token automaticamente quando expira (401).
|
|
6
7
|
* - Converte erros em AetherError.
|
|
7
8
|
*/
|
|
8
9
|
export declare function createHttpClient(client: PlataformaClient): AxiosInstance;
|
package/dist/http-client.js
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
// src/http-client.ts
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { handleAxiosError } from "./errors.js";
|
|
4
|
+
// Flag para evitar múltiplos refreshes simultâneos
|
|
5
|
+
let isRefreshing = false;
|
|
6
|
+
// Fila de requisições aguardando o refresh
|
|
7
|
+
let failedQueue = [];
|
|
8
|
+
/**
|
|
9
|
+
* Processa a fila de requisições após o refresh.
|
|
10
|
+
*/
|
|
11
|
+
const processQueue = (error, token = null) => {
|
|
12
|
+
failedQueue.forEach((prom) => {
|
|
13
|
+
if (error) {
|
|
14
|
+
prom.reject(error);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
prom.resolve(token);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
failedQueue = [];
|
|
21
|
+
};
|
|
4
22
|
/**
|
|
5
23
|
* Cria uma instância do Axios pré-configurada.
|
|
6
24
|
* - Injeta automaticamente headers de Auth e Projeto.
|
|
25
|
+
* - Renova token automaticamente quando expira (401).
|
|
7
26
|
* - Converte erros em AetherError.
|
|
8
27
|
*/
|
|
9
28
|
export function createHttpClient(client) {
|
|
10
29
|
const http = axios.create({
|
|
11
|
-
// Adiciona o /v1 automaticamente em todas as chamadas do SDK
|
|
12
30
|
baseURL: `${client.apiUrl}/v1`,
|
|
13
31
|
headers: {
|
|
14
32
|
"Content-Type": "application/json",
|
|
15
33
|
},
|
|
16
34
|
});
|
|
17
|
-
//
|
|
35
|
+
// ===========================================================================
|
|
36
|
+
// INTERCEPTOR DE REQUEST
|
|
37
|
+
// Injeta token e projectId em todas as requisições
|
|
38
|
+
// ===========================================================================
|
|
18
39
|
http.interceptors.request.use((config) => {
|
|
19
40
|
const token = client.getToken();
|
|
20
41
|
if (token) {
|
|
@@ -25,7 +46,73 @@ export function createHttpClient(client) {
|
|
|
25
46
|
}
|
|
26
47
|
return config;
|
|
27
48
|
});
|
|
28
|
-
//
|
|
29
|
-
|
|
49
|
+
// ===========================================================================
|
|
50
|
+
// INTERCEPTOR DE RESPONSE
|
|
51
|
+
// Detecta 401 e tenta refresh automático do token
|
|
52
|
+
// ===========================================================================
|
|
53
|
+
http.interceptors.response.use((response) => response, async (error) => {
|
|
54
|
+
const originalRequest = error.config;
|
|
55
|
+
// Verifica se é erro 401 (não autorizado) e não é retry
|
|
56
|
+
const isUnauthorized = error.response?.status === 401;
|
|
57
|
+
const isRetry = originalRequest?._retry;
|
|
58
|
+
const isAuthRoute = originalRequest?.url?.includes("/auth/");
|
|
59
|
+
// Não tenta refresh se:
|
|
60
|
+
// - Não é 401
|
|
61
|
+
// - Já é um retry
|
|
62
|
+
// - É uma rota de auth (login, register, refresh)
|
|
63
|
+
// - Não tem refresh token disponível
|
|
64
|
+
if (!isUnauthorized || isRetry || isAuthRoute) {
|
|
65
|
+
return handleAxiosError(error);
|
|
66
|
+
}
|
|
67
|
+
const refreshToken = client.getRefreshToken();
|
|
68
|
+
if (!refreshToken) {
|
|
69
|
+
// Sem refresh token, limpa sessão e propaga erro
|
|
70
|
+
client.clearSession();
|
|
71
|
+
return handleAxiosError(error);
|
|
72
|
+
}
|
|
73
|
+
// Se já está fazendo refresh, aguarda na fila
|
|
74
|
+
if (isRefreshing) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
failedQueue.push({ resolve, reject });
|
|
77
|
+
})
|
|
78
|
+
.then((token) => {
|
|
79
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
80
|
+
return http(originalRequest);
|
|
81
|
+
})
|
|
82
|
+
.catch((err) => handleAxiosError(err));
|
|
83
|
+
}
|
|
84
|
+
// Marca como retry e inicia refresh
|
|
85
|
+
originalRequest._retry = true;
|
|
86
|
+
isRefreshing = true;
|
|
87
|
+
try {
|
|
88
|
+
// Chama endpoint de refresh diretamente (sem interceptor)
|
|
89
|
+
const { data } = await axios.post(`${client.apiUrl}/v1/auth/refresh`, { refreshToken }, { headers: { "Content-Type": "application/json" } });
|
|
90
|
+
const newAccessToken = data.accessToken;
|
|
91
|
+
const newRefreshToken = data.refreshToken;
|
|
92
|
+
// Atualiza tokens no client (persiste no localStorage)
|
|
93
|
+
client.setToken(newAccessToken);
|
|
94
|
+
if (newRefreshToken) {
|
|
95
|
+
client.setRefreshToken(newRefreshToken);
|
|
96
|
+
}
|
|
97
|
+
// Processa fila de requisições pendentes
|
|
98
|
+
processQueue(null, newAccessToken);
|
|
99
|
+
// Refaz a requisição original com novo token
|
|
100
|
+
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
|
101
|
+
return http(originalRequest);
|
|
102
|
+
}
|
|
103
|
+
catch (refreshError) {
|
|
104
|
+
// Refresh falhou - sessão expirada
|
|
105
|
+
processQueue(refreshError, null);
|
|
106
|
+
client.clearSession();
|
|
107
|
+
// Emite evento de sessão expirada (se houver listener)
|
|
108
|
+
if (typeof window !== "undefined") {
|
|
109
|
+
window.dispatchEvent(new CustomEvent("aether:session-expired"));
|
|
110
|
+
}
|
|
111
|
+
return handleAxiosError(refreshError);
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
isRefreshing = false;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
30
117
|
return http;
|
|
31
118
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AxiosInstance } from "axios";
|
|
2
|
-
import { AuthModule } from "./auth.js";
|
|
2
|
+
import { AuthModule, User } from "./auth.js";
|
|
3
3
|
import { DatabaseModule } from "./database.js";
|
|
4
4
|
import { StorageModule } from "./storage.js";
|
|
5
5
|
import { FunctionsModule } from "./functions.js";
|
|
@@ -12,6 +12,11 @@ export type ClientConfig = {
|
|
|
12
12
|
baseUrl?: string;
|
|
13
13
|
projectId?: string;
|
|
14
14
|
apiKey?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Habilita persistência automática de sessão no localStorage.
|
|
17
|
+
* Padrão: true em browsers, false em Node.js/SSR.
|
|
18
|
+
*/
|
|
19
|
+
persistSession?: boolean;
|
|
15
20
|
};
|
|
16
21
|
export declare class PlataformaClient {
|
|
17
22
|
auth: AuthModule;
|
|
@@ -24,9 +29,48 @@ export declare class PlataformaClient {
|
|
|
24
29
|
projectId: string;
|
|
25
30
|
http: AxiosInstance;
|
|
26
31
|
private _token;
|
|
32
|
+
private _persistSession;
|
|
27
33
|
constructor(config: ClientConfig);
|
|
34
|
+
/**
|
|
35
|
+
* Define o token de acesso (JWT).
|
|
36
|
+
* Se persistSession estiver ativo, salva automaticamente no localStorage.
|
|
37
|
+
*/
|
|
28
38
|
setToken(token: string | null): void;
|
|
39
|
+
/**
|
|
40
|
+
* Retorna o token de acesso atual.
|
|
41
|
+
*/
|
|
29
42
|
getToken(): string | null;
|
|
43
|
+
/**
|
|
44
|
+
* Salva o refresh token.
|
|
45
|
+
* Usado internamente pelo AuthModule após login.
|
|
46
|
+
*/
|
|
47
|
+
setRefreshToken(token: string | null): void;
|
|
48
|
+
/**
|
|
49
|
+
* Retorna o refresh token salvo no localStorage.
|
|
50
|
+
*/
|
|
51
|
+
getRefreshToken(): string | null;
|
|
52
|
+
/**
|
|
53
|
+
* Salva dados do usuário logado no localStorage.
|
|
54
|
+
*/
|
|
55
|
+
setUser(user: User | null): void;
|
|
56
|
+
/**
|
|
57
|
+
* Retorna dados do usuário salvo no localStorage.
|
|
58
|
+
*/
|
|
59
|
+
getUser(): User | null;
|
|
60
|
+
/**
|
|
61
|
+
* Limpa toda a sessão (token, refresh, user).
|
|
62
|
+
* Chamado automaticamente no logout.
|
|
63
|
+
*/
|
|
64
|
+
clearSession(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Verifica se existe uma sessão salva (token presente).
|
|
67
|
+
*/
|
|
68
|
+
hasSession(): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Restaura sessão do localStorage ao inicializar o client.
|
|
71
|
+
* Executado automaticamente no constructor.
|
|
72
|
+
*/
|
|
73
|
+
private _restoreSession;
|
|
30
74
|
}
|
|
31
75
|
export { AetherError } from "./errors.js";
|
|
32
76
|
export type { LoginResponse, Session, User } from "./auth.js";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { createHttpClient } from "./http-client.js";
|
|
2
|
-
import { AuthModule } from "./auth.js";
|
|
2
|
+
import { AuthModule } from "./auth.js";
|
|
3
3
|
import { DatabaseModule } from "./database.js";
|
|
4
4
|
import { StorageModule } from "./storage.js";
|
|
5
5
|
import { FunctionsModule } from "./functions.js";
|
|
6
6
|
import { PushModule } from "./push.js";
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// CONSTANTES DE STORAGE
|
|
9
|
+
// Chaves padronizadas para localStorage - evita conflito com outros SDKs
|
|
10
|
+
// =============================================================================
|
|
11
|
+
const STORAGE_KEYS = {
|
|
12
|
+
TOKEN: "aether_token",
|
|
13
|
+
REFRESH_TOKEN: "aether_refresh_token",
|
|
14
|
+
USER: "aether_user",
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Verifica se estamos em ambiente browser com localStorage disponível.
|
|
18
|
+
* Necessário para evitar erros em SSR (Next.js, Nuxt, etc).
|
|
19
|
+
*/
|
|
20
|
+
function isBrowser() {
|
|
21
|
+
return (typeof window !== "undefined" &&
|
|
22
|
+
typeof window.localStorage !== "undefined");
|
|
23
|
+
}
|
|
7
24
|
export class PlataformaClient {
|
|
8
25
|
constructor(config) {
|
|
9
26
|
this._token = null;
|
|
@@ -15,6 +32,11 @@ export class PlataformaClient {
|
|
|
15
32
|
}
|
|
16
33
|
this.apiUrl = url.replace(/\/+$/, "");
|
|
17
34
|
this.projectId = project;
|
|
35
|
+
// Persistência habilitada por padrão apenas em browsers
|
|
36
|
+
this._persistSession = config.persistSession ?? isBrowser();
|
|
37
|
+
// Restaura sessão salva ANTES de criar o httpClient
|
|
38
|
+
// Isso garante que requisições iniciais já tenham o token
|
|
39
|
+
this._restoreSession();
|
|
18
40
|
this.http = createHttpClient(this);
|
|
19
41
|
// Inicializa módulos
|
|
20
42
|
this.auth = new AuthModule(this, this.http);
|
|
@@ -25,12 +47,125 @@ export class PlataformaClient {
|
|
|
25
47
|
// Cria o alias que o Showcase App espera
|
|
26
48
|
this.database = this.db;
|
|
27
49
|
}
|
|
50
|
+
// ===========================================================================
|
|
51
|
+
// TOKEN DE ACESSO
|
|
52
|
+
// ===========================================================================
|
|
53
|
+
/**
|
|
54
|
+
* Define o token de acesso (JWT).
|
|
55
|
+
* Se persistSession estiver ativo, salva automaticamente no localStorage.
|
|
56
|
+
*/
|
|
28
57
|
setToken(token) {
|
|
29
58
|
this._token = token;
|
|
59
|
+
if (this._persistSession && isBrowser()) {
|
|
60
|
+
if (token) {
|
|
61
|
+
localStorage.setItem(STORAGE_KEYS.TOKEN, token);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
30
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Retorna o token de acesso atual.
|
|
70
|
+
*/
|
|
31
71
|
getToken() {
|
|
32
72
|
return this._token;
|
|
33
73
|
}
|
|
74
|
+
// ===========================================================================
|
|
75
|
+
// REFRESH TOKEN
|
|
76
|
+
// ===========================================================================
|
|
77
|
+
/**
|
|
78
|
+
* Salva o refresh token.
|
|
79
|
+
* Usado internamente pelo AuthModule após login.
|
|
80
|
+
*/
|
|
81
|
+
setRefreshToken(token) {
|
|
82
|
+
if (this._persistSession && isBrowser()) {
|
|
83
|
+
if (token) {
|
|
84
|
+
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, token);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Retorna o refresh token salvo no localStorage.
|
|
93
|
+
*/
|
|
94
|
+
getRefreshToken() {
|
|
95
|
+
if (this._persistSession && isBrowser()) {
|
|
96
|
+
return localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// ===========================================================================
|
|
101
|
+
// DADOS DO USUÁRIO
|
|
102
|
+
// ===========================================================================
|
|
103
|
+
/**
|
|
104
|
+
* Salva dados do usuário logado no localStorage.
|
|
105
|
+
*/
|
|
106
|
+
setUser(user) {
|
|
107
|
+
if (this._persistSession && isBrowser()) {
|
|
108
|
+
if (user) {
|
|
109
|
+
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(user));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Retorna dados do usuário salvo no localStorage.
|
|
118
|
+
*/
|
|
119
|
+
getUser() {
|
|
120
|
+
if (this._persistSession && isBrowser()) {
|
|
121
|
+
const saved = localStorage.getItem(STORAGE_KEYS.USER);
|
|
122
|
+
if (saved) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(saved);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// JSON corrompido - limpa
|
|
128
|
+
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
// ===========================================================================
|
|
136
|
+
// GERENCIAMENTO DE SESSÃO
|
|
137
|
+
// ===========================================================================
|
|
138
|
+
/**
|
|
139
|
+
* Limpa toda a sessão (token, refresh, user).
|
|
140
|
+
* Chamado automaticamente no logout.
|
|
141
|
+
*/
|
|
142
|
+
clearSession() {
|
|
143
|
+
this._token = null;
|
|
144
|
+
if (isBrowser()) {
|
|
145
|
+
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
|
146
|
+
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
147
|
+
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Verifica se existe uma sessão salva (token presente).
|
|
152
|
+
*/
|
|
153
|
+
hasSession() {
|
|
154
|
+
return this._token !== null;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Restaura sessão do localStorage ao inicializar o client.
|
|
158
|
+
* Executado automaticamente no constructor.
|
|
159
|
+
*/
|
|
160
|
+
_restoreSession() {
|
|
161
|
+
if (!this._persistSession || !isBrowser()) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const savedToken = localStorage.getItem(STORAGE_KEYS.TOKEN);
|
|
165
|
+
if (savedToken) {
|
|
166
|
+
this._token = savedToken;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
34
169
|
}
|
|
35
170
|
// ===== EXPORTS =====
|
|
36
171
|
export { AetherError } from "./errors.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allanfsouza/aether-sdk",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6",
|
|
4
4
|
"description": "SDK do Cliente para a Plataforma Aether",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -32,4 +32,4 @@
|
|
|
32
32
|
"@types/ws": "^8.5.10",
|
|
33
33
|
"typescript": "^5.3.0"
|
|
34
34
|
}
|
|
35
|
-
}
|
|
35
|
+
}
|
package/src/auth.ts
CHANGED
|
@@ -31,18 +31,22 @@ export interface Session {
|
|
|
31
31
|
export class AuthModule {
|
|
32
32
|
private client: PlataformaClient;
|
|
33
33
|
private http: AxiosInstance;
|
|
34
|
-
|
|
35
|
-
// Armazena o user
|
|
34
|
+
|
|
35
|
+
// Armazena o user em memória para acesso rápido
|
|
36
|
+
// Persistência fica no PlataformaClient (localStorage)
|
|
36
37
|
private currentUser: User | null = null;
|
|
37
38
|
|
|
38
39
|
constructor(client: PlataformaClient, http: AxiosInstance) {
|
|
39
40
|
this.client = client;
|
|
40
41
|
this.http = http;
|
|
42
|
+
|
|
43
|
+
// Restaura user do localStorage se existir
|
|
44
|
+
this.currentUser = this.client.getUser();
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
//
|
|
44
|
-
// MÉTODOS
|
|
45
|
-
//
|
|
47
|
+
// ==========================================================================
|
|
48
|
+
// MÉTODOS PRINCIPAIS
|
|
49
|
+
// ==========================================================================
|
|
46
50
|
|
|
47
51
|
async login(email: string, password: string): Promise<LoginResponse> {
|
|
48
52
|
try {
|
|
@@ -52,15 +56,17 @@ export class AuthModule {
|
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
if (data.accessToken) {
|
|
59
|
+
// Persiste automaticamente via PlataformaClient
|
|
55
60
|
this.client.setToken(data.accessToken);
|
|
56
|
-
this.
|
|
61
|
+
this.client.setRefreshToken(data.refreshToken);
|
|
62
|
+
this.client.setUser(data.user);
|
|
57
63
|
this.currentUser = data.user;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
66
|
return data;
|
|
61
67
|
} catch (e) {
|
|
62
|
-
|
|
63
|
-
this.
|
|
68
|
+
// Limpa tudo em caso de erro
|
|
69
|
+
this.client.clearSession();
|
|
64
70
|
this.currentUser = null;
|
|
65
71
|
throw e;
|
|
66
72
|
}
|
|
@@ -73,11 +79,15 @@ export class AuthModule {
|
|
|
73
79
|
}) {
|
|
74
80
|
try {
|
|
75
81
|
const { data } = await this.http.post("/auth/register", credentials);
|
|
76
|
-
|
|
82
|
+
|
|
83
|
+
// Se o register retornar token (auto-login), configura sessão
|
|
77
84
|
if (data.accessToken) {
|
|
78
85
|
this.client.setToken(data.accessToken);
|
|
86
|
+
this.client.setRefreshToken(data.refreshToken);
|
|
87
|
+
this.client.setUser(data.user);
|
|
79
88
|
this.currentUser = data.user;
|
|
80
89
|
}
|
|
90
|
+
|
|
81
91
|
return data;
|
|
82
92
|
} catch (e) {
|
|
83
93
|
throw e;
|
|
@@ -85,23 +95,32 @@ export class AuthModule {
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
async refresh(): Promise<{ accessToken: string }> {
|
|
88
|
-
|
|
98
|
+
// Tenta pegar do localStorage primeiro, depois da memória
|
|
99
|
+
const refreshToken = this.client.getRefreshToken() || this.getRefreshToken();
|
|
100
|
+
|
|
101
|
+
if (!refreshToken) {
|
|
89
102
|
throw new Error("Nenhum refresh token disponível");
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
try {
|
|
93
106
|
const { data } = await this.http.post("/auth/refresh", {
|
|
94
|
-
refreshToken
|
|
107
|
+
refreshToken,
|
|
95
108
|
});
|
|
96
109
|
|
|
97
110
|
if (data.accessToken) {
|
|
98
111
|
this.client.setToken(data.accessToken);
|
|
112
|
+
|
|
113
|
+
// Se vier novo refresh token, atualiza
|
|
114
|
+
if (data.refreshToken) {
|
|
115
|
+
this.client.setRefreshToken(data.refreshToken);
|
|
116
|
+
}
|
|
99
117
|
}
|
|
100
118
|
|
|
101
119
|
return data;
|
|
102
120
|
} catch (e) {
|
|
103
|
-
|
|
104
|
-
this.
|
|
121
|
+
// Token expirado - limpa sessão
|
|
122
|
+
this.client.clearSession();
|
|
123
|
+
this.currentUser = null;
|
|
105
124
|
throw e;
|
|
106
125
|
}
|
|
107
126
|
}
|
|
@@ -111,17 +130,18 @@ export class AuthModule {
|
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
async logout(): Promise<void> {
|
|
114
|
-
|
|
133
|
+
const refreshToken = this.client.getRefreshToken();
|
|
134
|
+
|
|
135
|
+
if (refreshToken) {
|
|
115
136
|
try {
|
|
116
|
-
await this.http.post("/auth/logout", {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} catch (e) {
|
|
120
|
-
// Ignora erro de logout
|
|
137
|
+
await this.http.post("/auth/logout", { refreshToken });
|
|
138
|
+
} catch {
|
|
139
|
+
// Ignora erro de logout no servidor
|
|
121
140
|
}
|
|
122
141
|
}
|
|
123
|
-
|
|
124
|
-
|
|
142
|
+
|
|
143
|
+
// Limpa tudo (memória + localStorage)
|
|
144
|
+
this.client.clearSession();
|
|
125
145
|
this.currentUser = null;
|
|
126
146
|
}
|
|
127
147
|
|
|
@@ -129,8 +149,7 @@ export class AuthModule {
|
|
|
129
149
|
try {
|
|
130
150
|
await this.http.post("/auth/logout-all");
|
|
131
151
|
} finally {
|
|
132
|
-
this.client.
|
|
133
|
-
this.refreshToken = null;
|
|
152
|
+
this.client.clearSession();
|
|
134
153
|
this.currentUser = null;
|
|
135
154
|
}
|
|
136
155
|
}
|
|
@@ -155,17 +174,23 @@ export class AuthModule {
|
|
|
155
174
|
return data;
|
|
156
175
|
}
|
|
157
176
|
|
|
177
|
+
/**
|
|
178
|
+
* @deprecated Use client.getRefreshToken() - mantido para compatibilidade
|
|
179
|
+
*/
|
|
158
180
|
getRefreshToken(): string | null {
|
|
159
|
-
return this.
|
|
181
|
+
return this.client.getRefreshToken();
|
|
160
182
|
}
|
|
161
183
|
|
|
184
|
+
/**
|
|
185
|
+
* @deprecated Use client.setRefreshToken() - mantido para compatibilidade
|
|
186
|
+
*/
|
|
162
187
|
setRefreshToken(token: string | null) {
|
|
163
|
-
this.
|
|
188
|
+
this.client.setRefreshToken(token);
|
|
164
189
|
}
|
|
165
190
|
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
191
|
+
// ==========================================================================
|
|
192
|
+
// MÉTODOS COMPATÍVEIS COM SUPABASE / SHOWCASE STYLE
|
|
193
|
+
// ==========================================================================
|
|
169
194
|
|
|
170
195
|
/**
|
|
171
196
|
* Alias para login, retornando { user, error } padrão do Showcase
|
|
@@ -182,12 +207,16 @@ export class AuthModule {
|
|
|
182
207
|
/**
|
|
183
208
|
* Alias para register, retornando { user, error }
|
|
184
209
|
*/
|
|
185
|
-
async signUp(credentials: {
|
|
210
|
+
async signUp(credentials: {
|
|
211
|
+
email: string;
|
|
212
|
+
password: string;
|
|
213
|
+
data?: { name: string };
|
|
214
|
+
}) {
|
|
186
215
|
try {
|
|
187
216
|
const data = await this.register({
|
|
188
217
|
email: credentials.email,
|
|
189
218
|
password: credentials.password,
|
|
190
|
-
name: credentials.data?.name || credentials.email.split(
|
|
219
|
+
name: credentials.data?.name || credentials.email.split("@")[0],
|
|
191
220
|
});
|
|
192
221
|
return { user: data.user, error: null };
|
|
193
222
|
} catch (err: any) {
|
|
@@ -205,21 +234,53 @@ export class AuthModule {
|
|
|
205
234
|
|
|
206
235
|
/**
|
|
207
236
|
* Recupera a sessão atual (User + Token).
|
|
208
|
-
*
|
|
237
|
+
* Primeiro tenta memória, depois localStorage.
|
|
238
|
+
* Se não encontrar, retorna null.
|
|
209
239
|
*/
|
|
210
240
|
async getSession() {
|
|
211
241
|
const token = this.client.getToken();
|
|
212
242
|
if (!token) return null;
|
|
213
243
|
|
|
214
|
-
//
|
|
215
|
-
// O ideal seria validar o token no backend (/auth/me), mas vamos simplificar.
|
|
244
|
+
// Tenta memória primeiro
|
|
216
245
|
if (this.currentUser) {
|
|
217
246
|
return {
|
|
218
247
|
user: this.currentUser,
|
|
219
|
-
access_token: token
|
|
248
|
+
access_token: token,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Tenta localStorage
|
|
253
|
+
const savedUser = this.client.getUser();
|
|
254
|
+
if (savedUser) {
|
|
255
|
+
this.currentUser = savedUser;
|
|
256
|
+
return {
|
|
257
|
+
user: savedUser,
|
|
258
|
+
access_token: token,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Última opção: valida token no backend
|
|
263
|
+
try {
|
|
264
|
+
const { data } = await this.http.get<{ user: User }>("/auth/me");
|
|
265
|
+
this.currentUser = data.user;
|
|
266
|
+
this.client.setUser(data.user);
|
|
267
|
+
return {
|
|
268
|
+
user: data.user,
|
|
269
|
+
access_token: token,
|
|
220
270
|
};
|
|
271
|
+
} catch {
|
|
272
|
+
// Token inválido/expirado - limpa sessão
|
|
273
|
+
this.client.clearSession();
|
|
274
|
+
this.currentUser = null;
|
|
275
|
+
return null;
|
|
221
276
|
}
|
|
277
|
+
}
|
|
222
278
|
|
|
223
|
-
|
|
279
|
+
/**
|
|
280
|
+
* Retorna o usuário atual (sem validar token).
|
|
281
|
+
* Útil para acesso síncrono aos dados do usuário.
|
|
282
|
+
*/
|
|
283
|
+
getCurrentUser(): User | null {
|
|
284
|
+
return this.currentUser || this.client.getUser();
|
|
224
285
|
}
|
|
225
286
|
}
|
package/src/http-client.ts
CHANGED
|
@@ -1,23 +1,53 @@
|
|
|
1
1
|
// src/http-client.ts
|
|
2
|
-
import axios, {
|
|
2
|
+
import axios, {
|
|
3
|
+
type AxiosInstance,
|
|
4
|
+
type AxiosError,
|
|
5
|
+
type InternalAxiosRequestConfig,
|
|
6
|
+
} from "axios";
|
|
3
7
|
import type { PlataformaClient } from "./index.js";
|
|
4
8
|
import { handleAxiosError } from "./errors.js";
|
|
5
9
|
|
|
10
|
+
// Flag para evitar múltiplos refreshes simultâneos
|
|
11
|
+
let isRefreshing = false;
|
|
12
|
+
|
|
13
|
+
// Fila de requisições aguardando o refresh
|
|
14
|
+
let failedQueue: Array<{
|
|
15
|
+
resolve: (token: string) => void;
|
|
16
|
+
reject: (error: any) => void;
|
|
17
|
+
}> = [];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Processa a fila de requisições após o refresh.
|
|
21
|
+
*/
|
|
22
|
+
const processQueue = (error: any, token: string | null = null) => {
|
|
23
|
+
failedQueue.forEach((prom) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
prom.reject(error);
|
|
26
|
+
} else {
|
|
27
|
+
prom.resolve(token!);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
failedQueue = [];
|
|
31
|
+
};
|
|
32
|
+
|
|
6
33
|
/**
|
|
7
34
|
* Cria uma instância do Axios pré-configurada.
|
|
8
35
|
* - Injeta automaticamente headers de Auth e Projeto.
|
|
36
|
+
* - Renova token automaticamente quando expira (401).
|
|
9
37
|
* - Converte erros em AetherError.
|
|
10
38
|
*/
|
|
11
39
|
export function createHttpClient(client: PlataformaClient): AxiosInstance {
|
|
12
40
|
const http = axios.create({
|
|
13
|
-
// Adiciona o /v1 automaticamente em todas as chamadas do SDK
|
|
14
41
|
baseURL: `${client.apiUrl}/v1`,
|
|
15
42
|
headers: {
|
|
16
43
|
"Content-Type": "application/json",
|
|
17
44
|
},
|
|
18
45
|
});
|
|
19
46
|
|
|
20
|
-
//
|
|
47
|
+
// ===========================================================================
|
|
48
|
+
// INTERCEPTOR DE REQUEST
|
|
49
|
+
// Injeta token e projectId em todas as requisições
|
|
50
|
+
// ===========================================================================
|
|
21
51
|
http.interceptors.request.use((config) => {
|
|
22
52
|
const token = client.getToken();
|
|
23
53
|
if (token) {
|
|
@@ -31,10 +61,92 @@ export function createHttpClient(client: PlataformaClient): AxiosInstance {
|
|
|
31
61
|
return config;
|
|
32
62
|
});
|
|
33
63
|
|
|
34
|
-
//
|
|
64
|
+
// ===========================================================================
|
|
65
|
+
// INTERCEPTOR DE RESPONSE
|
|
66
|
+
// Detecta 401 e tenta refresh automático do token
|
|
67
|
+
// ===========================================================================
|
|
35
68
|
http.interceptors.response.use(
|
|
36
69
|
(response) => response,
|
|
37
|
-
(error) =>
|
|
70
|
+
async (error: AxiosError) => {
|
|
71
|
+
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
|
72
|
+
_retry?: boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Verifica se é erro 401 (não autorizado) e não é retry
|
|
76
|
+
const isUnauthorized = error.response?.status === 401;
|
|
77
|
+
const isRetry = originalRequest?._retry;
|
|
78
|
+
const isAuthRoute = originalRequest?.url?.includes("/auth/");
|
|
79
|
+
|
|
80
|
+
// Não tenta refresh se:
|
|
81
|
+
// - Não é 401
|
|
82
|
+
// - Já é um retry
|
|
83
|
+
// - É uma rota de auth (login, register, refresh)
|
|
84
|
+
// - Não tem refresh token disponível
|
|
85
|
+
if (!isUnauthorized || isRetry || isAuthRoute) {
|
|
86
|
+
return handleAxiosError(error);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const refreshToken = client.getRefreshToken();
|
|
90
|
+
if (!refreshToken) {
|
|
91
|
+
// Sem refresh token, limpa sessão e propaga erro
|
|
92
|
+
client.clearSession();
|
|
93
|
+
return handleAxiosError(error);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Se já está fazendo refresh, aguarda na fila
|
|
97
|
+
if (isRefreshing) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
failedQueue.push({ resolve, reject });
|
|
100
|
+
})
|
|
101
|
+
.then((token) => {
|
|
102
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
103
|
+
return http(originalRequest);
|
|
104
|
+
})
|
|
105
|
+
.catch((err) => handleAxiosError(err));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Marca como retry e inicia refresh
|
|
109
|
+
originalRequest._retry = true;
|
|
110
|
+
isRefreshing = true;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Chama endpoint de refresh diretamente (sem interceptor)
|
|
114
|
+
const { data } = await axios.post(
|
|
115
|
+
`${client.apiUrl}/v1/auth/refresh`,
|
|
116
|
+
{ refreshToken },
|
|
117
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const newAccessToken = data.accessToken;
|
|
121
|
+
const newRefreshToken = data.refreshToken;
|
|
122
|
+
|
|
123
|
+
// Atualiza tokens no client (persiste no localStorage)
|
|
124
|
+
client.setToken(newAccessToken);
|
|
125
|
+
if (newRefreshToken) {
|
|
126
|
+
client.setRefreshToken(newRefreshToken);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Processa fila de requisições pendentes
|
|
130
|
+
processQueue(null, newAccessToken);
|
|
131
|
+
|
|
132
|
+
// Refaz a requisição original com novo token
|
|
133
|
+
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
|
134
|
+
return http(originalRequest);
|
|
135
|
+
} catch (refreshError: any) {
|
|
136
|
+
// Refresh falhou - sessão expirada
|
|
137
|
+
processQueue(refreshError, null);
|
|
138
|
+
client.clearSession();
|
|
139
|
+
|
|
140
|
+
// Emite evento de sessão expirada (se houver listener)
|
|
141
|
+
if (typeof window !== "undefined") {
|
|
142
|
+
window.dispatchEvent(new CustomEvent("aether:session-expired"));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return handleAxiosError(refreshError);
|
|
146
|
+
} finally {
|
|
147
|
+
isRefreshing = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
38
150
|
);
|
|
39
151
|
|
|
40
152
|
return http;
|
package/src/index.ts
CHANGED
|
@@ -1,26 +1,50 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import type { AxiosInstance } from "axios";
|
|
3
3
|
import { createHttpClient } from "./http-client.js";
|
|
4
|
-
import { AuthModule, User } from "./auth.js";
|
|
4
|
+
import { AuthModule, User } from "./auth.js";
|
|
5
5
|
import { DatabaseModule } from "./database.js";
|
|
6
6
|
import { StorageModule } from "./storage.js";
|
|
7
7
|
import { FunctionsModule } from "./functions.js";
|
|
8
8
|
import { PushModule } from "./push.js";
|
|
9
9
|
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// CONSTANTES DE STORAGE
|
|
12
|
+
// Chaves padronizadas para localStorage - evita conflito com outros SDKs
|
|
13
|
+
// =============================================================================
|
|
14
|
+
const STORAGE_KEYS = {
|
|
15
|
+
TOKEN: "aether_token",
|
|
16
|
+
REFRESH_TOKEN: "aether_refresh_token",
|
|
17
|
+
USER: "aether_user",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* Configuração usada para criar o cliente principal da plataforma.
|
|
12
22
|
*/
|
|
13
23
|
export type ClientConfig = {
|
|
14
24
|
// Suporte duplo para compatibilidade:
|
|
15
|
-
// apiUrl (nome original) OU baseUrl (nome novo do App)
|
|
16
25
|
apiUrl?: string;
|
|
17
26
|
baseUrl?: string;
|
|
18
|
-
|
|
19
|
-
// projectId (nome original) OU apiKey (nome novo do App)
|
|
20
27
|
projectId?: string;
|
|
21
28
|
apiKey?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Habilita persistência automática de sessão no localStorage.
|
|
32
|
+
* Padrão: true em browsers, false em Node.js/SSR.
|
|
33
|
+
*/
|
|
34
|
+
persistSession?: boolean;
|
|
22
35
|
};
|
|
23
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Verifica se estamos em ambiente browser com localStorage disponível.
|
|
39
|
+
* Necessário para evitar erros em SSR (Next.js, Nuxt, etc).
|
|
40
|
+
*/
|
|
41
|
+
function isBrowser(): boolean {
|
|
42
|
+
return (
|
|
43
|
+
typeof window !== "undefined" &&
|
|
44
|
+
typeof window.localStorage !== "undefined"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
export class PlataformaClient {
|
|
25
49
|
public auth: AuthModule;
|
|
26
50
|
public db: DatabaseModule;
|
|
@@ -35,7 +59,9 @@ export class PlataformaClient {
|
|
|
35
59
|
public projectId: string;
|
|
36
60
|
|
|
37
61
|
public http: AxiosInstance;
|
|
62
|
+
|
|
38
63
|
private _token: string | null = null;
|
|
64
|
+
private _persistSession: boolean;
|
|
39
65
|
|
|
40
66
|
constructor(config: ClientConfig) {
|
|
41
67
|
// Resolve URL (prioridade para baseUrl se existir, senão apiUrl)
|
|
@@ -49,6 +75,13 @@ export class PlataformaClient {
|
|
|
49
75
|
this.apiUrl = url.replace(/\/+$/, "");
|
|
50
76
|
this.projectId = project;
|
|
51
77
|
|
|
78
|
+
// Persistência habilitada por padrão apenas em browsers
|
|
79
|
+
this._persistSession = config.persistSession ?? isBrowser();
|
|
80
|
+
|
|
81
|
+
// Restaura sessão salva ANTES de criar o httpClient
|
|
82
|
+
// Isso garante que requisições iniciais já tenham o token
|
|
83
|
+
this._restoreSession();
|
|
84
|
+
|
|
52
85
|
this.http = createHttpClient(this);
|
|
53
86
|
|
|
54
87
|
// Inicializa módulos
|
|
@@ -62,18 +95,141 @@ export class PlataformaClient {
|
|
|
62
95
|
this.database = this.db;
|
|
63
96
|
}
|
|
64
97
|
|
|
65
|
-
|
|
98
|
+
// ===========================================================================
|
|
99
|
+
// TOKEN DE ACESSO
|
|
100
|
+
// ===========================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Define o token de acesso (JWT).
|
|
104
|
+
* Se persistSession estiver ativo, salva automaticamente no localStorage.
|
|
105
|
+
*/
|
|
106
|
+
setToken(token: string | null): void {
|
|
66
107
|
this._token = token;
|
|
108
|
+
|
|
109
|
+
if (this._persistSession && isBrowser()) {
|
|
110
|
+
if (token) {
|
|
111
|
+
localStorage.setItem(STORAGE_KEYS.TOKEN, token);
|
|
112
|
+
} else {
|
|
113
|
+
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
67
116
|
}
|
|
68
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Retorna o token de acesso atual.
|
|
120
|
+
*/
|
|
69
121
|
getToken(): string | null {
|
|
70
122
|
return this._token;
|
|
71
123
|
}
|
|
124
|
+
|
|
125
|
+
// ===========================================================================
|
|
126
|
+
// REFRESH TOKEN
|
|
127
|
+
// ===========================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Salva o refresh token.
|
|
131
|
+
* Usado internamente pelo AuthModule após login.
|
|
132
|
+
*/
|
|
133
|
+
setRefreshToken(token: string | null): void {
|
|
134
|
+
if (this._persistSession && isBrowser()) {
|
|
135
|
+
if (token) {
|
|
136
|
+
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, token);
|
|
137
|
+
} else {
|
|
138
|
+
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Retorna o refresh token salvo no localStorage.
|
|
145
|
+
*/
|
|
146
|
+
getRefreshToken(): string | null {
|
|
147
|
+
if (this._persistSession && isBrowser()) {
|
|
148
|
+
return localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ===========================================================================
|
|
154
|
+
// DADOS DO USUÁRIO
|
|
155
|
+
// ===========================================================================
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Salva dados do usuário logado no localStorage.
|
|
159
|
+
*/
|
|
160
|
+
setUser(user: User | null): void {
|
|
161
|
+
if (this._persistSession && isBrowser()) {
|
|
162
|
+
if (user) {
|
|
163
|
+
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(user));
|
|
164
|
+
} else {
|
|
165
|
+
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Retorna dados do usuário salvo no localStorage.
|
|
172
|
+
*/
|
|
173
|
+
getUser(): User | null {
|
|
174
|
+
if (this._persistSession && isBrowser()) {
|
|
175
|
+
const saved = localStorage.getItem(STORAGE_KEYS.USER);
|
|
176
|
+
if (saved) {
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(saved) as User;
|
|
179
|
+
} catch {
|
|
180
|
+
// JSON corrompido - limpa
|
|
181
|
+
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ===========================================================================
|
|
190
|
+
// GERENCIAMENTO DE SESSÃO
|
|
191
|
+
// ===========================================================================
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Limpa toda a sessão (token, refresh, user).
|
|
195
|
+
* Chamado automaticamente no logout.
|
|
196
|
+
*/
|
|
197
|
+
clearSession(): void {
|
|
198
|
+
this._token = null;
|
|
199
|
+
|
|
200
|
+
if (isBrowser()) {
|
|
201
|
+
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
|
202
|
+
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
203
|
+
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Verifica se existe uma sessão salva (token presente).
|
|
209
|
+
*/
|
|
210
|
+
hasSession(): boolean {
|
|
211
|
+
return this._token !== null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Restaura sessão do localStorage ao inicializar o client.
|
|
216
|
+
* Executado automaticamente no constructor.
|
|
217
|
+
*/
|
|
218
|
+
private _restoreSession(): void {
|
|
219
|
+
if (!this._persistSession || !isBrowser()) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const savedToken = localStorage.getItem(STORAGE_KEYS.TOKEN);
|
|
224
|
+
if (savedToken) {
|
|
225
|
+
this._token = savedToken;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
72
228
|
}
|
|
73
229
|
|
|
74
230
|
// ===== EXPORTS =====
|
|
75
231
|
export { AetherError } from "./errors.js";
|
|
76
|
-
export type { LoginResponse, Session, User } from "./auth.js";
|
|
232
|
+
export type { LoginResponse, Session, User } from "./auth.js";
|
|
77
233
|
export type { ListOptions } from "./database.js";
|
|
78
234
|
export type {
|
|
79
235
|
PushPlatform,
|