@sentroy-co/client-sdk 2.13.7 → 2.13.9
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 +153 -0
- package/dist/auth/admin/index.d.ts.map +1 -0
- package/dist/auth/admin/index.js +228 -0
- package/dist/auth/admin/index.js.map +1 -0
- package/dist/auth/client.d.ts +212 -0
- package/dist/auth/client.d.ts.map +1 -0
- package/dist/auth/client.js +623 -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 +100 -0
- package/dist/auth/react/index.d.ts.map +1 -0
- package/dist/auth/react/index.js +231 -0
- package/dist/auth/react/index.js.map +1 -0
- package/dist/auth/types.d.ts +105 -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 +20 -1
- package/src/auth/admin/index.ts +387 -0
- package/src/auth/client.ts +778 -0
- package/src/auth/http.ts +101 -0
- package/src/auth/index.ts +35 -0
- package/src/auth/react/index.tsx +351 -0
- package/src/auth/types.ts +126 -0
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,35 @@
|
|
|
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
|
+
type LoginOutcome,
|
|
27
|
+
type MfaChallengeResponse,
|
|
28
|
+
type SessionSummary,
|
|
29
|
+
type ActivityEntry,
|
|
30
|
+
type MfaStatus,
|
|
31
|
+
type MfaEnrollResponse,
|
|
32
|
+
type MfaVerifyEnrollmentResponse,
|
|
33
|
+
type PasskeySummary,
|
|
34
|
+
type SocialProvider,
|
|
35
|
+
} from "./types"
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useState,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react"
|
|
12
|
+
import { SentroyAuth, type SentroyAuthOptions } from "../client"
|
|
13
|
+
import type {
|
|
14
|
+
SentroyAuthUser,
|
|
15
|
+
SessionSummary,
|
|
16
|
+
ActivityEntry,
|
|
17
|
+
MfaStatus,
|
|
18
|
+
PasskeySummary,
|
|
19
|
+
} from "../types"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sentroy Auth React integration.
|
|
23
|
+
*
|
|
24
|
+
* <SentroyAuthProvider projectSlug="my-app">
|
|
25
|
+
* <App />
|
|
26
|
+
* </SentroyAuthProvider>
|
|
27
|
+
*
|
|
28
|
+
* const { user, loading, signIn, signOut } = useAuth()
|
|
29
|
+
*
|
|
30
|
+
* Provider içeride tek bir `SentroyAuth` instance tutar (mount/unmount
|
|
31
|
+
* arasında stable), `onAuthStateChanged` ile React state'i senkron tutar.
|
|
32
|
+
* `loading` ilk render → restore tamam mı henüz değil ayrımı için.
|
|
33
|
+
*
|
|
34
|
+
* Yeni method'lar (MFA, magic link, passkey, social, /me/*) tüm
|
|
35
|
+
* `auth` instance üzerinden erişilebilir — context'te kısayol olarak
|
|
36
|
+
* en sık kullanılanlar mevcut.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
interface AuthContextValue {
|
|
40
|
+
/** Underlying SDK instance — gelişmiş senaryolarda doğrudan kullan. */
|
|
41
|
+
auth: SentroyAuth
|
|
42
|
+
user: SentroyAuthUser | null
|
|
43
|
+
/** True iken provider ilk state'i restore etmiş değil — UI'da
|
|
44
|
+
* "spinner" göster, "redirect to /login" tetikleme. */
|
|
45
|
+
loading: boolean
|
|
46
|
+
// Sık kullanılan kısayollar (proxies)
|
|
47
|
+
signIn: SentroyAuth["signIn"]
|
|
48
|
+
signUp: SentroyAuth["signUp"]
|
|
49
|
+
signOut: SentroyAuth["signOut"]
|
|
50
|
+
sendPasswordReset: SentroyAuth["sendPasswordReset"]
|
|
51
|
+
verifyEmail: SentroyAuth["verifyEmail"]
|
|
52
|
+
verifyMfa: SentroyAuth["verifyMfa"]
|
|
53
|
+
sendMagicLink: SentroyAuth["sendMagicLink"]
|
|
54
|
+
consumeMagicLink: SentroyAuth["consumeMagicLink"]
|
|
55
|
+
acceptInvitation: SentroyAuth["acceptInvitation"]
|
|
56
|
+
socialAuthorizeUrl: SentroyAuth["socialAuthorizeUrl"]
|
|
57
|
+
consumeRedirectFragment: SentroyAuth["consumeRedirectFragment"]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const AuthContext = createContext<AuthContextValue | null>(null)
|
|
61
|
+
|
|
62
|
+
export function SentroyAuthProvider({
|
|
63
|
+
children,
|
|
64
|
+
autoConsumeFragment = true,
|
|
65
|
+
...opts
|
|
66
|
+
}: SentroyAuthOptions & {
|
|
67
|
+
children: ReactNode
|
|
68
|
+
/** Mount'ta `window.location.hash`'ten social-login fragment'ı
|
|
69
|
+
* consume et — default true. RP'nin redirectUri'sinde session
|
|
70
|
+
* otomatik kurulur. */
|
|
71
|
+
autoConsumeFragment?: boolean
|
|
72
|
+
}) {
|
|
73
|
+
// Single instance — opts deep-compare'a girersek dependency drift'i
|
|
74
|
+
// restart'a yol açar. Caller `projectSlug` değiştirmemeli runtime'da.
|
|
75
|
+
const auth = useMemo(
|
|
76
|
+
() => new SentroyAuth(opts),
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
[opts.projectSlug, opts.authBaseUrl, opts.apiKey],
|
|
79
|
+
)
|
|
80
|
+
const [user, setUser] = useState<SentroyAuthUser | null>(auth.user)
|
|
81
|
+
const [loading, setLoading] = useState(true)
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const unsubscribe = auth.onAuthStateChanged((u) => {
|
|
85
|
+
setUser(u)
|
|
86
|
+
setLoading(false)
|
|
87
|
+
})
|
|
88
|
+
return unsubscribe
|
|
89
|
+
}, [auth])
|
|
90
|
+
|
|
91
|
+
// Social login redirect handler — fragment varsa otomatik consume
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!autoConsumeFragment) return
|
|
94
|
+
if (typeof window === "undefined") return
|
|
95
|
+
if (!window.location.hash.includes("access_token=")) return
|
|
96
|
+
auth.consumeRedirectFragment().catch(() => {
|
|
97
|
+
// Fragment varsa ama consume fail ise sessizce yut — caller
|
|
98
|
+
// gerekirse manuel tekrar dener.
|
|
99
|
+
})
|
|
100
|
+
}, [auth, autoConsumeFragment])
|
|
101
|
+
|
|
102
|
+
const value = useMemo<AuthContextValue>(
|
|
103
|
+
() => ({
|
|
104
|
+
auth,
|
|
105
|
+
user,
|
|
106
|
+
loading,
|
|
107
|
+
signIn: (i) => auth.signIn(i),
|
|
108
|
+
signUp: (i) => auth.signUp(i),
|
|
109
|
+
signOut: () => auth.signOut(),
|
|
110
|
+
sendPasswordReset: (e) => auth.sendPasswordReset(e),
|
|
111
|
+
verifyEmail: (t) => auth.verifyEmail(t),
|
|
112
|
+
verifyMfa: (i) => auth.verifyMfa(i),
|
|
113
|
+
sendMagicLink: (i) => auth.sendMagicLink(i),
|
|
114
|
+
consumeMagicLink: (t) => auth.consumeMagicLink(t),
|
|
115
|
+
acceptInvitation: (i) => auth.acceptInvitation(i),
|
|
116
|
+
socialAuthorizeUrl: (p, o) => auth.socialAuthorizeUrl(p, o),
|
|
117
|
+
consumeRedirectFragment: () => auth.consumeRedirectFragment(),
|
|
118
|
+
}),
|
|
119
|
+
[auth, user, loading],
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function useAuth(): AuthContextValue {
|
|
126
|
+
const ctx = useContext(AuthContext)
|
|
127
|
+
if (!ctx) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"useAuth must be used inside <SentroyAuthProvider> — wrap your app root.",
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
return ctx
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Convenience: yalnızca current user istenirse. `loading` durumunda null
|
|
137
|
+
* dönerken bekleyebilirsin.
|
|
138
|
+
*/
|
|
139
|
+
export function useUser(): SentroyAuthUser | null {
|
|
140
|
+
return useAuth().user
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Reactive sessions hook — `/me/sessions` çağırır, refresh trigger eden
|
|
145
|
+
* `refresh()` döner. Manual revoke sonrası caller `refresh()` çağırır.
|
|
146
|
+
*/
|
|
147
|
+
export function useSessions(): {
|
|
148
|
+
sessions: SessionSummary[] | null
|
|
149
|
+
loading: boolean
|
|
150
|
+
error: Error | null
|
|
151
|
+
refresh: () => Promise<void>
|
|
152
|
+
revoke: (id: string) => Promise<void>
|
|
153
|
+
} {
|
|
154
|
+
const { auth, user } = useAuth()
|
|
155
|
+
const [sessions, setSessions] = useState<SessionSummary[] | null>(null)
|
|
156
|
+
const [loading, setLoading] = useState(false)
|
|
157
|
+
const [error, setError] = useState<Error | null>(null)
|
|
158
|
+
|
|
159
|
+
const refresh = useCallback(async () => {
|
|
160
|
+
if (!user) {
|
|
161
|
+
setSessions(null)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
setLoading(true)
|
|
165
|
+
setError(null)
|
|
166
|
+
try {
|
|
167
|
+
const data = await auth.listSessions()
|
|
168
|
+
setSessions(data)
|
|
169
|
+
} catch (e) {
|
|
170
|
+
setError(e instanceof Error ? e : new Error(String(e)))
|
|
171
|
+
} finally {
|
|
172
|
+
setLoading(false)
|
|
173
|
+
}
|
|
174
|
+
}, [auth, user])
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
refresh()
|
|
178
|
+
}, [refresh])
|
|
179
|
+
|
|
180
|
+
const revoke = useCallback(
|
|
181
|
+
async (id: string) => {
|
|
182
|
+
await auth.revokeSession(id)
|
|
183
|
+
await refresh()
|
|
184
|
+
},
|
|
185
|
+
[auth, refresh],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return { sessions, loading, error, refresh, revoke }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Reactive activity log hook — `/me/activity`. RP'nin "recent activity"
|
|
193
|
+
* tab'ı için.
|
|
194
|
+
*/
|
|
195
|
+
export function useActivity(): {
|
|
196
|
+
activity: ActivityEntry[] | null
|
|
197
|
+
loading: boolean
|
|
198
|
+
error: Error | null
|
|
199
|
+
refresh: () => Promise<void>
|
|
200
|
+
} {
|
|
201
|
+
const { auth, user } = useAuth()
|
|
202
|
+
const [activity, setActivity] = useState<ActivityEntry[] | null>(null)
|
|
203
|
+
const [loading, setLoading] = useState(false)
|
|
204
|
+
const [error, setError] = useState<Error | null>(null)
|
|
205
|
+
|
|
206
|
+
const refresh = useCallback(async () => {
|
|
207
|
+
if (!user) {
|
|
208
|
+
setActivity(null)
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
setLoading(true)
|
|
212
|
+
setError(null)
|
|
213
|
+
try {
|
|
214
|
+
const data = await auth.getActivity()
|
|
215
|
+
setActivity(data)
|
|
216
|
+
} catch (e) {
|
|
217
|
+
setError(e instanceof Error ? e : new Error(String(e)))
|
|
218
|
+
} finally {
|
|
219
|
+
setLoading(false)
|
|
220
|
+
}
|
|
221
|
+
}, [auth, user])
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
refresh()
|
|
225
|
+
}, [refresh])
|
|
226
|
+
|
|
227
|
+
return { activity, loading, error, refresh }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Reactive MFA status. enroll / verify / disable wrapper'ları, status
|
|
232
|
+
* otomatik yeniden çekilir.
|
|
233
|
+
*/
|
|
234
|
+
export function useMfa(): {
|
|
235
|
+
status: MfaStatus | null
|
|
236
|
+
loading: boolean
|
|
237
|
+
error: Error | null
|
|
238
|
+
refresh: () => Promise<void>
|
|
239
|
+
enrollTotp: SentroyAuth["mfa"]["enrollTotp"]
|
|
240
|
+
verifyTotpEnrollment: (code: string) => Promise<void>
|
|
241
|
+
disableTotp: (currentPassword: string) => Promise<void>
|
|
242
|
+
} {
|
|
243
|
+
const { auth, user } = useAuth()
|
|
244
|
+
const [status, setStatus] = useState<MfaStatus | null>(null)
|
|
245
|
+
const [loading, setLoading] = useState(false)
|
|
246
|
+
const [error, setError] = useState<Error | null>(null)
|
|
247
|
+
|
|
248
|
+
const refresh = useCallback(async () => {
|
|
249
|
+
if (!user) {
|
|
250
|
+
setStatus(null)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
setLoading(true)
|
|
254
|
+
setError(null)
|
|
255
|
+
try {
|
|
256
|
+
const data = await auth.mfa.getStatus()
|
|
257
|
+
setStatus(data)
|
|
258
|
+
} catch (e) {
|
|
259
|
+
setError(e instanceof Error ? e : new Error(String(e)))
|
|
260
|
+
} finally {
|
|
261
|
+
setLoading(false)
|
|
262
|
+
}
|
|
263
|
+
}, [auth, user])
|
|
264
|
+
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
refresh()
|
|
267
|
+
}, [refresh])
|
|
268
|
+
|
|
269
|
+
const verifyTotpEnrollment = useCallback(
|
|
270
|
+
async (code: string) => {
|
|
271
|
+
await auth.mfa.verifyTotpEnrollment(code)
|
|
272
|
+
await refresh()
|
|
273
|
+
},
|
|
274
|
+
[auth, refresh],
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const disableTotp = useCallback(
|
|
278
|
+
async (currentPassword: string) => {
|
|
279
|
+
await auth.mfa.disableTotp(currentPassword)
|
|
280
|
+
await refresh()
|
|
281
|
+
},
|
|
282
|
+
[auth, refresh],
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
status,
|
|
287
|
+
loading,
|
|
288
|
+
error,
|
|
289
|
+
refresh,
|
|
290
|
+
enrollTotp: () => auth.mfa.enrollTotp(),
|
|
291
|
+
verifyTotpEnrollment,
|
|
292
|
+
disableTotp,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Reactive passkey list — list/delete/register, mutation sonrası
|
|
298
|
+
* otomatik refresh.
|
|
299
|
+
*/
|
|
300
|
+
export function usePasskeys(): {
|
|
301
|
+
passkeys: PasskeySummary[] | null
|
|
302
|
+
loading: boolean
|
|
303
|
+
error: Error | null
|
|
304
|
+
refresh: () => Promise<void>
|
|
305
|
+
register: (deviceName?: string) => Promise<void>
|
|
306
|
+
remove: (id: string) => Promise<void>
|
|
307
|
+
} {
|
|
308
|
+
const { auth, user } = useAuth()
|
|
309
|
+
const [passkeys, setPasskeys] = useState<PasskeySummary[] | null>(null)
|
|
310
|
+
const [loading, setLoading] = useState(false)
|
|
311
|
+
const [error, setError] = useState<Error | null>(null)
|
|
312
|
+
|
|
313
|
+
const refresh = useCallback(async () => {
|
|
314
|
+
if (!user) {
|
|
315
|
+
setPasskeys(null)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
setLoading(true)
|
|
319
|
+
setError(null)
|
|
320
|
+
try {
|
|
321
|
+
const data = await auth.passkey.list()
|
|
322
|
+
setPasskeys(data)
|
|
323
|
+
} catch (e) {
|
|
324
|
+
setError(e instanceof Error ? e : new Error(String(e)))
|
|
325
|
+
} finally {
|
|
326
|
+
setLoading(false)
|
|
327
|
+
}
|
|
328
|
+
}, [auth, user])
|
|
329
|
+
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
refresh()
|
|
332
|
+
}, [refresh])
|
|
333
|
+
|
|
334
|
+
const register = useCallback(
|
|
335
|
+
async (deviceName?: string) => {
|
|
336
|
+
await auth.passkey.register(deviceName)
|
|
337
|
+
await refresh()
|
|
338
|
+
},
|
|
339
|
+
[auth, refresh],
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
const remove = useCallback(
|
|
343
|
+
async (id: string) => {
|
|
344
|
+
await auth.passkey.delete(id)
|
|
345
|
+
await refresh()
|
|
346
|
+
},
|
|
347
|
+
[auth, refresh],
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
return { passkeys, loading, error, refresh, register, remove }
|
|
351
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Login response when MFA is enrolled — first step returns mfaRequired+
|
|
64
|
+
* mfaToken; second step `verifyMfa(mfaToken, code)` issues final tokens.
|
|
65
|
+
*/
|
|
66
|
+
export interface MfaChallengeResponse {
|
|
67
|
+
mfaRequired: true
|
|
68
|
+
mfaToken: string
|
|
69
|
+
factorType: "totp"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Discriminated union — Login may resolve to tokens OR MFA challenge. */
|
|
73
|
+
export type LoginOutcome =
|
|
74
|
+
| { kind: "tokens"; data: LoginResponse }
|
|
75
|
+
| { kind: "mfa"; data: MfaChallengeResponse }
|
|
76
|
+
|
|
77
|
+
export interface SessionSummary {
|
|
78
|
+
id: string
|
|
79
|
+
refreshTokenPrefix: string
|
|
80
|
+
userAgent: string | null
|
|
81
|
+
ip: string | null
|
|
82
|
+
expiresAt: string
|
|
83
|
+
createdAt: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ActivityEntry {
|
|
87
|
+
id: string
|
|
88
|
+
action: string
|
|
89
|
+
ipAddress: string | null
|
|
90
|
+
createdAt: string
|
|
91
|
+
details: Record<string, unknown> | null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface MfaStatus {
|
|
95
|
+
enrolled: boolean
|
|
96
|
+
factorType?: "totp"
|
|
97
|
+
verifiedAt?: string | null
|
|
98
|
+
recoveryCodesRemaining?: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface MfaEnrollResponse {
|
|
102
|
+
secret: string
|
|
103
|
+
otpauthUri: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface MfaVerifyEnrollmentResponse {
|
|
107
|
+
enrolled: true
|
|
108
|
+
recoveryCodes: string[]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface PasskeySummary {
|
|
112
|
+
id: string
|
|
113
|
+
credentialIdPrefix: string
|
|
114
|
+
deviceName: string | null
|
|
115
|
+
transports: string[]
|
|
116
|
+
lastUsedAt: string | null
|
|
117
|
+
createdAt: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type SocialProvider =
|
|
121
|
+
| "google"
|
|
122
|
+
| "github"
|
|
123
|
+
| "facebook"
|
|
124
|
+
| "microsoft"
|
|
125
|
+
| "twitter"
|
|
126
|
+
| "apple"
|