@fayz-ai/plugin-auth 0.9.0 → 0.10.0
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/{AmbassadorBubbles.d.ts → components/AmbassadorBubbles.d.ts} +1 -1
- package/dist/components/AmbassadorBubbles.d.ts.map +1 -0
- package/dist/{forms.d.ts → components/forms.d.ts} +12 -2
- package/dist/components/forms.d.ts.map +1 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -19
- package/dist/index.js.map +1 -1
- package/dist/{runtime.d.ts → lib/runtime.d.ts} +1 -1
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/{pages.d.ts → views/pages.d.ts} +4 -2
- package/dist/views/pages.d.ts.map +1 -0
- package/dist/views/set-password.d.ts.map +1 -0
- package/dist/website/components/AuthModalProvider.d.ts.map +1 -0
- package/dist/website/components/PhoneAuthModal.d.ts.map +1 -0
- package/dist/website/index.d.ts +5 -5
- package/dist/website/index.d.ts.map +1 -1
- package/dist/website/index.js +1 -1
- package/dist/website/index.js.map +1 -1
- package/dist/website/lib/phone.d.ts.map +1 -0
- package/package.json +5 -8
- package/dist/AmbassadorBubbles.d.ts.map +0 -1
- package/dist/forms.d.ts.map +0 -1
- package/dist/pages.d.ts.map +0 -1
- package/dist/runtime.d.ts.map +0 -1
- package/dist/set-password.d.ts.map +0 -1
- package/dist/website/AuthModalProvider.d.ts.map +0 -1
- package/dist/website/PhoneAuthModal.d.ts.map +0 -1
- package/dist/website/phone.d.ts.map +0 -1
- package/src/AmbassadorBubbles.tsx +0 -61
- package/src/forms.tsx +0 -307
- package/src/index.ts +0 -56
- package/src/pages.tsx +0 -209
- package/src/runtime.ts +0 -67
- package/src/set-password.tsx +0 -229
- package/src/types.ts +0 -65
- package/src/website/AuthModalProvider.tsx +0 -48
- package/src/website/PhoneAuthModal.tsx +0 -178
- package/src/website/index.tsx +0 -17
- package/src/website/phone.ts +0 -12
- /package/dist/{set-password.d.ts → views/set-password.d.ts} +0 -0
- /package/dist/website/{AuthModalProvider.d.ts → components/AuthModalProvider.d.ts} +0 -0
- /package/dist/website/{PhoneAuthModal.d.ts → components/PhoneAuthModal.d.ts} +0 -0
- /package/dist/website/{phone.d.ts → lib/phone.d.ts} +0 -0
package/src/pages.tsx
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import { useAuth } from '@fayz-ai/auth'
|
|
3
|
-
import type { AuthProvider } from '@fayz-ai/core'
|
|
4
|
-
import { ForgotPasswordForm, LoginForm, SignupForm } from './forms'
|
|
5
|
-
import { SetPasswordPage } from './set-password'
|
|
6
|
-
import { AmbassadorBubbles } from './AmbassadorBubbles'
|
|
7
|
-
import type { AuthFormView, AuthLayout, LoginAmbassador } from './types'
|
|
8
|
-
|
|
9
|
-
export interface LoginPageProps {
|
|
10
|
-
appName?: string
|
|
11
|
-
logo?: React.ReactNode
|
|
12
|
-
loginLogo?: React.ReactNode
|
|
13
|
-
layout?: AuthLayout
|
|
14
|
-
tagline?: string
|
|
15
|
-
description?: string
|
|
16
|
-
/** Product ambassadors shown as a small social-proof avatar strip above the
|
|
17
|
-
* tagline on the split-layout brand panel. Renders nothing when empty/imageless. */
|
|
18
|
-
loginAmbassadors?: LoginAmbassador[]
|
|
19
|
-
/** Optional microtext beside the ambassador strip, e.g. "+2 mil profissionais". */
|
|
20
|
-
loginAmbassadorsLabel?: string
|
|
21
|
-
showOAuth?: boolean
|
|
22
|
-
oauthProviders?: Exclude<AuthProvider, 'email'>[]
|
|
23
|
-
resetPasswordUrl?: string
|
|
24
|
-
onSuccess?: () => void
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function LoginPage({
|
|
28
|
-
appName = 'App',
|
|
29
|
-
logo,
|
|
30
|
-
loginLogo,
|
|
31
|
-
layout = 'split',
|
|
32
|
-
tagline,
|
|
33
|
-
description,
|
|
34
|
-
loginAmbassadors,
|
|
35
|
-
loginAmbassadorsLabel,
|
|
36
|
-
showOAuth,
|
|
37
|
-
oauthProviders,
|
|
38
|
-
resetPasswordUrl,
|
|
39
|
-
onSuccess,
|
|
40
|
-
}: LoginPageProps) {
|
|
41
|
-
const [view, setView] = React.useState<AuthFormView>('login')
|
|
42
|
-
// Lifted out of the forms so switching view carries the e-mail across: someone
|
|
43
|
-
// who typed it and then remembered they forgot the password should not have to
|
|
44
|
-
// type it again (and back on the login form it is still there).
|
|
45
|
-
const [email, setEmail] = React.useState('')
|
|
46
|
-
const brandLogo = loginLogo ?? logo
|
|
47
|
-
const heading = view === 'login' ? 'Welcome back' : view === 'signup' ? 'Create account' : 'Reset password'
|
|
48
|
-
const subtitle =
|
|
49
|
-
view === 'login'
|
|
50
|
-
? 'Enter your credentials to continue'
|
|
51
|
-
: view === 'signup'
|
|
52
|
-
? 'Create your account to get started'
|
|
53
|
-
: 'Enter your email and we will send reset instructions'
|
|
54
|
-
const form =
|
|
55
|
-
view === 'login' ? (
|
|
56
|
-
<LoginForm
|
|
57
|
-
onSuccess={onSuccess}
|
|
58
|
-
onForgotPassword={() => setView('recovery')}
|
|
59
|
-
showOAuth={showOAuth}
|
|
60
|
-
oauthProviders={oauthProviders}
|
|
61
|
-
initialEmail={email}
|
|
62
|
-
onEmailChange={setEmail}
|
|
63
|
-
/>
|
|
64
|
-
) : view === 'signup' ? (
|
|
65
|
-
<SignupForm onSuccess={onSuccess} />
|
|
66
|
-
) : (
|
|
67
|
-
<ForgotPasswordForm
|
|
68
|
-
redirectTo={resetPasswordUrl}
|
|
69
|
-
onBackToLogin={() => setView('login')}
|
|
70
|
-
initialEmail={email}
|
|
71
|
-
onEmailChange={setEmail}
|
|
72
|
-
/>
|
|
73
|
-
)
|
|
74
|
-
const switcher =
|
|
75
|
-
view === 'login' ? (
|
|
76
|
-
<p className="text-center text-sm text-muted-foreground">
|
|
77
|
-
Do not have an account?{' '}
|
|
78
|
-
<button type="button" className="font-medium text-primary hover:underline" onClick={() => setView('signup')}>
|
|
79
|
-
Sign up
|
|
80
|
-
</button>
|
|
81
|
-
</p>
|
|
82
|
-
) : (
|
|
83
|
-
<p className="text-center text-sm text-muted-foreground">
|
|
84
|
-
{view === 'signup' ? 'Already have an account?' : 'Remembered your password?'}{' '}
|
|
85
|
-
<button type="button" className="font-medium text-primary hover:underline" onClick={() => setView('login')}>
|
|
86
|
-
Sign in
|
|
87
|
-
</button>
|
|
88
|
-
</p>
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
if (layout === 'centered') {
|
|
92
|
-
return (
|
|
93
|
-
<div className="flex min-h-screen items-center justify-center bg-sidebar p-4">
|
|
94
|
-
<div className="w-full max-w-md rounded-2xl border border-border/50 bg-card p-8 shadow-2xl sm:p-10">
|
|
95
|
-
<div className="mb-8 text-center">
|
|
96
|
-
{brandLogo && <div className="mb-4 flex justify-center">{brandLogo}</div>}
|
|
97
|
-
<h1 className="text-2xl font-bold text-foreground">{heading}</h1>
|
|
98
|
-
<p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
|
|
99
|
-
</div>
|
|
100
|
-
<div className="space-y-6">
|
|
101
|
-
{form}
|
|
102
|
-
{switcher}
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return (
|
|
110
|
-
<div className="flex min-h-screen bg-sidebar">
|
|
111
|
-
<div className="relative hidden w-1/2 overflow-hidden lg:block">
|
|
112
|
-
<div className="absolute inset-0 bg-gradient-to-br from-sidebar-accent/50 via-transparent to-sidebar/80" />
|
|
113
|
-
<div className="relative z-20 flex h-full flex-col justify-end p-12">
|
|
114
|
-
<div className="space-y-4">
|
|
115
|
-
<AmbassadorBubbles ambassadors={loginAmbassadors} label={loginAmbassadorsLabel} />
|
|
116
|
-
<p className="text-sm font-medium text-sidebar-muted">{appName}</p>
|
|
117
|
-
<h2 className="text-3xl font-bold text-sidebar-foreground">{tagline ?? `Welcome to ${appName}`}</h2>
|
|
118
|
-
{description && <p className="max-w-md text-sm leading-relaxed text-sidebar-muted">{description}</p>}
|
|
119
|
-
</div>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
<div className="flex flex-1 flex-col lg:p-3 lg:pl-3">
|
|
123
|
-
<div className="flex flex-1 flex-col justify-center bg-background px-6 py-12 lg:rounded-[1.25rem] lg:px-16">
|
|
124
|
-
<div className="mx-auto w-full max-w-sm">
|
|
125
|
-
{brandLogo && <div className="mb-8">{brandLogo}</div>}
|
|
126
|
-
<div className="mb-8">
|
|
127
|
-
<h1 className="text-2xl font-bold text-foreground">{heading}</h1>
|
|
128
|
-
<p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
|
|
129
|
-
</div>
|
|
130
|
-
<div className="space-y-6">
|
|
131
|
-
{form}
|
|
132
|
-
{switcher}
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface AuthGateProps extends LoginPageProps {
|
|
142
|
-
requireAuth?: boolean
|
|
143
|
-
loadingFallback?: React.ReactNode
|
|
144
|
-
children: React.ReactNode
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function AuthGate({ requireAuth = true, loadingFallback, children, ...loginProps }: AuthGateProps) {
|
|
148
|
-
const { isAuthenticated, isLoading } = useAuth()
|
|
149
|
-
if (!requireAuth || isAuthenticated) return <>{children}</>
|
|
150
|
-
if (isLoading) {
|
|
151
|
-
return (
|
|
152
|
-
<>
|
|
153
|
-
{loadingFallback ?? (
|
|
154
|
-
<div className="flex min-h-screen items-center justify-center bg-background">
|
|
155
|
-
<span className="text-sm text-muted-foreground">Loading...</span>
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
</>
|
|
159
|
-
)
|
|
160
|
-
}
|
|
161
|
-
return <LoginPage {...loginProps} />
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface AuthCallbackPageProps {
|
|
165
|
-
onSuccess?: () => void
|
|
166
|
-
onError?: (error: Error) => void
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function AuthCallbackPage({ onSuccess, onError }: AuthCallbackPageProps) {
|
|
170
|
-
const { handleCallback } = useAuth()
|
|
171
|
-
const [error, setError] = React.useState<string | null>(null)
|
|
172
|
-
|
|
173
|
-
React.useEffect(() => {
|
|
174
|
-
let cancelled = false
|
|
175
|
-
handleCallback(typeof window === 'undefined' ? undefined : window.location.href)
|
|
176
|
-
.then(() => {
|
|
177
|
-
if (!cancelled) onSuccess?.()
|
|
178
|
-
})
|
|
179
|
-
.catch((err: unknown) => {
|
|
180
|
-
const errorObject = err instanceof Error ? err : new Error(String(err))
|
|
181
|
-
if (!cancelled) {
|
|
182
|
-
setError(errorObject.message)
|
|
183
|
-
onError?.(errorObject)
|
|
184
|
-
}
|
|
185
|
-
})
|
|
186
|
-
return () => {
|
|
187
|
-
cancelled = true
|
|
188
|
-
}
|
|
189
|
-
}, [handleCallback, onError, onSuccess])
|
|
190
|
-
|
|
191
|
-
return (
|
|
192
|
-
<div className="flex min-h-screen items-center justify-center bg-background p-6 text-center">
|
|
193
|
-
<div>
|
|
194
|
-
<h1 className="text-xl font-semibold text-foreground">{error ? 'Authentication failed' : 'Completing sign in...'}</h1>
|
|
195
|
-
{error && <p className="mt-2 text-sm text-destructive">{error}</p>}
|
|
196
|
-
</div>
|
|
197
|
-
</div>
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export interface ResetPasswordPageProps {
|
|
202
|
-
onSuccess?: () => void
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** Kept for apps that route `/auth/reset-password` themselves. The shell no
|
|
206
|
-
* longer needs a route: SetPasswordPage is mounted by usePasswordSetupGate. */
|
|
207
|
-
export function ResetPasswordPage({ onSuccess }: ResetPasswordPageProps) {
|
|
208
|
-
return <SetPasswordPage reason="recovery" onSuccess={onSuccess} />
|
|
209
|
-
}
|
package/src/runtime.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { AuthAdapter } from '@fayz-ai/core'
|
|
2
|
-
import { createMockAuthAdapter, createSupabaseAuthAdapter } from '@fayz-ai/auth'
|
|
3
|
-
import type { AuthPluginOptions, ResolvedAuthPlugin } from './types'
|
|
4
|
-
|
|
5
|
-
function absoluteUrl(path: string): string | undefined {
|
|
6
|
-
if (typeof window === 'undefined') return undefined
|
|
7
|
-
if (/^https?:\/\//.test(path)) return path
|
|
8
|
-
return `${window.location.origin}${path.startsWith('/') ? path : `/${path}`}`
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function resolveAuthAdapter(options: AuthPluginOptions = {}): AuthAdapter {
|
|
12
|
-
const configured = options.adapter ?? options.provider
|
|
13
|
-
if (configured && typeof configured === 'object') return configured
|
|
14
|
-
|
|
15
|
-
const provider = configured ?? (options.supabase?.url ? 'supabase' : 'mock')
|
|
16
|
-
if (provider === 'supabase') {
|
|
17
|
-
const url = options.supabase?.url
|
|
18
|
-
const anonKey = options.supabase?.anonKey
|
|
19
|
-
if (!url || !anonKey) {
|
|
20
|
-
throw new Error('plugin-auth provider="supabase" requires supabase.url and supabase.anonKey.')
|
|
21
|
-
}
|
|
22
|
-
return createSupabaseAuthAdapter({
|
|
23
|
-
supabaseUrl: url,
|
|
24
|
-
supabaseAnonKey: anonKey,
|
|
25
|
-
supabaseClient: options.supabase?.client,
|
|
26
|
-
callbackUrl: options.supabase?.callbackUrl ?? absoluteUrl(options.routes?.callback ?? '/auth/callback'),
|
|
27
|
-
// Only when the host asked for a specific URL. Left undefined, the adapter
|
|
28
|
-
// points recovery at the app root, which needs no route and no extra entry
|
|
29
|
-
// in Supabase's redirect allow-list.
|
|
30
|
-
resetPasswordUrl:
|
|
31
|
-
options.supabase?.resetPasswordUrl ??
|
|
32
|
-
(options.routes?.resetPassword ? absoluteUrl(options.routes.resetPassword) : undefined),
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return createMockAuthAdapter()
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function createAuthRuntime(options: AuthPluginOptions = {}): ResolvedAuthPlugin {
|
|
40
|
-
return {
|
|
41
|
-
kind: 'auth',
|
|
42
|
-
adapter: resolveAuthAdapter(options),
|
|
43
|
-
requireAuth: options.requireAuth ?? true,
|
|
44
|
-
layout: options.layout ?? 'split',
|
|
45
|
-
logo: options.logo,
|
|
46
|
-
tagline: options.tagline,
|
|
47
|
-
description: options.description,
|
|
48
|
-
loginAmbassadors: options.loginAmbassadors,
|
|
49
|
-
loginAmbassadorsLabel: options.loginAmbassadorsLabel,
|
|
50
|
-
oauth: {
|
|
51
|
-
enabled: options.oauth?.enabled ?? false,
|
|
52
|
-
providers: options.oauth?.providers ?? ['google'],
|
|
53
|
-
},
|
|
54
|
-
routes: {
|
|
55
|
-
login: options.routes?.login ?? '/login',
|
|
56
|
-
callback: options.routes?.callback ?? '/auth/callback',
|
|
57
|
-
// The app root — recovery links are recognised by their own params, not by
|
|
58
|
-
// the path they land on. See resetPassword() in the Supabase adapter.
|
|
59
|
-
resetPassword: options.routes?.resetPassword ?? '/',
|
|
60
|
-
afterSignIn: options.routes?.afterSignIn ?? '/',
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function createAuthPlugin(options: AuthPluginOptions = {}): ResolvedAuthPlugin {
|
|
66
|
-
return createAuthRuntime(options)
|
|
67
|
-
}
|
package/src/set-password.tsx
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import { useAuth, readAuthUrlIntent, clearAuthUrlTokens, decidePasswordSetup } from '@fayz-ai/auth'
|
|
3
|
-
import { useTranslation } from '@fayz-ai/core'
|
|
4
|
-
import { ForgotPasswordForm } from './forms'
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Set-password surface — the one screen behind two flows:
|
|
8
|
-
//
|
|
9
|
-
// • recovery — the user clicked "forgot password" and came back on a recovery
|
|
10
|
-
// link. GoTrue already signed them in from the link, so all that is missing
|
|
11
|
-
// is `updateUser({ password })`. Without this screen they land straight in
|
|
12
|
-
// the app (or on the login form) and are never asked, which is exactly the
|
|
13
|
-
// "the link does nothing" bug.
|
|
14
|
-
//
|
|
15
|
-
// • invite — the user was added to an organisation. The invite is a magic link,
|
|
16
|
-
// so the account exists with NO password: unless they choose one here, every
|
|
17
|
-
// future sign-in needs another e-mail.
|
|
18
|
-
//
|
|
19
|
-
// Mounted by the shell above its auth gate, driven by usePasswordSetupGate().
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
export type SetPasswordReason = 'recovery' | 'invite'
|
|
23
|
-
|
|
24
|
-
const MIN_LENGTH = 8
|
|
25
|
-
|
|
26
|
-
function fieldClass() {
|
|
27
|
-
return 'h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50'
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function buttonClass() {
|
|
31
|
-
return 'inline-flex h-10 w-full items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:pointer-events-none disabled:opacity-50'
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function useTr() {
|
|
35
|
-
const t = useTranslation()
|
|
36
|
-
return React.useCallback(
|
|
37
|
-
(key: string, fallback: string, params?: Record<string, string | number>) => {
|
|
38
|
-
const value = t(key, params)
|
|
39
|
-
return !value || value === key ? fallback : value
|
|
40
|
-
},
|
|
41
|
-
[t],
|
|
42
|
-
)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface PasswordSetupGate {
|
|
46
|
-
/** null when nothing is pending and the shell should render normally. */
|
|
47
|
-
reason: SetPasswordReason | null
|
|
48
|
-
/** The link is dead (already used / expired) — offer a fresh one instead. */
|
|
49
|
-
expired: boolean
|
|
50
|
-
/** Still resolving the session the link carries. */
|
|
51
|
-
checking: boolean
|
|
52
|
-
/** Called by the screen once the password is set (or the user gives up). */
|
|
53
|
-
complete: () => void
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Decides whether the user owes us a password before they see the app.
|
|
58
|
-
*
|
|
59
|
-
* The URL intent is read from a snapshot taken at module load: supabase-js wipes
|
|
60
|
-
* the token fragment as soon as it initialises, so the reason the user is here is
|
|
61
|
-
* gone by the time React renders.
|
|
62
|
-
*/
|
|
63
|
-
export function usePasswordSetupGate(): PasswordSetupGate {
|
|
64
|
-
const { user, isLoading, isAuthenticated } = useAuth()
|
|
65
|
-
const [done, setDone] = React.useState(false)
|
|
66
|
-
const intent = React.useMemo(() => readAuthUrlIntent(), [])
|
|
67
|
-
|
|
68
|
-
const complete = React.useCallback(() => {
|
|
69
|
-
clearAuthUrlTokens()
|
|
70
|
-
setDone(true)
|
|
71
|
-
}, [])
|
|
72
|
-
|
|
73
|
-
if (done) return { reason: null, expired: false, checking: false, complete }
|
|
74
|
-
|
|
75
|
-
const decision = decidePasswordSetup({
|
|
76
|
-
intent,
|
|
77
|
-
isLoading,
|
|
78
|
-
isAuthenticated,
|
|
79
|
-
mustSetPassword: user?.mustSetPassword,
|
|
80
|
-
})
|
|
81
|
-
return { ...decision, complete }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface SetPasswordPageProps {
|
|
85
|
-
reason: SetPasswordReason
|
|
86
|
-
appName?: string
|
|
87
|
-
logo?: React.ReactNode
|
|
88
|
-
/** Show the "ask for a new link" form instead of the password fields. */
|
|
89
|
-
expired?: boolean
|
|
90
|
-
/** Waiting on the link's session — renders a quiet placeholder. */
|
|
91
|
-
checking?: boolean
|
|
92
|
-
onSuccess?: () => void
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function SetPasswordPage({
|
|
96
|
-
reason,
|
|
97
|
-
appName = 'App',
|
|
98
|
-
logo,
|
|
99
|
-
expired,
|
|
100
|
-
checking,
|
|
101
|
-
onSuccess,
|
|
102
|
-
}: SetPasswordPageProps) {
|
|
103
|
-
const tr = useTr()
|
|
104
|
-
const { user, updatePassword, signOut, error, isLoading } = useAuth()
|
|
105
|
-
const [password, setPassword] = React.useState('')
|
|
106
|
-
const [confirmPassword, setConfirmPassword] = React.useState('')
|
|
107
|
-
const [localError, setLocalError] = React.useState<string | null>(null)
|
|
108
|
-
const [busy, setBusy] = React.useState(false)
|
|
109
|
-
const disabled = busy || isLoading
|
|
110
|
-
|
|
111
|
-
const invite = reason === 'invite'
|
|
112
|
-
const title = invite
|
|
113
|
-
? tr('auth.setPassword.welcomeTitle', `Welcome to ${appName}`, { appName })
|
|
114
|
-
: tr('auth.setPassword.recoveryTitle', 'Choose a new password')
|
|
115
|
-
const subtitle = invite
|
|
116
|
-
? tr('auth.setPassword.welcomeSubtitle', 'Choose a password to finish setting up your access.')
|
|
117
|
-
: tr('auth.setPassword.recoverySubtitle', 'Set a new password to get back into your account.')
|
|
118
|
-
|
|
119
|
-
async function submit(event: React.FormEvent) {
|
|
120
|
-
event.preventDefault()
|
|
121
|
-
setLocalError(null)
|
|
122
|
-
if (password.length < MIN_LENGTH) {
|
|
123
|
-
setLocalError(tr('auth.setPassword.tooShort', `Password must be at least ${MIN_LENGTH} characters.`))
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
if (password !== confirmPassword) {
|
|
127
|
-
setLocalError(tr('auth.setPassword.mismatch', 'Passwords do not match.'))
|
|
128
|
-
return
|
|
129
|
-
}
|
|
130
|
-
setBusy(true)
|
|
131
|
-
try {
|
|
132
|
-
await updatePassword(password)
|
|
133
|
-
onSuccess?.()
|
|
134
|
-
} catch {
|
|
135
|
-
// useAuth already surfaced it; keep the user on the form.
|
|
136
|
-
} finally {
|
|
137
|
-
setBusy(false)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<div className="flex min-h-screen items-center justify-center bg-sidebar p-4">
|
|
143
|
-
<div className="w-full max-w-md rounded-2xl border border-border/50 bg-card p-8 shadow-2xl sm:p-10">
|
|
144
|
-
{logo && <div className="mb-6 flex justify-center">{logo}</div>}
|
|
145
|
-
{checking ? (
|
|
146
|
-
<p className="py-6 text-center text-sm text-muted-foreground">
|
|
147
|
-
{tr('auth.setPassword.checking', 'Checking your link...')}
|
|
148
|
-
</p>
|
|
149
|
-
) : expired ? (
|
|
150
|
-
<>
|
|
151
|
-
<div className="mb-6">
|
|
152
|
-
<h1 className="text-2xl font-bold text-foreground">
|
|
153
|
-
{tr('auth.setPassword.expiredTitle', 'This link has expired')}
|
|
154
|
-
</h1>
|
|
155
|
-
<p className="mt-1 text-sm text-muted-foreground">
|
|
156
|
-
{tr(
|
|
157
|
-
'auth.setPassword.expiredSubtitle',
|
|
158
|
-
'Reset links can only be used once. Enter your e-mail and we will send a new one.',
|
|
159
|
-
)}
|
|
160
|
-
</p>
|
|
161
|
-
</div>
|
|
162
|
-
<ForgotPasswordForm
|
|
163
|
-
initialEmail={user?.email}
|
|
164
|
-
submitLabel={tr('auth.setPassword.sendNewLink', 'Send new link')}
|
|
165
|
-
sendingLabel={tr('auth.setPassword.sending', 'Sending...')}
|
|
166
|
-
sentLabel={tr('auth.setPassword.sent', 'Check your inbox — a new link is on the way.')}
|
|
167
|
-
emailLabel={tr('auth.setPassword.email', 'E-mail')}
|
|
168
|
-
backLabel={tr('auth.setPassword.backToLogin', 'Back to sign in')}
|
|
169
|
-
onBackToLogin={onSuccess}
|
|
170
|
-
/>
|
|
171
|
-
</>
|
|
172
|
-
) : (
|
|
173
|
-
<>
|
|
174
|
-
<div className="mb-6">
|
|
175
|
-
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
|
|
176
|
-
<p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>
|
|
177
|
-
</div>
|
|
178
|
-
<form onSubmit={submit} className="space-y-4">
|
|
179
|
-
<label className="block space-y-1.5 text-sm font-medium">
|
|
180
|
-
<span>{tr('auth.setPassword.newPassword', 'New password')}</span>
|
|
181
|
-
<input
|
|
182
|
-
className={fieldClass()}
|
|
183
|
-
type="password"
|
|
184
|
-
autoComplete="new-password"
|
|
185
|
-
autoFocus
|
|
186
|
-
required
|
|
187
|
-
value={password}
|
|
188
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
189
|
-
disabled={disabled}
|
|
190
|
-
/>
|
|
191
|
-
</label>
|
|
192
|
-
<label className="block space-y-1.5 text-sm font-medium">
|
|
193
|
-
<span>{tr('auth.setPassword.confirmPassword', 'Confirm password')}</span>
|
|
194
|
-
<input
|
|
195
|
-
className={fieldClass()}
|
|
196
|
-
type="password"
|
|
197
|
-
autoComplete="new-password"
|
|
198
|
-
required
|
|
199
|
-
value={confirmPassword}
|
|
200
|
-
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
201
|
-
disabled={disabled}
|
|
202
|
-
/>
|
|
203
|
-
</label>
|
|
204
|
-
{(localError || error) && (
|
|
205
|
-
<p className="text-sm text-destructive">{localError ?? error?.message}</p>
|
|
206
|
-
)}
|
|
207
|
-
<button className={buttonClass()} type="submit" disabled={disabled}>
|
|
208
|
-
{disabled
|
|
209
|
-
? tr('auth.setPassword.saving', 'Saving...')
|
|
210
|
-
: tr('auth.setPassword.submit', 'Save password')}
|
|
211
|
-
</button>
|
|
212
|
-
</form>
|
|
213
|
-
{/* An invitee who does not want to continue needs a way out that does
|
|
214
|
-
not leave them stuck on this screen with a live session. */}
|
|
215
|
-
<button
|
|
216
|
-
type="button"
|
|
217
|
-
className="mt-4 w-full text-center text-sm text-muted-foreground hover:text-foreground"
|
|
218
|
-
onClick={() => {
|
|
219
|
-
void signOut().finally(() => onSuccess?.())
|
|
220
|
-
}}
|
|
221
|
-
>
|
|
222
|
-
{tr('auth.setPassword.backToLogin', 'Back to sign in')}
|
|
223
|
-
</button>
|
|
224
|
-
</>
|
|
225
|
-
)}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
)
|
|
229
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import type React from 'react'
|
|
2
|
-
import type { AuthAdapter, AuthProvider } from '@fayz-ai/core'
|
|
3
|
-
import type { SupabaseAuthConfig } from '@fayz-ai/auth'
|
|
4
|
-
|
|
5
|
-
export type AuthProviderKey = 'supabase' | 'mock' | (string & {})
|
|
6
|
-
export type AuthLayout = 'split' | 'centered'
|
|
7
|
-
|
|
8
|
-
/** A product ambassador rendered as a floating circular photo bubble on the
|
|
9
|
-
* split-login decorative panel. `image` may be a local path or a data URI. */
|
|
10
|
-
export interface LoginAmbassador {
|
|
11
|
-
name: string
|
|
12
|
-
role?: string
|
|
13
|
-
image: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface AuthRoutesConfig {
|
|
17
|
-
login?: string
|
|
18
|
-
callback?: string
|
|
19
|
-
resetPassword?: string
|
|
20
|
-
afterSignIn?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface AuthOAuthConfig {
|
|
24
|
-
enabled?: boolean
|
|
25
|
-
providers?: Exclude<AuthProvider, 'email'>[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface AuthSupabaseConfig {
|
|
29
|
-
url?: string
|
|
30
|
-
anonKey?: string
|
|
31
|
-
client?: SupabaseAuthConfig['supabaseClient']
|
|
32
|
-
callbackUrl?: string
|
|
33
|
-
resetPasswordUrl?: string
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface AuthPluginOptions {
|
|
37
|
-
provider?: AuthProviderKey | AuthAdapter
|
|
38
|
-
adapter?: AuthProviderKey | AuthAdapter
|
|
39
|
-
supabase?: AuthSupabaseConfig
|
|
40
|
-
requireAuth?: boolean
|
|
41
|
-
layout?: AuthLayout
|
|
42
|
-
logo?: React.ReactNode
|
|
43
|
-
tagline?: string
|
|
44
|
-
description?: string
|
|
45
|
-
loginAmbassadors?: LoginAmbassador[]
|
|
46
|
-
loginAmbassadorsLabel?: string
|
|
47
|
-
oauth?: AuthOAuthConfig
|
|
48
|
-
routes?: AuthRoutesConfig
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface ResolvedAuthPlugin {
|
|
52
|
-
kind: 'auth'
|
|
53
|
-
adapter: AuthAdapter
|
|
54
|
-
requireAuth: boolean
|
|
55
|
-
layout: AuthLayout
|
|
56
|
-
logo?: React.ReactNode
|
|
57
|
-
tagline?: string
|
|
58
|
-
description?: string
|
|
59
|
-
loginAmbassadors?: LoginAmbassador[]
|
|
60
|
-
loginAmbassadorsLabel?: string
|
|
61
|
-
oauth: Required<AuthOAuthConfig>
|
|
62
|
-
routes: Required<AuthRoutesConfig>
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export type AuthFormView = 'login' | 'signup' | 'recovery'
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useState, type ReactNode } from 'react'
|
|
2
|
-
import { PhoneAuthModal, type PhoneAuthModalConfig } from './PhoneAuthModal'
|
|
3
|
-
|
|
4
|
-
interface AuthModalContextValue {
|
|
5
|
-
openAuthModal: () => void
|
|
6
|
-
closeAuthModal: () => void
|
|
7
|
-
isAuthModalOpen: boolean
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const AuthModalContext = createContext<AuthModalContextValue | null>(null)
|
|
11
|
-
|
|
12
|
-
export interface AuthModalProviderProps {
|
|
13
|
-
/** Enables the phone sign-in/up modal (default for dogfood sites). */
|
|
14
|
-
phoneAuth?: boolean
|
|
15
|
-
config?: PhoneAuthModalConfig
|
|
16
|
-
children: ReactNode
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Mounts the default phone-auth login modal and exposes openAuthModal() to the
|
|
21
|
-
* host (e.g. an "Acessar área do cliente" button). Render INSIDE <AuthProvider>
|
|
22
|
-
* so the modal can sign the user in through the mounted adapter.
|
|
23
|
-
*/
|
|
24
|
-
export function AuthModalProvider({ phoneAuth = true, config, children }: AuthModalProviderProps) {
|
|
25
|
-
const [open, setOpen] = useState(false)
|
|
26
|
-
const value: AuthModalContextValue = {
|
|
27
|
-
openAuthModal: () => setOpen(true),
|
|
28
|
-
closeAuthModal: () => setOpen(false),
|
|
29
|
-
isAuthModalOpen: open,
|
|
30
|
-
}
|
|
31
|
-
return (
|
|
32
|
-
<AuthModalContext.Provider value={value}>
|
|
33
|
-
{children}
|
|
34
|
-
{phoneAuth ? <PhoneAuthModal open={open} onClose={() => setOpen(false)} config={config} /> : null}
|
|
35
|
-
</AuthModalContext.Provider>
|
|
36
|
-
)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Access the auth modal controls. Returns a no-op opener if no provider is mounted. */
|
|
40
|
-
export function useAuthModal(): AuthModalContextValue {
|
|
41
|
-
return (
|
|
42
|
-
useContext(AuthModalContext) ?? {
|
|
43
|
-
openAuthModal: () => {},
|
|
44
|
-
closeAuthModal: () => {},
|
|
45
|
-
isAuthModalOpen: false,
|
|
46
|
-
}
|
|
47
|
-
)
|
|
48
|
-
}
|