@svadmin/supabase 0.11.0 → 0.11.1
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/package.json +3 -3
- package/src/auth-provider.ts +59 -4
- package/src/supabase.test.ts +44 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/supabase",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Supabase adapters — DataProvider, AuthProvider, LiveProvider, AuditHandler",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@svadmin/core": "^0.25.
|
|
23
|
+
"@svadmin/core": "^0.25.2",
|
|
24
24
|
"@supabase/supabase-js": "^2.0.0",
|
|
25
25
|
"@refinedev/supabase": "^5.0.0",
|
|
26
|
-
"@supacloud/js": "^0.
|
|
26
|
+
"@supacloud/js": "^0.12.0"
|
|
27
27
|
},
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"author": "zuohuadong",
|
package/src/auth-provider.ts
CHANGED
|
@@ -3,6 +3,26 @@ import type { SupabaseClient } from '@supabase/supabase-js';
|
|
|
3
3
|
import type { AuthProvider, Identity, AuthActionResult, CheckResult } from '@svadmin/core';
|
|
4
4
|
import { audit } from '@svadmin/core';
|
|
5
5
|
|
|
6
|
+
const INVALID_REFRESH_TOKEN_MESSAGES = [
|
|
7
|
+
'refresh token is not valid',
|
|
8
|
+
'invalid refresh token',
|
|
9
|
+
'refresh_token_not_found',
|
|
10
|
+
'jwt expired',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function isInvalidRefreshTokenError(error: unknown): error is Error {
|
|
14
|
+
if (!(error instanceof Error)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const message = error.message.toLowerCase();
|
|
19
|
+
return INVALID_REFRESH_TOKEN_MESSAGES.some((fragment) => message.includes(fragment));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function clearInvalidSession(client: SupabaseClient): Promise<void> {
|
|
23
|
+
await client.auth.signOut({ scope: 'local' });
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider {
|
|
7
27
|
return {
|
|
8
28
|
async login({ email, password }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
@@ -23,16 +43,42 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
|
|
|
23
43
|
},
|
|
24
44
|
|
|
25
45
|
async check(): Promise<CheckResult> {
|
|
26
|
-
const { data: { session } } = await client.auth.getSession();
|
|
46
|
+
const { data: { session }, error } = await client.auth.getSession();
|
|
47
|
+
if (error) {
|
|
48
|
+
if (isInvalidRefreshTokenError(error)) {
|
|
49
|
+
await clearInvalidSession(client);
|
|
50
|
+
return {
|
|
51
|
+
authenticated: false,
|
|
52
|
+
redirectTo: '/login',
|
|
53
|
+
logout: true,
|
|
54
|
+
error: { message: 'Session expired, please sign in again.' },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
authenticated: false,
|
|
60
|
+
redirectTo: '/login',
|
|
61
|
+
logout: true,
|
|
62
|
+
error: { message: 'Session validation failed.' },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
27
65
|
if (session) return { authenticated: true };
|
|
28
66
|
return { authenticated: false, redirectTo: '/login', logout: true };
|
|
29
67
|
},
|
|
30
68
|
|
|
31
69
|
async getIdentity(): Promise<Identity | null> {
|
|
32
|
-
const { data: { user } } = await client.auth.getUser();
|
|
70
|
+
const { data: { user }, error } = await client.auth.getUser();
|
|
71
|
+
if (error && isInvalidRefreshTokenError(error)) {
|
|
72
|
+
await clearInvalidSession(client);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
33
75
|
if (!user) return null;
|
|
34
76
|
|
|
35
|
-
const { data: { session } } = await client.auth.getSession();
|
|
77
|
+
const { data: { session }, error: sessionError } = await client.auth.getSession();
|
|
78
|
+
if (sessionError && isInvalidRefreshTokenError(sessionError)) {
|
|
79
|
+
await clearInvalidSession(client);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
36
82
|
|
|
37
83
|
return {
|
|
38
84
|
id: user.id,
|
|
@@ -44,7 +90,11 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
|
|
|
44
90
|
},
|
|
45
91
|
|
|
46
92
|
async getPermissions(): Promise<unknown> {
|
|
47
|
-
const { data: { user } } = await client.auth.getUser();
|
|
93
|
+
const { data: { user }, error } = await client.auth.getUser();
|
|
94
|
+
if (error && isInvalidRefreshTokenError(error)) {
|
|
95
|
+
await clearInvalidSession(client);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
48
98
|
return user?.user_metadata?.role ?? 'user';
|
|
49
99
|
},
|
|
50
100
|
|
|
@@ -71,6 +121,11 @@ export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider
|
|
|
71
121
|
},
|
|
72
122
|
|
|
73
123
|
async onError(error: unknown): Promise<{ redirectTo?: string; logout?: boolean }> {
|
|
124
|
+
if (isInvalidRefreshTokenError(error)) {
|
|
125
|
+
await clearInvalidSession(client);
|
|
126
|
+
return { redirectTo: '/login', logout: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
74
129
|
if (error instanceof Error && error.message.includes('401')) {
|
|
75
130
|
// Pre-flight check: race-condition guard
|
|
76
131
|
// Supabase might have just refreshed the token in the background right after our fetch failed
|
package/src/supabase.test.ts
CHANGED
|
@@ -135,6 +135,24 @@ describe('Supabase AuthProvider', () => {
|
|
|
135
135
|
expect(result.logout).toBe(true);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test('check clears invalid refresh token sessions', async () => {
|
|
139
|
+
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
140
|
+
const client = createMockSupabaseClient({
|
|
141
|
+
auth: {
|
|
142
|
+
getSession: mock(async () => ({
|
|
143
|
+
data: { session: null },
|
|
144
|
+
error: new Error('Refresh token is not valid'),
|
|
145
|
+
})),
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const auth = createSupabaseAuthProvider(client);
|
|
149
|
+
const result = await auth.check();
|
|
150
|
+
expect(result.authenticated).toBe(false);
|
|
151
|
+
expect(result.logout).toBe(true);
|
|
152
|
+
expect(result.redirectTo).toBe('/login');
|
|
153
|
+
expect(client.auth.signOut).toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
|
|
138
156
|
test('getIdentity surfaces user metadata', async () => {
|
|
139
157
|
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
140
158
|
const auth = createSupabaseAuthProvider(createMockSupabaseClient());
|
|
@@ -152,6 +170,22 @@ describe('Supabase AuthProvider', () => {
|
|
|
152
170
|
expect(role).toBe('admin');
|
|
153
171
|
});
|
|
154
172
|
|
|
173
|
+
test('getIdentity clears invalid refresh token sessions', async () => {
|
|
174
|
+
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
175
|
+
const client = createMockSupabaseClient({
|
|
176
|
+
auth: {
|
|
177
|
+
getUser: mock(async () => ({
|
|
178
|
+
data: { user: null },
|
|
179
|
+
error: new Error('Invalid refresh token'),
|
|
180
|
+
})),
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
const auth = createSupabaseAuthProvider(client);
|
|
184
|
+
const identity = await auth.getIdentity();
|
|
185
|
+
expect(identity).toBeNull();
|
|
186
|
+
expect(client.auth.signOut).toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
|
|
155
189
|
test('onError returns logout true on 401 with no session', async () => {
|
|
156
190
|
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
157
191
|
const client = createMockSupabaseClient({
|
|
@@ -172,6 +206,16 @@ describe('Supabase AuthProvider', () => {
|
|
|
172
206
|
expect(result.logout).toBeUndefined();
|
|
173
207
|
expect(client.auth.signOut).not.toHaveBeenCalled();
|
|
174
208
|
});
|
|
209
|
+
|
|
210
|
+
test('onError logs out on invalid refresh token', async () => {
|
|
211
|
+
const { createSupabaseAuthProvider } = await import('./auth-provider');
|
|
212
|
+
const client = createMockSupabaseClient();
|
|
213
|
+
const auth = createSupabaseAuthProvider(client);
|
|
214
|
+
const result = await auth.onError!(new Error('Refresh token is not valid'));
|
|
215
|
+
expect(result.logout).toBe(true);
|
|
216
|
+
expect(result.redirectTo).toBe('/login');
|
|
217
|
+
expect(client.auth.signOut).toHaveBeenCalled();
|
|
218
|
+
});
|
|
175
219
|
});
|
|
176
220
|
|
|
177
221
|
|