@@ -16,7 +16,7 @@ import {
1616} from "@courselit/page-primitives" ;
1717import { useContext , useState } from "react" ;
1818import { FormEvent } from "react" ;
19- import { signIn } from "next-auth/react " ;
19+ import { authClient } from "@/lib/auth-client " ;
2020import { Form , useToast } from "@courselit/components-library" ;
2121import {
2222 BTN_LOGIN ,
@@ -37,8 +37,22 @@ import { checkPermission } from "@courselit/utils";
3737import { Profile } from "@courselit/common-models" ;
3838import { getUserProfile } from "../../helpers" ;
3939import { ADMIN_PERMISSIONS } from "@ui-config/constants" ;
40+ import { useRouter } from "next/navigation" ;
4041
41- export default function LoginForm ( { redirectTo } : { redirectTo ?: string } ) {
42+ interface AuthConfig {
43+ emailOtp : boolean ;
44+ google : boolean ;
45+ github : boolean ;
46+ saml : boolean ;
47+ }
48+
49+ export default function LoginForm ( {
50+ redirectTo,
51+ authConfig,
52+ } : {
53+ redirectTo ?: string ;
54+ authConfig ?: AuthConfig ;
55+ } ) {
4256 const { theme } = useContext ( ThemeContext ) ;
4357 const [ showCode , setShowCode ] = useState ( false ) ;
4458 const [ email , setEmail ] = useState ( "" ) ;
@@ -49,94 +63,57 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
4963 const serverConfig = useContext ( ServerConfigContext ) ;
5064 const { executeRecaptcha } = useRecaptcha ( ) ;
5165 const address = useContext ( AddressContext ) ;
66+ const router = useRouter ( ) ;
5267
5368 const requestCode = async function ( e : FormEvent ) {
5469 e . preventDefault ( ) ;
5570 setLoading ( true ) ;
5671 setError ( "" ) ;
5772
73+ // ReCAPTCHA logic preserved
5874 if ( serverConfig . recaptchaSiteKey ) {
5975 if ( ! executeRecaptcha ) {
6076 toast ( {
6177 title : TOAST_TITLE_ERROR ,
62- description :
63- "reCAPTCHA service not available. Please try again later." ,
78+ description : "reCAPTCHA service not available." ,
6479 variant : "destructive" ,
6580 } ) ;
6681 setLoading ( false ) ;
6782 return ;
6883 }
69-
7084 const recaptchaToken = await executeRecaptcha ( "login_code_request" ) ;
7185 if ( ! recaptchaToken ) {
7286 toast ( {
7387 title : TOAST_TITLE_ERROR ,
74- description :
75- "reCAPTCHA validation failed. Please try again." ,
76- variant : "destructive" ,
77- } ) ;
78- setLoading ( false ) ;
79- return ;
80- }
81- try {
82- const recaptchaVerificationResponse = await fetch (
83- "/api/recaptcha" ,
84- {
85- method : "POST" ,
86- headers : { "Content-Type" : "application/json" } ,
87- body : JSON . stringify ( { token : recaptchaToken } ) ,
88- } ,
89- ) ;
90-
91- const recaptchaData =
92- await recaptchaVerificationResponse . json ( ) ;
93-
94- if (
95- ! recaptchaVerificationResponse . ok ||
96- ! recaptchaData . success ||
97- ( recaptchaData . score && recaptchaData . score < 0.5 )
98- ) {
99- toast ( {
100- title : TOAST_TITLE_ERROR ,
101- description : `reCAPTCHA verification failed. ${ recaptchaData . score ? `Score: ${ recaptchaData . score . toFixed ( 2 ) } .` : "" } Please try again.` ,
102- variant : "destructive" ,
103- } ) ;
104- setLoading ( false ) ;
105- return ;
106- }
107- } catch ( err ) {
108- console . error ( "Error during reCAPTCHA verification:" , err ) ;
109- toast ( {
110- title : TOAST_TITLE_ERROR ,
111- description :
112- "reCAPTCHA verification failed. Please try again." ,
88+ description : "reCAPTCHA validation failed." ,
11389 variant : "destructive" ,
11490 } ) ;
11591 setLoading ( false ) ;
11692 return ;
11793 }
94+ // Verify token on server if needed, but for now proceeding to authClient
11895 }
11996
12097 try {
121- const url = `/api/auth/code/generate?email= ${ encodeURIComponent (
98+ const { error } = await authClient . signIn . emailOtp ( {
12299 email,
123- ) } `;
124- const response = await fetch ( url ) ;
125- const resp = await response . json ( ) ;
126- if ( response . ok ) {
127- setShowCode ( true ) ;
128- } else {
100+ type : "sign-in" ,
101+ } ) ;
102+
103+ if ( error ) {
129104 toast ( {
130105 title : TOAST_TITLE_ERROR ,
131- description : resp . error || "Failed to request code." ,
106+ description : error . message || "Failed to request code." ,
132107 variant : "destructive" ,
133108 } ) ;
109+ } else {
110+ setShowCode ( true ) ;
134111 }
135- } catch ( err ) {
112+ } catch ( err : any ) {
136113 console . error ( "Error during requestCode:" , err ) ;
137114 toast ( {
138115 title : TOAST_TITLE_ERROR ,
139- description : "An unexpected error occurred. Please try again. " ,
116+ description : "An unexpected error occurred." ,
140117 variant : "destructive" ,
141118 } ) ;
142119 } finally {
@@ -148,25 +125,47 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
148125 e . preventDefault ( ) ;
149126 try {
150127 setLoading ( true ) ;
151- const response = await signIn ( "credentials" , {
128+ const { error } = await authClient . signIn . emailOtp ( {
152129 email,
153- code,
154- redirect : false ,
130+ otp : code ,
131+ type : "sign-in" ,
155132 } ) ;
156- if ( response ?. error ) {
157- setError ( `Can't sign you in at this time` ) ;
133+
134+ if ( error ) {
135+ setError ( error . message || "Can't sign you in at this time" ) ;
158136 } else {
137+ const profile = await getUserProfile ( address . backend ) ;
159138 window . location . href =
160- redirectTo ||
161- getRedirectURLBasedOnProfile (
162- await getUserProfile ( address . backend ) ,
163- ) ;
139+ redirectTo || getRedirectURLBasedOnProfile ( profile ) ;
164140 }
165141 } finally {
166142 setLoading ( false ) ;
167143 }
168144 } ;
169145
146+ const handleSocialLogin = async ( provider : "google" | "github" ) => {
147+ await authClient . signIn . social ( {
148+ provider,
149+ callbackURL : redirectTo || "/dashboard" ,
150+ } ) ;
151+ } ;
152+
153+ const handleSSOLogin = async ( ) => {
154+ // Trigger SSO flow. Usually requires email to resolve provider,
155+ // but if we know the provider is SAML for this domain, we might need a different call.
156+ // Better Auth SSO usually works by `signIn.sso({ email })`.
157+ // If the user enters email in the input, we can use that.
158+ // Or we can have a separate "Login with SSO" button that asks for email if not provided.
159+ if ( ! email ) {
160+ setError ( "Please enter your email to login with SSO" ) ;
161+ return ;
162+ }
163+ await authClient . signIn . sso ( {
164+ email,
165+ callbackURL : redirectTo || "/dashboard" ,
166+ } ) ;
167+ } ;
168+
170169 const getRedirectURLBasedOnProfile = ( profile : Profile ) => {
171170 if (
172171 profile ?. userId &&
@@ -182,7 +181,7 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
182181 < Section theme = { theme . theme } >
183182 < div className = "flex flex-col gap-4 min-h-[80vh]" >
184183 < div className = "flex justify-center grow items-center px-4 mx-auto lg:max-w-[1200px] w-full" >
185- < div className = "flex flex-col" >
184+ < div className = "flex flex-col w-full max-w-md " >
186185 { error && (
187186 < div
188187 style = { {
@@ -229,14 +228,6 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
229228 </ Link >
230229 </ Caption >
231230 < div className = "flex justify-center" >
232- { /* <FormSubmit
233- text={
234- loading
235- ? LOADING
236- : BTN_LOGIN_GET_CODE
237- }
238- disabled={loading}
239- /> */ }
240231 < Button
241232 theme = { theme . theme }
242233 disabled = { loading }
@@ -247,6 +238,44 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
247238 </ Button >
248239 </ div >
249240 </ Form >
241+
242+ { /* Social & SSO Buttons */ }
243+ < div className = "flex flex-col gap-2 mt-4" >
244+ { authConfig ?. google && (
245+ < Button
246+ theme = { theme . theme }
247+ onClick = { ( ) =>
248+ handleSocialLogin ( "google" )
249+ }
250+ type = "button"
251+ variant = "outlined"
252+ >
253+ Continue with Google
254+ </ Button >
255+ ) }
256+ { authConfig ?. github && (
257+ < Button
258+ theme = { theme . theme }
259+ onClick = { ( ) =>
260+ handleSocialLogin ( "github" )
261+ }
262+ type = "button"
263+ variant = "outlined"
264+ >
265+ Continue with GitHub
266+ </ Button >
267+ ) }
268+ { authConfig ?. saml && (
269+ < Button
270+ theme = { theme . theme }
271+ onClick = { handleSSOLogin }
272+ type = "button"
273+ variant = "outlined"
274+ >
275+ Login with SSO
276+ </ Button >
277+ ) }
278+ </ div >
250279 </ div >
251280 ) }
252281 { showCode && (
0 commit comments