@better-auth-ui/heroui 1.6.5 → 1.6.7
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/components/auth/api-key/api-key-skeleton.d.ts +1 -0
- package/dist/components/auth/api-key/api-key-skeleton.js +21 -0
- package/dist/components/auth/api-key/api-key.d.ts +5 -0
- package/dist/components/auth/api-key/api-key.js +55 -0
- package/dist/components/auth/api-key/api-keys-empty.d.ts +4 -0
- package/dist/components/auth/api-key/api-keys-empty.js +35 -0
- package/dist/components/auth/api-key/api-keys.d.ts +6 -0
- package/dist/components/auth/api-key/api-keys.js +41 -0
- package/dist/components/auth/api-key/create-api-key-dialog.d.ts +5 -0
- package/dist/components/auth/api-key/create-api-key-dialog.js +74 -0
- package/dist/components/auth/api-key/delete-api-key-dialog.d.ts +7 -0
- package/dist/components/auth/api-key/delete-api-key-dialog.js +51 -0
- package/dist/components/auth/api-key/new-api-key-dialog.d.ts +7 -0
- package/dist/components/auth/api-key/new-api-key-dialog.js +60 -0
- package/dist/components/auth/delete-user/danger-zone.js +1 -1
- package/dist/components/auth/delete-user/delete-user.js +53 -53
- package/dist/components/auth/multi-session/switch-account-submenu-content.js +14 -15
- package/dist/components/auth/passkey/add-passkey-dialog.d.ts +0 -0
- package/dist/components/auth/passkey/delete-passkey-dialog.d.ts +0 -0
- package/dist/components/auth/passkey/passkey-skeleton.d.ts +0 -0
- package/dist/components/auth/passkey/passkeys-empty.d.ts +0 -0
- package/dist/components/auth/settings/security/linked-account.js +1 -1
- package/dist/components/auth/theme/theme-toggle-item.js +50 -46
- package/dist/components/auth/user/user-button.d.ts +27 -2
- package/dist/components/auth/user/user-button.js +48 -29
- package/dist/lib/auth/api-key-plugin.d.ts +23 -0
- package/dist/lib/auth/api-key-plugin.js +10 -0
- package/dist/plugins.d.ts +2 -0
- package/dist/plugins.js +22 -20
- package/package.json +14 -14
- package/src/components/auth/api-key/api-key-skeleton.tsx +17 -0
- package/src/components/auth/api-key/api-key.tsx +64 -0
- package/src/components/auth/api-key/api-keys-empty.tsx +33 -0
- package/src/components/auth/api-key/api-keys.tsx +71 -0
- package/src/components/auth/api-key/create-api-key-dialog.tsx +134 -0
- package/src/components/auth/api-key/delete-api-key-dialog.tsx +92 -0
- package/src/components/auth/api-key/new-api-key-dialog.tsx +94 -0
- package/src/components/auth/delete-user/danger-zone.tsx +1 -1
- package/src/components/auth/delete-user/delete-user.tsx +2 -7
- package/src/components/auth/multi-session/switch-account-submenu-content.tsx +1 -3
- package/src/components/auth/passkey/add-passkey-dialog.tsx +0 -0
- package/src/components/auth/passkey/delete-passkey-dialog.tsx +0 -0
- package/src/components/auth/passkey/passkey-skeleton.tsx +0 -0
- package/src/components/auth/passkey/passkeys-empty.tsx +0 -0
- package/src/components/auth/settings/security/linked-account.tsx +2 -4
- package/src/components/auth/theme/theme-toggle-item.tsx +2 -1
- package/src/components/auth/user/user-button.tsx +80 -13
- package/src/lib/auth/api-key-plugin.ts +15 -0
- package/src/plugins.ts +2 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ApiKeyAuthClient,
|
|
3
|
+
useAuth,
|
|
4
|
+
useAuthPlugin,
|
|
5
|
+
useCreateApiKey
|
|
6
|
+
} from "@better-auth-ui/react"
|
|
7
|
+
import { Key } from "@gravity-ui/icons"
|
|
8
|
+
import {
|
|
9
|
+
AlertDialog,
|
|
10
|
+
Button,
|
|
11
|
+
FieldError,
|
|
12
|
+
Form,
|
|
13
|
+
Input,
|
|
14
|
+
Label,
|
|
15
|
+
Spinner,
|
|
16
|
+
TextField
|
|
17
|
+
} from "@heroui/react"
|
|
18
|
+
import { type SyntheticEvent, useState } from "react"
|
|
19
|
+
|
|
20
|
+
import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
|
|
21
|
+
|
|
22
|
+
import { NewApiKeyDialog } from "./new-api-key-dialog"
|
|
23
|
+
|
|
24
|
+
export type CreateApiKeyDialogProps = {
|
|
25
|
+
isOpen: boolean
|
|
26
|
+
onOpenChange: (open: boolean) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function CreateApiKeyDialog({
|
|
30
|
+
isOpen,
|
|
31
|
+
onOpenChange
|
|
32
|
+
}: CreateApiKeyDialogProps) {
|
|
33
|
+
const { authClient, localization } = useAuth()
|
|
34
|
+
const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
|
|
35
|
+
|
|
36
|
+
const { mutate: createApiKey, isPending: isCreating } = useCreateApiKey(
|
|
37
|
+
authClient as ApiKeyAuthClient
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const [isNewKeyDialogOpen, setIsNewKeyDialogOpen] = useState(false)
|
|
41
|
+
const [keyName, setKeyName] = useState<string | null>(null)
|
|
42
|
+
const [secretKey, setSecretKey] = useState<string | null>(null)
|
|
43
|
+
|
|
44
|
+
const handleOpenChange = (open: boolean) => {
|
|
45
|
+
if (!open) {
|
|
46
|
+
setKeyName(null)
|
|
47
|
+
setSecretKey(null)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onOpenChange(open)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
|
|
56
|
+
const formData = new FormData(e.target as HTMLFormElement)
|
|
57
|
+
const name = formData.get("name") as string
|
|
58
|
+
|
|
59
|
+
createApiKey(name ? { name: name.trim() } : undefined, {
|
|
60
|
+
onSuccess: (result) => {
|
|
61
|
+
handleOpenChange(false)
|
|
62
|
+
setKeyName(name)
|
|
63
|
+
setSecretKey(result.key)
|
|
64
|
+
setIsNewKeyDialogOpen(true)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<>
|
|
71
|
+
<AlertDialog.Backdrop isOpen={isOpen} onOpenChange={handleOpenChange}>
|
|
72
|
+
<AlertDialog.Container>
|
|
73
|
+
<AlertDialog.Dialog>
|
|
74
|
+
<Form onSubmit={handleSubmit}>
|
|
75
|
+
<AlertDialog.CloseTrigger />
|
|
76
|
+
|
|
77
|
+
<AlertDialog.Header>
|
|
78
|
+
<AlertDialog.Icon status="default">
|
|
79
|
+
<Key />
|
|
80
|
+
</AlertDialog.Icon>
|
|
81
|
+
|
|
82
|
+
<AlertDialog.Heading>
|
|
83
|
+
{apiKeyLocalization.createApiKey}
|
|
84
|
+
</AlertDialog.Heading>
|
|
85
|
+
</AlertDialog.Header>
|
|
86
|
+
|
|
87
|
+
<AlertDialog.Body className="overflow-visible">
|
|
88
|
+
<p className="text-muted text-sm">
|
|
89
|
+
{apiKeyLocalization.apiKeysDescription}
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
<TextField
|
|
93
|
+
className="mt-4"
|
|
94
|
+
id="name"
|
|
95
|
+
name="name"
|
|
96
|
+
isDisabled={isCreating}
|
|
97
|
+
>
|
|
98
|
+
<Label>{apiKeyLocalization.name}</Label>
|
|
99
|
+
|
|
100
|
+
<Input
|
|
101
|
+
autoFocus
|
|
102
|
+
placeholder={localization.settings.optional}
|
|
103
|
+
variant="secondary"
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<FieldError />
|
|
107
|
+
</TextField>
|
|
108
|
+
</AlertDialog.Body>
|
|
109
|
+
|
|
110
|
+
<AlertDialog.Footer>
|
|
111
|
+
<Button slot="close" variant="tertiary" isDisabled={isCreating}>
|
|
112
|
+
{localization.settings.cancel}
|
|
113
|
+
</Button>
|
|
114
|
+
|
|
115
|
+
<Button type="submit" isPending={isCreating}>
|
|
116
|
+
{isCreating && <Spinner color="current" size="sm" />}
|
|
117
|
+
|
|
118
|
+
{apiKeyLocalization.createApiKey}
|
|
119
|
+
</Button>
|
|
120
|
+
</AlertDialog.Footer>
|
|
121
|
+
</Form>
|
|
122
|
+
</AlertDialog.Dialog>
|
|
123
|
+
</AlertDialog.Container>
|
|
124
|
+
</AlertDialog.Backdrop>
|
|
125
|
+
|
|
126
|
+
<NewApiKeyDialog
|
|
127
|
+
isOpen={isNewKeyDialogOpen}
|
|
128
|
+
onOpenChange={setIsNewKeyDialogOpen}
|
|
129
|
+
secretKey={secretKey}
|
|
130
|
+
name={keyName}
|
|
131
|
+
/>
|
|
132
|
+
</>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ApiKeyAuthClient,
|
|
3
|
+
type ListedApiKey,
|
|
4
|
+
useAuth,
|
|
5
|
+
useAuthPlugin,
|
|
6
|
+
useDeleteApiKey
|
|
7
|
+
} from "@better-auth-ui/react"
|
|
8
|
+
import { Key } from "@gravity-ui/icons"
|
|
9
|
+
import {
|
|
10
|
+
AlertDialog,
|
|
11
|
+
Button,
|
|
12
|
+
Input,
|
|
13
|
+
Label,
|
|
14
|
+
Spinner,
|
|
15
|
+
TextField
|
|
16
|
+
} from "@heroui/react"
|
|
17
|
+
|
|
18
|
+
import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
|
|
19
|
+
|
|
20
|
+
export type DeleteApiKeyDialogProps = {
|
|
21
|
+
isOpen: boolean
|
|
22
|
+
onOpenChange: (open: boolean) => void
|
|
23
|
+
apiKey: ListedApiKey
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DeleteApiKeyDialog({
|
|
27
|
+
isOpen,
|
|
28
|
+
onOpenChange,
|
|
29
|
+
apiKey
|
|
30
|
+
}: DeleteApiKeyDialogProps) {
|
|
31
|
+
const { authClient, localization } = useAuth()
|
|
32
|
+
const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
|
|
33
|
+
const preview = `${apiKey.start}${"*".repeat(16)}`
|
|
34
|
+
const { mutate: deleteApiKey, isPending: isDeleting } = useDeleteApiKey(
|
|
35
|
+
authClient as ApiKeyAuthClient,
|
|
36
|
+
{
|
|
37
|
+
onSuccess: () => onOpenChange(false)
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
|
|
43
|
+
<AlertDialog.Container>
|
|
44
|
+
<AlertDialog.Dialog>
|
|
45
|
+
<AlertDialog.CloseTrigger />
|
|
46
|
+
|
|
47
|
+
<AlertDialog.Header>
|
|
48
|
+
<AlertDialog.Icon status="danger">
|
|
49
|
+
<Key />
|
|
50
|
+
</AlertDialog.Icon>
|
|
51
|
+
|
|
52
|
+
<AlertDialog.Heading>
|
|
53
|
+
{apiKeyLocalization.deleteApiKey}
|
|
54
|
+
</AlertDialog.Heading>
|
|
55
|
+
</AlertDialog.Header>
|
|
56
|
+
|
|
57
|
+
<AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
|
|
58
|
+
<p className="text-muted text-sm">
|
|
59
|
+
{apiKeyLocalization.deleteApiKeyWarning}
|
|
60
|
+
</p>
|
|
61
|
+
|
|
62
|
+
<TextField
|
|
63
|
+
value={preview}
|
|
64
|
+
className="font-mono text-xs"
|
|
65
|
+
variant="secondary"
|
|
66
|
+
>
|
|
67
|
+
<Label>{apiKey.name || apiKeyLocalization.apiKey}</Label>
|
|
68
|
+
|
|
69
|
+
<Input readOnly className="font-mono text-xs" />
|
|
70
|
+
</TextField>
|
|
71
|
+
</AlertDialog.Body>
|
|
72
|
+
|
|
73
|
+
<AlertDialog.Footer>
|
|
74
|
+
<Button slot="close" variant="tertiary" isDisabled={isDeleting}>
|
|
75
|
+
{localization.settings.cancel}
|
|
76
|
+
</Button>
|
|
77
|
+
|
|
78
|
+
<Button
|
|
79
|
+
variant="danger"
|
|
80
|
+
onPress={() => deleteApiKey({ keyId: apiKey.id })}
|
|
81
|
+
isPending={isDeleting}
|
|
82
|
+
>
|
|
83
|
+
{isDeleting && <Spinner color="current" size="sm" />}
|
|
84
|
+
|
|
85
|
+
{apiKeyLocalization.deleteApiKey}
|
|
86
|
+
</Button>
|
|
87
|
+
</AlertDialog.Footer>
|
|
88
|
+
</AlertDialog.Dialog>
|
|
89
|
+
</AlertDialog.Container>
|
|
90
|
+
</AlertDialog.Backdrop>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useAuth, useAuthPlugin } from "@better-auth-ui/react"
|
|
2
|
+
import { Check, Copy, Key } from "@gravity-ui/icons"
|
|
3
|
+
import {
|
|
4
|
+
AlertDialog,
|
|
5
|
+
Button,
|
|
6
|
+
InputGroup,
|
|
7
|
+
Label,
|
|
8
|
+
TextField,
|
|
9
|
+
toast
|
|
10
|
+
} from "@heroui/react"
|
|
11
|
+
import { useState } from "react"
|
|
12
|
+
|
|
13
|
+
import { apiKeyPlugin } from "../../../lib/auth/api-key-plugin"
|
|
14
|
+
|
|
15
|
+
export type NewApiKeyDialogProps = {
|
|
16
|
+
isOpen: boolean
|
|
17
|
+
onOpenChange: (open: boolean) => void
|
|
18
|
+
name: string | null
|
|
19
|
+
secretKey: string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function NewApiKeyDialog({
|
|
23
|
+
isOpen,
|
|
24
|
+
onOpenChange,
|
|
25
|
+
name,
|
|
26
|
+
secretKey
|
|
27
|
+
}: NewApiKeyDialogProps) {
|
|
28
|
+
const { localization } = useAuth()
|
|
29
|
+
const { localization: apiKeyLocalization } = useAuthPlugin(apiKeyPlugin)
|
|
30
|
+
|
|
31
|
+
const [copied, setCopied] = useState(false)
|
|
32
|
+
|
|
33
|
+
const copySecretKey = async () => {
|
|
34
|
+
if (!secretKey) return
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await navigator.clipboard.writeText(secretKey)
|
|
38
|
+
setCopied(true)
|
|
39
|
+
setTimeout(() => setCopied(false), 1500)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
toast.danger(error instanceof Error ? error.message : String(error))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
|
|
47
|
+
<AlertDialog.Container>
|
|
48
|
+
<AlertDialog.Dialog>
|
|
49
|
+
<AlertDialog.CloseTrigger />
|
|
50
|
+
|
|
51
|
+
<AlertDialog.Header>
|
|
52
|
+
<AlertDialog.Icon status="warning">
|
|
53
|
+
<Key />
|
|
54
|
+
</AlertDialog.Icon>
|
|
55
|
+
|
|
56
|
+
<AlertDialog.Heading>
|
|
57
|
+
{apiKeyLocalization.newApiKey}
|
|
58
|
+
</AlertDialog.Heading>
|
|
59
|
+
</AlertDialog.Header>
|
|
60
|
+
|
|
61
|
+
<AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
|
|
62
|
+
<p className="text-muted text-sm">
|
|
63
|
+
{apiKeyLocalization.newApiKeyWarning}
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<TextField value={secretKey ?? ""} className="font-mono text-xs">
|
|
67
|
+
<Label>{name || apiKeyLocalization.apiKey}</Label>
|
|
68
|
+
|
|
69
|
+
<InputGroup variant="secondary">
|
|
70
|
+
<InputGroup.Input readOnly className="font-mono text-xs" />
|
|
71
|
+
|
|
72
|
+
<InputGroup.Suffix className="px-0">
|
|
73
|
+
<Button
|
|
74
|
+
isIconOnly
|
|
75
|
+
aria-label={localization.settings.copyToClipboard}
|
|
76
|
+
size="sm"
|
|
77
|
+
variant="ghost"
|
|
78
|
+
onPress={copySecretKey}
|
|
79
|
+
>
|
|
80
|
+
{copied ? <Check /> : <Copy />}
|
|
81
|
+
</Button>
|
|
82
|
+
</InputGroup.Suffix>
|
|
83
|
+
</InputGroup>
|
|
84
|
+
</TextField>
|
|
85
|
+
</AlertDialog.Body>
|
|
86
|
+
|
|
87
|
+
<AlertDialog.Footer>
|
|
88
|
+
<Button slot="close">{apiKeyLocalization.dismissNewKey}</Button>
|
|
89
|
+
</AlertDialog.Footer>
|
|
90
|
+
</AlertDialog.Dialog>
|
|
91
|
+
</AlertDialog.Container>
|
|
92
|
+
</AlertDialog.Backdrop>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -22,7 +22,7 @@ export function DangerZone({
|
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
24
|
<div className={cn("flex w-full flex-col", className)} {...props}>
|
|
25
|
-
<h2 className={cn("text-sm font-semibold mb-3")}>
|
|
25
|
+
<h2 className={cn("text-sm font-semibold mb-3 text-danger")}>
|
|
26
26
|
{localization.settings.dangerZone}
|
|
27
27
|
</h2>
|
|
28
28
|
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
Button,
|
|
12
12
|
Card,
|
|
13
13
|
type CardProps,
|
|
14
|
-
cn,
|
|
15
14
|
FieldError,
|
|
16
15
|
Form,
|
|
17
16
|
Input,
|
|
@@ -91,11 +90,7 @@ export function DeleteUser({
|
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
return (
|
|
94
|
-
<Card
|
|
95
|
-
className={cn("border border-danger", className)}
|
|
96
|
-
variant={variant}
|
|
97
|
-
{...props}
|
|
98
|
-
>
|
|
93
|
+
<Card className={className} variant={variant} {...props}>
|
|
99
94
|
<Card.Content className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
100
95
|
<div>
|
|
101
96
|
<p className="text-sm font-medium leading-tight">
|
|
@@ -111,7 +106,7 @@ export function DeleteUser({
|
|
|
111
106
|
<Button
|
|
112
107
|
isDisabled={!accounts}
|
|
113
108
|
size="sm"
|
|
114
|
-
variant="danger"
|
|
109
|
+
variant="danger-soft"
|
|
115
110
|
onPress={() => setConfirmOpen(true)}
|
|
116
111
|
>
|
|
117
112
|
{deleteUserLocalization.deleteUser}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
useSession
|
|
7
7
|
} from "@better-auth-ui/react"
|
|
8
8
|
import { Check, CirclePlus } from "@gravity-ui/icons"
|
|
9
|
-
import { Dropdown, Label
|
|
9
|
+
import { Dropdown, Label } from "@heroui/react"
|
|
10
10
|
|
|
11
11
|
import { multiSessionPlugin } from "../../../lib/auth/multi-session-plugin"
|
|
12
12
|
import { UserView } from "../user/user-view"
|
|
@@ -50,8 +50,6 @@ export function SwitchAccountSubmenuContent() {
|
|
|
50
50
|
/>
|
|
51
51
|
))}
|
|
52
52
|
|
|
53
|
-
<Separator />
|
|
54
|
-
|
|
55
53
|
<Dropdown.Item
|
|
56
54
|
textValue={multiSessionLocalization.addAccount}
|
|
57
55
|
href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -60,11 +60,9 @@ export function LinkedAccount({ account, provider }: LinkedAccountProps) {
|
|
|
60
60
|
)}
|
|
61
61
|
>
|
|
62
62
|
{ProviderIcon ? (
|
|
63
|
-
<ProviderIcon className=
|
|
63
|
+
<ProviderIcon className="size-4.5" />
|
|
64
64
|
) : (
|
|
65
|
-
<PlugConnection
|
|
66
|
-
className={cn("size-4.5", !account && "opacity-50")}
|
|
67
|
-
/>
|
|
65
|
+
<PlugConnection className="size-4.5" />
|
|
68
66
|
)}
|
|
69
67
|
</div>
|
|
70
68
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useAuthPlugin } from "@better-auth-ui/react"
|
|
2
|
-
import { Display, Moon, Sun } from "@gravity-ui/icons"
|
|
2
|
+
import { Display, Moon, Palette, Sun } from "@gravity-ui/icons"
|
|
3
3
|
import { Dropdown, Label, Tabs } from "@heroui/react"
|
|
4
4
|
|
|
5
5
|
import { themePlugin } from "../../../lib/auth/theme-plugin"
|
|
@@ -67,6 +67,7 @@ export function ThemeToggleItem() {
|
|
|
67
67
|
// menu-item activation.
|
|
68
68
|
shouldCloseOnSelect={false}
|
|
69
69
|
>
|
|
70
|
+
<Palette className="text-muted" />
|
|
70
71
|
<Label>{localization.theme}</Label>
|
|
71
72
|
|
|
72
73
|
<Tabs
|
|
@@ -11,14 +11,38 @@ import {
|
|
|
11
11
|
type ButtonProps,
|
|
12
12
|
cn,
|
|
13
13
|
Dropdown,
|
|
14
|
+
type DropdownItemProps,
|
|
14
15
|
type DropdownPopoverProps,
|
|
15
|
-
Label
|
|
16
|
-
Separator
|
|
16
|
+
Label
|
|
17
17
|
} from "@heroui/react"
|
|
18
|
+
import { isValidElement, type ReactElement, type ReactNode } from "react"
|
|
18
19
|
|
|
19
20
|
import { UserAvatar } from "./user-avatar"
|
|
20
21
|
import { UserView } from "./user-view"
|
|
21
22
|
|
|
23
|
+
/** Auth states a `UserButton` link can be visible in. */
|
|
24
|
+
export type UserButtonLinkVisibility =
|
|
25
|
+
| "authenticated"
|
|
26
|
+
| "unauthenticated"
|
|
27
|
+
| "always"
|
|
28
|
+
|
|
29
|
+
/** A simple link entry rendered as a `Dropdown.Item` in the `UserButton` menu. */
|
|
30
|
+
export type UserButtonLink = {
|
|
31
|
+
/** Visible label. */
|
|
32
|
+
label: ReactNode
|
|
33
|
+
/** Destination URL. */
|
|
34
|
+
href: string
|
|
35
|
+
/** Optional leading icon. Sized/coloured to match built-in items. */
|
|
36
|
+
icon?: ReactNode
|
|
37
|
+
/** Forwarded to the underlying `Dropdown.Item`. */
|
|
38
|
+
variant?: DropdownItemProps["variant"]
|
|
39
|
+
/**
|
|
40
|
+
* When this link is visible based on auth state.
|
|
41
|
+
* @default "always"
|
|
42
|
+
*/
|
|
43
|
+
visibility?: UserButtonLinkVisibility
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
export type UserButtonProps = {
|
|
23
47
|
className?: string
|
|
24
48
|
size?: "default" | "icon"
|
|
@@ -28,6 +52,30 @@ export type UserButtonProps = {
|
|
|
28
52
|
*/
|
|
29
53
|
placement?: DropdownPopoverProps["placement"]
|
|
30
54
|
variant?: ButtonProps["variant"]
|
|
55
|
+
/** Additional menu entries rendered above the built-in items. */
|
|
56
|
+
links?: (UserButtonLink | ReactElement)[]
|
|
57
|
+
/** Hide the built-in "Settings" link. Useful when replacing it via `links`. */
|
|
58
|
+
hideSettings?: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderUserLink(
|
|
62
|
+
link: UserButtonLink | ReactElement,
|
|
63
|
+
fallbackKey: string
|
|
64
|
+
): ReactNode {
|
|
65
|
+
if (isValidElement(link)) return link
|
|
66
|
+
|
|
67
|
+
const { label, href, icon, variant } = link
|
|
68
|
+
return (
|
|
69
|
+
<Dropdown.Item
|
|
70
|
+
key={fallbackKey}
|
|
71
|
+
href={href}
|
|
72
|
+
variant={variant}
|
|
73
|
+
textValue={typeof label === "string" ? label : undefined}
|
|
74
|
+
>
|
|
75
|
+
{icon}
|
|
76
|
+
<Label>{label}</Label>
|
|
77
|
+
</Dropdown.Item>
|
|
78
|
+
)
|
|
31
79
|
}
|
|
32
80
|
|
|
33
81
|
/**
|
|
@@ -37,13 +85,17 @@ export type UserButtonProps = {
|
|
|
37
85
|
* @param placement - Dropdown popover placement (e.g., "bottom", "top-start", "bottom-end")
|
|
38
86
|
* @param size - "icon" renders an avatar-only trigger; "default" renders a button with label and chevron
|
|
39
87
|
* @param variant - Button visual variant passed to the underlying Button component
|
|
88
|
+
* @param links - Additional menu entries rendered above the built-in items
|
|
89
|
+
* @param hideSettings - Hide the built-in "Settings" link
|
|
40
90
|
* @returns The user button and its dropdown menu as a JSX element
|
|
41
91
|
*/
|
|
42
92
|
export function UserButton({
|
|
43
93
|
className,
|
|
44
94
|
placement = "bottom",
|
|
45
95
|
size = "default",
|
|
46
|
-
variant = "ghost"
|
|
96
|
+
variant = "ghost",
|
|
97
|
+
links,
|
|
98
|
+
hideSettings = false
|
|
47
99
|
}: UserButtonProps) {
|
|
48
100
|
const { authClient, basePaths, viewPaths, localization, plugins } = useAuth()
|
|
49
101
|
|
|
@@ -55,6 +107,16 @@ export function UserButton({
|
|
|
55
107
|
<Item key={`${plugin.id}-${index.toString()}`} />
|
|
56
108
|
)) ?? []
|
|
57
109
|
)
|
|
110
|
+
|
|
111
|
+
const userLinks = links?.flatMap((link, index) => {
|
|
112
|
+
if (!isValidElement(link)) {
|
|
113
|
+
const visibility = link.visibility ?? "always"
|
|
114
|
+
if (visibility === "authenticated" && !session) return []
|
|
115
|
+
if (visibility === "unauthenticated" && session) return []
|
|
116
|
+
}
|
|
117
|
+
return [renderUserLink(link, `user-button-link-${index.toString()}`)]
|
|
118
|
+
})
|
|
119
|
+
|
|
58
120
|
return (
|
|
59
121
|
<Dropdown>
|
|
60
122
|
{size === "icon" ? (
|
|
@@ -96,30 +158,35 @@ export function UserButton({
|
|
|
96
158
|
<Dropdown.Menu>
|
|
97
159
|
{session ? (
|
|
98
160
|
<>
|
|
99
|
-
|
|
100
|
-
textValue={localization.settings.settings}
|
|
101
|
-
href={`${basePaths.settings}/${viewPaths.settings.account}`}
|
|
102
|
-
>
|
|
103
|
-
<Gear className="text-muted" />
|
|
161
|
+
{userLinks}
|
|
104
162
|
|
|
105
|
-
|
|
106
|
-
|
|
163
|
+
{!hideSettings && (
|
|
164
|
+
<Dropdown.Item
|
|
165
|
+
textValue={localization.settings.settings}
|
|
166
|
+
href={`${basePaths.settings}/${viewPaths.settings.account}`}
|
|
167
|
+
>
|
|
168
|
+
<Gear className="text-muted" />
|
|
107
169
|
|
|
108
|
-
|
|
170
|
+
<Label>{localization.settings.settings}</Label>
|
|
171
|
+
</Dropdown.Item>
|
|
172
|
+
)}
|
|
109
173
|
|
|
110
|
-
|
|
174
|
+
{userMenuItems}
|
|
111
175
|
|
|
112
176
|
<Dropdown.Item
|
|
113
177
|
textValue={localization.auth.signOut}
|
|
114
178
|
href={`${basePaths.auth}/${viewPaths.auth.signOut}`}
|
|
179
|
+
variant="danger"
|
|
115
180
|
>
|
|
116
|
-
<ArrowRightFromSquare className="text-
|
|
181
|
+
<ArrowRightFromSquare className="text-danger" />
|
|
117
182
|
|
|
118
183
|
<Label>{localization.auth.signOut}</Label>
|
|
119
184
|
</Dropdown.Item>
|
|
120
185
|
</>
|
|
121
186
|
) : (
|
|
122
187
|
<>
|
|
188
|
+
{userLinks}
|
|
189
|
+
|
|
123
190
|
<Dropdown.Item
|
|
124
191
|
textValue={localization.auth.signIn}
|
|
125
192
|
href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createAuthPlugin } from "@better-auth-ui/core"
|
|
2
|
+
import {
|
|
3
|
+
type ApiKeyPluginOptions,
|
|
4
|
+
apiKeyPlugin as coreApiKeyPlugin
|
|
5
|
+
} from "@better-auth-ui/core/plugins"
|
|
6
|
+
|
|
7
|
+
import { ApiKeys } from "../../components/auth/api-key/api-keys"
|
|
8
|
+
|
|
9
|
+
export const apiKeyPlugin = createAuthPlugin(
|
|
10
|
+
coreApiKeyPlugin.id,
|
|
11
|
+
(options: ApiKeyPluginOptions = {}) => ({
|
|
12
|
+
...coreApiKeyPlugin(options),
|
|
13
|
+
securityCards: [ApiKeys]
|
|
14
|
+
})
|
|
15
|
+
)
|
package/src/plugins.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
export * from "./components/auth/api-key/api-keys"
|
|
3
4
|
export * from "./components/auth/delete-user/danger-zone"
|
|
4
5
|
export * from "./components/auth/delete-user/delete-user"
|
|
5
6
|
// Plugin-contributed components that may be used standalone
|
|
@@ -14,6 +15,7 @@ export * from "./components/auth/theme/appearance"
|
|
|
14
15
|
export * from "./components/auth/theme/theme-toggle-item"
|
|
15
16
|
export * from "./components/auth/username/sign-in-username"
|
|
16
17
|
export * from "./components/auth/username/username-field"
|
|
18
|
+
export * from "./lib/auth/api-key-plugin"
|
|
17
19
|
export * from "./lib/auth/auth-plugin"
|
|
18
20
|
export * from "./lib/auth/delete-user-plugin"
|
|
19
21
|
export * from "./lib/auth/magic-link-plugin"
|