@erikey/react 0.4.26 → 0.4.28
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.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/auth-client.test.ts +105 -0
- package/src/__tests__/security/localStorage-encryption.test.ts +171 -0
- package/src/auth-client.ts +158 -0
- package/src/dashboard-client.ts +60 -0
- package/src/index.ts +88 -0
- package/src/kv-client.ts +316 -0
- package/src/lib/cross-origin-auth.ts +99 -0
- package/src/stubs/captcha.ts +24 -0
- package/src/stubs/hashes.ts +16 -0
- package/src/stubs/index.ts +17 -0
- package/src/stubs/passkey.ts +12 -0
- package/src/stubs/qr-code.ts +10 -0
- package/src/stubs/query.ts +16 -0
- package/src/stubs/realtime.ts +17 -0
- package/src/stubs/use-sync-external-store.ts +12 -0
- package/src/styles.css +141 -0
- package/src/types.ts +14 -0
- package/src/ui/components/auth/auth-callback.tsx +36 -0
- package/src/ui/components/auth/auth-form.tsx +310 -0
- package/src/ui/components/auth/auth-view.tsx +435 -0
- package/src/ui/components/auth/email-otp-button.tsx +53 -0
- package/src/ui/components/auth/forms/email-otp-form.tsx +312 -0
- package/src/ui/components/auth/forms/email-verification-form.tsx +271 -0
- package/src/ui/components/auth/forms/forgot-password-form.tsx +173 -0
- package/src/ui/components/auth/forms/magic-link-form.tsx +196 -0
- package/src/ui/components/auth/forms/recover-account-form.tsx +143 -0
- package/src/ui/components/auth/forms/reset-password-form.tsx +220 -0
- package/src/ui/components/auth/forms/sign-in-form.tsx +323 -0
- package/src/ui/components/auth/forms/sign-up-form.tsx +820 -0
- package/src/ui/components/auth/forms/two-factor-form.tsx +381 -0
- package/src/ui/components/auth/magic-link-button.tsx +54 -0
- package/src/ui/components/auth/one-tap.tsx +53 -0
- package/src/ui/components/auth/otp-input-group.tsx +65 -0
- package/src/ui/components/auth/passkey-button.tsx +91 -0
- package/src/ui/components/auth/provider-button.tsx +155 -0
- package/src/ui/components/auth/sign-out.tsx +25 -0
- package/src/ui/components/auth/wallet-button.tsx +192 -0
- package/src/ui/components/auth-loading.tsx +21 -0
- package/src/ui/components/captcha/captcha.tsx +91 -0
- package/src/ui/components/captcha/recaptcha-badge.tsx +61 -0
- package/src/ui/components/captcha/recaptcha-v2.tsx +58 -0
- package/src/ui/components/captcha/recaptcha-v3.tsx +73 -0
- package/src/ui/components/email/email-template.tsx +216 -0
- package/src/ui/components/form-error.tsx +27 -0
- package/src/ui/components/password-input.tsx +56 -0
- package/src/ui/components/provider-icons.tsx +404 -0
- package/src/ui/components/redirect-to-sign-in.tsx +16 -0
- package/src/ui/components/redirect-to-sign-up.tsx +16 -0
- package/src/ui/components/signed-in.tsx +20 -0
- package/src/ui/components/signed-out.tsx +20 -0
- package/src/ui/components/ui/alert.tsx +66 -0
- package/src/ui/components/ui/button.tsx +70 -0
- package/src/ui/components/ui/card.tsx +92 -0
- package/src/ui/components/ui/checkbox.tsx +66 -0
- package/src/ui/components/ui/field.tsx +248 -0
- package/src/ui/components/ui/form.tsx +165 -0
- package/src/ui/components/ui/input-otp.tsx +77 -0
- package/src/ui/components/ui/input.tsx +21 -0
- package/src/ui/components/ui/label.tsx +23 -0
- package/src/ui/components/ui/separator.tsx +34 -0
- package/src/ui/components/ui/skeleton.tsx +13 -0
- package/src/ui/components/ui/textarea.tsx +18 -0
- package/src/ui/components/user-avatar.tsx +151 -0
- package/src/ui/hooks/use-auth-data.ts +193 -0
- package/src/ui/hooks/use-authenticate.ts +64 -0
- package/src/ui/hooks/use-captcha.tsx +151 -0
- package/src/ui/hooks/use-hydrated.ts +13 -0
- package/src/ui/hooks/use-lang.ts +32 -0
- package/src/ui/hooks/use-success-transition.ts +41 -0
- package/src/ui/hooks/use-theme.ts +39 -0
- package/src/ui/index.ts +46 -0
- package/src/ui/instantdb.ts +1 -0
- package/src/ui/lib/auth-data-cache.ts +90 -0
- package/src/ui/lib/auth-ui-provider.tsx +769 -0
- package/src/ui/lib/gravatar-utils.ts +58 -0
- package/src/ui/lib/image-utils.ts +55 -0
- package/src/ui/lib/instantdb/model-names.ts +24 -0
- package/src/ui/lib/instantdb/use-instant-options.ts +98 -0
- package/src/ui/lib/instantdb/use-list-accounts.ts +38 -0
- package/src/ui/lib/instantdb/use-list-sessions.ts +53 -0
- package/src/ui/lib/instantdb/use-session.ts +55 -0
- package/src/ui/lib/social-providers.ts +150 -0
- package/src/ui/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
- package/src/ui/lib/tanstack/use-tanstack-options.ts +112 -0
- package/src/ui/lib/triplit/model-names.ts +24 -0
- package/src/ui/lib/triplit/use-conditional-query.ts +82 -0
- package/src/ui/lib/triplit/use-list-accounts.ts +31 -0
- package/src/ui/lib/triplit/use-list-sessions.ts +33 -0
- package/src/ui/lib/triplit/use-session.ts +42 -0
- package/src/ui/lib/triplit/use-triplit-hooks.ts +68 -0
- package/src/ui/lib/triplit/use-triplit-token.ts +44 -0
- package/src/ui/lib/utils.ts +119 -0
- package/src/ui/lib/view-paths.ts +61 -0
- package/src/ui/lib/wallet.ts +129 -0
- package/src/ui/localization/admin-error-codes.ts +20 -0
- package/src/ui/localization/anonymous-error-codes.ts +6 -0
- package/src/ui/localization/api-key-error-codes.ts +32 -0
- package/src/ui/localization/auth-localization.ts +865 -0
- package/src/ui/localization/base-error-codes.ts +27 -0
- package/src/ui/localization/captcha-error-codes.ts +17 -0
- package/src/ui/localization/email-otp-error-codes.ts +7 -0
- package/src/ui/localization/generic-oauth-error-codes.ts +3 -0
- package/src/ui/localization/haveibeenpwned-error-codes.ts +4 -0
- package/src/ui/localization/multi-session-error-codes.ts +3 -0
- package/src/ui/localization/organization-error-codes.ts +57 -0
- package/src/ui/localization/passkey-error-codes.ts +10 -0
- package/src/ui/localization/phone-number-error-codes.ts +10 -0
- package/src/ui/localization/stripe-localization.ts +12 -0
- package/src/ui/localization/team-error-codes.ts +12 -0
- package/src/ui/localization/two-factor-error-codes.ts +12 -0
- package/src/ui/localization/username-error-codes.ts +9 -0
- package/src/ui/server.ts +4 -0
- package/src/ui/style.css +146 -0
- package/src/ui/tanstack.ts +1 -0
- package/src/ui/triplit.ts +1 -0
- package/src/ui/types/account-options.ts +35 -0
- package/src/ui/types/additional-fields.ts +21 -0
- package/src/ui/types/any-auth-client.ts +6 -0
- package/src/ui/types/api-key.ts +9 -0
- package/src/ui/types/auth-client.ts +41 -0
- package/src/ui/types/auth-hooks.ts +81 -0
- package/src/ui/types/auth-mutators.ts +21 -0
- package/src/ui/types/avatar-options.ts +29 -0
- package/src/ui/types/captcha-options.ts +32 -0
- package/src/ui/types/captcha-provider.ts +7 -0
- package/src/ui/types/credentials-options.ts +38 -0
- package/src/ui/types/delete-user-options.ts +7 -0
- package/src/ui/types/email-verification-options.ts +7 -0
- package/src/ui/types/fetch-error.ts +6 -0
- package/src/ui/types/generic-oauth-options.ts +16 -0
- package/src/ui/types/gravatar-options.ts +21 -0
- package/src/ui/types/image.ts +7 -0
- package/src/ui/types/invitation.ts +10 -0
- package/src/ui/types/link.ts +7 -0
- package/src/ui/types/organization-options.ts +106 -0
- package/src/ui/types/password-validation.ts +16 -0
- package/src/ui/types/profile.ts +15 -0
- package/src/ui/types/refetch.ts +1 -0
- package/src/ui/types/render-toast.ts +9 -0
- package/src/ui/types/sign-up-options.ts +7 -0
- package/src/ui/types/social-options.ts +16 -0
- package/src/ui/types/team-options.ts +47 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js"
|
|
2
|
+
import { bytesToHex } from "@noble/hashes/utils.js"
|
|
3
|
+
import type { GravatarOptions } from "../types/gravatar-options"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a Gravatar URL for an email address
|
|
7
|
+
* @param email - Email address
|
|
8
|
+
* @param options - Gravatar options
|
|
9
|
+
* @returns Gravatar URL or null if email is invalid
|
|
10
|
+
*/
|
|
11
|
+
export function getGravatarUrl(
|
|
12
|
+
email?: string | null,
|
|
13
|
+
options?: GravatarOptions
|
|
14
|
+
): string | null {
|
|
15
|
+
if (!email) return null
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Normalize email: trim and lowercase
|
|
19
|
+
const normalizedEmail = email.trim().toLowerCase()
|
|
20
|
+
// sha256 expects Uint8Array, so encode string to Uint8Array
|
|
21
|
+
const encoder = new TextEncoder()
|
|
22
|
+
const emailBytes = encoder.encode(normalizedEmail)
|
|
23
|
+
const hash = bytesToHex(sha256(emailBytes))
|
|
24
|
+
const extension = options?.jpg ? ".jpg" : ""
|
|
25
|
+
let url = `https://gravatar.com/avatar/${hash}${extension}`
|
|
26
|
+
|
|
27
|
+
const params = new URLSearchParams()
|
|
28
|
+
|
|
29
|
+
// Add size parameter
|
|
30
|
+
if (options?.size) {
|
|
31
|
+
params.append(
|
|
32
|
+
"s",
|
|
33
|
+
Math.min(Math.max(options.size, 1), 2048).toString()
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Add default image parameter
|
|
38
|
+
if (options?.d) {
|
|
39
|
+
params.append("d", options.d)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add force default parameter
|
|
43
|
+
if (options?.forceDefault) {
|
|
44
|
+
params.append("f", "y")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Append parameters if any
|
|
48
|
+
const queryString = params.toString()
|
|
49
|
+
if (queryString) {
|
|
50
|
+
url += `?${queryString}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return url
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error("Error generating Gravatar URL:", error)
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export async function resizeAndCropImage(
|
|
2
|
+
file: File,
|
|
3
|
+
name: string,
|
|
4
|
+
size: number,
|
|
5
|
+
extension: string
|
|
6
|
+
): Promise<File> {
|
|
7
|
+
const image = await loadImage(file)
|
|
8
|
+
|
|
9
|
+
const canvas = document.createElement("canvas")
|
|
10
|
+
canvas.width = canvas.height = size
|
|
11
|
+
|
|
12
|
+
const ctx = canvas.getContext("2d")
|
|
13
|
+
|
|
14
|
+
const minEdge = Math.min(image.width, image.height)
|
|
15
|
+
|
|
16
|
+
const sx = (image.width - minEdge) / 2
|
|
17
|
+
const sy = (image.height - minEdge) / 2
|
|
18
|
+
const sWidth = minEdge
|
|
19
|
+
const sHeight = minEdge
|
|
20
|
+
|
|
21
|
+
ctx?.drawImage(image, sx, sy, sWidth, sHeight, 0, 0, size, size)
|
|
22
|
+
|
|
23
|
+
const resizedImageBlob = await new Promise<Blob | null>((resolve) =>
|
|
24
|
+
canvas.toBlob(resolve, `image/${extension}`)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return new File([resizedImageBlob as BlobPart], `${name}.${extension}`, {
|
|
28
|
+
type: `image/${extension}`
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadImage(file: File): Promise<HTMLImageElement> {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const image = new Image()
|
|
35
|
+
const reader = new FileReader()
|
|
36
|
+
|
|
37
|
+
reader.onload = (e) => {
|
|
38
|
+
image.src = e.target?.result as string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
image.onload = () => resolve(image)
|
|
42
|
+
image.onerror = (err) => reject(err)
|
|
43
|
+
|
|
44
|
+
reader.readAsDataURL(file)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function fileToBase64(file: File): Promise<string> {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const reader = new FileReader()
|
|
51
|
+
reader.onloadend = () => resolve(reader.result as string)
|
|
52
|
+
reader.onerror = reject
|
|
53
|
+
reader.readAsDataURL(file)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const namespaces = [
|
|
2
|
+
"user",
|
|
3
|
+
"session",
|
|
4
|
+
"account",
|
|
5
|
+
"passkey",
|
|
6
|
+
"twoFactor"
|
|
7
|
+
] as const
|
|
8
|
+
type Namespace = (typeof namespaces)[number]
|
|
9
|
+
|
|
10
|
+
export type ModelNames = {
|
|
11
|
+
[key in Namespace]: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getModelName = ({
|
|
15
|
+
namespace,
|
|
16
|
+
modelNames,
|
|
17
|
+
usePlural = false
|
|
18
|
+
}: {
|
|
19
|
+
namespace: Namespace
|
|
20
|
+
modelNames?: Partial<ModelNames>
|
|
21
|
+
usePlural?: boolean
|
|
22
|
+
}) => {
|
|
23
|
+
return modelNames?.[namespace] || `${namespace}${usePlural ? "s" : ""}`
|
|
24
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { InstantReactWebDatabase } from "@instantdb/react"
|
|
2
|
+
import type { User } from "better-auth"
|
|
3
|
+
import { useMemo } from "react"
|
|
4
|
+
|
|
5
|
+
import type { Session } from "../../types/auth-client"
|
|
6
|
+
import type { AuthHooks } from "../../types/auth-hooks"
|
|
7
|
+
import type { AuthMutators } from "../../types/auth-mutators"
|
|
8
|
+
import type { Refetch } from "../../types/refetch"
|
|
9
|
+
import { getModelName } from "./model-names"
|
|
10
|
+
import { useListAccounts } from "./use-list-accounts"
|
|
11
|
+
import { useListSessions } from "./use-list-sessions"
|
|
12
|
+
import { useSession } from "./use-session"
|
|
13
|
+
|
|
14
|
+
const namespaces = ["user", "session", "account", "passkey"] as const
|
|
15
|
+
type Namespace = (typeof namespaces)[number]
|
|
16
|
+
|
|
17
|
+
type ModelNames = {
|
|
18
|
+
[key in Namespace]: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UseInstantOptionsProps {
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
|
23
|
+
db: InstantReactWebDatabase<any>
|
|
24
|
+
modelNames?: Partial<ModelNames>
|
|
25
|
+
usePlural?: boolean
|
|
26
|
+
sessionData?: { user: User; session: Session }
|
|
27
|
+
refetch?: Refetch
|
|
28
|
+
user?: { id: string } | null
|
|
29
|
+
isPending: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useInstantOptions({
|
|
33
|
+
db,
|
|
34
|
+
usePlural = true,
|
|
35
|
+
modelNames,
|
|
36
|
+
sessionData,
|
|
37
|
+
isPending,
|
|
38
|
+
user
|
|
39
|
+
}: UseInstantOptionsProps) {
|
|
40
|
+
const userId = user?.id || sessionData?.user.id
|
|
41
|
+
|
|
42
|
+
const hooks = useMemo(() => {
|
|
43
|
+
return {
|
|
44
|
+
useSession: () =>
|
|
45
|
+
useSession({
|
|
46
|
+
db,
|
|
47
|
+
modelNames,
|
|
48
|
+
usePlural,
|
|
49
|
+
sessionData,
|
|
50
|
+
isPending
|
|
51
|
+
}),
|
|
52
|
+
useListAccounts: () =>
|
|
53
|
+
useListAccounts({
|
|
54
|
+
db,
|
|
55
|
+
modelNames,
|
|
56
|
+
usePlural,
|
|
57
|
+
sessionData,
|
|
58
|
+
isPending
|
|
59
|
+
}),
|
|
60
|
+
useListSessions: () =>
|
|
61
|
+
useListSessions({
|
|
62
|
+
db,
|
|
63
|
+
modelNames,
|
|
64
|
+
usePlural,
|
|
65
|
+
sessionData,
|
|
66
|
+
isPending
|
|
67
|
+
})
|
|
68
|
+
} as AuthHooks
|
|
69
|
+
}, [db, modelNames, usePlural, sessionData, isPending])
|
|
70
|
+
|
|
71
|
+
const mutators = useMemo(() => {
|
|
72
|
+
return {
|
|
73
|
+
updateUser: async (data) => {
|
|
74
|
+
if (!userId) {
|
|
75
|
+
throw new Error("Unauthenticated")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const modelName = getModelName({
|
|
79
|
+
namespace: "user",
|
|
80
|
+
modelNames,
|
|
81
|
+
usePlural
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
db.transact([
|
|
85
|
+
db.tx[modelName][userId].update({
|
|
86
|
+
...data,
|
|
87
|
+
updatedAt: Date.now()
|
|
88
|
+
})
|
|
89
|
+
])
|
|
90
|
+
}
|
|
91
|
+
} as AuthMutators
|
|
92
|
+
}, [db, userId, modelNames, usePlural])
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
hooks,
|
|
96
|
+
mutators
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BetterFetchError } from "@better-fetch/fetch"
|
|
2
|
+
import type { Account } from "better-auth"
|
|
3
|
+
import { useMemo } from "react"
|
|
4
|
+
import type { AuthHooks } from "../../types/auth-hooks"
|
|
5
|
+
import { getModelName } from "./model-names"
|
|
6
|
+
import type { UseInstantOptionsProps } from "./use-instant-options"
|
|
7
|
+
|
|
8
|
+
export function useListAccounts({
|
|
9
|
+
db,
|
|
10
|
+
modelNames,
|
|
11
|
+
usePlural,
|
|
12
|
+
isPending
|
|
13
|
+
}: UseInstantOptionsProps): ReturnType<AuthHooks["useListAccounts"]> {
|
|
14
|
+
const { user: authUser, isLoading: authLoading } = db.useAuth()
|
|
15
|
+
|
|
16
|
+
const modelName = getModelName({
|
|
17
|
+
namespace: "account",
|
|
18
|
+
modelNames,
|
|
19
|
+
usePlural
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const { data, isLoading, error } = db.useQuery(
|
|
23
|
+
authUser
|
|
24
|
+
? { [modelName]: { $: { where: { userId: authUser?.id } } } }
|
|
25
|
+
: null
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const accounts = useMemo(
|
|
29
|
+
() => data?.[modelName] as Account[],
|
|
30
|
+
[data, modelName]
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
data: accounts,
|
|
35
|
+
isPending: !accounts && (isPending || authLoading || isLoading),
|
|
36
|
+
error: (error as BetterFetchError) || null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Session } from "better-auth"
|
|
2
|
+
import { useMemo } from "react"
|
|
3
|
+
import type { AuthHooks } from "../../types/auth-hooks"
|
|
4
|
+
import { getModelName } from "./model-names"
|
|
5
|
+
import type { UseInstantOptionsProps } from "./use-instant-options"
|
|
6
|
+
|
|
7
|
+
export function useListSessions({
|
|
8
|
+
db,
|
|
9
|
+
modelNames,
|
|
10
|
+
usePlural,
|
|
11
|
+
isPending
|
|
12
|
+
}: UseInstantOptionsProps): ReturnType<AuthHooks["useListSessions"]> {
|
|
13
|
+
const { user: authUser, isLoading: authLoading } = db.useAuth()
|
|
14
|
+
|
|
15
|
+
const modelName = getModelName({
|
|
16
|
+
namespace: "session",
|
|
17
|
+
modelNames,
|
|
18
|
+
usePlural
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const now = useMemo(() => Date.now(), [])
|
|
22
|
+
|
|
23
|
+
const { data, isLoading } = db.useQuery(
|
|
24
|
+
authUser
|
|
25
|
+
? {
|
|
26
|
+
[modelName]: {
|
|
27
|
+
$: {
|
|
28
|
+
where: {
|
|
29
|
+
userId: authUser?.id,
|
|
30
|
+
expiresAt: { $gte: now }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
: null
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const sessions = useMemo(() => {
|
|
39
|
+
if (data?.[modelName]) {
|
|
40
|
+
return data[modelName].map((session) => ({
|
|
41
|
+
...session,
|
|
42
|
+
expiresAt: new Date(session.expiresAt as string),
|
|
43
|
+
createdAt: new Date(session.createdAt as string),
|
|
44
|
+
updatedAt: new Date(session.updatedAt as string)
|
|
45
|
+
})) as Session[]
|
|
46
|
+
}
|
|
47
|
+
}, [data, modelName])
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
data: sessions,
|
|
51
|
+
isPending: !sessions && (isPending || authLoading || isLoading)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { BetterFetchError } from "better-auth/react"
|
|
2
|
+
import { useMemo } from "react"
|
|
3
|
+
import type { User } from "../../types/auth-client"
|
|
4
|
+
import type { AuthHooks } from "../../types/auth-hooks"
|
|
5
|
+
import { getModelName } from "./model-names"
|
|
6
|
+
import type { UseInstantOptionsProps } from "./use-instant-options"
|
|
7
|
+
|
|
8
|
+
export function useSession({
|
|
9
|
+
db,
|
|
10
|
+
sessionData,
|
|
11
|
+
isPending,
|
|
12
|
+
refetch,
|
|
13
|
+
usePlural,
|
|
14
|
+
modelNames
|
|
15
|
+
}: UseInstantOptionsProps): ReturnType<AuthHooks["useSession"]> {
|
|
16
|
+
const { user: authUser, error } = db.useAuth()
|
|
17
|
+
|
|
18
|
+
const modelName = getModelName({
|
|
19
|
+
namespace: "user",
|
|
20
|
+
modelNames,
|
|
21
|
+
usePlural
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const { data } = db.useQuery(
|
|
25
|
+
authUser
|
|
26
|
+
? { [modelName]: { $: { where: { id: authUser?.id } } } }
|
|
27
|
+
: null
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const user = useMemo(() => {
|
|
31
|
+
if (data?.[modelName]?.length) {
|
|
32
|
+
const user = data[modelName][0]
|
|
33
|
+
return {
|
|
34
|
+
...user,
|
|
35
|
+
createdAt: new Date(user.createdAt as string),
|
|
36
|
+
updatedAt: new Date(user.updatedAt as string)
|
|
37
|
+
} as User
|
|
38
|
+
}
|
|
39
|
+
return null
|
|
40
|
+
}, [data, modelName])
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
data: sessionData
|
|
44
|
+
? {
|
|
45
|
+
session: sessionData.session,
|
|
46
|
+
user: (sessionData?.user.id === user?.id
|
|
47
|
+
? user
|
|
48
|
+
: sessionData.user) as User
|
|
49
|
+
}
|
|
50
|
+
: null,
|
|
51
|
+
isPending,
|
|
52
|
+
refetch: refetch || (() => {}),
|
|
53
|
+
error: (error as BetterFetchError) || null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppleIcon,
|
|
3
|
+
DiscordIcon,
|
|
4
|
+
DropboxIcon,
|
|
5
|
+
FacebookIcon,
|
|
6
|
+
GitHubIcon,
|
|
7
|
+
GitLabIcon,
|
|
8
|
+
GoogleIcon,
|
|
9
|
+
HuggingFaceIcon,
|
|
10
|
+
KickIcon,
|
|
11
|
+
LinearIcon,
|
|
12
|
+
LinkedInIcon,
|
|
13
|
+
MicrosoftIcon,
|
|
14
|
+
NotionIcon,
|
|
15
|
+
type ProviderIcon,
|
|
16
|
+
RedditIcon,
|
|
17
|
+
RobloxIcon,
|
|
18
|
+
SlackIcon,
|
|
19
|
+
SpotifyIcon,
|
|
20
|
+
TikTokIcon,
|
|
21
|
+
TwitchIcon,
|
|
22
|
+
VercelIcon,
|
|
23
|
+
VKIcon,
|
|
24
|
+
XIcon,
|
|
25
|
+
ZoomIcon
|
|
26
|
+
} from "../components/provider-icons"
|
|
27
|
+
|
|
28
|
+
export const socialProviders = [
|
|
29
|
+
{
|
|
30
|
+
provider: "apple",
|
|
31
|
+
name: "Apple",
|
|
32
|
+
icon: AppleIcon
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
provider: "discord",
|
|
36
|
+
name: "Discord",
|
|
37
|
+
icon: DiscordIcon
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
provider: "dropbox",
|
|
41
|
+
name: "Dropbox",
|
|
42
|
+
icon: DropboxIcon
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
provider: "facebook",
|
|
46
|
+
name: "Facebook",
|
|
47
|
+
icon: FacebookIcon
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
provider: "github",
|
|
51
|
+
name: "GitHub",
|
|
52
|
+
icon: GitHubIcon
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
provider: "gitlab",
|
|
56
|
+
name: "GitLab",
|
|
57
|
+
icon: GitLabIcon
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
provider: "google",
|
|
61
|
+
name: "Google",
|
|
62
|
+
icon: GoogleIcon
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
provider: "huggingface",
|
|
66
|
+
name: "Hugging Face",
|
|
67
|
+
icon: HuggingFaceIcon
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
provider: "kick",
|
|
71
|
+
name: "Kick",
|
|
72
|
+
icon: KickIcon
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
provider: "linear",
|
|
76
|
+
name: "Linear",
|
|
77
|
+
icon: LinearIcon
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
provider: "linkedin",
|
|
81
|
+
name: "LinkedIn",
|
|
82
|
+
icon: LinkedInIcon
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
provider: "microsoft",
|
|
86
|
+
name: "Microsoft",
|
|
87
|
+
icon: MicrosoftIcon
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
provider: "notion",
|
|
91
|
+
name: "Notion",
|
|
92
|
+
icon: NotionIcon
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
provider: "reddit",
|
|
96
|
+
name: "Reddit",
|
|
97
|
+
icon: RedditIcon
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
provider: "roblox",
|
|
101
|
+
name: "Roblox",
|
|
102
|
+
icon: RobloxIcon
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
provider: "slack",
|
|
106
|
+
name: "Slack",
|
|
107
|
+
icon: SlackIcon
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
provider: "spotify",
|
|
111
|
+
name: "Spotify",
|
|
112
|
+
icon: SpotifyIcon
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
provider: "tiktok",
|
|
116
|
+
name: "TikTok",
|
|
117
|
+
icon: TikTokIcon
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
provider: "twitch",
|
|
121
|
+
name: "Twitch",
|
|
122
|
+
icon: TwitchIcon
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
provider: "vercel",
|
|
126
|
+
name: "Vercel",
|
|
127
|
+
icon: VercelIcon
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
provider: "vk",
|
|
131
|
+
name: "VK",
|
|
132
|
+
icon: VKIcon
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
provider: "twitter",
|
|
136
|
+
name: "X",
|
|
137
|
+
icon: XIcon
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
provider: "zoom",
|
|
141
|
+
name: "Zoom",
|
|
142
|
+
icon: ZoomIcon
|
|
143
|
+
}
|
|
144
|
+
] as const
|
|
145
|
+
|
|
146
|
+
export type Provider = {
|
|
147
|
+
provider: string
|
|
148
|
+
name: string
|
|
149
|
+
icon?: ProviderIcon
|
|
150
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react"
|
|
2
|
+
import {
|
|
3
|
+
AuthUIProvider,
|
|
4
|
+
type AuthUIProviderProps
|
|
5
|
+
} from "../../lib/auth-ui-provider"
|
|
6
|
+
import { useTanstackOptions } from "./use-tanstack-options"
|
|
7
|
+
|
|
8
|
+
export function AuthUIProviderTanstack({
|
|
9
|
+
children,
|
|
10
|
+
authClient,
|
|
11
|
+
hooks: hooksProp,
|
|
12
|
+
mutators: mutatorsProp,
|
|
13
|
+
onSessionChange: onSessionChangeProp,
|
|
14
|
+
...props
|
|
15
|
+
}: AuthUIProviderProps) {
|
|
16
|
+
const {
|
|
17
|
+
hooks: contextHooks,
|
|
18
|
+
mutators: contextMutators,
|
|
19
|
+
onSessionChange,
|
|
20
|
+
optimistic
|
|
21
|
+
} = useTanstackOptions({ authClient })
|
|
22
|
+
|
|
23
|
+
const hooks = useMemo(
|
|
24
|
+
() => ({ ...contextHooks, ...hooksProp }),
|
|
25
|
+
[contextHooks, hooksProp]
|
|
26
|
+
)
|
|
27
|
+
const mutators = useMemo(
|
|
28
|
+
() => ({ ...contextMutators, ...mutatorsProp }),
|
|
29
|
+
[contextMutators, mutatorsProp]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const onSessionChangeCallback = useCallback(async () => {
|
|
33
|
+
await onSessionChange()
|
|
34
|
+
await onSessionChangeProp?.()
|
|
35
|
+
}, [onSessionChangeProp, onSessionChange])
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<AuthUIProvider
|
|
39
|
+
authClient={authClient}
|
|
40
|
+
hooks={hooks}
|
|
41
|
+
mutators={mutators}
|
|
42
|
+
onSessionChange={onSessionChangeCallback}
|
|
43
|
+
optimistic={optimistic}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
{children}
|
|
47
|
+
</AuthUIProvider>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthQueryContext,
|
|
3
|
+
createAuthHooks
|
|
4
|
+
} from "@daveyplate/better-auth-tanstack"
|
|
5
|
+
import { useIsRestoring, useQueryClient } from "@tanstack/react-query"
|
|
6
|
+
import { useCallback, useContext, useMemo } from "react"
|
|
7
|
+
import type { AuthHooks } from "../../types/auth-hooks"
|
|
8
|
+
import type { AuthMutators } from "../../types/auth-mutators"
|
|
9
|
+
|
|
10
|
+
export function useTanstackOptions({
|
|
11
|
+
authClient
|
|
12
|
+
}: {
|
|
13
|
+
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
|
14
|
+
authClient: any
|
|
15
|
+
}) {
|
|
16
|
+
const {
|
|
17
|
+
useUnlinkAccount,
|
|
18
|
+
useUpdateUser,
|
|
19
|
+
useDeletePasskey,
|
|
20
|
+
useRevokeSession,
|
|
21
|
+
useRevokeDeviceSession,
|
|
22
|
+
useSetActiveSession
|
|
23
|
+
} = createAuthHooks(authClient)
|
|
24
|
+
const queryClient = useQueryClient()
|
|
25
|
+
|
|
26
|
+
const { mutateAsync: updateUserAsync } = useUpdateUser()
|
|
27
|
+
const { mutateAsync: deletePasskeyAsync } = useDeletePasskey()
|
|
28
|
+
const { mutateAsync: unlinkAccountAsync } = useUnlinkAccount()
|
|
29
|
+
const { mutateAsync: revokeSessionAsync } = useRevokeSession()
|
|
30
|
+
const { mutateAsync: revokeDeviceSessionAsync } = useRevokeDeviceSession()
|
|
31
|
+
const { setActiveSessionAsync } = useSetActiveSession()
|
|
32
|
+
const { sessionKey } = useContext(AuthQueryContext)
|
|
33
|
+
|
|
34
|
+
const hooks = useMemo(
|
|
35
|
+
() => ({
|
|
36
|
+
...(createAuthHooks(authClient) as Partial<AuthHooks>),
|
|
37
|
+
useIsRestoring
|
|
38
|
+
}),
|
|
39
|
+
[authClient]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const mutators = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
({
|
|
45
|
+
updateUser: async (params) => {
|
|
46
|
+
const { error } = await updateUserAsync({
|
|
47
|
+
...params,
|
|
48
|
+
fetchOptions: { throw: false }
|
|
49
|
+
})
|
|
50
|
+
if (error) throw error
|
|
51
|
+
},
|
|
52
|
+
unlinkAccount: async (params) => {
|
|
53
|
+
const { error } = await unlinkAccountAsync({
|
|
54
|
+
...params,
|
|
55
|
+
fetchOptions: { throw: false }
|
|
56
|
+
})
|
|
57
|
+
if (error) throw error
|
|
58
|
+
},
|
|
59
|
+
deletePasskey: async (params) => {
|
|
60
|
+
const { error } = await deletePasskeyAsync({
|
|
61
|
+
...params,
|
|
62
|
+
fetchOptions: { throw: false }
|
|
63
|
+
})
|
|
64
|
+
if (error) throw error
|
|
65
|
+
},
|
|
66
|
+
revokeSession: async (params) => {
|
|
67
|
+
const { error } = await revokeSessionAsync({
|
|
68
|
+
...params,
|
|
69
|
+
fetchOptions: { throw: false }
|
|
70
|
+
})
|
|
71
|
+
if (error) throw error
|
|
72
|
+
},
|
|
73
|
+
setActiveSession: async (params) => {
|
|
74
|
+
const { error } = await setActiveSessionAsync({
|
|
75
|
+
...params,
|
|
76
|
+
fetchOptions: { throw: false }
|
|
77
|
+
})
|
|
78
|
+
if (error) throw error
|
|
79
|
+
},
|
|
80
|
+
revokeDeviceSession: async (params) => {
|
|
81
|
+
const { error } = await revokeDeviceSessionAsync({
|
|
82
|
+
...params,
|
|
83
|
+
fetchOptions: { throw: false }
|
|
84
|
+
})
|
|
85
|
+
if (error) throw error
|
|
86
|
+
}
|
|
87
|
+
}) as AuthMutators,
|
|
88
|
+
[
|
|
89
|
+
updateUserAsync,
|
|
90
|
+
deletePasskeyAsync,
|
|
91
|
+
unlinkAccountAsync,
|
|
92
|
+
revokeSessionAsync,
|
|
93
|
+
revokeDeviceSessionAsync,
|
|
94
|
+
setActiveSessionAsync
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const onSessionChange = useCallback(async () => {
|
|
99
|
+
await queryClient.refetchQueries({ queryKey: sessionKey })
|
|
100
|
+
|
|
101
|
+
queryClient.invalidateQueries({
|
|
102
|
+
predicate: (query) => query.queryKey !== sessionKey
|
|
103
|
+
})
|
|
104
|
+
}, [queryClient, sessionKey])
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
hooks,
|
|
108
|
+
mutators,
|
|
109
|
+
onSessionChange,
|
|
110
|
+
optimistic: true
|
|
111
|
+
}
|
|
112
|
+
}
|