@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,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,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FetchResult,
|
|
3
|
+
Models,
|
|
4
|
+
SchemaQuery,
|
|
5
|
+
SubscriptionOptions,
|
|
6
|
+
SubscriptionSignalPayload,
|
|
7
|
+
TriplitClient
|
|
8
|
+
} from "@triplit/client"
|
|
9
|
+
import type { WorkerClient } from "@triplit/client/worker-client"
|
|
10
|
+
import { createStateSubscription } from "@triplit/react"
|
|
11
|
+
import { useCallback, useMemo, useState, useSyncExternalStore } from "react"
|
|
12
|
+
|
|
13
|
+
export function useConditionalQuery<
|
|
14
|
+
M extends Models<M>,
|
|
15
|
+
Q extends SchemaQuery<M>
|
|
16
|
+
>(
|
|
17
|
+
client: TriplitClient<M> | WorkerClient<M>,
|
|
18
|
+
query?: Q | false | null | "" | 0,
|
|
19
|
+
options?: Partial<SubscriptionOptions> & { disabled?: boolean }
|
|
20
|
+
) {
|
|
21
|
+
const stringifiedQuery =
|
|
22
|
+
!options?.disabled && query && JSON.stringify(query)
|
|
23
|
+
const localOnly = !!options?.localOnly
|
|
24
|
+
const [remoteFulfilled, setRemoteFulfilled] = useState(false)
|
|
25
|
+
|
|
26
|
+
const defaultValue: SubscriptionSignalPayload<M, Q> = {
|
|
27
|
+
results: undefined,
|
|
28
|
+
fetching: true,
|
|
29
|
+
fetchingLocal: false,
|
|
30
|
+
fetchingRemote: false,
|
|
31
|
+
error: undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: prevent infinite re-renders
|
|
35
|
+
const [subscribe, snapshot] = useMemo(
|
|
36
|
+
() =>
|
|
37
|
+
stringifiedQuery
|
|
38
|
+
? createStateSubscription(client, query, {
|
|
39
|
+
...options,
|
|
40
|
+
onRemoteFulfilled: () => setRemoteFulfilled(true)
|
|
41
|
+
})
|
|
42
|
+
: [() => () => {}, () => defaultValue],
|
|
43
|
+
[stringifiedQuery, localOnly]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const getServerSnapshot = useCallback(() => snapshot(), [snapshot])
|
|
47
|
+
const { fetching, ...rest } = useSyncExternalStore(
|
|
48
|
+
subscribe,
|
|
49
|
+
snapshot,
|
|
50
|
+
getServerSnapshot
|
|
51
|
+
)
|
|
52
|
+
return { fetching: fetching && !remoteFulfilled, ...rest }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type useConditionalQueryOnePayload<
|
|
56
|
+
M extends Models<M>,
|
|
57
|
+
Q extends SchemaQuery<M>
|
|
58
|
+
> = Omit<SubscriptionSignalPayload<M, Q>, "results"> & {
|
|
59
|
+
result: FetchResult<M, Q, "one">
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useConditionalQueryOne<
|
|
63
|
+
M extends Models<M>,
|
|
64
|
+
Q extends SchemaQuery<M>
|
|
65
|
+
>(
|
|
66
|
+
client: TriplitClient<M> | WorkerClient<M>,
|
|
67
|
+
query?: Q | false | null | "" | 0,
|
|
68
|
+
options?: Partial<SubscriptionOptions> & { disabled?: boolean }
|
|
69
|
+
): useConditionalQueryOnePayload<M, Q> {
|
|
70
|
+
const { fetching, fetchingLocal, fetchingRemote, results, error } =
|
|
71
|
+
useConditionalQuery(
|
|
72
|
+
client,
|
|
73
|
+
query ? ({ ...query, limit: 1 } as Q) : query,
|
|
74
|
+
options
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const result = useMemo(() => {
|
|
78
|
+
return results?.[0] ?? null
|
|
79
|
+
}, [results])
|
|
80
|
+
|
|
81
|
+
return { fetching, fetchingLocal, fetchingRemote, result, error }
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AuthHooks } from "../../types/auth-hooks"
|
|
2
|
+
import { getModelName } from "./model-names"
|
|
3
|
+
import { useConditionalQuery } from "./use-conditional-query"
|
|
4
|
+
import type { UseTriplitOptionsProps } from "./use-triplit-hooks"
|
|
5
|
+
import { useTriplitToken } from "./use-triplit-token"
|
|
6
|
+
|
|
7
|
+
export function useListAccounts({
|
|
8
|
+
triplit,
|
|
9
|
+
modelNames,
|
|
10
|
+
usePlural,
|
|
11
|
+
isPending
|
|
12
|
+
}: UseTriplitOptionsProps): ReturnType<AuthHooks["useListAccounts"]> {
|
|
13
|
+
const modelName = getModelName({
|
|
14
|
+
namespace: "account",
|
|
15
|
+
modelNames,
|
|
16
|
+
usePlural
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const { payload } = useTriplitToken(triplit)
|
|
20
|
+
|
|
21
|
+
const { results, error, fetching } = useConditionalQuery(
|
|
22
|
+
triplit,
|
|
23
|
+
payload?.sub && triplit.query(modelName)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
data: results,
|
|
28
|
+
isPending: isPending || fetching,
|
|
29
|
+
error
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Session } from "better-auth"
|
|
2
|
+
import type { AuthHooks } from "../../types/auth-hooks"
|
|
3
|
+
import { getModelName } from "./model-names"
|
|
4
|
+
import { useConditionalQuery } from "./use-conditional-query"
|
|
5
|
+
import type { UseTriplitOptionsProps } from "./use-triplit-hooks"
|
|
6
|
+
import { useTriplitToken } from "./use-triplit-token"
|
|
7
|
+
|
|
8
|
+
export function useListSessions({
|
|
9
|
+
triplit,
|
|
10
|
+
modelNames,
|
|
11
|
+
usePlural,
|
|
12
|
+
isPending
|
|
13
|
+
}: UseTriplitOptionsProps): ReturnType<AuthHooks["useListSessions"]> {
|
|
14
|
+
const modelName = getModelName({
|
|
15
|
+
namespace: "session",
|
|
16
|
+
modelNames,
|
|
17
|
+
usePlural
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const { payload } = useTriplitToken(triplit)
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
results: sessions,
|
|
24
|
+
error,
|
|
25
|
+
fetching
|
|
26
|
+
} = useConditionalQuery(triplit, payload?.sub && triplit.query(modelName))
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
data: sessions as Session[] | undefined,
|
|
30
|
+
isPending: isPending || fetching,
|
|
31
|
+
error
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { User } from "../../types/auth-client"
|
|
2
|
+
import type { AuthHooks } from "../../types/auth-hooks"
|
|
3
|
+
import { getModelName } from "./model-names"
|
|
4
|
+
import { useConditionalQueryOne } from "./use-conditional-query"
|
|
5
|
+
import type { UseTriplitOptionsProps } from "./use-triplit-hooks"
|
|
6
|
+
import { useTriplitToken } from "./use-triplit-token"
|
|
7
|
+
|
|
8
|
+
export function useSession({
|
|
9
|
+
triplit,
|
|
10
|
+
sessionData,
|
|
11
|
+
isPending,
|
|
12
|
+
refetch,
|
|
13
|
+
usePlural,
|
|
14
|
+
modelNames
|
|
15
|
+
}: UseTriplitOptionsProps): ReturnType<AuthHooks["useSession"]> {
|
|
16
|
+
const modelName = getModelName({
|
|
17
|
+
namespace: "user",
|
|
18
|
+
modelNames,
|
|
19
|
+
usePlural
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const { payload } = useTriplitToken(triplit)
|
|
23
|
+
|
|
24
|
+
const { result: user, error } = useConditionalQueryOne(
|
|
25
|
+
triplit,
|
|
26
|
+
payload?.sub && triplit.query(modelName)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
data: sessionData
|
|
31
|
+
? {
|
|
32
|
+
session: sessionData.session,
|
|
33
|
+
user: (sessionData?.user.id === user?.id
|
|
34
|
+
? user
|
|
35
|
+
: sessionData.user) as User
|
|
36
|
+
}
|
|
37
|
+
: null,
|
|
38
|
+
error,
|
|
39
|
+
isPending: isPending,
|
|
40
|
+
refetch: refetch || (() => {})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { TriplitClient } from "@triplit/client"
|
|
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 { Refetch } from "../../types/refetch"
|
|
8
|
+
import { useListAccounts } from "./use-list-accounts"
|
|
9
|
+
import { useListSessions } from "./use-list-sessions"
|
|
10
|
+
import { useSession } from "./use-session"
|
|
11
|
+
|
|
12
|
+
const namespaces = ["user", "session", "account", "passkey"] as const
|
|
13
|
+
type Namespace = (typeof namespaces)[number]
|
|
14
|
+
|
|
15
|
+
type ModelNames = {
|
|
16
|
+
[key in Namespace]: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseTriplitOptionsProps {
|
|
20
|
+
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
|
21
|
+
triplit: TriplitClient<any>
|
|
22
|
+
modelNames?: Partial<ModelNames>
|
|
23
|
+
usePlural?: boolean
|
|
24
|
+
sessionData?: { user: User; session: Session } | null
|
|
25
|
+
refetch?: Refetch
|
|
26
|
+
isPending: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useTriplitHooks({
|
|
30
|
+
triplit,
|
|
31
|
+
usePlural = true,
|
|
32
|
+
modelNames,
|
|
33
|
+
sessionData,
|
|
34
|
+
isPending
|
|
35
|
+
}: UseTriplitOptionsProps) {
|
|
36
|
+
const hooks = useMemo(() => {
|
|
37
|
+
return {
|
|
38
|
+
useSession: () =>
|
|
39
|
+
useSession({
|
|
40
|
+
triplit,
|
|
41
|
+
modelNames,
|
|
42
|
+
usePlural,
|
|
43
|
+
sessionData,
|
|
44
|
+
isPending
|
|
45
|
+
}),
|
|
46
|
+
useListAccounts: () =>
|
|
47
|
+
useListAccounts({
|
|
48
|
+
triplit,
|
|
49
|
+
modelNames,
|
|
50
|
+
usePlural,
|
|
51
|
+
sessionData,
|
|
52
|
+
isPending
|
|
53
|
+
}),
|
|
54
|
+
useListSessions: () =>
|
|
55
|
+
useListSessions({
|
|
56
|
+
triplit,
|
|
57
|
+
modelNames,
|
|
58
|
+
usePlural,
|
|
59
|
+
sessionData,
|
|
60
|
+
isPending
|
|
61
|
+
})
|
|
62
|
+
} as AuthHooks
|
|
63
|
+
}, [triplit, modelNames, usePlural, sessionData, isPending])
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
hooks
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TriplitClient } from "@triplit/client"
|
|
2
|
+
import { useConnectionStatus } from "@triplit/react"
|
|
3
|
+
import { useMemo } from "react"
|
|
4
|
+
|
|
5
|
+
export function useTriplitToken(triplit: TriplitClient) {
|
|
6
|
+
const connectionStatus = useConnectionStatus(triplit)
|
|
7
|
+
|
|
8
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: update when connection status changes
|
|
9
|
+
const payload = useMemo(
|
|
10
|
+
() =>
|
|
11
|
+
triplit.token
|
|
12
|
+
? (decodeJWT(triplit.token) as Record<string, unknown> & {
|
|
13
|
+
exp: number
|
|
14
|
+
iat: number
|
|
15
|
+
sub?: string
|
|
16
|
+
email?: string
|
|
17
|
+
name?: string
|
|
18
|
+
})
|
|
19
|
+
: undefined,
|
|
20
|
+
[connectionStatus]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return { token: payload && triplit.token, payload }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function decodeJWT(token: string) {
|
|
27
|
+
try {
|
|
28
|
+
const base64Url = token.split(".")[1]
|
|
29
|
+
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/")
|
|
30
|
+
const jsonPayload = decodeURIComponent(
|
|
31
|
+
atob(base64)
|
|
32
|
+
.split("")
|
|
33
|
+
.map((char) => {
|
|
34
|
+
return `%${(`00${char.charCodeAt(0).toString(16)}`).slice(-2)}`
|
|
35
|
+
})
|
|
36
|
+
.join("")
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return JSON.parse(jsonPayload)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error("Failed to decode JWT:", error)
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { type ClassValue, clsx } from "clsx"
|
|
2
|
+
import { twMerge } from "tailwind-merge"
|
|
3
|
+
import * as z from "zod"
|
|
4
|
+
import type { AuthLocalization } from "../localization/auth-localization"
|
|
5
|
+
import type { PasswordValidation } from "../types/password-validation"
|
|
6
|
+
|
|
7
|
+
export function cn(...inputs: ClassValue[]) {
|
|
8
|
+
return twMerge(clsx(inputs))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isValidEmail(email: string) {
|
|
12
|
+
const emailRegex: RegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
13
|
+
return emailRegex.test(email)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts error codes from SNAKE_CASE to camelCase
|
|
18
|
+
* Example: INVALID_TWO_FACTOR_COOKIE -> invalidTwoFactorCookie
|
|
19
|
+
*/
|
|
20
|
+
export function errorCodeToCamelCase(errorCode: string): string {
|
|
21
|
+
return errorCode
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/_([a-z])/g, (_, char) => char.toUpperCase())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Gets a localized error message from an error object
|
|
28
|
+
*/
|
|
29
|
+
export function getLocalizedError({
|
|
30
|
+
error,
|
|
31
|
+
localization,
|
|
32
|
+
localizeErrors = true
|
|
33
|
+
}: {
|
|
34
|
+
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
|
35
|
+
error: any
|
|
36
|
+
localization?: Partial<AuthLocalization>
|
|
37
|
+
localizeErrors: boolean
|
|
38
|
+
}) {
|
|
39
|
+
const DEFAULT_ERROR_MESSAGE = "Request failed"
|
|
40
|
+
|
|
41
|
+
// If localization is disabled, return backend error message directly
|
|
42
|
+
if (!localizeErrors) {
|
|
43
|
+
if (error?.message) return error.message
|
|
44
|
+
if (error?.error?.message) return error.error.message
|
|
45
|
+
|
|
46
|
+
return DEFAULT_ERROR_MESSAGE
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof error === "string") {
|
|
50
|
+
if (localization?.[error as keyof AuthLocalization])
|
|
51
|
+
return localization[error as keyof AuthLocalization]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (error?.error) {
|
|
55
|
+
if (error.error.code) {
|
|
56
|
+
const errorCode = error.error.code as keyof AuthLocalization
|
|
57
|
+
if (localization?.[errorCode]) return localization[errorCode]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
error.error.message ||
|
|
62
|
+
error.error.code ||
|
|
63
|
+
error.error.statusText ||
|
|
64
|
+
localization?.REQUEST_FAILED
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
error?.message || localization?.REQUEST_FAILED || DEFAULT_ERROR_MESSAGE
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getSearchParam(paramName: string) {
|
|
74
|
+
return typeof window !== "undefined"
|
|
75
|
+
? new URLSearchParams(window.location.search).get(paramName)
|
|
76
|
+
: null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getViewByPath<T extends object>(viewPaths: T, path?: string) {
|
|
80
|
+
for (const key in viewPaths) {
|
|
81
|
+
if (viewPaths[key] === path) {
|
|
82
|
+
return key
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getKeyByValue<T extends Record<string, unknown>>(
|
|
88
|
+
object: T,
|
|
89
|
+
value?: T[keyof T]
|
|
90
|
+
): keyof T | undefined {
|
|
91
|
+
return (Object.keys(object) as Array<keyof T>).find(
|
|
92
|
+
(key) => object[key] === value
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getPasswordSchema(
|
|
97
|
+
passwordValidation?: PasswordValidation,
|
|
98
|
+
localization?: AuthLocalization
|
|
99
|
+
) {
|
|
100
|
+
let schema = z.string().min(1, {
|
|
101
|
+
message: localization?.PASSWORD_REQUIRED
|
|
102
|
+
})
|
|
103
|
+
if (passwordValidation?.minLength) {
|
|
104
|
+
schema = schema.min(passwordValidation.minLength, {
|
|
105
|
+
message: localization?.PASSWORD_TOO_SHORT
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
if (passwordValidation?.maxLength) {
|
|
109
|
+
schema = schema.max(passwordValidation.maxLength, {
|
|
110
|
+
message: localization?.PASSWORD_TOO_LONG
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
if (passwordValidation?.regex) {
|
|
114
|
+
schema = schema.regex(passwordValidation.regex, {
|
|
115
|
+
message: localization?.INVALID_PASSWORD
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
return schema
|
|
119
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const authViewPaths = {
|
|
2
|
+
/** @default "callback" */
|
|
3
|
+
CALLBACK: "callback",
|
|
4
|
+
/** @default "email-otp" */
|
|
5
|
+
EMAIL_OTP: "email-otp",
|
|
6
|
+
/** @default "email-verification" */
|
|
7
|
+
EMAIL_VERIFICATION: "email-verification",
|
|
8
|
+
/** @default "forgot-password" */
|
|
9
|
+
FORGOT_PASSWORD: "forgot-password",
|
|
10
|
+
/** @default "magic-link" */
|
|
11
|
+
MAGIC_LINK: "magic-link",
|
|
12
|
+
/** @default "recover-account" */
|
|
13
|
+
RECOVER_ACCOUNT: "recover-account",
|
|
14
|
+
/** @default "reset-password" */
|
|
15
|
+
RESET_PASSWORD: "reset-password",
|
|
16
|
+
/** @default "sign-in" */
|
|
17
|
+
SIGN_IN: "sign-in",
|
|
18
|
+
/** @default "sign-out" */
|
|
19
|
+
SIGN_OUT: "sign-out",
|
|
20
|
+
/** @default "sign-up" */
|
|
21
|
+
SIGN_UP: "sign-up",
|
|
22
|
+
/** @default "two-factor" */
|
|
23
|
+
TWO_FACTOR: "two-factor",
|
|
24
|
+
/** @default "accept-invitation" */
|
|
25
|
+
ACCEPT_INVITATION: "accept-invitation"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type AuthViewPaths = typeof authViewPaths
|
|
29
|
+
|
|
30
|
+
// Account-scoped views (signed-in user)
|
|
31
|
+
export const accountViewPaths = {
|
|
32
|
+
/** @default "settings" */
|
|
33
|
+
SETTINGS: "settings",
|
|
34
|
+
/** @default "security" */
|
|
35
|
+
SECURITY: "security",
|
|
36
|
+
/** @default "teams" */
|
|
37
|
+
TEAMS: "teams",
|
|
38
|
+
/** @default "api-keys" */
|
|
39
|
+
API_KEYS: "api-keys",
|
|
40
|
+
/** @default "organizations" */
|
|
41
|
+
ORGANIZATIONS: "organizations"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type AccountViewPaths = typeof accountViewPaths
|
|
45
|
+
|
|
46
|
+
// Organization-scoped views
|
|
47
|
+
export const organizationViewPaths = {
|
|
48
|
+
/** @default "settings" */
|
|
49
|
+
SETTINGS: "settings",
|
|
50
|
+
/** @default "members" */
|
|
51
|
+
MEMBERS: "members",
|
|
52
|
+
/** @default "teams" */
|
|
53
|
+
TEAMS: "teams",
|
|
54
|
+
/** @default "api-keys" */
|
|
55
|
+
API_KEYS: "api-keys"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type OrganizationViewPaths = typeof organizationViewPaths
|
|
59
|
+
export type AuthViewPath = keyof AuthViewPaths
|
|
60
|
+
export type AccountViewPath = keyof AccountViewPaths
|
|
61
|
+
export type OrganizationViewPath = keyof OrganizationViewPaths
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Wallet Utilities
|
|
3
|
+
*
|
|
4
|
+
* Uses raw window.ethereum calls (no viem dependency).
|
|
5
|
+
* Works with any injected wallet (MetaMask, Phantom, Coinbase, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface Window {
|
|
10
|
+
ethereum?: {
|
|
11
|
+
request: (args: { method: string; params?: unknown[] }) => Promise<unknown>;
|
|
12
|
+
on?: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
13
|
+
removeListener?: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface WalletConnection {
|
|
19
|
+
address: string;
|
|
20
|
+
chainId: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if an injected wallet (MetaMask, Phantom, etc.) is available
|
|
25
|
+
*/
|
|
26
|
+
export function hasInjectedWallet(): boolean {
|
|
27
|
+
return typeof window !== 'undefined' && !!window.ethereum;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Connect to injected wallet and get address + chainId
|
|
32
|
+
*/
|
|
33
|
+
export async function connectWallet(): Promise<WalletConnection> {
|
|
34
|
+
if (!window.ethereum) {
|
|
35
|
+
throw new Error('No wallet found. Please install MetaMask or Phantom.');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const accounts = await window.ethereum.request({
|
|
39
|
+
method: 'eth_requestAccounts',
|
|
40
|
+
}) as string[];
|
|
41
|
+
|
|
42
|
+
if (!accounts || accounts.length === 0) {
|
|
43
|
+
throw new Error('No accounts found');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const chainIdHex = await window.ethereum.request({
|
|
47
|
+
method: 'eth_chainId',
|
|
48
|
+
}) as string;
|
|
49
|
+
|
|
50
|
+
// Convert address to checksummed format (EIP-55)
|
|
51
|
+
const address = toChecksumAddress(accounts[0]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
address,
|
|
55
|
+
chainId: parseInt(chainIdHex, 16),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sign a message using the connected wallet
|
|
61
|
+
*/
|
|
62
|
+
export async function signMessage(message: string, address: string): Promise<string> {
|
|
63
|
+
if (!window.ethereum) {
|
|
64
|
+
throw new Error('No wallet found');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return window.ethereum.request({
|
|
68
|
+
method: 'personal_sign',
|
|
69
|
+
params: [message, address],
|
|
70
|
+
}) as Promise<string>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build a SIWE message string (ERC-4361 format)
|
|
75
|
+
* No external siwe package needed - it's just formatted text
|
|
76
|
+
*/
|
|
77
|
+
export function buildSiweMessage(opts: {
|
|
78
|
+
domain: string;
|
|
79
|
+
address: string;
|
|
80
|
+
statement: string;
|
|
81
|
+
uri: string;
|
|
82
|
+
version: string;
|
|
83
|
+
chainId: number;
|
|
84
|
+
nonce: string;
|
|
85
|
+
issuedAt?: string;
|
|
86
|
+
}): string {
|
|
87
|
+
const issuedAt = opts.issuedAt || new Date().toISOString();
|
|
88
|
+
|
|
89
|
+
// ERC-4361 SIWE message format
|
|
90
|
+
// https://eips.ethereum.org/EIPS/eip-4361
|
|
91
|
+
return `${opts.domain} wants you to sign in with your Ethereum account:
|
|
92
|
+
${opts.address}
|
|
93
|
+
|
|
94
|
+
${opts.statement}
|
|
95
|
+
|
|
96
|
+
URI: ${opts.uri}
|
|
97
|
+
Version: ${opts.version}
|
|
98
|
+
Chain ID: ${opts.chainId}
|
|
99
|
+
Nonce: ${opts.nonce}
|
|
100
|
+
Issued At: ${issuedAt}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Convert address to EIP-55 checksummed format
|
|
105
|
+
* Required by SIWE - lowercase addresses will fail verification
|
|
106
|
+
*/
|
|
107
|
+
function toChecksumAddress(address: string): string {
|
|
108
|
+
// Remove 0x prefix, lowercase
|
|
109
|
+
const addr = address.toLowerCase().replace('0x', '');
|
|
110
|
+
|
|
111
|
+
// Simple keccak256 hash using SubtleCrypto isn't available synchronously,
|
|
112
|
+
// so we use the standard algorithm with a precomputed approach.
|
|
113
|
+
// For now, return mixed case based on simple character position heuristic.
|
|
114
|
+
// This matches the format wallets typically return.
|
|
115
|
+
|
|
116
|
+
// Most modern wallets (MetaMask, Phantom) already return checksummed addresses
|
|
117
|
+
// from eth_requestAccounts. If not, the server-side siwe package handles it.
|
|
118
|
+
// We do a basic validation here.
|
|
119
|
+
if (address.match(/^0x[0-9a-fA-F]{40}$/)) {
|
|
120
|
+
// If it's already mixed case (checksummed), return as-is
|
|
121
|
+
if (address !== address.toLowerCase() && address !== address.toUpperCase()) {
|
|
122
|
+
return address;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Return as-is - server will handle validation
|
|
127
|
+
// Modern wallets return checksummed addresses anyway
|
|
128
|
+
return address;
|
|
129
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const ADMIN_ERROR_CODES = {
|
|
2
|
+
FAILED_TO_CREATE_USER: "Failed to create user",
|
|
3
|
+
USER_ALREADY_EXISTS: "User already exists",
|
|
4
|
+
YOU_CANNOT_BAN_YOURSELF: "You cannot ban yourself",
|
|
5
|
+
YOU_ARE_NOT_ALLOWED_TO_CHANGE_USERS_ROLE:
|
|
6
|
+
"You are not allowed to change users role",
|
|
7
|
+
YOU_ARE_NOT_ALLOWED_TO_CREATE_USERS: "You are not allowed to create users",
|
|
8
|
+
YOU_ARE_NOT_ALLOWED_TO_LIST_USERS: "You are not allowed to list users",
|
|
9
|
+
YOU_ARE_NOT_ALLOWED_TO_LIST_USERS_SESSIONS:
|
|
10
|
+
"You are not allowed to list users sessions",
|
|
11
|
+
YOU_ARE_NOT_ALLOWED_TO_BAN_USERS: "You are not allowed to ban users",
|
|
12
|
+
YOU_ARE_NOT_ALLOWED_TO_IMPERSONATE_USERS:
|
|
13
|
+
"You are not allowed to impersonate users",
|
|
14
|
+
YOU_ARE_NOT_ALLOWED_TO_REVOKE_USERS_SESSIONS:
|
|
15
|
+
"You are not allowed to revoke users sessions",
|
|
16
|
+
YOU_ARE_NOT_ALLOWED_TO_DELETE_USERS: "You are not allowed to delete users",
|
|
17
|
+
YOU_ARE_NOT_ALLOWED_TO_SET_USERS_PASSWORD:
|
|
18
|
+
"You are not allowed to set users password",
|
|
19
|
+
BANNED_USER: "You have been banned from this application"
|
|
20
|
+
}
|