index.ts1395 lines · main
1/**
2 * @briven/auth/vue — Vue 3 bindings for `@briven/auth`.
3 *
4 * import { createBrivenAuth } from '@briven/auth';
5 * import { BrivenAuthProvider, useSession, useUser } from '@briven/auth/vue';
6 *
7 * const auth = createBrivenAuth({ projectId: 'p_abc123', publicKey: '...' });
8 *
9 * <BrivenAuthProvider :value="auth">
10 * <App />
11 * </BrivenAuthProvider>
12 *
13 * function App() {
14 * const { session, isLoading } = useSession();
15 * return session ? <Home /> : <BrivenSignIn />;
16 * }
17 *
18 * Zero hard dependency on Nuxt — works in any Vue 3 environment.
19 */
20
21import {
22 type InjectionKey,
23 type PropType,
24 type Ref,
25 type VNode,
26 h,
27 inject,
28 onMounted,
29 provide,
30 ref,
31 watch,
32} from 'vue';
33
34import {
35 type BrivenAuthClient,
36 type ClientSession,
37 type MembershipRequest,
38 type OAuthProvider,
39 type Org,
40 type OrgDomain,
41 type OrgInvite,
42 type OrgMember,
43 type OrgPermission,
44 type OrgRole,
45 type Passkey,
46 type SessionResponse,
47 type SignInResult,
48 type SimpleResult,
49 type SsoConnection,
50 type SsoProviderType,
51 type User,
52 type UserEmail,
53} from '../index.js';
54
55const BrivenAuthKey: InjectionKey<BrivenAuthClient> = Symbol('briven-auth');
56
57// ─── Provider ──────────────────────────────────────────────────────────────
58
59export interface BrivenAuthProviderProps {
60 value: BrivenAuthClient;
61}
62
63export const BrivenAuthProvider = {
64 name: 'BrivenAuthProvider',
65 props: {
66 value: { type: Object as PropType<BrivenAuthClient>, required: true },
67 },
68 setup(props: BrivenAuthProviderProps, { slots }: { slots: { default?: () => VNode[] } }) {
69 provide(BrivenAuthKey, props.value);
70 return () => (slots.default ? slots.default() : null);
71 },
72};
73
74/** Throws when called outside a `<BrivenAuthProvider>`. */
75export function useBrivenAuth(): BrivenAuthClient {
76 const client = inject(BrivenAuthKey);
77 if (!client) {
78 throw new Error('useBrivenAuth must be called inside <BrivenAuthProvider>');
79 }
80 return client;
81}
82
83// ─── Composables ───────────────────────────────────────────────────────────
84
85export interface UseSessionResult {
86 session: Ref<SessionResponse | null>;
87 isLoading: Ref<boolean>;
88 refresh: () => Promise<void>;
89}
90
91export function useSession(): UseSessionResult {
92 const client = useBrivenAuth();
93 const session = ref<SessionResponse | null>(null);
94 const isLoading = ref(true);
95
96 const refresh = async () => {
97 isLoading.value = true;
98 session.value = await client.getSession();
99 isLoading.value = false;
100 };
101
102 onMounted(() => {
103 void refresh();
104 });
105
106 return { session, isLoading, refresh };
107}
108
109export interface UseUserResult {
110 user: Ref<User | null>;
111 isLoading: Ref<boolean>;
112 refresh: () => Promise<void>;
113}
114
115export function useUser(): UseUserResult {
116 const client = useBrivenAuth();
117 const user = ref<User | null>(null);
118 const isLoading = ref(true);
119
120 const refresh = async () => {
121 isLoading.value = true;
122 user.value = await client.getUser();
123 isLoading.value = false;
124 };
125
126 onMounted(() => {
127 void refresh();
128 });
129
130 return { user, isLoading, refresh };
131}
132
133export interface UseUserMetadataResult {
134 metadata: Ref<Record<string, unknown> | null>;
135 isLoading: Ref<boolean>;
136 refresh: () => Promise<void>;
137 set: (patch: Record<string, unknown>) => Promise<void>;
138}
139
140export function useUserMetadata(): UseUserMetadataResult {
141 const client = useBrivenAuth();
142 const metadata = ref<Record<string, unknown> | null>(null);
143 const isLoading = ref(true);
144
145 const refresh = async () => {
146 isLoading.value = true;
147 const result = await client.user.getMetadata();
148 metadata.value = result.ok ? result.publicMetadata : null;
149 isLoading.value = false;
150 };
151
152 const set = async (patch: Record<string, unknown>) => {
153 const result = await client.user.setMetadata(patch);
154 if (result.ok) {
155 metadata.value = result.publicMetadata;
156 }
157 };
158
159 onMounted(() => {
160 void refresh();
161 });
162
163 return { metadata, isLoading, refresh, set };
164}
165
166export interface UseUserEmailsResult {
167 emails: Ref<UserEmail[] | null>;
168 isLoading: Ref<boolean>;
169 refresh: () => Promise<void>;
170 add: (email: string) => Promise<void>;
171 remove: (emailId: string) => Promise<void>;
172}
173
174export function useUserEmails(): UseUserEmailsResult {
175 const client = useBrivenAuth();
176 const emails = ref<UserEmail[] | null>(null);
177 const isLoading = ref(true);
178
179 const refresh = async () => {
180 isLoading.value = true;
181 const result = await client.user.listEmails();
182 emails.value = result.ok ? result.emails : null;
183 isLoading.value = false;
184 };
185
186 const add = async (email: string) => {
187 const result = await client.user.addEmail(email);
188 if (result.ok) await refresh();
189 };
190
191 const remove = async (emailId: string) => {
192 const result = await client.user.removeEmail(emailId);
193 if (result.ok) await refresh();
194 };
195
196 onMounted(() => {
197 void refresh();
198 });
199
200 return { emails, isLoading, refresh, add, remove };
201}
202
203export interface UseActiveOrganizationResult {
204 activeOrg: Ref<Org | null>;
205 isLoading: Ref<boolean>;
206 refresh: () => Promise<void>;
207 setActive: (orgId: string) => Promise<void>;
208}
209
210export function useActiveOrganization(): UseActiveOrganizationResult {
211 const client = useBrivenAuth();
212 const activeOrg = ref<Org | null>(null);
213 const isLoading = ref(true);
214
215 const refresh = async () => {
216 isLoading.value = true;
217 const result = await client.organization.getActive();
218 if (result.ok) activeOrg.value = result.data;
219 isLoading.value = false;
220 };
221
222 const setActive = async (orgId: string) => {
223 const result = await client.organization.setActive(orgId);
224 if (result.ok) await refresh();
225 };
226
227 onMounted(() => {
228 void refresh();
229 });
230
231 return { activeOrg, isLoading, refresh, setActive };
232}
233
234// ─── Shared helpers ────────────────────────────────────────────────────────
235
236function useRedirectToHosted(auth: BrivenAuthClient, redirectTo?: string, locale?: string) {
237 return (flow: 'sign-in' | 'sign-up' | 'magic-link') => {
238 const url = auth.hostedPageURL(flow, redirectTo, locale);
239 if (typeof window !== 'undefined') {
240 window.location.assign(url);
241 }
242 };
243}
244
245// ─── BrivenSignIn ──────────────────────────────────────────────────────────
246
247export interface BrivenSignInProps {
248 providers?: ReadonlyArray<OAuthProvider>;
249 showEmailPassword?: boolean;
250 showMagicLink?: boolean;
251 redirectTo?: string;
252 onSuccess?: (result: { userId: string }) => void;
253 className?: string;
254 mode?: 'direct' | 'hosted';
255 locale?: string;
256}
257
258const DEFAULT_PROVIDERS: ReadonlyArray<OAuthProvider> = [
259 'google',
260 'github',
261 'discord',
262 'microsoft',
263 'apple',
264 'twitter',
265 'linkedin',
266 'gitlab',
267];
268
269export const BrivenSignIn = {
270 name: 'BrivenSignIn',
271 props: {
272 providers: { type: Array as PropType<ReadonlyArray<OAuthProvider>>, default: () => DEFAULT_PROVIDERS },
273 showEmailPassword: { type: Boolean, default: true },
274 showMagicLink: { type: Boolean, default: true },
275 redirectTo: { type: String, default: undefined },
276 onSuccess: { type: Function as PropType<(result: { userId: string }) => void>, default: undefined },
277 className: { type: String, default: undefined },
278 mode: { type: String as PropType<'direct' | 'hosted'>, default: 'direct' },
279 locale: { type: String, default: undefined },
280 },
281 setup(props: BrivenSignInProps) {
282 const auth = useBrivenAuth();
283 const email = ref('');
284 const password = ref('');
285 const magicEmail = ref('');
286 const pending = ref<'password' | 'magic' | null>(null);
287 const error = ref<string | null>(null);
288 const magicSent = ref(false);
289
290 const redirectToHosted = useRedirectToHosted(auth, props.redirectTo, props.locale);
291
292 const handlePassword = async (e: Event) => {
293 e.preventDefault();
294 if (props.mode === 'hosted') {
295 redirectToHosted('sign-in');
296 return;
297 }
298 pending.value = 'password';
299 error.value = null;
300 const result: SignInResult = await auth.signIn.email({ email: email.value, password: password.value });
301 if (result.ok && 'userId' in result) {
302 props.onSuccess?.({ userId: result.userId });
303 } else if (result.ok && 'twoFactorRequired' in result) {
304 error.value = 'two-factor required — complete the challenge';
305 } else if (!result.ok) {
306 error.value = result.message;
307 }
308 pending.value = null;
309 };
310
311 const handleMagic = async (e: Event) => {
312 e.preventDefault();
313 if (props.mode === 'hosted') {
314 redirectToHosted('magic-link');
315 return;
316 }
317 pending.value = 'magic';
318 error.value = null;
319 const result = await auth.signIn.magicLink({ email: magicEmail.value, redirectTo: props.redirectTo });
320 if (result.ok) {
321 magicSent.value = true;
322 } else {
323 error.value = result.message;
324 }
325 pending.value = null;
326 };
327
328 const handleOAuth = (provider: OAuthProvider) => {
329 const { redirectUrl } = auth.signIn.social({ provider, redirectTo: props.redirectTo });
330 if (typeof window !== 'undefined') {
331 window.location.assign(redirectUrl);
332 }
333 };
334
335 return () =>
336 h(
337 'div',
338 { class: props.className ?? 'briven-auth-signin', 'data-briven-auth': 'signin' },
339 [
340 props.showEmailPassword
341 ? h(
342 'form',
343 {
344 key: 'password',
345 onSubmit: handlePassword,
346 class: 'briven-auth-form',
347 'data-briven-auth-flow': 'password',
348 },
349 [
350 h('input', {
351 key: 'email',
352 type: 'email',
353 required: true,
354 placeholder: 'email',
355 value: email.value,
356 onInput: (e: Event) => { email.value = (e.target as HTMLInputElement).value; },
357 autocomplete: 'email',
358 class: 'briven-auth-input',
359 }),
360 h('input', {
361 key: 'password',
362 type: 'password',
363 required: true,
364 placeholder: 'password',
365 value: password.value,
366 onInput: (e: Event) => { password.value = (e.target as HTMLInputElement).value; },
367 autocomplete: 'current-password',
368 class: 'briven-auth-input',
369 }),
370 h(
371 'button',
372 {
373 key: 'submit',
374 type: 'submit',
375 disabled: pending.value !== null,
376 class: 'briven-auth-submit',
377 },
378 pending.value === 'password' ? 'signing in…' : 'sign in',
379 ),
380 ],
381 )
382 : null,
383 props.showMagicLink
384 ? magicSent.value
385 ? h('p', { key: 'magic-sent', class: 'briven-auth-message' }, 'check your inbox for the sign-in link.')
386 : h(
387 'form',
388 {
389 key: 'magic',
390 onSubmit: handleMagic,
391 class: 'briven-auth-form',
392 'data-briven-auth-flow': 'magic-link',
393 },
394 [
395 h('input', {
396 key: 'email',
397 type: 'email',
398 required: true,
399 placeholder: 'email for magic link',
400 value: magicEmail.value,
401 onInput: (e: Event) => { magicEmail.value = (e.target as HTMLInputElement).value; },
402 autocomplete: 'email',
403 class: 'briven-auth-input',
404 }),
405 h(
406 'button',
407 {
408 key: 'submit',
409 type: 'submit',
410 disabled: pending.value !== null,
411 class: 'briven-auth-submit',
412 },
413 pending.value === 'magic' ? 'sending…' : 'send magic link',
414 ),
415 ],
416 )
417 : null,
418 (props.providers ?? DEFAULT_PROVIDERS).length > 0
419 ? h(
420 'div',
421 { key: 'oauth', class: 'briven-auth-oauth', 'data-briven-auth-flow': 'oauth' },
422 (props.providers ?? DEFAULT_PROVIDERS).map((provider) =>
423 h(
424 'button',
425 {
426 key: provider,
427 type: 'button',
428 'data-briven-auth-provider': provider,
429 onClick: () => handleOAuth(provider),
430 class: 'briven-auth-oauth-button',
431 },
432 `continue with ${provider}`,
433 ),
434 ),
435 )
436 : null,
437 error.value
438 ? h('p', { key: 'error', class: 'briven-auth-error', role: 'alert' }, error.value)
439 : null,
440 ],
441 );
442 },
443};
444
445// ─── BrivenSignUp ──────────────────────────────────────────────────────────
446
447export interface BrivenSignUpProps {
448 providers?: ReadonlyArray<OAuthProvider>;
449 showEmailPassword?: boolean;
450 redirectTo?: string;
451 onSuccess?: (result: { userId: string }) => void;
452 className?: string;
453 mode?: 'direct' | 'hosted';
454 locale?: string;
455}
456
457export const BrivenSignUp = {
458 name: 'BrivenSignUp',
459 props: {
460 providers: { type: Array as PropType<ReadonlyArray<OAuthProvider>>, default: () => DEFAULT_PROVIDERS },
461 showEmailPassword: { type: Boolean, default: true },
462 redirectTo: { type: String, default: undefined },
463 onSuccess: { type: Function as PropType<(result: { userId: string }) => void>, default: undefined },
464 className: { type: String, default: undefined },
465 mode: { type: String as PropType<'direct' | 'hosted'>, default: 'direct' },
466 locale: { type: String, default: undefined },
467 },
468 setup(props: BrivenSignUpProps) {
469 const auth = useBrivenAuth();
470 const name = ref('');
471 const email = ref('');
472 const password = ref('');
473 const pending = ref(false);
474 const error = ref<string | null>(null);
475
476 const redirectToHosted = useRedirectToHosted(auth, props.redirectTo, props.locale);
477
478 const handleSubmit = async (e: Event) => {
479 e.preventDefault();
480 if (props.mode === 'hosted') {
481 redirectToHosted('sign-up');
482 return;
483 }
484 pending.value = true;
485 error.value = null;
486 const result: SignInResult = await auth.signUp.email({
487 email: email.value,
488 password: password.value,
489 name: name.value || undefined,
490 });
491 if (result.ok && 'userId' in result) {
492 props.onSuccess?.({ userId: result.userId });
493 } else if (result.ok && 'twoFactorRequired' in result) {
494 error.value = 'two-factor required — complete the challenge';
495 } else if (!result.ok) {
496 error.value = result.message;
497 }
498 pending.value = false;
499 };
500
501 const handleOAuth = (provider: OAuthProvider) => {
502 const { redirectUrl } = auth.signIn.social({ provider, redirectTo: props.redirectTo });
503 if (typeof window !== 'undefined') {
504 window.location.assign(redirectUrl);
505 }
506 };
507
508 return () =>
509 h(
510 'div',
511 { class: props.className ?? 'briven-auth-signup', 'data-briven-auth': 'signup' },
512 [
513 props.showEmailPassword
514 ? h(
515 'form',
516 {
517 key: 'password',
518 onSubmit: handleSubmit,
519 class: 'briven-auth-form',
520 'data-briven-auth-flow': 'password',
521 },
522 [
523 h('input', {
524 key: 'name',
525 type: 'text',
526 placeholder: 'name (optional)',
527 value: name.value,
528 onInput: (e: Event) => { name.value = (e.target as HTMLInputElement).value; },
529 autocomplete: 'name',
530 class: 'briven-auth-input',
531 }),
532 h('input', {
533 key: 'email',
534 type: 'email',
535 required: true,
536 placeholder: 'email',
537 value: email.value,
538 onInput: (e: Event) => { email.value = (e.target as HTMLInputElement).value; },
539 autocomplete: 'email',
540 class: 'briven-auth-input',
541 }),
542 h('input', {
543 key: 'password',
544 type: 'password',
545 required: true,
546 placeholder: 'password',
547 value: password.value,
548 onInput: (e: Event) => { password.value = (e.target as HTMLInputElement).value; },
549 autocomplete: 'new-password',
550 class: 'briven-auth-input',
551 }),
552 h(
553 'button',
554 {
555 key: 'submit',
556 type: 'submit',
557 disabled: pending.value,
558 class: 'briven-auth-submit',
559 },
560 pending.value ? 'creating account…' : 'create account',
561 ),
562 ],
563 )
564 : null,
565 (props.providers ?? DEFAULT_PROVIDERS).length > 0
566 ? h(
567 'div',
568 { key: 'oauth', class: 'briven-auth-oauth', 'data-briven-auth-flow': 'oauth' },
569 (props.providers ?? DEFAULT_PROVIDERS).map((provider) =>
570 h(
571 'button',
572 {
573 key: provider,
574 type: 'button',
575 'data-briven-auth-provider': provider,
576 onClick: () => handleOAuth(provider),
577 class: 'briven-auth-oauth-button',
578 },
579 `continue with ${provider}`,
580 ),
581 ),
582 )
583 : null,
584 error.value
585 ? h('p', { key: 'error', class: 'briven-auth-error', role: 'alert' }, error.value)
586 : null,
587 ],
588 );
589 },
590};
591
592// ─── UserButton ────────────────────────────────────────────────────────────
593
594export interface UserButtonProps {
595 className?: string;
596 profileUrl?: string;
597}
598
599export const UserButton = {
600 name: 'UserButton',
601 props: {
602 className: { type: String, default: undefined },
603 profileUrl: { type: String, default: undefined },
604 },
605 setup(props: UserButtonProps) {
606 const auth = useBrivenAuth();
607 const { user, isLoading } = useUser();
608 const open = ref(false);
609
610 const handleSignOut = async () => {
611 await auth.signOut();
612 if (typeof window !== 'undefined') {
613 window.location.reload();
614 }
615 };
616
617 const handleProfile = () => {
618 const url = props.profileUrl ?? auth.hostedPageURL('profile');
619 if (typeof window !== 'undefined') {
620 window.location.assign(url);
621 }
622 };
623
624 return () => {
625 if (isLoading.value || !user.value) return null;
626 const label = user.value.name ?? user.value.email;
627 return h(
628 'div',
629 { class: props.className ?? 'briven-auth-userbutton', 'data-briven-auth': 'userbutton' },
630 [
631 h(
632 'button',
633 {
634 type: 'button',
635 onClick: () => { open.value = !open.value; },
636 class: 'briven-auth-userbutton-trigger',
637 },
638 label,
639 ),
640 open.value
641 ? h(
642 'div',
643 { class: 'briven-auth-userbutton-dropdown', 'data-briven-auth-dropdown': 'open' },
644 [
645 h(
646 'button',
647 { type: 'button', onClick: handleProfile, class: 'briven-auth-userbutton-item' },
648 'profile',
649 ),
650 h(
651 'button',
652 { type: 'button', onClick: handleSignOut, class: 'briven-auth-userbutton-item' },
653 'sign out',
654 ),
655 ],
656 )
657 : null,
658 ],
659 );
660 };
661 },
662};
663
664// ─── UserProfile ───────────────────────────────────────────────────────────
665
666export interface UserProfileProps {
667 className?: string;
668 onUpdate?: () => void;
669}
670
671export const UserProfile = {
672 name: 'UserProfile',
673 props: {
674 className: { type: String, default: undefined },
675 onUpdate: { type: Function as PropType<() => void>, default: undefined },
676 },
677 setup(props: UserProfileProps) {
678 const auth = useBrivenAuth();
679 const { user, refresh } = useUser();
680
681 const name = ref('');
682 const currentPassword = ref('');
683 const newPassword = ref('');
684 const updatePending = ref(false);
685 const pwPending = ref(false);
686 const deletePending = ref(false);
687 const message = ref<string | null>(null);
688 const error = ref<string | null>(null);
689
690 watch(
691 () => user.value?.name,
692 (n) => { if (n) name.value = n; },
693 { immediate: true },
694 );
695
696 const handleUpdate = async (e: Event) => {
697 e.preventDefault();
698 updatePending.value = true;
699 error.value = null;
700 message.value = null;
701 const result = await auth.user.update({ name: name.value || undefined });
702 if (result.ok) {
703 message.value = 'profile updated';
704 await refresh();
705 props.onUpdate?.();
706 } else {
707 error.value = result.message;
708 }
709 updatePending.value = false;
710 };
711
712 const handleChangePassword = async (e: Event) => {
713 e.preventDefault();
714 pwPending.value = true;
715 error.value = null;
716 message.value = null;
717 const result = await auth.user.changePassword({ currentPassword: currentPassword.value, newPassword: newPassword.value });
718 if (result.ok) {
719 message.value = 'password changed';
720 currentPassword.value = '';
721 newPassword.value = '';
722 } else {
723 error.value = result.message;
724 }
725 pwPending.value = false;
726 };
727
728 const handleDelete = async () => {
729 if (typeof window !== 'undefined' && !window.confirm('Delete your account? This cannot be undone.')) return;
730 deletePending.value = true;
731 error.value = null;
732 message.value = null;
733 const result = await auth.user.delete();
734 if (result.ok) {
735 if (typeof window !== 'undefined') {
736 window.location.reload();
737 }
738 } else {
739 error.value = result.message;
740 deletePending.value = false;
741 }
742 };
743
744 return () => {
745 if (!user.value) {
746 return h('p', { class: 'briven-auth-message' }, 'not authenticated');
747 }
748 return h(
749 'div',
750 { class: props.className ?? 'briven-auth-userprofile', 'data-briven-auth': 'userprofile' },
751 [
752 h(
753 'form',
754 {
755 key: 'profile',
756 onSubmit: handleUpdate,
757 class: 'briven-auth-form',
758 'data-briven-auth-flow': 'profile-update',
759 },
760 [
761 h('h3', { class: 'briven-auth-heading' }, 'profile'),
762 h('input', {
763 key: 'name',
764 type: 'text',
765 placeholder: 'name',
766 value: name.value,
767 onInput: (e: Event) => { name.value = (e.target as HTMLInputElement).value; },
768 class: 'briven-auth-input',
769 }),
770 h('input', {
771 key: 'email',
772 type: 'email',
773 disabled: true,
774 value: user.value.email,
775 class: 'briven-auth-input',
776 }),
777 h(
778 'button',
779 {
780 key: 'submit',
781 type: 'submit',
782 disabled: updatePending.value,
783 class: 'briven-auth-submit',
784 },
785 updatePending.value ? 'saving…' : 'save profile',
786 ),
787 ],
788 ),
789 h(
790 'form',
791 {
792 key: 'password',
793 onSubmit: handleChangePassword,
794 class: 'briven-auth-form',
795 'data-briven-auth-flow': 'change-password',
796 },
797 [
798 h('h3', { class: 'briven-auth-heading' }, 'change password'),
799 h('input', {
800 key: 'current',
801 type: 'password',
802 required: true,
803 placeholder: 'current password',
804 value: currentPassword.value,
805 onInput: (e: Event) => { currentPassword.value = (e.target as HTMLInputElement).value; },
806 autocomplete: 'current-password',
807 class: 'briven-auth-input',
808 }),
809 h('input', {
810 key: 'new',
811 type: 'password',
812 required: true,
813 placeholder: 'new password',
814 value: newPassword.value,
815 onInput: (e: Event) => { newPassword.value = (e.target as HTMLInputElement).value; },
816 autocomplete: 'new-password',
817 class: 'briven-auth-input',
818 }),
819 h(
820 'button',
821 {
822 key: 'submit',
823 type: 'submit',
824 disabled: pwPending.value,
825 class: 'briven-auth-submit',
826 },
827 pwPending.value ? 'changing…' : 'change password',
828 ),
829 ],
830 ),
831 h(
832 'div',
833 { key: 'danger', class: 'briven-auth-danger-zone', 'data-briven-auth-flow': 'delete-account' },
834 [
835 h('h3', { class: 'briven-auth-heading' }, 'danger zone'),
836 h(
837 'button',
838 {
839 type: 'button',
840 onClick: handleDelete,
841 disabled: deletePending.value,
842 class: 'briven-auth-danger-button',
843 },
844 deletePending.value ? 'deleting…' : 'delete account',
845 ),
846 ],
847 ),
848 message.value ? h('p', { key: 'message', class: 'briven-auth-message' }, message.value) : null,
849 error.value ? h('p', { key: 'error', class: 'briven-auth-error', role: 'alert' }, error.value) : null,
850 ],
851 );
852 };
853 },
854};
855
856// ─── SessionManager ────────────────────────────────────────────────────────
857
858export interface SessionManagerProps {
859 className?: string;
860}
861
862export const SessionManager = {
863 name: 'SessionManager',
864 props: {
865 className: { type: String, default: undefined },
866 },
867 setup(props: SessionManagerProps) {
868 const auth = useBrivenAuth();
869 const sessions = ref<ClientSession[]>([]);
870 const isLoading = ref(true);
871 const error = ref<string | null>(null);
872
873 const load = async () => {
874 isLoading.value = true;
875 error.value = null;
876 const result = await auth.sessions.list();
877 if (result.ok) {
878 sessions.value = result.sessions;
879 } else {
880 error.value = result.message;
881 }
882 isLoading.value = false;
883 };
884
885 onMounted(() => {
886 void load();
887 });
888
889 const handleRevoke = async (sessionId: string) => {
890 const result = await auth.sessions.revoke(sessionId);
891 if (result.ok) {
892 await load();
893 } else {
894 error.value = result.message;
895 }
896 };
897
898 return () =>
899 h(
900 'div',
901 { class: props.className ?? 'briven-auth-sessionmanager', 'data-briven-auth': 'sessionmanager' },
902 [
903 h('h3', { class: 'briven-auth-heading' }, 'active sessions'),
904 isLoading.value
905 ? h('p', { class: 'briven-auth-message' }, 'loading…')
906 : sessions.value.length === 0
907 ? h('p', { class: 'briven-auth-message' }, 'no active sessions')
908 : h(
909 'ul',
910 { class: 'briven-auth-session-list' },
911 sessions.value.map((s) =>
912 h(
913 'li',
914 { key: s.id, class: 'briven-auth-session-item' },
915 [
916 h('span', { class: 'briven-auth-session-info' }, s.userAgent ?? 'unknown device'),
917 h(
918 'button',
919 {
920 type: 'button',
921 onClick: () => handleRevoke(s.id),
922 class: 'briven-auth-session-revoke',
923 },
924 'revoke',
925 ),
926 ],
927 ),
928 ),
929 ),
930 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
931 ],
932 );
933 },
934};
935
936// ─── OrganizationSwitcher ─────────────────────────────────────────────────
937
938export interface OrganizationSwitcherProps {
939 className?: string;
940}
941
942export const OrganizationSwitcher = {
943 name: 'OrganizationSwitcher',
944 props: {
945 className: { type: String, default: undefined },
946 },
947 setup(props: OrganizationSwitcherProps) {
948 const auth = useBrivenAuth();
949 const { activeOrg, setActive } = useActiveOrganization();
950 const orgs = ref<Org[]>([]);
951 const isLoading = ref(true);
952 const open = ref(false);
953 const showCreate = ref(false);
954
955 const load = async () => {
956 isLoading.value = true;
957 const result = await auth.organization.list();
958 if (result.ok) orgs.value = result.data;
959 isLoading.value = false;
960 };
961
962 onMounted(() => {
963 void load();
964 });
965
966 const handleCreate = async (name: string, slug: string) => {
967 const result = await auth.organization.create({ name, slug });
968 if (result.ok) {
969 showCreate.value = false;
970 await load();
971 }
972 return result;
973 };
974
975 const handleSwitch = async (orgId: string) => {
976 await setActive(orgId);
977 open.value = false;
978 };
979
980 return () => {
981 if (isLoading.value) return null;
982
983 if (orgs.value.length === 0) {
984 return h(
985 'button',
986 { type: 'button', onClick: () => { showCreate.value = true; }, class: props.className ?? 'briven-auth-org-switcher' },
987 'create organization',
988 );
989 }
990 return h(
991 'div',
992 { class: props.className ?? 'briven-auth-org-switcher', 'data-briven-auth': 'org-switcher' },
993 [
994 h(
995 'button',
996 { type: 'button', onClick: () => { open.value = !open.value; }, class: 'briven-auth-org-switcher-trigger' },
997 activeOrg.value?.name ?? 'switch organization',
998 ),
999 open.value
1000 ? h(
1001 'div',
1002 { class: 'briven-auth-org-switcher-dropdown' },
1003 [
1004 ...orgs.value.map((org) =>
1005 h(
1006 'button',
1007 {
1008 key: org.id,
1009 type: 'button',
1010 onClick: () => handleSwitch(org.id),
1011 class:
1012 org.id === activeOrg.value?.id
1013 ? 'briven-auth-org-switcher-item briven-auth-org-switcher-item-active'
1014 : 'briven-auth-org-switcher-item',
1015 },
1016 org.name,
1017 ),
1018 ),
1019 h(
1020 'button',
1021 { type: 'button', onClick: () => { showCreate.value = true; }, class: 'briven-auth-org-switcher-create' },
1022 '+ create organization',
1023 ),
1024 ],
1025 )
1026 : null,
1027 showCreate.value
1028 ? h(CreateOrganization, {
1029 key: 'create',
1030 onCreate: handleCreate,
1031 onCancel: () => { showCreate.value = false; },
1032 })
1033 : null,
1034 ],
1035 );
1036 };
1037 },
1038};
1039
1040// ─── CreateOrganization ───────────────────────────────────────────────────
1041
1042export interface CreateOrganizationProps {
1043 onCreate(name: string, slug: string): Promise<unknown>;
1044 onCancel(): void;
1045}
1046
1047export const CreateOrganization = {
1048 name: 'CreateOrganization',
1049 props: {
1050 onCreate: { type: Function as PropType<(name: string, slug: string) => Promise<unknown>>, required: true },
1051 onCancel: { type: Function as PropType<() => void>, required: true },
1052 },
1053 setup(props: CreateOrganizationProps) {
1054 const name = ref('');
1055 const slug = ref('');
1056 const pending = ref(false);
1057 const error = ref<string | null>(null);
1058
1059 const handleSubmit = async (e: Event) => {
1060 e.preventDefault();
1061 pending.value = true;
1062 error.value = null;
1063 const result = await props.onCreate(name.value, slug.value);
1064 if (result && typeof result === 'object' && 'ok' in result && !result.ok) {
1065 error.value = (result as { message?: string }).message ?? 'create failed';
1066 }
1067 pending.value = false;
1068 };
1069
1070 return () =>
1071 h(
1072 'div',
1073 { class: 'briven-auth-create-org' },
1074 [
1075 h('h3', { class: 'briven-auth-heading' }, 'create organization'),
1076 h(
1077 'form',
1078 { class: 'briven-auth-form', onSubmit: handleSubmit },
1079 [
1080 h('input', {
1081 type: 'text',
1082 required: true,
1083 placeholder: 'organization name',
1084 value: name.value,
1085 onInput: (e: Event) => { name.value = (e.target as HTMLInputElement).value; },
1086 class: 'briven-auth-input',
1087 }),
1088 h('input', {
1089 type: 'text',
1090 required: true,
1091 placeholder: 'slug (lowercase-hyphens)',
1092 value: slug.value,
1093 onInput: (e: Event) => { slug.value = (e.target as HTMLInputElement).value; },
1094 pattern: '[a-z0-9-]{1,64}',
1095 class: 'briven-auth-input',
1096 }),
1097 h(
1098 'button',
1099 { type: 'submit', disabled: pending.value, class: 'briven-auth-submit' },
1100 pending.value ? 'creating…' : 'create',
1101 ),
1102 ],
1103 ),
1104 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
1105 h(
1106 'button',
1107 { type: 'button', onClick: props.onCancel, class: 'briven-auth-cancel' },
1108 'cancel',
1109 ),
1110 ],
1111 );
1112 },
1113};
1114
1115// ─── OrganizationProfile ──────────────────────────────────────────────────
1116
1117export interface OrganizationProfileProps {
1118 orgId: string;
1119 className?: string;
1120}
1121
1122export const OrganizationProfile = {
1123 name: 'OrganizationProfile',
1124 props: {
1125 orgId: { type: String, required: true },
1126 className: { type: String, default: undefined },
1127 },
1128 setup(props: OrganizationProfileProps) {
1129 const auth = useBrivenAuth();
1130 const members = ref<OrgMember[]>([]);
1131 const invites = ref<OrgInvite[]>([]);
1132 const inviteEmail = ref('');
1133 const isLoading = ref(true);
1134 const error = ref<string | null>(null);
1135
1136 const load = async () => {
1137 isLoading.value = true;
1138 const [mResult, iResult] = await Promise.all([
1139 auth.organization.listMembers(props.orgId),
1140 auth.organization.listInvites(props.orgId),
1141 ]);
1142 if (mResult.ok) members.value = mResult.data;
1143 if (iResult.ok) invites.value = iResult.data;
1144 isLoading.value = false;
1145 };
1146
1147 onMounted(() => {
1148 void load();
1149 });
1150
1151 const handleInvite = async (e: Event) => {
1152 e.preventDefault();
1153 error.value = null;
1154 const result = await auth.organization.createInvite(props.orgId, { email: inviteEmail.value });
1155 if (result.ok) {
1156 inviteEmail.value = '';
1157 await load();
1158 } else {
1159 error.value = result.message;
1160 }
1161 };
1162
1163 const handleRemove = async (userId: string) => {
1164 const result = await auth.organization.removeMember(props.orgId, userId);
1165 if (result.ok) await load();
1166 else error.value = result.message;
1167 };
1168
1169 return () =>
1170 h(
1171 'div',
1172 { class: props.className ?? 'briven-auth-org-profile', 'data-briven-auth': 'org-profile' },
1173 [
1174 h('h3', { class: 'briven-auth-heading' }, 'members'),
1175 isLoading.value
1176 ? h('p', { class: 'briven-auth-message' }, 'loading…')
1177 : h(
1178 'ul',
1179 { class: 'briven-auth-member-list' },
1180 members.value.map((m) =>
1181 h(
1182 'li',
1183 { key: m.id, class: 'briven-auth-member-item' },
1184 [
1185 h('span', { class: 'briven-auth-member-role' }, m.role),
1186 h('span', { class: 'briven-auth-member-id' }, m.userId),
1187 m.role !== 'owner'
1188 ? h(
1189 'button',
1190 {
1191 type: 'button',
1192 onClick: () => handleRemove(m.userId),
1193 class: 'briven-auth-member-remove',
1194 },
1195 'remove',
1196 )
1197 : null,
1198 ],
1199 ),
1200 ),
1201 ),
1202 h('h3', { class: 'briven-auth-heading' }, 'invites'),
1203 h(
1204 'form',
1205 { class: 'briven-auth-form', onSubmit: handleInvite },
1206 [
1207 h('input', {
1208 type: 'email',
1209 required: true,
1210 placeholder: 'email to invite',
1211 value: inviteEmail.value,
1212 onInput: (e: Event) => { inviteEmail.value = (e.target as HTMLInputElement).value; },
1213 class: 'briven-auth-input',
1214 }),
1215 h('button', { type: 'submit', class: 'briven-auth-submit' }, 'send invite'),
1216 ],
1217 ),
1218 invites.value.length > 0
1219 ? h(
1220 'ul',
1221 { class: 'briven-auth-invite-list' },
1222 invites.value.map((i) => h('li', { key: i.id, class: 'briven-auth-invite-item' }, [`${i.email} · ${i.role}`])),
1223 )
1224 : null,
1225 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
1226 ],
1227 );
1228 },
1229};
1230
1231// ─── TwoFactorSetup ───────────────────────────────────────────────────────
1232
1233export interface TwoFactorSetupProps {
1234 className?: string;
1235 onEnabled?: () => void;
1236}
1237
1238export const TwoFactorSetup = {
1239 name: 'TwoFactorSetup',
1240 props: {
1241 className: { type: String, default: undefined },
1242 onEnabled: { type: Function as PropType<() => void>, default: undefined },
1243 },
1244 setup(props: TwoFactorSetupProps) {
1245 const auth = useBrivenAuth();
1246 const step = ref<'idle' | 'enabling' | 'verify'>('idle');
1247 const code = ref('');
1248 const backupCodes = ref<string[]>([]);
1249 const error = ref<string | null>(null);
1250
1251 const handleEnable = async () => {
1252 step.value = 'enabling';
1253 error.value = null;
1254 const result = await auth.twoFactor.enable();
1255 if (result.ok) {
1256 step.value = 'verify';
1257 } else {
1258 error.value = result.message;
1259 step.value = 'idle';
1260 }
1261 };
1262
1263 const handleVerify = async (e: Event) => {
1264 e.preventDefault();
1265 error.value = null;
1266 const result = await auth.twoFactor.verify(code.value);
1267 if (result.ok) {
1268 const codes = await auth.twoFactor.generateBackupCodes();
1269 if (codes.ok) backupCodes.value = codes.codes;
1270 props.onEnabled?.();
1271 } else {
1272 error.value = result.message;
1273 }
1274 };
1275
1276 return () =>
1277 h(
1278 'div',
1279 { class: props.className ?? 'briven-auth-2fa-setup' },
1280 [
1281 step.value === 'idle'
1282 ? h(
1283 'button',
1284 { type: 'button', onClick: handleEnable, class: 'briven-auth-submit' },
1285 'enable two-factor',
1286 )
1287 : null,
1288 step.value === 'verify'
1289 ? h(
1290 'form',
1291 { onSubmit: handleVerify, class: 'briven-auth-form' },
1292 [
1293 h('p', { class: 'briven-auth-message' }, 'enter the 6-digit code from your authenticator app'),
1294 h('input', {
1295 type: 'text',
1296 required: true,
1297 placeholder: '6-digit code',
1298 value: code.value,
1299 onInput: (e: Event) => { code.value = (e.target as HTMLInputElement).value; },
1300 pattern: '\\d{6}',
1301 maxlength: 6,
1302 class: 'briven-auth-input',
1303 }),
1304 h('button', { type: 'submit', class: 'briven-auth-submit' }, 'verify'),
1305 ],
1306 )
1307 : null,
1308 backupCodes.value.length > 0
1309 ? h(
1310 'div',
1311 { class: 'briven-auth-backup-codes' },
1312 [
1313 h('p', { class: 'briven-auth-message' }, 'save these backup codes:'),
1314 h('ul', {}, backupCodes.value.map((c) => h('li', { key: c, class: 'briven-auth-code' }, c))),
1315 ],
1316 )
1317 : null,
1318 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
1319 ],
1320 );
1321 },
1322};
1323
1324// ─── PasskeyButton ────────────────────────────────────────────────────────
1325
1326export interface PasskeyButtonProps {
1327 className?: string;
1328 mode?: 'register' | 'sign-in';
1329}
1330
1331export const PasskeyButton = {
1332 name: 'PasskeyButton',
1333 props: {
1334 className: { type: String, default: undefined },
1335 mode: { type: String as PropType<'register' | 'sign-in'>, default: 'register' },
1336 },
1337 setup(props: PasskeyButtonProps) {
1338 const auth = useBrivenAuth();
1339 const pending = ref(false);
1340 const error = ref<string | null>(null);
1341
1342 const handleClick = async () => {
1343 pending.value = true;
1344 error.value = null;
1345 if (props.mode === 'register') {
1346 const result = await auth.passkey.register();
1347 if (!result.ok) error.value = result.message;
1348 } else {
1349 const result = await auth.passkey.signIn();
1350 if (!result.ok) error.value = result.message;
1351 }
1352 pending.value = false;
1353 };
1354
1355 return () =>
1356 h(
1357 'div',
1358 { class: props.className ?? 'briven-auth-passkey' },
1359 [
1360 h(
1361 'button',
1362 {
1363 type: 'button',
1364 onClick: handleClick,
1365 disabled: pending.value,
1366 class: 'briven-auth-passkey-button',
1367 },
1368 props.mode === 'register' ? 'register passkey' : 'sign in with passkey',
1369 ),
1370 error.value ? h('p', { class: 'briven-auth-error', role: 'alert' }, error.value) : null,
1371 ],
1372 );
1373 },
1374};
1375
1376export type {
1377 BrivenAuthClient,
1378 ClientSession,
1379 MembershipRequest,
1380 OAuthProvider,
1381 Org,
1382 OrgDomain,
1383 OrgInvite,
1384 OrgMember,
1385 OrgPermission,
1386 OrgRole,
1387 Passkey,
1388 SessionResponse,
1389 SignInResult,
1390 SimpleResult,
1391 SsoConnection,
1392 SsoProviderType,
1393 User,
1394 UserEmail,
1395};