@sentroy-co/client-sdk 2.13.7 → 2.13.8
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/admin/index.d.ts +52 -0
- package/dist/auth/admin/index.d.ts.map +1 -0
- package/dist/auth/admin/index.js +123 -0
- package/dist/auth/admin/index.js.map +1 -0
- package/dist/auth/client.d.ts +86 -0
- package/dist/auth/client.d.ts.map +1 -0
- package/dist/auth/client.js +265 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/http.d.ts +19 -0
- package/dist/auth/http.d.ts.map +1 -0
- package/dist/auth/http.js +74 -0
- package/dist/auth/http.js.map +1 -0
- package/dist/auth/index.d.ts +16 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +20 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/react/index.d.ts +41 -0
- package/dist/auth/react/index.d.ts.map +1 -0
- package/dist/auth/react/index.js +52 -0
- package/dist/auth/react/index.js.map +1 -0
- package/dist/auth/types.d.ts +50 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +21 -0
- package/dist/auth/types.js.map +1 -0
- package/package.json +16 -1
- package/src/auth/admin/index.ts +191 -0
- package/src/auth/client.ts +344 -0
- package/src/auth/http.ts +101 -0
- package/src/auth/index.ts +26 -0
- package/src/auth/react/index.tsx +100 -0
- package/src/auth/types.ts +60 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type SentroyAuthUser,
|
|
3
|
+
type SignupResponse,
|
|
4
|
+
type LoginResponse,
|
|
5
|
+
type AuthTokensResponse,
|
|
6
|
+
} from "./types"
|
|
7
|
+
import { AuthHttp, type AuthHttpOptions } from "./http"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Browser-facing Sentroy Auth SDK — Firebase Auth tarzı session API.
|
|
11
|
+
*
|
|
12
|
+
* `apiKey` BROWSER'DA OLMAMALI; bu sınıf `apiKey`'i header'a koyacaktır.
|
|
13
|
+
* RP backend gerçek api-key tutar; browser'da end-user kendi access/refresh
|
|
14
|
+
* token'larıyla yaşar. Yine de DX için sınıf hem apiKey-less browser
|
|
15
|
+
* akışına (signup/login backend proxy üzerinden) hem apiKey'li server
|
|
16
|
+
* akışına (admin) tek tip API sunar — caller hangi mod'da olduğunu
|
|
17
|
+
* `SentroyAuthAdmin` (admin SDK, sunucu) vs `SentroyAuth` (browser SDK,
|
|
18
|
+
* apiKey-less) seçimiyle netleştirir.
|
|
19
|
+
*
|
|
20
|
+
* Storage: browser'da access + refresh `storage` adapter'a yazılır
|
|
21
|
+
* (default `localStorage`). Refresh expire'a 5dk kala arka planda
|
|
22
|
+
* yenilenir; fail olursa `onAuthStateChanged(null)` ve storage silinir.
|
|
23
|
+
*
|
|
24
|
+
* **Server-side rendering**: `typeof window === "undefined"` korumalı —
|
|
25
|
+
* Node ortamında `localStorage` yok, default `memory` storage'a düşer.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type AuthStateChangeListener = (user: SentroyAuthUser | null) => void
|
|
29
|
+
|
|
30
|
+
export interface AuthStorageAdapter {
|
|
31
|
+
read(): { accessToken: string; refreshToken: string; user: SentroyAuthUser } | null
|
|
32
|
+
write(value: {
|
|
33
|
+
accessToken: string
|
|
34
|
+
refreshToken: string
|
|
35
|
+
user: SentroyAuthUser
|
|
36
|
+
}): void
|
|
37
|
+
clear(): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const STORAGE_KEY_PREFIX = "sentroy.auth"
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Base64URL → UTF-8 decode. Browser'da `atob` + manuel UTF-8 reconstruction;
|
|
44
|
+
* Node'da `Buffer.from(..., "base64url")`. Tek kod yolu, runtime detect.
|
|
45
|
+
*/
|
|
46
|
+
function decodeBase64Url(s: string): string {
|
|
47
|
+
// Pad + standard base64
|
|
48
|
+
const padded = s.replace(/-/g, "+").replace(/_/g, "/")
|
|
49
|
+
const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4))
|
|
50
|
+
if (typeof atob === "function") {
|
|
51
|
+
const binary = atob(padded + pad)
|
|
52
|
+
// UTF-8 reconstruction (JWT claims yabancı karakter içerebilir)
|
|
53
|
+
const bytes = new Uint8Array(binary.length)
|
|
54
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
|
|
55
|
+
return new TextDecoder().decode(bytes)
|
|
56
|
+
}
|
|
57
|
+
// Node fallback
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
const B = (globalThis as any).Buffer
|
|
60
|
+
if (B) return B.from(padded + pad, "base64").toString("utf8")
|
|
61
|
+
throw new Error("No base64 decoder available")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function localStorageAdapter(projectSlug: string): AuthStorageAdapter {
|
|
65
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
66
|
+
return memoryStorageAdapter()
|
|
67
|
+
}
|
|
68
|
+
const key = `${STORAGE_KEY_PREFIX}.${projectSlug}`
|
|
69
|
+
return {
|
|
70
|
+
read() {
|
|
71
|
+
try {
|
|
72
|
+
const raw = window.localStorage.getItem(key)
|
|
73
|
+
if (!raw) return null
|
|
74
|
+
return JSON.parse(raw) as {
|
|
75
|
+
accessToken: string
|
|
76
|
+
refreshToken: string
|
|
77
|
+
user: SentroyAuthUser
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
write(value) {
|
|
84
|
+
try {
|
|
85
|
+
window.localStorage.setItem(key, JSON.stringify(value))
|
|
86
|
+
} catch {
|
|
87
|
+
// QuotaExceeded, etc — degrade to memory silently.
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
clear() {
|
|
91
|
+
try {
|
|
92
|
+
window.localStorage.removeItem(key)
|
|
93
|
+
} catch {
|
|
94
|
+
// ignore
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function memoryStorageAdapter(): AuthStorageAdapter {
|
|
101
|
+
let store: {
|
|
102
|
+
accessToken: string
|
|
103
|
+
refreshToken: string
|
|
104
|
+
user: SentroyAuthUser
|
|
105
|
+
} | null = null
|
|
106
|
+
return {
|
|
107
|
+
read: () => store,
|
|
108
|
+
write: (value) => {
|
|
109
|
+
store = value
|
|
110
|
+
},
|
|
111
|
+
clear: () => {
|
|
112
|
+
store = null
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface SentroyAuthOptions extends AuthHttpOptions {
|
|
118
|
+
/** Token persistence stratejisi. Default `"localStorage"` browser'da,
|
|
119
|
+
* Node'da otomatik `"memory"`. Custom için adapter geçilebilir. */
|
|
120
|
+
storage?: "localStorage" | "memory" | AuthStorageAdapter
|
|
121
|
+
/** Background refresh tetikleme süresi (saniye, expiresIn altında).
|
|
122
|
+
* Default 300 (5dk). */
|
|
123
|
+
refreshSkew?: number
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class SentroyAuth {
|
|
127
|
+
private readonly http: AuthHttp
|
|
128
|
+
private readonly storage: AuthStorageAdapter
|
|
129
|
+
private readonly listeners = new Set<AuthStateChangeListener>()
|
|
130
|
+
private readonly refreshSkew: number
|
|
131
|
+
private refreshTimer: ReturnType<typeof setTimeout> | null = null
|
|
132
|
+
private currentUser: SentroyAuthUser | null = null
|
|
133
|
+
|
|
134
|
+
constructor(opts: SentroyAuthOptions) {
|
|
135
|
+
this.http = new AuthHttp(opts)
|
|
136
|
+
this.refreshSkew = opts.refreshSkew ?? 300
|
|
137
|
+
|
|
138
|
+
if (opts.storage === "memory") {
|
|
139
|
+
this.storage = memoryStorageAdapter()
|
|
140
|
+
} else if (
|
|
141
|
+
opts.storage &&
|
|
142
|
+
typeof opts.storage === "object" &&
|
|
143
|
+
"read" in opts.storage
|
|
144
|
+
) {
|
|
145
|
+
this.storage = opts.storage
|
|
146
|
+
} else {
|
|
147
|
+
this.storage = localStorageAdapter(opts.projectSlug)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Restore from storage on construct — `onAuthStateChanged` listener'ları
|
|
151
|
+
// henüz yok; ilk subscribe sırasında dispatch edilir.
|
|
152
|
+
const restored = this.storage.read()
|
|
153
|
+
if (restored) {
|
|
154
|
+
this.currentUser = restored.user
|
|
155
|
+
this.scheduleRefresh(this.estimateExpiry(restored.accessToken))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
get user(): SentroyAuthUser | null {
|
|
162
|
+
return this.currentUser
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get accessToken(): string | null {
|
|
166
|
+
return this.storage.read()?.accessToken ?? null
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async signUp(input: {
|
|
170
|
+
email: string
|
|
171
|
+
password: string
|
|
172
|
+
displayName?: string
|
|
173
|
+
metadata?: Record<string, unknown>
|
|
174
|
+
}): Promise<SignupResponse> {
|
|
175
|
+
const res = await this.http.request<SignupResponse>("/signup", {
|
|
176
|
+
method: "POST",
|
|
177
|
+
json: input,
|
|
178
|
+
})
|
|
179
|
+
if (res.accessToken && res.refreshToken) {
|
|
180
|
+
this.persist({
|
|
181
|
+
accessToken: res.accessToken,
|
|
182
|
+
refreshToken: res.refreshToken,
|
|
183
|
+
user: res.user,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
return res
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async signIn(input: {
|
|
190
|
+
email: string
|
|
191
|
+
password: string
|
|
192
|
+
}): Promise<LoginResponse> {
|
|
193
|
+
const res = await this.http.request<LoginResponse>("/login", {
|
|
194
|
+
method: "POST",
|
|
195
|
+
json: input,
|
|
196
|
+
})
|
|
197
|
+
this.persist({
|
|
198
|
+
accessToken: res.accessToken,
|
|
199
|
+
refreshToken: res.refreshToken,
|
|
200
|
+
user: res.user,
|
|
201
|
+
})
|
|
202
|
+
return res
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async signOut(): Promise<void> {
|
|
206
|
+
const restored = this.storage.read()
|
|
207
|
+
if (restored?.refreshToken) {
|
|
208
|
+
// Best-effort revoke — fail'ı sessizce yut (network problem
|
|
209
|
+
// sign-out'u bloklamasın).
|
|
210
|
+
await this.http
|
|
211
|
+
.request("/logout", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
json: { refreshToken: restored.refreshToken },
|
|
214
|
+
})
|
|
215
|
+
.catch(() => {})
|
|
216
|
+
}
|
|
217
|
+
this.clearSession()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async sendPasswordReset(email: string): Promise<void> {
|
|
221
|
+
await this.http.request("/password-reset/request", {
|
|
222
|
+
method: "POST",
|
|
223
|
+
json: { email },
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async verifyEmail(token: string): Promise<SentroyAuthUser> {
|
|
228
|
+
const res = await this.http.request<{ user: SentroyAuthUser }>(
|
|
229
|
+
"/verify-email",
|
|
230
|
+
{ method: "POST", json: { token } },
|
|
231
|
+
)
|
|
232
|
+
if (this.currentUser && this.currentUser.id === res.user.id) {
|
|
233
|
+
const restored = this.storage.read()
|
|
234
|
+
if (restored) {
|
|
235
|
+
this.persist({ ...restored, user: res.user })
|
|
236
|
+
} else {
|
|
237
|
+
this.currentUser = res.user
|
|
238
|
+
this.notify()
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return res.user
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Subscription pattern — Firebase Auth uyumlu. Caller'ın hemen mevcut
|
|
246
|
+
* state'i alabilmesi için constructor'da restore edilen user
|
|
247
|
+
* subscribe sırasında bir kez dispatch edilir.
|
|
248
|
+
*/
|
|
249
|
+
onAuthStateChanged(listener: AuthStateChangeListener): () => void {
|
|
250
|
+
this.listeners.add(listener)
|
|
251
|
+
// Microtask gibi async dispatch — caller's `useEffect` cleanup race
|
|
252
|
+
// problemlerini önler.
|
|
253
|
+
Promise.resolve().then(() => listener(this.currentUser))
|
|
254
|
+
return () => {
|
|
255
|
+
this.listeners.delete(listener)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── Internals ───────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
private persist(value: {
|
|
262
|
+
accessToken: string
|
|
263
|
+
refreshToken: string
|
|
264
|
+
user: SentroyAuthUser
|
|
265
|
+
}): void {
|
|
266
|
+
this.storage.write(value)
|
|
267
|
+
this.currentUser = value.user
|
|
268
|
+
this.notify()
|
|
269
|
+
this.scheduleRefresh(this.estimateExpiry(value.accessToken))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private clearSession(): void {
|
|
273
|
+
this.storage.clear()
|
|
274
|
+
this.currentUser = null
|
|
275
|
+
if (this.refreshTimer) {
|
|
276
|
+
clearTimeout(this.refreshTimer)
|
|
277
|
+
this.refreshTimer = null
|
|
278
|
+
}
|
|
279
|
+
this.notify()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private notify(): void {
|
|
283
|
+
for (const l of this.listeners) {
|
|
284
|
+
try {
|
|
285
|
+
l(this.currentUser)
|
|
286
|
+
} catch {
|
|
287
|
+
// Listener hatası diğer subscriber'ları engellemesin.
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* JWT'nin `exp` claim'inden expiry'i tahmin et. Parsing fail ise
|
|
294
|
+
* 1 saat varsay (default access TTL). Refresh window: exp - skew.
|
|
295
|
+
*
|
|
296
|
+
* **Browser-safe**: `Buffer` Node'a özel, tarayıcıda yok. `atob`
|
|
297
|
+
* + URL-safe charset normalization ile decode ediyoruz.
|
|
298
|
+
*/
|
|
299
|
+
private estimateExpiry(accessToken: string): number {
|
|
300
|
+
try {
|
|
301
|
+
const [, payloadB64] = accessToken.split(".")
|
|
302
|
+
const payload = JSON.parse(decodeBase64Url(payloadB64)) as {
|
|
303
|
+
exp?: number
|
|
304
|
+
}
|
|
305
|
+
if (typeof payload.exp === "number") {
|
|
306
|
+
return payload.exp * 1000
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
// ignore — fall through to default
|
|
310
|
+
}
|
|
311
|
+
return Date.now() + 60 * 60 * 1000
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private scheduleRefresh(expiryMs: number): void {
|
|
315
|
+
if (typeof window === "undefined") return // SSR'da auto-refresh yok
|
|
316
|
+
if (this.refreshTimer) clearTimeout(this.refreshTimer)
|
|
317
|
+
const fireAt = expiryMs - this.refreshSkew * 1000
|
|
318
|
+
const delay = Math.max(fireAt - Date.now(), 5_000)
|
|
319
|
+
this.refreshTimer = setTimeout(() => {
|
|
320
|
+
this.refresh().catch(() => {
|
|
321
|
+
// Refresh fail → session cleared, listener'lar null user görür
|
|
322
|
+
this.clearSession()
|
|
323
|
+
})
|
|
324
|
+
}, delay)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async refresh(): Promise<void> {
|
|
328
|
+
const restored = this.storage.read()
|
|
329
|
+
if (!restored?.refreshToken) {
|
|
330
|
+
this.clearSession()
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
const res = await this.http.request<AuthTokensResponse>("/refresh", {
|
|
334
|
+
method: "POST",
|
|
335
|
+
json: { refreshToken: restored.refreshToken },
|
|
336
|
+
})
|
|
337
|
+
this.storage.write({
|
|
338
|
+
...restored,
|
|
339
|
+
accessToken: res.accessToken,
|
|
340
|
+
refreshToken: res.refreshToken,
|
|
341
|
+
})
|
|
342
|
+
this.scheduleRefresh(this.estimateExpiry(res.accessToken))
|
|
343
|
+
}
|
|
344
|
+
}
|
package/src/auth/http.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { SentroyAuthError } from "./types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auth-as-a-Service shared HTTP layer. Project's signup/login/refresh
|
|
5
|
+
* endpoints'iyle aynı format (JSON request + JSON response, 401/403/4xx
|
|
6
|
+
* tek-tip `{error, error_description}` shape).
|
|
7
|
+
*
|
|
8
|
+
* `apiKey` opsiyonel — browser SDK end-user akışında apiKey-less
|
|
9
|
+
* (server-only güvenlik), admin SDK her zaman set'li.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const DEFAULT_AUTH_BASE_URL = "https://auth.sentroy.com"
|
|
13
|
+
|
|
14
|
+
export interface AuthHttpOptions {
|
|
15
|
+
authBaseUrl?: string
|
|
16
|
+
projectSlug: string
|
|
17
|
+
apiKey?: string
|
|
18
|
+
/** Hata-fırlatma yerine raw response döndür — caller fine-grained handling. */
|
|
19
|
+
rawErrors?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class AuthHttp {
|
|
23
|
+
readonly baseUrl: string
|
|
24
|
+
readonly projectSlug: string
|
|
25
|
+
readonly apiKey?: string
|
|
26
|
+
|
|
27
|
+
constructor(opts: AuthHttpOptions) {
|
|
28
|
+
this.baseUrl = (opts.authBaseUrl || DEFAULT_AUTH_BASE_URL).replace(
|
|
29
|
+
/\/+$/,
|
|
30
|
+
"",
|
|
31
|
+
)
|
|
32
|
+
this.projectSlug = opts.projectSlug
|
|
33
|
+
this.apiKey = opts.apiKey
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
url(path: string): string {
|
|
37
|
+
const p = path.startsWith("/") ? path : `/${path}`
|
|
38
|
+
return `${this.baseUrl}/api/v1/auth/${this.projectSlug}${p}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async request<T>(
|
|
42
|
+
path: string,
|
|
43
|
+
init: RequestInit & {
|
|
44
|
+
json?: unknown
|
|
45
|
+
bearer?: string
|
|
46
|
+
} = {},
|
|
47
|
+
): Promise<T> {
|
|
48
|
+
const headers = new Headers(init.headers)
|
|
49
|
+
headers.set("Accept", "application/json")
|
|
50
|
+
if (init.json !== undefined) {
|
|
51
|
+
headers.set("Content-Type", "application/json")
|
|
52
|
+
}
|
|
53
|
+
// Auth precedence: explicit `bearer` (user access token) > project `apiKey`.
|
|
54
|
+
// Caller chooses the one that fits the endpoint.
|
|
55
|
+
if (init.bearer) {
|
|
56
|
+
headers.set("Authorization", `Bearer ${init.bearer}`)
|
|
57
|
+
} else if (this.apiKey) {
|
|
58
|
+
headers.set("Authorization", `Bearer ${this.apiKey}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const res = await fetch(this.url(path), {
|
|
62
|
+
...init,
|
|
63
|
+
headers,
|
|
64
|
+
body: init.json !== undefined ? JSON.stringify(init.json) : init.body,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
let payload: unknown = null
|
|
68
|
+
const ct = res.headers.get("content-type") ?? ""
|
|
69
|
+
if (ct.includes("application/json")) {
|
|
70
|
+
try {
|
|
71
|
+
payload = await res.json()
|
|
72
|
+
} catch {
|
|
73
|
+
payload = null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const err =
|
|
79
|
+
payload && typeof payload === "object"
|
|
80
|
+
? (payload as { error?: string; error_description?: string })
|
|
81
|
+
: {}
|
|
82
|
+
throw new SentroyAuthError(
|
|
83
|
+
err.error ?? "http_error",
|
|
84
|
+
err.error_description ?? `HTTP ${res.status}`,
|
|
85
|
+
res.status,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sentroy admin endpoints wrap in `{data}`; public endpoints sometimes
|
|
90
|
+
// too. SDK auto-unwraps when present so callers don't keep .data on
|
|
91
|
+
// every call.
|
|
92
|
+
if (
|
|
93
|
+
payload &&
|
|
94
|
+
typeof payload === "object" &&
|
|
95
|
+
"data" in (payload as Record<string, unknown>)
|
|
96
|
+
) {
|
|
97
|
+
return (payload as { data: T }).data
|
|
98
|
+
}
|
|
99
|
+
return payload as T
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentroy Auth-as-a-Service — browser/server SDK entry.
|
|
3
|
+
*
|
|
4
|
+
* import { SentroyAuth } from "@sentroy-co/client-sdk/auth"
|
|
5
|
+
* const auth = new SentroyAuth({ projectSlug: "my-app" })
|
|
6
|
+
*
|
|
7
|
+
* For server admin operations (verifyIdToken, etc):
|
|
8
|
+
* import { SentroyAuthAdmin } from "@sentroy-co/client-sdk/auth/admin"
|
|
9
|
+
*
|
|
10
|
+
* For React integration:
|
|
11
|
+
* import { SentroyAuthProvider, useAuth } from "@sentroy-co/client-sdk/auth/react"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export { SentroyAuth } from "./client"
|
|
15
|
+
export type {
|
|
16
|
+
SentroyAuthOptions,
|
|
17
|
+
AuthStateChangeListener,
|
|
18
|
+
AuthStorageAdapter,
|
|
19
|
+
} from "./client"
|
|
20
|
+
export {
|
|
21
|
+
SentroyAuthError,
|
|
22
|
+
type SentroyAuthUser,
|
|
23
|
+
type SignupResponse,
|
|
24
|
+
type LoginResponse,
|
|
25
|
+
type AuthTokensResponse,
|
|
26
|
+
} from "./types"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react"
|
|
11
|
+
import { SentroyAuth, type SentroyAuthOptions } from "../client"
|
|
12
|
+
import type { SentroyAuthUser } from "../types"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sentroy Auth React integration.
|
|
16
|
+
*
|
|
17
|
+
* <SentroyAuthProvider projectSlug="my-app">
|
|
18
|
+
* <App />
|
|
19
|
+
* </SentroyAuthProvider>
|
|
20
|
+
*
|
|
21
|
+
* const { user, loading, signIn, signOut } = useAuth()
|
|
22
|
+
*
|
|
23
|
+
* Provider içeride tek bir `SentroyAuth` instance tutar (mount/unmount
|
|
24
|
+
* arasında stable), `onAuthStateChanged` ile React state'i senkron tutar.
|
|
25
|
+
* `loading` ilk render → restore tamam mı henüz değil ayrımı için.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface AuthContextValue {
|
|
29
|
+
auth: SentroyAuth
|
|
30
|
+
user: SentroyAuthUser | null
|
|
31
|
+
/** True iken provider ilk state'i restore etmiş değil — UI'da
|
|
32
|
+
* "spinner" göster, "redirect to /login" tetikleme. */
|
|
33
|
+
loading: boolean
|
|
34
|
+
/** Convenience proxies — caller `auth.signIn(...)` yerine doğrudan
|
|
35
|
+
* `signIn(...)` kullanabilir. */
|
|
36
|
+
signIn: SentroyAuth["signIn"]
|
|
37
|
+
signUp: SentroyAuth["signUp"]
|
|
38
|
+
signOut: SentroyAuth["signOut"]
|
|
39
|
+
sendPasswordReset: SentroyAuth["sendPasswordReset"]
|
|
40
|
+
verifyEmail: SentroyAuth["verifyEmail"]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const AuthContext = createContext<AuthContextValue | null>(null)
|
|
44
|
+
|
|
45
|
+
export function SentroyAuthProvider({
|
|
46
|
+
children,
|
|
47
|
+
...opts
|
|
48
|
+
}: SentroyAuthOptions & { children: ReactNode }) {
|
|
49
|
+
// Single instance — opts deep-compare'a girersek dependency drift'i
|
|
50
|
+
// restart'a yol açar. Caller `projectSlug` değiştirmemeli runtime'da.
|
|
51
|
+
const auth = useMemo(
|
|
52
|
+
() => new SentroyAuth(opts),
|
|
53
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
54
|
+
[opts.projectSlug, opts.authBaseUrl, opts.apiKey],
|
|
55
|
+
)
|
|
56
|
+
const [user, setUser] = useState<SentroyAuthUser | null>(auth.user)
|
|
57
|
+
const [loading, setLoading] = useState(true)
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const unsubscribe = auth.onAuthStateChanged((u) => {
|
|
61
|
+
setUser(u)
|
|
62
|
+
setLoading(false)
|
|
63
|
+
})
|
|
64
|
+
return unsubscribe
|
|
65
|
+
}, [auth])
|
|
66
|
+
|
|
67
|
+
const value = useMemo<AuthContextValue>(
|
|
68
|
+
() => ({
|
|
69
|
+
auth,
|
|
70
|
+
user,
|
|
71
|
+
loading,
|
|
72
|
+
signIn: (i) => auth.signIn(i),
|
|
73
|
+
signUp: (i) => auth.signUp(i),
|
|
74
|
+
signOut: () => auth.signOut(),
|
|
75
|
+
sendPasswordReset: (e) => auth.sendPasswordReset(e),
|
|
76
|
+
verifyEmail: (t) => auth.verifyEmail(t),
|
|
77
|
+
}),
|
|
78
|
+
[auth, user, loading],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function useAuth(): AuthContextValue {
|
|
85
|
+
const ctx = useContext(AuthContext)
|
|
86
|
+
if (!ctx) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"useAuth must be used inside <SentroyAuthProvider> — wrap your app root.",
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return ctx
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Convenience: yalnızca current user istenirse. `loading` durumunda null
|
|
96
|
+
* dönerken bekleyebilirsin.
|
|
97
|
+
*/
|
|
98
|
+
export function useUser(): SentroyAuthUser | null {
|
|
99
|
+
return useAuth().user
|
|
100
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentroy Auth-as-a-Service — SDK types.
|
|
3
|
+
*
|
|
4
|
+
* Public types are kept narrow on purpose: SDK shapes evolve with backend;
|
|
5
|
+
* caller code should depend on these names, not on hand-coded interfaces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface SentroyAuthUser {
|
|
9
|
+
id: string
|
|
10
|
+
authProjectId: string
|
|
11
|
+
email: string
|
|
12
|
+
emailVerified: boolean
|
|
13
|
+
displayName: string | null
|
|
14
|
+
image: string | null
|
|
15
|
+
metadata: Record<string, unknown>
|
|
16
|
+
lastLoginAt: string | null
|
|
17
|
+
createdAt: string
|
|
18
|
+
updatedAt: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthTokensResponse {
|
|
22
|
+
accessToken: string
|
|
23
|
+
refreshToken: string
|
|
24
|
+
expiresIn: number
|
|
25
|
+
tokenType: "Bearer"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SignupResponse {
|
|
29
|
+
user: SentroyAuthUser
|
|
30
|
+
/** Email verification gerekiyorsa undefined; aksi halde set. */
|
|
31
|
+
accessToken?: string
|
|
32
|
+
refreshToken?: string
|
|
33
|
+
expiresIn?: number
|
|
34
|
+
tokenType?: "Bearer"
|
|
35
|
+
emailVerificationRequired?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface LoginResponse {
|
|
39
|
+
user: SentroyAuthUser
|
|
40
|
+
accessToken: string
|
|
41
|
+
refreshToken: string
|
|
42
|
+
expiresIn: number
|
|
43
|
+
tokenType: "Bearer"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AuthApiError {
|
|
47
|
+
error: string
|
|
48
|
+
error_description: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class SentroyAuthError extends Error {
|
|
52
|
+
readonly code: string
|
|
53
|
+
readonly status: number
|
|
54
|
+
constructor(code: string, message: string, status: number) {
|
|
55
|
+
super(message)
|
|
56
|
+
this.name = "SentroyAuthError"
|
|
57
|
+
this.code = code
|
|
58
|
+
this.status = status
|
|
59
|
+
}
|
|
60
|
+
}
|