@loka-sms/sso 1.1.7 → 1.2.0
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/README.md +2 -2
- package/dist/auth.js +47 -47
- package/dist/components/OAuthCallback.d.ts +1 -1
- package/dist/components/OAuthCallback.js +5 -2
- package/dist/components/OAuthTransfer.d.ts +2 -1
- package/dist/components/OAuthTransfer.js +78 -21
- package/dist/hooks/useCrossAppLogout.js +10 -4
- package/dist/hooks/useOAuthCallback.js +25 -18
- package/dist/interceptor.js +2 -12
- package/dist/navigation.js +18 -3
- package/dist/oauthRedirect.js +13 -27
- package/dist/ticket.js +1 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -206,8 +206,8 @@ function AuthCallback() {
|
|
|
206
206
|
|
|
207
207
|
| Export | Description |
|
|
208
208
|
|--------|-------------|
|
|
209
|
-
| `useCrossAppLogout()` | Listens on BroadcastChannel `loka-sso-logout`. Clears auth state and redirects to `/signin`. |
|
|
210
|
-
| `useOAuthCallback(input)` | Handles OAuth2 PKCE code exchange. Returns `{ error, loading, phase }`. |
|
|
209
|
+
| `useCrossAppLogout()` | Listens on BroadcastChannel `loka-sso-logout`. Clears auth state and redirects to `/signin`. Built-in 2-second dedup to prevent re-broadcast loops. |
|
|
210
|
+
| `useOAuthCallback(input)` | Handles OAuth2 PKCE code exchange. Returns `{ error, loading, phase }`. Awaits cookie sync before redirect to prevent landing-page race. |
|
|
211
211
|
| `useIssueTicket(apiBase)` | Issues a one-time ticket using current localStorage/cookie access token. |
|
|
212
212
|
| `useNavigateToApp(apiBase)` | React state wrapper for `navigateToApp()`. |
|
|
213
213
|
|
package/dist/auth.js
CHANGED
|
@@ -12,13 +12,6 @@ const constants_1 = require("./constants");
|
|
|
12
12
|
const navigation_1 = require("./navigation");
|
|
13
13
|
const ticket_1 = require("./ticket");
|
|
14
14
|
const AuthContext = (0, react_1.createContext)(null);
|
|
15
|
-
function getCookie(name) {
|
|
16
|
-
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
|
17
|
-
return match ? decodeURIComponent(match[2]) : '';
|
|
18
|
-
}
|
|
19
|
-
function clearCookie(name) {
|
|
20
|
-
document.cookie = `${name}=; path=/; Max-Age=0; SameSite=Lax`;
|
|
21
|
-
}
|
|
22
15
|
function decodeJwt(token) {
|
|
23
16
|
try {
|
|
24
17
|
return JSON.parse(atob(token.split('.')[1]));
|
|
@@ -51,17 +44,7 @@ function userFromToken(token) {
|
|
|
51
44
|
};
|
|
52
45
|
}
|
|
53
46
|
function persistAuth(token, refreshToken, user) {
|
|
54
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.
|
|
55
|
-
if (refreshToken)
|
|
56
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
|
57
|
-
const nextUser = user || userFromToken(token);
|
|
58
|
-
if (nextUser) {
|
|
59
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_PROFILE, JSON.stringify(nextUser));
|
|
60
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_ID, nextUser.sub || nextUser.id || '');
|
|
61
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_ROLE, (nextUser.role || '').toLowerCase());
|
|
62
|
-
if (nextUser.schoolId)
|
|
63
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.SCHOOL_ID, nextUser.schoolId);
|
|
64
|
-
}
|
|
47
|
+
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_PROFILE, JSON.stringify(user || userFromToken(token) || {}));
|
|
65
48
|
}
|
|
66
49
|
function clearAuthStorage() {
|
|
67
50
|
localStorage.removeItem(constants_1.SSO_STORAGE_KEYS.ACCESS_TOKEN);
|
|
@@ -70,29 +53,34 @@ function clearAuthStorage() {
|
|
|
70
53
|
localStorage.removeItem(constants_1.SSO_STORAGE_KEYS.USER_ID);
|
|
71
54
|
localStorage.removeItem(constants_1.SSO_STORAGE_KEYS.USER_ROLE);
|
|
72
55
|
localStorage.removeItem(constants_1.SSO_STORAGE_KEYS.SCHOOL_ID);
|
|
73
|
-
clearCookie('sms_ac_token');
|
|
74
|
-
clearCookie('sms_refresh_token');
|
|
75
|
-
clearCookie('sms_user_profile');
|
|
76
|
-
clearCookie('sms_school_id');
|
|
77
56
|
}
|
|
78
|
-
function readStoredAuth() {
|
|
79
|
-
const token = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.ACCESS_TOKEN) || getCookie(constants_1.SSO_STORAGE_KEYS.ACCESS_TOKEN);
|
|
80
|
-
const refreshToken = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.REFRESH_TOKEN) || getCookie(constants_1.SSO_STORAGE_KEYS.REFRESH_TOKEN);
|
|
81
|
-
if (!token || isExpired(token))
|
|
82
|
-
return { token: null, refreshToken: null, user: null };
|
|
83
|
-
let user = null;
|
|
57
|
+
async function readStoredAuth(apiUrl) {
|
|
84
58
|
try {
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
user
|
|
59
|
+
const res = await fetch(`${apiUrl}/auth/me`, { credentials: 'include' });
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
return { token: null, refreshToken: null, user: null };
|
|
62
|
+
const body = await res.json();
|
|
63
|
+
const data = body?.data ?? body;
|
|
64
|
+
const user = {
|
|
65
|
+
id: data.id || data.sub,
|
|
66
|
+
sub: data.sub || data.id,
|
|
67
|
+
name: data.fullName || data.name || data.email,
|
|
68
|
+
fullName: data.fullName || data.name,
|
|
69
|
+
email: data.email,
|
|
70
|
+
role: data.role,
|
|
71
|
+
schoolId: data.schoolId,
|
|
72
|
+
accessibleSchools: data.schools || [],
|
|
73
|
+
features: data.features || [],
|
|
74
|
+
capabilities: data.capabilities || [],
|
|
75
|
+
};
|
|
76
|
+
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_PROFILE, JSON.stringify(user));
|
|
77
|
+
return { token: 'cookie-based', refreshToken: null, user };
|
|
88
78
|
}
|
|
89
79
|
catch {
|
|
90
|
-
user
|
|
80
|
+
return { token: null, refreshToken: null, user: null };
|
|
91
81
|
}
|
|
92
|
-
user = user || userFromToken(token);
|
|
93
|
-
persistAuth(token, refreshToken || undefined, user);
|
|
94
|
-
return { token, refreshToken, user };
|
|
95
82
|
}
|
|
83
|
+
let refreshAuthPromise = null;
|
|
96
84
|
function broadcast(type, payload) {
|
|
97
85
|
try {
|
|
98
86
|
const channel = new BroadcastChannel(type === 'login' ? constants_1.SSO_CHANNELS.LOGIN : constants_1.SSO_CHANNELS.LOGOUT);
|
|
@@ -131,19 +119,24 @@ function SignInPage({ apiUrl, coreUrl }) {
|
|
|
131
119
|
return ((0, jsx_runtime_1.jsx)("div", { className: "flex min-h-screen items-center justify-center bg-gray-50 px-4", children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "w-full max-w-md rounded-2xl bg-white p-8 shadow-lg", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-semibold text-gray-900", children: "Masuk ke Loka SMS" }), (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-sm text-gray-500", children: "Gunakan akun sekolah Anda untuk melanjutkan." }), error && (0, jsx_runtime_1.jsx)("p", { className: "mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600", children: error }), (0, jsx_runtime_1.jsxs)("label", { className: "mt-6 block text-sm font-medium text-gray-700", children: ["Email", (0, jsx_runtime_1.jsx)("input", { value: email, onChange: (event) => setEmail(event.target.value), type: "email", required: true, className: "mt-2 w-full rounded-lg border border-gray-300 px-3 py-2 outline-none focus:border-blue-500" })] }), (0, jsx_runtime_1.jsxs)("label", { className: "mt-4 block text-sm font-medium text-gray-700", children: ["Password", (0, jsx_runtime_1.jsx)("input", { value: password, onChange: (event) => setPassword(event.target.value), type: "password", required: true, className: "mt-2 w-full rounded-lg border border-gray-300 px-3 py-2 outline-none focus:border-blue-500" })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: loading, className: "mt-6 w-full rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 disabled:opacity-60", children: loading ? 'Memproses...' : 'Masuk' }), apiUrl && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleCoreLogin, className: "mt-3 w-full rounded-lg border border-gray-300 px-4 py-2 font-medium text-gray-700 hover:bg-gray-50", children: "Masuk via Core" }))] }) }));
|
|
132
120
|
}
|
|
133
121
|
function AuthProvider({ apiUrl = '/api', clientId, children }) {
|
|
134
|
-
const [state, setState] = (0, react_1.useState)(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
refreshToken: stored.refreshToken,
|
|
140
|
-
isLoading: false,
|
|
141
|
-
};
|
|
122
|
+
const [state, setState] = (0, react_1.useState)({
|
|
123
|
+
user: null,
|
|
124
|
+
token: null,
|
|
125
|
+
refreshToken: null,
|
|
126
|
+
isLoading: true,
|
|
142
127
|
});
|
|
143
|
-
const refreshAuth = () => {
|
|
144
|
-
|
|
145
|
-
|
|
128
|
+
const refreshAuth = async () => {
|
|
129
|
+
if (refreshAuthPromise)
|
|
130
|
+
return refreshAuthPromise;
|
|
131
|
+
refreshAuthPromise = (async () => {
|
|
132
|
+
const stored = await readStoredAuth(apiUrl);
|
|
133
|
+
setState((prev) => ({ ...prev, ...stored, isLoading: false }));
|
|
134
|
+
})().finally(() => { refreshAuthPromise = null; });
|
|
135
|
+
return refreshAuthPromise;
|
|
146
136
|
};
|
|
137
|
+
(0, react_1.useEffect)(() => {
|
|
138
|
+
refreshAuth();
|
|
139
|
+
}, [apiUrl]);
|
|
147
140
|
(0, react_1.useEffect)(() => {
|
|
148
141
|
const onStorage = (event) => {
|
|
149
142
|
if (event.key === constants_1.SSO_STORAGE_KEYS.ACCESS_TOKEN || event.key === constants_1.SSO_STORAGE_KEYS.USER_PROFILE)
|
|
@@ -201,8 +194,15 @@ function AuthProvider({ apiUrl = '/api', clientId, children }) {
|
|
|
201
194
|
const token = data.token || data.accessToken;
|
|
202
195
|
const refreshToken = data.refreshToken;
|
|
203
196
|
const user = data.user || userFromToken(token);
|
|
204
|
-
if (!token
|
|
197
|
+
if (!token)
|
|
205
198
|
throw new Error('Token login tidak lengkap.');
|
|
199
|
+
if (refreshToken) {
|
|
200
|
+
await fetch(`${apiUrl}/auth/set-cookie`, {
|
|
201
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
202
|
+
credentials: 'include',
|
|
203
|
+
body: JSON.stringify({ token, refreshToken }),
|
|
204
|
+
}).catch(() => { });
|
|
205
|
+
}
|
|
206
206
|
persistAuth(token, refreshToken, user);
|
|
207
207
|
setState({ user, token, refreshToken, isLoading: false });
|
|
208
208
|
broadcast('login', { token, user });
|
|
@@ -7,4 +7,4 @@ export interface OAuthCallbackProps extends Omit<OAuthCallbackInput, 'apiBase' |
|
|
|
7
7
|
loginLinkText?: string;
|
|
8
8
|
loginPath?: string;
|
|
9
9
|
}
|
|
10
|
-
export declare function OAuthCallback({ clientId, apiBase, redirectPath, loadingText, errorText, loginLinkText, loginPath, }: OAuthCallbackProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function OAuthCallback({ clientId, apiBase, redirectPath, loadingText, errorText, loginLinkText, loginPath, }: OAuthCallbackProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -6,7 +6,10 @@ const useOAuthCallback_1 = require("../hooks/useOAuthCallback");
|
|
|
6
6
|
function OAuthCallback({ clientId, apiBase, redirectPath, loadingText = 'Menyelesaikan login...', errorText, loginLinkText = 'Kembali ke login', loginPath = '/signin', }) {
|
|
7
7
|
const { error, loading, phase } = (0, useOAuthCallback_1.useOAuthCallback)({ clientId, apiBase, redirectPath });
|
|
8
8
|
if (error) {
|
|
9
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center min-h-screen", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("
|
|
9
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950 px-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "w-full max-w-sm text-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex justify-center mb-6", children: (0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center w-16 h-16 rounded-2xl bg-red-100 dark:bg-red-900/30", children: (0, jsx_runtime_1.jsx)("svg", { className: "w-8 h-8 text-red-600 dark:text-red-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" }) }) }) }), (0, jsx_runtime_1.jsx)("h1", { className: "text-xl font-semibold text-gray-900 dark:text-white mb-2", children: "Login Gagal" }), (0, jsx_runtime_1.jsx)("div", { className: "rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4 mb-4 text-left", children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-red-700 dark:text-red-300", children: errorText || error }) }), (0, jsx_runtime_1.jsx)("a", { href: loginPath, className: "inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-brand-500 text-white text-sm font-medium hover:bg-brand-600 transition", children: loginLinkText })] }) }));
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
if (loading) {
|
|
12
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950 px-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "w-full max-w-sm text-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex justify-center mb-6", children: (0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center w-16 h-16 rounded-2xl bg-brand-500", children: (0, jsx_runtime_1.jsxs)("svg", { className: "w-8 h-8 text-white animate-spin", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })] }) }) }), (0, jsx_runtime_1.jsx)("h1", { className: "text-xl font-semibold text-gray-900 dark:text-white mb-1", children: "Login" }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: loadingText })] }) }));
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
12
15
|
}
|
|
@@ -3,5 +3,6 @@ export interface OAuthTransferProps {
|
|
|
3
3
|
redirectPath?: string;
|
|
4
4
|
loginPath?: string;
|
|
5
5
|
clientId?: string;
|
|
6
|
+
coreFrontendUrl?: string;
|
|
6
7
|
}
|
|
7
|
-
export declare function OAuthTransfer({ apiBase, redirectPath, loginPath, clientId, }: OAuthTransferProps): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export declare function OAuthTransfer({ apiBase, redirectPath, loginPath, clientId, coreFrontendUrl, }: OAuthTransferProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -6,9 +6,42 @@ const react_1 = require("react");
|
|
|
6
6
|
const constants_1 = require("../constants");
|
|
7
7
|
const ticket_1 = require("../ticket");
|
|
8
8
|
const processedTickets = new Set();
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const APP_NAMES = {
|
|
10
|
+
core: 'Portal Utama',
|
|
11
|
+
administrasi: 'Administrasi',
|
|
12
|
+
elearning: 'E-Learning',
|
|
13
|
+
presensi: 'Presensi Digital',
|
|
14
|
+
erapor: 'E-Rapor',
|
|
15
|
+
keuangan: 'Keuangan',
|
|
16
|
+
ppdb: 'PPDB',
|
|
17
|
+
notifikasi: 'Notifikasi',
|
|
18
|
+
cms: 'CMS Platform',
|
|
19
|
+
};
|
|
20
|
+
function resolveAppName(clientId) {
|
|
21
|
+
return (clientId && APP_NAMES[clientId]) || 'Aplikasi';
|
|
22
|
+
}
|
|
23
|
+
function isSafeRedirect(destination, origin) {
|
|
24
|
+
if (!destination || destination === '' || destination === '/')
|
|
25
|
+
return true;
|
|
26
|
+
if (destination.startsWith('/') && !destination.startsWith('//'))
|
|
27
|
+
return true;
|
|
28
|
+
try {
|
|
29
|
+
const url = new URL(destination, origin);
|
|
30
|
+
return url.origin === origin;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function stripQueryParams() {
|
|
37
|
+
const url = new URL(window.location.href);
|
|
38
|
+
url.search = '';
|
|
39
|
+
window.history.replaceState({}, '', url.toString());
|
|
40
|
+
}
|
|
41
|
+
function OAuthTransfer({ apiBase, redirectPath = '/', loginPath = '/signin', clientId, coreFrontendUrl = '/', }) {
|
|
42
|
+
const [phase, setPhase] = (0, react_1.useState)('init');
|
|
11
43
|
const [error, setError] = (0, react_1.useState)('');
|
|
44
|
+
const [destApp, setDestApp] = (0, react_1.useState)(resolveAppName(clientId));
|
|
12
45
|
const hasProcessed = (0, react_1.useRef)(false);
|
|
13
46
|
(0, react_1.useEffect)(() => {
|
|
14
47
|
if (hasProcessed.current)
|
|
@@ -18,9 +51,12 @@ function OAuthTransfer({ apiBase, redirectPath = '/', loginPath = '/signin', cli
|
|
|
18
51
|
const ticket = params.get('ticket');
|
|
19
52
|
const token = params.get('token');
|
|
20
53
|
const refreshToken = params.get('refreshToken') || params.get('refresh_token');
|
|
21
|
-
const
|
|
54
|
+
const rawNext = params.get('next') || redirectPath;
|
|
22
55
|
const base = apiBase || '/api';
|
|
56
|
+
const next = isSafeRedirect(rawNext, window.location.origin) ? rawNext : '/';
|
|
57
|
+
stripQueryParams();
|
|
23
58
|
async function fetchProfile(accessToken) {
|
|
59
|
+
setPhase('fetching-profile');
|
|
24
60
|
try {
|
|
25
61
|
const profileRes = await fetch(`${base}/auth/me`, {
|
|
26
62
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
@@ -30,11 +66,10 @@ function OAuthTransfer({ apiBase, redirectPath = '/', loginPath = '/signin', cli
|
|
|
30
66
|
const profile = profileData?.data ?? profileData;
|
|
31
67
|
if (profile) {
|
|
32
68
|
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_PROFILE, JSON.stringify(profile));
|
|
33
|
-
document.cookie = `sms_user_profile=${encodeURIComponent(JSON.stringify(profile))}; path=/; SameSite=Lax`;
|
|
34
69
|
}
|
|
35
70
|
}
|
|
36
71
|
catch {
|
|
37
|
-
// Profile sync is non-critical
|
|
72
|
+
// Profile sync is non-critical
|
|
38
73
|
}
|
|
39
74
|
}
|
|
40
75
|
async function handleTicketExchange() {
|
|
@@ -44,36 +79,43 @@ function OAuthTransfer({ apiBase, redirectPath = '/', loginPath = '/signin', cli
|
|
|
44
79
|
return;
|
|
45
80
|
processedTickets.add(ticket);
|
|
46
81
|
try {
|
|
82
|
+
setPhase('exchanging');
|
|
47
83
|
const data = await (0, ticket_1.exchangeTicket)({ apiBase: base, ticket, clientId });
|
|
48
84
|
(0, ticket_1.persistTicketAuth)(data.accessToken, data.refreshToken, data.user);
|
|
49
85
|
(0, ticket_1.broadcastTicketLogin)(data.accessToken, data.user);
|
|
50
|
-
fetchProfile(data.accessToken);
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
await fetchProfile(data.accessToken);
|
|
87
|
+
setDestApp(resolveAppName(clientId));
|
|
88
|
+
setPhase('redirecting');
|
|
89
|
+
setTimeout(() => { window.location.href = next; }, 800);
|
|
53
90
|
}
|
|
54
91
|
catch (e) {
|
|
55
92
|
processedTickets.delete(ticket);
|
|
56
|
-
|
|
93
|
+
setPhase('error');
|
|
57
94
|
setError(e.message || 'Gagal menukar ticket.');
|
|
58
95
|
}
|
|
59
96
|
}
|
|
60
|
-
function handleLegacyToken() {
|
|
97
|
+
async function handleLegacyToken() {
|
|
61
98
|
if (!token) {
|
|
62
|
-
|
|
99
|
+
setPhase('error');
|
|
63
100
|
setError('Parameter tidak valid. Ticket atau token diperlukan.');
|
|
64
101
|
return;
|
|
65
102
|
}
|
|
103
|
+
setPhase('exchanging');
|
|
66
104
|
(0, ticket_1.persistTicketAuth)(token, refreshToken || undefined);
|
|
67
105
|
(0, ticket_1.broadcastTicketLogin)(token);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
try {
|
|
107
|
+
await fetch(`${base}/auth/set-cookie`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/json' },
|
|
110
|
+
credentials: 'include',
|
|
111
|
+
body: JSON.stringify({ token, refreshToken }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch { /* non-critical */ }
|
|
115
|
+
await fetchProfile(token);
|
|
116
|
+
setDestApp(resolveAppName(clientId));
|
|
117
|
+
setPhase('redirecting');
|
|
118
|
+
setTimeout(() => { window.location.href = next; }, 800);
|
|
77
119
|
}
|
|
78
120
|
if (ticket) {
|
|
79
121
|
handleTicketExchange();
|
|
@@ -81,5 +123,20 @@ function OAuthTransfer({ apiBase, redirectPath = '/', loginPath = '/signin', cli
|
|
|
81
123
|
}
|
|
82
124
|
handleLegacyToken();
|
|
83
125
|
}, [apiBase, clientId, redirectPath]);
|
|
84
|
-
|
|
126
|
+
const steps = [
|
|
127
|
+
{ key: 'exchanging', label: 'Memvalidasi sesi' },
|
|
128
|
+
{ key: 'fetching-profile', label: 'Memuat profil' },
|
|
129
|
+
{ key: 'redirecting', label: `Mengalihkan ke ${destApp}` },
|
|
130
|
+
];
|
|
131
|
+
const isError = phase === 'error';
|
|
132
|
+
const isDone = phase === 'redirecting' || phase === 'success';
|
|
133
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-950 px-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "w-full max-w-sm text-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex justify-center mb-6", children: (0, jsx_runtime_1.jsx)("div", { className: `flex items-center justify-center w-16 h-16 rounded-2xl ${isError ? 'bg-red-100 dark:bg-red-900/30' : isDone ? 'bg-green-100 dark:bg-green-900/30' : 'bg-brand-500'}`, children: isError ? ((0, jsx_runtime_1.jsx)("svg", { className: "w-8 h-8 text-red-600 dark:text-red-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" }) })) : isDone ? ((0, jsx_runtime_1.jsx)("svg", { className: "w-8 h-8 text-green-600 dark:text-green-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) })) : ((0, jsx_runtime_1.jsxs)("svg", { className: "w-8 h-8 text-white animate-spin", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" })] })) }) }), (0, jsx_runtime_1.jsx)("h1", { className: "text-xl font-semibold text-gray-900 dark:text-white mb-1", children: isError ? 'Login Gagal' : isDone ? 'Login Berhasil' : 'Menyelesaikan Login' }), (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400 mb-6", children: isError ? 'Terjadi kesalahan saat memproses sesi Anda' : `Mengalihkan Anda ke ${destApp}` }), !isError && !isDone && ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3 mb-6", children: steps.map((step, i) => {
|
|
134
|
+
const active = phase === step.key;
|
|
135
|
+
const completed = steps.findIndex(s => s.key === phase) > i;
|
|
136
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("div", { className: `w-6 h-6 rounded-full flex items-center justify-center text-xs font-medium shrink-0 ${completed ? 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400' :
|
|
137
|
+
active ? 'bg-brand-100 text-brand-600 dark:bg-brand-900/30 dark:text-brand-400' :
|
|
138
|
+
'bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500'}`, children: completed ? '✓' : i + 1 }), (0, jsx_runtime_1.jsx)("span", { className: `text-sm ${completed ? 'text-green-600 dark:text-green-400' :
|
|
139
|
+
active ? 'text-brand-600 dark:text-brand-400 font-medium' :
|
|
140
|
+
'text-gray-400 dark:text-gray-500'}`, children: step.label }), active && ((0, jsx_runtime_1.jsx)("div", { className: "h-4 w-4 animate-spin rounded-full border-2 border-brand-500 border-t-transparent ml-auto" }))] }, step.key));
|
|
141
|
+
}) })), isDone && ((0, jsx_runtime_1.jsx)("div", { className: "mb-6", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-center gap-2 text-sm text-green-600 dark:text-green-400", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-4 w-4 animate-spin rounded-full border-2 border-green-500 border-t-transparent" }), (0, jsx_runtime_1.jsx)("span", { children: "Mengalihkan..." })] }) })), isError && ((0, jsx_runtime_1.jsxs)("div", { className: "space-y-3", children: [(0, jsx_runtime_1.jsx)("div", { className: "rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-4 text-left", children: (0, jsx_runtime_1.jsx)("p", { className: "text-sm text-red-700 dark:text-red-300", children: error }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsx)("a", { href: coreFrontendUrl, className: "inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg bg-brand-500 text-white text-sm font-medium hover:bg-brand-600 transition", children: "Kembali ke Portal Utama" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => window.location.reload(), className: "text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition", children: "Coba lagi" })] })] }))] }) }));
|
|
85
142
|
}
|
|
@@ -2,20 +2,26 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.useCrossAppLogout = useCrossAppLogout;
|
|
4
4
|
const react_1 = require("react");
|
|
5
|
+
const LOGOUT_EVENT_WINDOW_MS = 2000;
|
|
6
|
+
let lastLogoutEventAt = 0;
|
|
5
7
|
function useCrossAppLogout(options) {
|
|
6
8
|
const redirectPath = options?.redirectPath || '/signin';
|
|
7
9
|
(0, react_1.useEffect)(() => {
|
|
8
10
|
const cleanup = [];
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
+
const processRemoteLogout = () => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
if (now - lastLogoutEventAt < LOGOUT_EVENT_WINDOW_MS)
|
|
14
|
+
return;
|
|
15
|
+
lastLogoutEventAt = now;
|
|
11
16
|
options?.onLogout?.();
|
|
12
17
|
window.location.href = redirectPath;
|
|
13
18
|
};
|
|
19
|
+
const bc = new BroadcastChannel('loka-sso-logout');
|
|
20
|
+
bc.onmessage = processRemoteLogout;
|
|
14
21
|
cleanup.push(() => bc.close());
|
|
15
22
|
const handler = (event) => {
|
|
16
23
|
if (event.key === 'sms_ac_token' && !event.newValue) {
|
|
17
|
-
|
|
18
|
-
window.location.href = redirectPath;
|
|
24
|
+
processRemoteLogout();
|
|
19
25
|
}
|
|
20
26
|
};
|
|
21
27
|
window.addEventListener('storage', handler);
|
|
@@ -4,19 +4,13 @@ exports.useOAuthCallback = useOAuthCallback;
|
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const processedMarkers = new Set();
|
|
6
6
|
function saveJwt(token, refreshToken) {
|
|
7
|
-
localStorage.setItem('sms_ac_token', token);
|
|
8
|
-
if (refreshToken)
|
|
9
|
-
localStorage.setItem('sms_refresh_token', refreshToken);
|
|
10
7
|
try {
|
|
11
8
|
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
12
9
|
const userObj = {
|
|
13
10
|
sub: payload.sub, email: payload.email, fullName: payload.fullName,
|
|
14
11
|
name: payload.fullName, role: payload.role, schoolId: payload.schoolId,
|
|
15
|
-
features: payload.features || [], accessibleSchools: payload.accessibleSchools || [],
|
|
16
12
|
};
|
|
17
|
-
localStorage.setItem('
|
|
18
|
-
localStorage.setItem('user_role', (payload.role || '').toLowerCase());
|
|
19
|
-
localStorage.setItem('user_id', payload.sub || '');
|
|
13
|
+
localStorage.setItem('sms_user_profile', JSON.stringify(userObj));
|
|
20
14
|
if (payload.schoolId)
|
|
21
15
|
localStorage.setItem('school_sms_id', payload.schoolId);
|
|
22
16
|
}
|
|
@@ -41,7 +35,7 @@ function useOAuthCallback(input) {
|
|
|
41
35
|
setState({ error: 'Token tidak ditemukan.', loading: false, phase: 'error' });
|
|
42
36
|
return;
|
|
43
37
|
}
|
|
44
|
-
// Phase 2 & 3 — Save JWT
|
|
38
|
+
// Phase 2 & 3 — Save JWT first, then synchronize Gateway cookies before redirect.
|
|
45
39
|
(async () => {
|
|
46
40
|
let accessToken = token || '';
|
|
47
41
|
let refreshTok = refreshToken || '';
|
|
@@ -50,6 +44,9 @@ function useOAuthCallback(input) {
|
|
|
50
44
|
setState({ error: null, loading: true, phase: 'exchange' });
|
|
51
45
|
const codeVerifier = sessionStorage.getItem(`oauth_verifier_${oauthState || ''}`) || '';
|
|
52
46
|
sessionStorage.removeItem(`oauth_verifier_${oauthState || ''}`);
|
|
47
|
+
if (!codeVerifier) {
|
|
48
|
+
throw new Error('Missing code verifier');
|
|
49
|
+
}
|
|
53
50
|
const res = await fetch(`${apiBase}/oauth/token`, {
|
|
54
51
|
method: 'POST',
|
|
55
52
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -75,22 +72,32 @@ function useOAuthCallback(input) {
|
|
|
75
72
|
}
|
|
76
73
|
// 1. Save token from JWT decode FIRST — no network needed
|
|
77
74
|
saveJwt(accessToken, refreshTok);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
// 2. Set cookie before redirecting so the landing app does not race /auth/me.
|
|
76
|
+
const controller = new AbortController();
|
|
77
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
78
|
+
try {
|
|
79
|
+
await fetch(`${apiBase}/auth/set-cookie`, {
|
|
80
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
81
|
+
credentials: 'include',
|
|
82
|
+
body: JSON.stringify({ token: accessToken, refreshToken: refreshTok }),
|
|
83
|
+
signal: controller.signal,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// /oauth/token also sets the auth cookie; do not fail the callback if this fallback is slow.
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
}
|
|
92
|
+
await fetch(`${apiBase}/auth/me`, {
|
|
84
93
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
85
94
|
credentials: 'include',
|
|
86
95
|
}).then(r => r.json()).then(d => {
|
|
87
96
|
const p = d?.data ?? d;
|
|
88
|
-
if (p)
|
|
97
|
+
if (p)
|
|
89
98
|
localStorage.setItem('sms_user_profile', JSON.stringify(p));
|
|
90
|
-
document.cookie = `sms_user_profile=${encodeURIComponent(JSON.stringify(p))}; path=/; SameSite=Lax`;
|
|
91
|
-
}
|
|
92
99
|
}).catch(() => { });
|
|
93
|
-
//
|
|
100
|
+
// 3. Redirect after the cookie/profile sync attempt completes.
|
|
94
101
|
setState({ error: null, loading: false, phase: 'done' });
|
|
95
102
|
window.location.href = input.redirectPath || '/';
|
|
96
103
|
}
|
package/dist/interceptor.js
CHANGED
|
@@ -2,23 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createAuthInterceptor = createAuthInterceptor;
|
|
4
4
|
const constants_1 = require("./constants");
|
|
5
|
-
function getCookie(name) {
|
|
6
|
-
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
|
7
|
-
return match ? decodeURIComponent(match[2]) : '';
|
|
8
|
-
}
|
|
9
5
|
function createAuthInterceptor() {
|
|
10
6
|
return (config) => {
|
|
11
|
-
const
|
|
12
|
-
if (token) {
|
|
13
|
-
config.headers[constants_1.API_HEADERS.AUTHORIZATION] = `Bearer ${token}`;
|
|
14
|
-
}
|
|
15
|
-
const schoolId = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.SCHOOL_ID) || getCookie('sms_school_id');
|
|
7
|
+
const schoolId = localStorage.getItem('school_sms_id');
|
|
16
8
|
if (schoolId) {
|
|
17
9
|
config.headers[constants_1.API_HEADERS.SCHOOL_ID] = schoolId;
|
|
18
10
|
}
|
|
19
|
-
config.headers[constants_1.API_HEADERS.
|
|
20
|
-
config.headers[constants_1.API_HEADERS.USER_ROLE] = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.USER_ROLE) || '';
|
|
21
|
-
config.headers[constants_1.API_HEADERS.DEVICE_ID] = localStorage.getItem(constants_1.SSO_STORAGE_KEYS.DEVICE_ID) || 'web-unknown';
|
|
11
|
+
config.headers[constants_1.API_HEADERS.DEVICE_ID] = localStorage.getItem('device_sms_id') || 'web-unknown';
|
|
22
12
|
config.headers[constants_1.API_HEADERS.REQUEST_ID] = crypto.randomUUID();
|
|
23
13
|
return config;
|
|
24
14
|
};
|
package/dist/navigation.js
CHANGED
|
@@ -3,11 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildTransferUrl = buildTransferUrl;
|
|
4
4
|
exports.navigateToApp = navigateToApp;
|
|
5
5
|
const ticket_1 = require("./ticket");
|
|
6
|
-
function
|
|
7
|
-
|
|
6
|
+
function isTrustedOrigin(origin) {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(origin);
|
|
9
|
+
return url.origin === window.location.origin;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
8
14
|
}
|
|
9
15
|
function buildTransferUrl(targetUrl, ticket, next) {
|
|
10
|
-
const target =
|
|
16
|
+
const target = new URL(targetUrl, window.location.origin);
|
|
17
|
+
if (!isTrustedOrigin(target.origin)) {
|
|
18
|
+
throw new Error('Target origin tidak dikenal');
|
|
19
|
+
}
|
|
11
20
|
const transferUrl = new URL('/auth/transfer', target.origin);
|
|
12
21
|
const nextPath = next || `${target.pathname}${target.search}${target.hash}` || '/';
|
|
13
22
|
transferUrl.searchParams.set('ticket', ticket);
|
|
@@ -28,6 +37,8 @@ function openDestination(url, targetBlank, existingTab) {
|
|
|
28
37
|
async function navigateToApp({ targetUrl, apiBase = '/api', accessToken, clientId, targetBlank = true, next, fallbackToPlainUrl = true, }) {
|
|
29
38
|
const token = accessToken || (0, ticket_1.getStoredAccessToken)();
|
|
30
39
|
if (!token) {
|
|
40
|
+
if (!fallbackToPlainUrl)
|
|
41
|
+
throw new Error('Token tidak ditemukan');
|
|
31
42
|
openDestination(targetUrl, targetBlank);
|
|
32
43
|
return;
|
|
33
44
|
}
|
|
@@ -40,6 +51,10 @@ async function navigateToApp({ targetUrl, apiBase = '/api', accessToken, clientI
|
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
try {
|
|
54
|
+
const target = new URL(targetUrl, window.location.origin);
|
|
55
|
+
if (!isTrustedOrigin(target.origin)) {
|
|
56
|
+
throw new Error('Target origin tidak dikenal');
|
|
57
|
+
}
|
|
43
58
|
const issued = await (0, ticket_1.issueTicket)({
|
|
44
59
|
apiBase,
|
|
45
60
|
accessToken: token,
|
package/dist/oauthRedirect.js
CHANGED
|
@@ -6,11 +6,6 @@ const pkce_1 = require("./pkce");
|
|
|
6
6
|
function trimTrailingSlash(value) {
|
|
7
7
|
return value.replace(/\/+$/, '');
|
|
8
8
|
}
|
|
9
|
-
function getCookie(name) {
|
|
10
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
11
|
-
const match = document.cookie.match(new RegExp(`(^| )${escaped}=([^;]+)`));
|
|
12
|
-
return match ? decodeURIComponent(match[2]) : '';
|
|
13
|
-
}
|
|
14
9
|
function decodeJwt(token) {
|
|
15
10
|
try {
|
|
16
11
|
const parts = token.split('.');
|
|
@@ -28,35 +23,26 @@ function isTokenExpired(token) {
|
|
|
28
23
|
return false;
|
|
29
24
|
return payload.exp * 1000 <= Date.now();
|
|
30
25
|
}
|
|
31
|
-
function hasCompleteAuth() {
|
|
32
|
-
|
|
26
|
+
async function hasCompleteAuth(apiBase) {
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(`${trimTrailingSlash(apiBase)}/auth/me`, {
|
|
29
|
+
credentials: 'include',
|
|
30
|
+
});
|
|
31
|
+
return res.ok;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
33
36
|
}
|
|
34
37
|
function hasSsoToken() {
|
|
35
|
-
|
|
36
|
-
if (!token)
|
|
37
|
-
return false;
|
|
38
|
-
if (isTokenExpired(token))
|
|
39
|
-
return false;
|
|
40
|
-
return true;
|
|
38
|
+
return false;
|
|
41
39
|
}
|
|
42
40
|
async function redirectToOAuthLogin({ clientId, apiBase = '/api', callbackPath = '/auth/callback', authenticatedPath = '/', }) {
|
|
43
|
-
|
|
41
|
+
const authed = await hasCompleteAuth(apiBase);
|
|
42
|
+
if (authed) {
|
|
44
43
|
window.location.href = authenticatedPath;
|
|
45
44
|
return;
|
|
46
45
|
}
|
|
47
|
-
// Only clear an expired token. A valid token without a cached profile can
|
|
48
|
-
// still bootstrap the target app through /auth/me.
|
|
49
|
-
const staleToken = getCookie('sms_ac_token') || localStorage.getItem('sms_ac_token');
|
|
50
|
-
if (staleToken && isTokenExpired(staleToken)) {
|
|
51
|
-
localStorage.removeItem('sms_ac_token');
|
|
52
|
-
localStorage.removeItem('sms_refresh_token');
|
|
53
|
-
localStorage.removeItem('lms_user');
|
|
54
|
-
localStorage.removeItem('user_role');
|
|
55
|
-
localStorage.removeItem('user_id');
|
|
56
|
-
localStorage.removeItem('school_sms_id');
|
|
57
|
-
document.cookie = 'sms_ac_token=; path=/; Max-Age=0; SameSite=Lax';
|
|
58
|
-
document.cookie = 'sms_refresh_token=; path=/; Max-Age=0; SameSite=Lax';
|
|
59
|
-
}
|
|
60
46
|
const state = (0, pkce_1.generateState)();
|
|
61
47
|
const codeVerifier = (0, pkce_1.generateCodeVerifier)();
|
|
62
48
|
const codeChallenge = await (0, pkce_1.generateCodeChallenge)(codeVerifier);
|
package/dist/ticket.js
CHANGED
|
@@ -10,13 +10,8 @@ const constants_1 = require("./constants");
|
|
|
10
10
|
function trimTrailingSlash(value) {
|
|
11
11
|
return value.replace(/\/+$/, '');
|
|
12
12
|
}
|
|
13
|
-
function getCookie(name) {
|
|
14
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
15
|
-
const match = document.cookie.match(new RegExp(`(^| )${escaped}=([^;]+)`));
|
|
16
|
-
return match ? decodeURIComponent(match[2]) : '';
|
|
17
|
-
}
|
|
18
13
|
function getStoredAccessToken() {
|
|
19
|
-
return
|
|
14
|
+
return '';
|
|
20
15
|
}
|
|
21
16
|
function getApiBase(apiBase) {
|
|
22
17
|
return trimTrailingSlash(apiBase || '/api');
|
|
@@ -29,14 +24,10 @@ async function parseJsonResponse(response) {
|
|
|
29
24
|
return body?.data ?? body;
|
|
30
25
|
}
|
|
31
26
|
async function issueTicket(options = {}) {
|
|
32
|
-
const accessToken = options.accessToken || getStoredAccessToken();
|
|
33
|
-
if (!accessToken)
|
|
34
|
-
throw new Error('Not authenticated');
|
|
35
27
|
const response = await fetch(`${getApiBase(options.apiBase)}/sso/issue-ticket`, {
|
|
36
28
|
method: 'POST',
|
|
37
29
|
headers: {
|
|
38
30
|
'Content-Type': 'application/json',
|
|
39
|
-
Authorization: `Bearer ${accessToken}`,
|
|
40
31
|
},
|
|
41
32
|
credentials: 'include',
|
|
42
33
|
signal: options.signal,
|
|
@@ -94,18 +85,11 @@ function decodeUserFromToken(token) {
|
|
|
94
85
|
}
|
|
95
86
|
}
|
|
96
87
|
function persistTicketAuth(accessToken, refreshToken, user) {
|
|
97
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
|
98
|
-
if (refreshToken)
|
|
99
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
|
100
88
|
const profile = user || decodeUserFromToken(accessToken);
|
|
101
89
|
if (profile) {
|
|
102
90
|
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_PROFILE, JSON.stringify(profile));
|
|
103
|
-
localStorage.setItem('lms_user', JSON.stringify(profile));
|
|
104
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_ROLE, (profile.role || '').toLowerCase());
|
|
105
|
-
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.USER_ID, profile.sub || profile.id || '');
|
|
106
91
|
if (profile.schoolId)
|
|
107
92
|
localStorage.setItem(constants_1.SSO_STORAGE_KEYS.SCHOOL_ID, profile.schoolId);
|
|
108
|
-
document.cookie = `sms_user_profile=${encodeURIComponent(JSON.stringify(profile))}; path=/; SameSite=Lax`;
|
|
109
93
|
}
|
|
110
94
|
}
|
|
111
95
|
function broadcastTicketLogin(accessToken, user) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loka-sms/sso",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "SSO utilities, hooks, and components for Loka SMS modules (OAuth2 PKCE, cross-app logout)",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "SSO utilities, hooks, and components for Loka SMS modules (OAuth2 PKCE, cross-app logout, infinite-loop protection)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
7
7
|
"main": "dist/index.js",
|