-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.js
More file actions
146 lines (121 loc) · 4.32 KB
/
Copy pathapi.js
File metadata and controls
146 lines (121 loc) · 4.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// auth-client/api.js
import axios from 'axios';
import { getConfig } from './config.js';
import { getToken, setToken, clearToken } from './token.js';
import { refreshToken as performRefresh } from './core.js';
import { diagnosticHeaders, emitAuthDiagnostic } from './diagnostics.js';
const api = axios.create({
withCredentials: true,
});
api.interceptors.request.use((config) => {
const runtimeConfig = getConfig();
if (!config.baseURL) {
config.baseURL = runtimeConfig?.authBaseUrl || 'http://auth.local.test:4000/auth';
}
if (!config.headers) {
config.headers = {};
}
if (runtimeConfig?.clientKey && !config.headers['X-Client-Key']) {
config.headers['X-Client-Key'] = runtimeConfig.clientKey;
}
Object.assign(config.headers, diagnosticHeaders());
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
let refreshPromise = null;
// Event dispatched when the server refuses a request because the client is in
// single-organization mode. Consumers (e.g. an organization context) can listen
// and re-resolve to the user's primary organization.
// - 403 TENANT_ACCESS_DENIED: the selected/sent organization is not accessible
// under single-org mode (a non-primary org).
// - 409 SINGLE_ORGANIZATION_MODE: a join/create was refused because the user
// may only belong to one organization.
export const SINGLE_ORG_DENIED_EVENT = 'auth:single-org-denied';
function responseErrorCode(response) {
const data = response?.data;
return data?.errors?.code || data?.code || null;
}
export function isTenantAccessDenied(response) {
return response?.status === 403 && responseErrorCode(response) === 'TENANT_ACCESS_DENIED';
}
export function isSingleOrganizationMode(response) {
return response?.status === 409 && responseErrorCode(response) === 'SINGLE_ORGANIZATION_MODE';
}
function dispatchSingleOrgDenied(response) {
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') return;
window.dispatchEvent(new CustomEvent(SINGLE_ORG_DENIED_EVENT, {
detail: {
code: responseErrorCode(response),
status: response?.status || null,
currentOrgId: response?.data?.errors?.current_org_id || null,
},
}));
}
api.interceptors.response.use(
(response) => response,
async (error) => {
const { response, config } = error || {};
if (!response || !config) {
return Promise.reject(error);
}
// Single-org refusals: surface a framework-agnostic event so the app can
// drop a stale non-primary selection and re-resolve to the primary org.
// The error still rejects so the immediate caller can handle it too.
if (isTenantAccessDenied(response) || isSingleOrganizationMode(response)) {
dispatchSingleOrgDenied(response);
return Promise.reject(error);
}
if (response.status !== 401 || config._retry) {
return Promise.reject(error);
}
config._retry = true;
emitAuthDiagnostic('API_401_REFRESH_STARTED', 'PENDING', 'HTTP_401', {
clientKey: getConfig().clientKey,
status: 401,
});
if (!refreshPromise) {
refreshPromise = performRefresh()
.then((newToken) => {
refreshPromise = null;
if (newToken) {
setToken(newToken);
}
return newToken;
})
.catch((refreshError) => {
refreshPromise = null;
// ❌ REMOVED: clearToken() here caused cascading logouts.
// The calling code (AuthContext) handles token clearing
// based on the specific error context (401 vs network error).
throw refreshError;
});
}
try {
const refreshedToken = await refreshPromise;
if (refreshedToken) {
config.headers.Authorization = `Bearer ${refreshedToken}`;
return api(config);
}
} catch (refreshErr) {
// Refresh failed — propagate the ORIGINAL 401 error so the caller
// knows the request was unauthorized (not a refresh-specific error).
return Promise.reject(error);
}
return Promise.reject(error);
}
);
api.validateSession = async () => {
try {
const response = await api.get('/account/validate-session');
return response.data.valid;
} catch (err) {
if (err.response?.status === 401) {
return false;
}
throw err;
}
};
export default api;