nocal-auth-sdk 0.1.9 → 0.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/dist/index.d.ts +11 -21
- package/dist/index.js +140 -78
- package/dist/types.d.ts +17 -5
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,32 +1,22 @@
|
|
|
1
|
-
import { NocalClientConfig, OAuthLoginReq,
|
|
1
|
+
import { NocalClientConfig, OAuthLoginReq, NocalUser, NocalSession } from "./types";
|
|
2
2
|
export declare class NocalAuthSDK {
|
|
3
|
-
private supabase;
|
|
4
3
|
private clientId;
|
|
5
4
|
private redirectUri;
|
|
5
|
+
private authServerUrl;
|
|
6
|
+
private currentSession;
|
|
7
|
+
private currentUser;
|
|
6
8
|
constructor(config: NocalClientConfig);
|
|
7
9
|
/**
|
|
8
|
-
* Mở Popup UI Đăng nhập
|
|
9
|
-
*/
|
|
10
|
-
signInWithNocal(req?: OAuthLoginReq): Promise<NocalUser>;
|
|
11
|
-
/**
|
|
12
|
-
* Lấy thông tin User hiện tại
|
|
10
|
+
* Mở Popup UI Đăng nhập & Ủy quyền OAuth 2.0 với PKCE
|
|
13
11
|
*/
|
|
12
|
+
signInWithNocal(req?: OAuthLoginReq): Promise<{
|
|
13
|
+
user: NocalUser;
|
|
14
|
+
session: NocalSession;
|
|
15
|
+
}>;
|
|
16
|
+
private exchangeCodeForToken;
|
|
17
|
+
private fetchUserInfo;
|
|
14
18
|
getUser(): Promise<NocalUser | null>;
|
|
15
|
-
/**
|
|
16
|
-
* Lấy Session / Access Token hiện tại
|
|
17
|
-
*/
|
|
18
19
|
getSession(): Promise<NocalSession | null>;
|
|
19
|
-
/**
|
|
20
|
-
* Đăng xuất khỏi hệ thống
|
|
21
|
-
*/
|
|
22
20
|
signOut(): Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* Lắng nghe sự kiện đăng nhập / đăng xuất
|
|
25
|
-
*/
|
|
26
|
-
onAuthStateChange(callback: (event: string, session: NocalSession | null) => void): {
|
|
27
|
-
data: {
|
|
28
|
-
subscription: import("@supabase/auth-js").Subscription;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
21
|
}
|
|
32
22
|
export * from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1,109 +1,171 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const DEFAULT_AUTH_SERVER_URL = "http://localhost:3001";
|
|
2
|
+
// Helper for PKCE crypto
|
|
3
|
+
function base64URLEncode(buffer) {
|
|
4
|
+
let binary = "";
|
|
5
|
+
const bytes = new Uint8Array(buffer);
|
|
6
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
7
|
+
binary += String.fromCharCode(bytes[i]);
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary)
|
|
10
|
+
.replace(/\+/g, "-")
|
|
11
|
+
.replace(/\//g, "_")
|
|
12
|
+
.replace(/=+$/, "");
|
|
13
|
+
}
|
|
14
|
+
async function generateCodeChallenge(verifier) {
|
|
15
|
+
const encoder = new TextEncoder();
|
|
16
|
+
const data = encoder.encode(verifier);
|
|
17
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
18
|
+
return base64URLEncode(new Uint8Array(digest));
|
|
19
|
+
}
|
|
20
|
+
function generateRandomString(length = 32) {
|
|
21
|
+
const array = new Uint8Array(length);
|
|
22
|
+
crypto.getRandomValues(array);
|
|
23
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
24
|
+
}
|
|
4
25
|
export class NocalAuthSDK {
|
|
5
26
|
constructor(config) {
|
|
27
|
+
this.currentSession = null;
|
|
28
|
+
this.currentUser = null;
|
|
6
29
|
this.clientId = config.clientId;
|
|
7
30
|
this.redirectUri = config.redirectUri;
|
|
8
|
-
this.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
this.authServerUrl = config.authServerUrl || DEFAULT_AUTH_SERVER_URL;
|
|
32
|
+
// Load session from localStorage if available
|
|
33
|
+
if (typeof window !== "undefined") {
|
|
34
|
+
try {
|
|
35
|
+
const stored = localStorage.getItem("nocal_auth_session");
|
|
36
|
+
if (stored) {
|
|
37
|
+
const parsed = JSON.parse(stored);
|
|
38
|
+
this.currentSession = parsed.session;
|
|
39
|
+
this.currentUser = parsed.user;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
// ignore
|
|
44
|
+
}
|
|
45
|
+
}
|
|
15
46
|
}
|
|
16
47
|
/**
|
|
17
|
-
* Mở Popup UI Đăng nhập
|
|
48
|
+
* Mở Popup UI Đăng nhập & Ủy quyền OAuth 2.0 với PKCE
|
|
18
49
|
*/
|
|
19
50
|
async signInWithNocal(req) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
if (error)
|
|
34
|
-
throw error;
|
|
35
|
-
if (!data?.url)
|
|
36
|
-
throw new Error("CANNOT_GENERATE_AUTH_URL");
|
|
37
|
-
// 2. Tính toán kích thước và căn giữa màn hình Popup
|
|
51
|
+
const state = req?.state || generateRandomString(16);
|
|
52
|
+
const codeVerifier = generateRandomString(32);
|
|
53
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
54
|
+
const authUrl = new URL(`${this.authServerUrl}/oauth/authorize`);
|
|
55
|
+
authUrl.searchParams.set("client_id", this.clientId);
|
|
56
|
+
authUrl.searchParams.set("redirect_uri", this.redirectUri);
|
|
57
|
+
authUrl.searchParams.set("response_type", "code");
|
|
58
|
+
authUrl.searchParams.set("state", state);
|
|
59
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
60
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
38
61
|
const width = 480;
|
|
39
62
|
const height = 680;
|
|
40
|
-
const left = typeof window !== "undefined"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const top = typeof window !== "undefined"
|
|
44
|
-
? window.screenY + (window.outerHeight - height) / 2
|
|
45
|
-
: 0;
|
|
46
|
-
// 3. Mở Cửa sổ Popup chứa UI đăng nhập Nocal
|
|
47
|
-
const popup = window.open(data.url, "NocalAuthPopup", `width=${width},height=${height},left=${left},top=${top},status=yes,scrollbars=yes`);
|
|
63
|
+
const left = typeof window !== "undefined" ? window.screenX + (window.outerWidth - width) / 2 : 0;
|
|
64
|
+
const top = typeof window !== "undefined" ? window.screenY + (window.outerHeight - height) / 2 : 0;
|
|
65
|
+
const popup = window.open(authUrl.toString(), "NocalAuthPopup", `width=${width},height=${height},left=${left},top=${top},status=yes,scrollbars=yes`);
|
|
48
66
|
if (!popup) {
|
|
49
67
|
throw new Error("POPUP_BLOCKED");
|
|
50
68
|
}
|
|
51
|
-
// 4. Lắng nghe và kiểm tra khi Popup đóng
|
|
52
69
|
return new Promise((resolve, reject) => {
|
|
53
|
-
const
|
|
70
|
+
const allowedOrigin = new URL(this.authServerUrl).origin;
|
|
71
|
+
const messageListener = async (event) => {
|
|
72
|
+
if (event.origin !== allowedOrigin)
|
|
73
|
+
return;
|
|
74
|
+
const data = event.data;
|
|
75
|
+
if (!data || (data.type !== "NOCAL_OAUTH_CODE" && data.type !== "NOCAL_OAUTH_ERROR")) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
window.removeEventListener("message", messageListener);
|
|
79
|
+
clearInterval(checkClosedTimer);
|
|
80
|
+
if (data.type === "NOCAL_OAUTH_ERROR" || data.error) {
|
|
81
|
+
popup.close();
|
|
82
|
+
reject(new Error(data.error || "OAUTH_ERROR"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (data.state && data.state !== state) {
|
|
86
|
+
popup.close();
|
|
87
|
+
reject(new Error("OAUTH_STATE_MISMATCH"));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const code = data.code;
|
|
91
|
+
if (!code) {
|
|
92
|
+
popup.close();
|
|
93
|
+
reject(new Error("MISSING_AUTHORIZATION_CODE"));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
54
96
|
try {
|
|
55
|
-
|
|
97
|
+
popup.close();
|
|
98
|
+
// Đổi code lấy token
|
|
99
|
+
const session = await this.exchangeCodeForToken(code, codeVerifier);
|
|
100
|
+
const user = await this.fetchUserInfo(session.access_token);
|
|
101
|
+
this.currentSession = session;
|
|
102
|
+
this.currentUser = user;
|
|
103
|
+
if (typeof window !== "undefined") {
|
|
104
|
+
localStorage.setItem("nocal_auth_session", JSON.stringify({ session, user }));
|
|
105
|
+
}
|
|
106
|
+
resolve({ user, session });
|
|
56
107
|
}
|
|
57
|
-
catch (
|
|
58
|
-
|
|
108
|
+
catch (err) {
|
|
109
|
+
reject(err);
|
|
59
110
|
}
|
|
60
111
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
reject(new Error("LOGIN_CANCELLED_OR_FAILED"));
|
|
112
|
+
window.addEventListener("message", messageListener);
|
|
113
|
+
const checkClosedTimer = setInterval(() => {
|
|
114
|
+
try {
|
|
115
|
+
if (popup.closed) {
|
|
116
|
+
clearInterval(checkClosedTimer);
|
|
117
|
+
window.removeEventListener("message", messageListener);
|
|
118
|
+
reject(new Error("POPUP_CLOSED_BY_USER"));
|
|
71
119
|
}
|
|
72
120
|
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
// ignore
|
|
123
|
+
}
|
|
73
124
|
}, 500);
|
|
74
125
|
});
|
|
75
126
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
127
|
+
async exchangeCodeForToken(code, codeVerifier) {
|
|
128
|
+
const res = await fetch(`${this.authServerUrl}/api/oauth/token`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { "Content-Type": "application/json" },
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
grant_type: "authorization_code",
|
|
133
|
+
code,
|
|
134
|
+
client_id: this.clientId,
|
|
135
|
+
redirect_uri: this.redirectUri,
|
|
136
|
+
code_verifier: codeVerifier,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
if (!res.ok) {
|
|
141
|
+
throw new Error(data.error_description || data.error || "FAILED_TO_EXCHANGE_TOKEN");
|
|
142
|
+
}
|
|
143
|
+
return data;
|
|
144
|
+
}
|
|
145
|
+
async fetchUserInfo(accessToken) {
|
|
146
|
+
const res = await fetch(`${this.authServerUrl}/api/oauth/userinfo`, {
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${accessToken}`,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
if (!res.ok) {
|
|
153
|
+
throw new Error(data.error_description || data.error || "FAILED_TO_FETCH_USERINFO");
|
|
154
|
+
}
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
79
157
|
async getUser() {
|
|
80
|
-
|
|
81
|
-
if (error)
|
|
82
|
-
throw error;
|
|
83
|
-
return user;
|
|
158
|
+
return this.currentUser;
|
|
84
159
|
}
|
|
85
|
-
/**
|
|
86
|
-
* Lấy Session / Access Token hiện tại
|
|
87
|
-
*/
|
|
88
160
|
async getSession() {
|
|
89
|
-
|
|
90
|
-
if (error)
|
|
91
|
-
throw error;
|
|
92
|
-
return session;
|
|
161
|
+
return this.currentSession;
|
|
93
162
|
}
|
|
94
|
-
/**
|
|
95
|
-
* Đăng xuất khỏi hệ thống
|
|
96
|
-
*/
|
|
97
163
|
async signOut() {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* Lắng nghe sự kiện đăng nhập / đăng xuất
|
|
104
|
-
*/
|
|
105
|
-
onAuthStateChange(callback) {
|
|
106
|
-
return this.supabase.auth.onAuthStateChange(callback);
|
|
164
|
+
this.currentSession = null;
|
|
165
|
+
this.currentUser = null;
|
|
166
|
+
if (typeof window !== "undefined") {
|
|
167
|
+
localStorage.removeItem("nocal_auth_session");
|
|
168
|
+
}
|
|
107
169
|
}
|
|
108
170
|
}
|
|
109
171
|
export * from "./types";
|
package/dist/types.d.ts
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { User, Session } from "@supabase/supabase-js";
|
|
2
1
|
export interface NocalClientConfig {
|
|
3
2
|
clientId: string;
|
|
4
3
|
redirectUri: string;
|
|
4
|
+
authServerUrl?: string;
|
|
5
5
|
}
|
|
6
|
-
export type NocalOAuthProvider = "google" | "facebook" | "github" | "apple";
|
|
7
6
|
export interface OAuthLoginReq {
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
state?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface NocalUser {
|
|
10
|
+
sub: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
phone?: string;
|
|
13
|
+
email_confirmed?: boolean;
|
|
14
|
+
phone_confirmed?: boolean;
|
|
15
|
+
user_metadata?: Record<string, any>;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
export interface NocalSession {
|
|
19
|
+
access_token: string;
|
|
20
|
+
refresh_token: string;
|
|
21
|
+
token_type: string;
|
|
22
|
+
expires_in: number;
|
|
10
23
|
}
|
|
11
|
-
export type { User, Session };
|