better-auth 0.2.3-beta.8 → 0.2.4
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/access.js +125 -1
- package/dist/adapters.d.ts +1 -2
- package/dist/adapters.js +1155 -14
- package/dist/api.js +2458 -4
- package/dist/cli.js +1010 -3
- package/dist/client/plugins.js +527 -2
- package/dist/client.js +382 -1
- package/dist/index.js +3670 -5
- package/dist/next-js.js +34 -1
- package/dist/node.js +8 -1
- package/dist/plugins.js +5529 -4
- package/dist/react.js +393 -1
- package/dist/social.js +586 -2
- package/dist/solid-start.js +13 -1
- package/dist/solid.js +389 -1
- package/dist/svelte-kit.js +34 -1
- package/dist/svelte.js +380 -1
- package/dist/utils.js +453 -2
- package/dist/vue.js +389 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,2029 @@
|
|
|
1
|
-
|
|
2
|
-
`)}}),T=Z();var ye=e=>{let t=new vt(e.clientId,e.clientSecret,w("google",e.redirectURI));return{id:"google",name:"Google",createAuthorizationURL({state:o,scopes:r,codeVerifier:n,redirectURI:s}){if(!e.clientId||!e.clientSecret)throw T.error("Client Id and Client Secret is required for Google. Make sure to provide them in the options."),new k("CLIENT_ID_AND_SECRET_REQUIRED");if(!n)throw new k("codeVerifier is required for Google");let i=r||["email","profile"];return t.createAuthorizationURL(o,n,i)},validateAuthorizationCode:async(o,r,n)=>R({code:o,codeVerifier:r,redirectURI:n||w("google",e.redirectURI),options:e,tokenEndpoint:"https://oauth2.googleapis.com/token"}),async getUserInfo(o){if(!o.idToken)return null;let r=It(o.idToken())?.payload;return{user:{id:r.sub,name:r.name,email:r.email,image:r.picture,emailVerified:r.email_verified},data:r}}}};import{betterFetch as St}from"@better-fetch/fetch";import{Spotify as Ot}from"arctic";var be=e=>{let t=new Ot(e.clientId,e.clientSecret,w("spotify",e.redirectURI));return{id:"spotify",name:"Spotify",createAuthorizationURL({state:o,scopes:r}){let n=r||["user-read-email"];return t.createAuthorizationURL(o,n)},validateAuthorizationCode:async(o,r,n)=>R({code:o,codeVerifier:r,redirectURI:n||w("spotify",e.redirectURI),options:e,tokenEndpoint:"https://accounts.spotify.com/api/token"}),async getUserInfo(o){let{data:r,error:n}=await St("https://api.spotify.com/v1/me",{method:"GET",headers:{Authorization:`Bearer ${o.accessToken()}`}});return n?null:{user:{id:r.id,name:r.display_name,email:r.email,image:r.images[0]?.url,emailVerified:!1},data:r}}}};import{betterFetch as Lt}from"@better-fetch/fetch";import{Twitch as Ct}from"arctic";var we=e=>{let t=new Ct(e.clientId,e.clientSecret,w("twitch",e.redirectURI));return{id:"twitch",name:"Twitch",createAuthorizationURL({state:o,scopes:r}){let n=r||["activity:write","read"];return t.createAuthorizationURL(o,n)},validateAuthorizationCode:async(o,r,n)=>R({code:o,codeVerifier:r,redirectURI:n||w("twitch",e.redirectURI),options:e,tokenEndpoint:"https://id.twitch.tv/oauth2/token"}),async getUserInfo(o){let{data:r,error:n}=await Lt("https://api.twitch.tv/helix/users",{method:"GET",headers:{Authorization:`Bearer ${o.accessToken()}`}});return n?null:{user:{id:r.sub,name:r.preferred_username,email:r.email,image:r.picture,emailVerified:!1},data:r}}}};import{betterFetch as Et}from"@better-fetch/fetch";import{Twitter as _t}from"arctic";var Ae=e=>{let t=new _t(e.clientId,e.clientSecret,w("twitter",e.redirectURI));return{id:"twitter",name:"Twitter",createAuthorizationURL(o){let r=o.scopes||["account_info.read"];return t.createAuthorizationURL(o.state,o.codeVerifier,r)},validateAuthorizationCode:async(o,r,n)=>R({code:o,codeVerifier:r,redirectURI:n||w("twitch",e.redirectURI),options:e,tokenEndpoint:"https://id.twitch.tv/oauth2/token"}),async getUserInfo(o){let{data:r,error:n}=await Et("https://api.x.com/2/users/me?user.fields=profile_image_url",{method:"GET",headers:{Authorization:`Bearer ${o.accessToken()}`}});return n||!r.data.email?null:{user:{id:r.data.id,name:r.data.name,email:r.data.email,image:r.data.profile_image_url,emailVerified:r.data.verified||!1},data:r}}}};import"arctic";var Q={apple:pe,discord:me,facebook:fe,github:he,google:ye,spotify:be,twitch:we,twitter:Ae},ke=Object.keys(Q);import{generateState as Bt}from"oslo/oauth2";import{z as D}from"zod";function Re(e,t,o){let r=Bt();return{state:JSON.stringify({code:r,callbackURL:e,currentURL:t,dontRememberMe:o}),code:r}}function X(e){return D.object({code:D.string(),callbackURL:D.string().optional(),currentURL:D.string().optional(),dontRememberMe:D.boolean().optional()}).safeParse(JSON.parse(e))}import{APIError as jt}from"better-call";var N=(e,t=!1)=>{let o=new Date;return new Date(o.getTime()+(t?e*1e3:e))};import{TimeSpan as qt}from"oslo";function Te(e){let o=!!e.advanced?.useSecureCookies||process.env.NODE_ENV!=="development"&&process.env.NODE_ENV!=="test"?"__Secure-":"",r="better-auth",n=new qt(7,"d").seconds();return{sessionToken:{name:`${o}${r}.session_token`,options:{httpOnly:!0,sameSite:"lax",path:"/",secure:!!o,maxAge:n}},csrfToken:{name:`${o?"__Host-":""}${r}.csrf_token`,options:{httpOnly:!0,sameSite:"lax",path:"/",secure:!!o,maxAge:60*60*24*7}},state:{name:`${o}${r}.state`,options:{httpOnly:!0,sameSite:"lax",path:"/",secure:!!o,maxAge:60*15}},pkCodeVerifier:{name:`${o}${r}.pk_code_verifier`,options:{httpOnly:!0,sameSite:"lax",path:"/",secure:!!o,maxAge:60*15}},dontRememberToken:{name:`${o}${r}.dont_remember`,options:{httpOnly:!0,sameSite:"lax",path:"/",secure:!!o}},nonce:{name:`${o}${r}.nonce`,options:{httpOnly:!0,sameSite:"lax",path:"/",secure:!!o,maxAge:60*15}}}}function xe(e){let o=!!e.advanced?.useSecureCookies||process.env.NODE_ENV==="production"?"__Secure-":"",r="better-auth";function n(s,i){return{name:process.env.NODE_ENV==="production"?`${o}${r}.${s}`:`${r}.${s}`,options:{secure:!!o,sameSite:"lax",path:"/",maxAge:60*15,...i}}}return n}async function P(e,t,o,r){let n=e.context.authCookies.sessionToken.options;n.maxAge=o?void 0:n.maxAge,await e.setSignedCookie(e.context.authCookies.sessionToken.name,t,e.context.secret,n),o&&await e.setSignedCookie(e.context.authCookies.dontRememberToken.name,"true",e.context.secret,e.context.authCookies.dontRememberToken.options)}function $(e){e.setCookie(e.context.authCookies.sessionToken.name,"",{maxAge:0}),e.setCookie(e.context.authCookies.dontRememberToken.name,"",{maxAge:0})}import{z as Ue}from"zod";function M(e){let t="127.0.0.1";if(process.env.NODE_ENV==="test")return t;let o=["x-client-ip","x-forwarded-for","cf-connecting-ip","fastly-client-ip","x-real-ip","x-cluster-client-ip","x-forwarded","forwarded-for","forwarded"];for(let r of o){let n=e.headers.get(r);if(typeof n=="string"){let s=n.split(",")[0].trim();if(s)return s}}return null}var Y=new Map;function Dt(e,t){if(!e.request)return"";let{method:o,url:r,headers:n}=e.request,s=e.request.headers.get("User-Agent")||"",i=M(e.request)||"",a=JSON.stringify(n);return`${o}:${r}:${a}:${s}:${i}:${t}`}var ee=()=>g("/session",{method:"GET",requireHeaders:!0},async e=>{try{let t=await e.getSignedCookie(e.context.authCookies.sessionToken.name,e.context.secret);if(!t)return e.json(null,{status:401});let o=Dt(e,t),r=Y.get(o);if(r){if(r.expiresAt>Date.now())return e.json(r.data);Y.delete(o)}let n=await e.context.internalAdapter.findSession(t);if(!n||n.session.expiresAt<new Date)return $(e),n&&await e.context.internalAdapter.deleteSession(n.session.id),e.json(null,{status:401});if(await e.getSignedCookie(e.context.authCookies.dontRememberToken.name,e.context.secret))return e.json(n);let i=e.context.sessionConfig.expiresIn,a=e.context.sessionConfig.updateAge;if(n.session.expiresAt.valueOf()-i*1e3+a*1e3<=Date.now()){let l=await e.context.internalAdapter.updateSession(n.session.id,{expiresAt:N(e.context.sessionConfig.expiresIn,!0)});if(!l)return $(e),e.json(null,{status:401});let u=(l.expiresAt.valueOf()-Date.now())/1e3;return await P(e,l.id,!1,{maxAge:u}),e.json({session:l,user:n.user})}return Y.set(o,{data:n,expiresAt:Date.now()+5e3}),e.json(n)}catch(t){return e.context.logger.error(t),e.json(null,{status:500})}}),te=async e=>await ee()({...e,_flag:void 0}),_=z(async e=>{let t=await te(e);if(!t?.session)throw new jt("UNAUTHORIZED");return{session:t}}),Pe=()=>g("/user/list-sessions",{method:"GET",use:[_],requireHeaders:!0},async e=>{let o=(await e.context.adapter.findMany({model:e.context.tables.session.tableName,where:[{field:"userId",value:e.context.session.user.id}]})).filter(r=>r.expiresAt>new Date);return e.json(o)}),ve=g("/user/revoke-session",{method:"POST",body:Ue.object({id:Ue.string()}),use:[_],requireHeaders:!0},async e=>{let t=e.body.id,o=await e.context.internalAdapter.findSession(t);if(!o)return e.json(null,{status:400});if(o.session.userId!==e.context.session.user.id)return e.json(null,{status:403});try{await e.context.internalAdapter.deleteSession(t)}catch(r){return e.context.logger.error(r),e.json(null,{status:500})}return e.json({status:!0})}),Ie=g("/user/revoke-sessions",{method:"POST",use:[_],requireHeaders:!0},async e=>{try{await e.context.internalAdapter.deleteSessions(e.context.session.user.id)}catch(t){return e.context.logger.error(t),e.json(null,{status:500})}return e.json({status:!0})});var Se=g("/sign-in/social",{method:"POST",requireHeaders:!0,query:x.object({currentURL:x.string().optional()}).optional(),body:x.object({callbackURL:x.string().optional(),provider:x.enum(ke),dontRememberMe:x.boolean().default(!1).optional()})},async e=>{let t=e.context.socialProviders.find(i=>i.id===e.body.provider);if(!t)throw e.context.logger.error("Provider not found. Make sure to add the provider to your auth config",{provider:e.body.provider}),new I("NOT_FOUND",{message:"Provider not found"});let o=e.context.authCookies,r=e.query?.currentURL?new URL(e.query?.currentURL):null,n=e.body.callbackURL?.startsWith("http")?e.body.callbackURL:`${r?.origin}${e.body.callbackURL||""}`,s=Re(n||r?.origin||e.context.baseURL,e.query?.currentURL);try{await e.setSignedCookie(o.state.name,s.code,e.context.secret,o.state.options);let i=Nt();await e.setSignedCookie(o.pkCodeVerifier.name,i,e.context.secret,o.pkCodeVerifier.options);let a=t.createAuthorizationURL({state:s.state,codeVerifier:i});return a.searchParams.set("redirect_uri",`${e.context.baseURL}/callback/${e.body.provider}`),{url:a.toString(),state:s.state,codeVerifier:i,redirect:!0}}catch{throw new I("INTERNAL_SERVER_ERROR")}}),Oe=g("/sign-in/email",{method:"POST",body:x.object({email:x.string().email(),password:x.string(),callbackURL:x.string().optional(),dontRememberMe:x.boolean().default(!1).optional()})},async e=>{if(!e.context.options?.emailAndPassword?.enabled)throw e.context.logger.error("Email and password is not enabled. Make sure to enable it in the options on you `auth.ts` file. Check `https://better-auth.com/docs/authentication/email-password` for more!"),new I("BAD_REQUEST",{message:"Email and password is not enabled"});let t=await te(e);t&&await e.context.internalAdapter.deleteSession(t.session.id);let{email:o,password:r}=e.body;if(!x.string().email().safeParse(o).success)throw new I("BAD_REQUEST",{message:"Invalid email"});let s=await e.context.internalAdapter.findUserByEmail(o);if(!s)throw await e.context.password.hash(r),e.context.logger.error("User not found",{email:o}),new I("UNAUTHORIZED",{message:"Invalid email or password"});let i=s.accounts.find(l=>l.providerId==="credential");if(!i)throw e.context.logger.error("Credential account not found",{email:o}),new I("UNAUTHORIZED",{message:"Invalid email or password"});let a=i?.password;if(!a)throw e.context.logger.error("Password not found",{email:o}),new I("UNAUTHORIZED",{message:"Unexpected error"});if(!await e.context.password.verify(a,r))throw e.context.logger.error("Invalid password"),new I("UNAUTHORIZED",{message:"Invalid email or password"});let c=await e.context.internalAdapter.createSession(s.user.id,e.headers,e.body.dontRememberMe);if(!c)throw e.context.logger.error("Failed to create session"),new I("INTERNAL_SERVER_ERROR");return await P(e,c.id,e.body.dontRememberMe),e.json({user:s.user,session:c,redirect:!!e.body.callbackURL,url:e.body.callbackURL})});import{APIError as zt}from"better-call";import{z as H}from"zod";import{z as b}from"zod";var Sn=b.object({id:b.string(),providerId:b.string(),accountId:b.string(),userId:b.string(),accessToken:b.string().nullable().optional(),refreshToken:b.string().nullable().optional(),idToken:b.string().nullable().optional(),expiresAt:b.date().nullable().optional(),password:b.string().optional().nullable()}),Le=b.object({id:b.string(),email:b.string().transform(e=>e.toLowerCase()),emailVerified:b.boolean().default(!1),name:b.string(),image:b.string().optional(),createdAt:b.date().default(new Date),updatedAt:b.date().default(new Date)}),On=b.object({id:b.string(),userId:b.string(),expiresAt:b.date(),ipAddress:b.string().optional(),userAgent:b.string().optional()});import{alphabet as $t,generateRandomString as Ft}from"oslo/crypto";var Ce=()=>Ft(36,$t("a-z","0-9"));var L={isAction:!1};function re(e){let t=e.accessToken(),o=e.hasRefreshToken()?e.refreshToken():void 0,r;try{r=e.accessTokenExpiresAt()}catch{}return{accessToken:t,refreshToken:o,expiresAt:r}}var Ee=g("/callback/:id",{method:"GET",query:H.object({state:H.string(),code:H.string().optional(),error:H.string().optional()}),metadata:L},async e=>{if(e.query.error||!e.query.code){let y=X(e.query.state).data?.callbackURL||`${e.context.baseURL}/error`;throw e.context.logger.error(e.query.error,e.params.id),e.redirect(`${y}?error=${e.query.error||"oAuth_code_missing"}`)}let t=e.context.socialProviders.find(f=>f.id===e.params.id);if(!t)throw e.context.logger.error("Oauth provider with id",e.params.id,"not found"),e.redirect(`${e.context.baseURL}/error?error=oauth_provider_not_found`);let o=await e.getSignedCookie(e.context.authCookies.pkCodeVerifier.name,e.context.secret),r;try{r=await t.validateAuthorizationCode(e.query.code,o,`${e.context.baseURL}/callback/${t.id}`)}catch(f){throw e.context.logger.error(f),e.redirect(`${e.context.baseURL}/error?error=oauth_code_verification_failed`)}let n=await t.getUserInfo(r).then(f=>f?.user),s=Ce(),i=Le.safeParse({...n,id:s}),a=X(e.query.state);if(!a.success)throw e.context.logger.error("Unable to parse state"),e.redirect(`${e.context.baseURL}/error?error=invalid_state_parameter`);let{callbackURL:d,currentURL:c,dontRememberMe:l}=a.data;if(!n||i.success===!1)throw e.redirect(`${e.context.baseURL}/error?error=oauth_validation_failed`);if(!d)throw e.redirect(`${e.context.baseURL}/error?error=oauth_callback_url_not_found`);let u=await e.context.internalAdapter.findUserByEmail(n.email),p=u?.user.id;if(u){let f=u.accounts.find(A=>A.providerId===t.id),y=e.context.options.account?.accountLinking?.trustedProviders,h=y?y.includes(t.id):!0;if(!f&&(!n.emailVerified||!h)){let A;try{A=new URL(c||d),A.searchParams.set("error","account_not_linked")}catch{throw e.redirect(`${e.context.baseURL}/error?error=account_not_linked`)}throw e.redirect(A.toString())}if(!f)try{await e.context.internalAdapter.linkAccount({providerId:t.id,accountId:n.id,id:`${t.id}:${n.id}`,userId:u.user.id,...re(r)})}catch(A){throw console.log(A),e.redirect(`${e.context.baseURL}/error?error=failed_linking_account`)}}else try{await e.context.internalAdapter.createOAuthUser(i.data,{...re(r),id:`${t.id}:${n.id}`,providerId:t.id,accountId:n.id,userId:s})}catch{let y=new URL(c||d);throw y.searchParams.set("error","unable_to_create_user"),e.setHeader("Location",y.toString()),e.redirect(y.toString())}if(!p&&!s)throw new zt("INTERNAL_SERVER_ERROR",{message:"Unable to create user"});let m=await e.context.internalAdapter.createSession(p||s,e.request,l);if(!m){let f=new URL(c||d);throw f.searchParams.set("error","unable_to_create_session"),e.redirect(f.toString())}try{await P(e,m.id,l)}catch(f){e.context.logger.error("Unable to set session cookie",f);let y=new URL(c||d);throw y.searchParams.set("error","unable_to_create_session"),e.redirect(y.toString())}throw e.redirect(d)});import{z as oe}from"zod";var _e=g("/sign-out",{method:"POST",body:oe.optional(oe.object({callbackURL:oe.string().optional()}))},async e=>{let t=await e.getSignedCookie(e.context.authCookies.sessionToken.name,e.context.secret);return t?(await e.context.internalAdapter.deleteSession(t),$(e),e.json(null,{body:{redirect:!!e.body?.callbackURL,url:e.body?.callbackURL}})):e.json(null)});import{TimeSpan as Vt}from"oslo";import{createJWT as Mt,parseJWT as Ht}from"oslo/jwt";import{validateJWT as Be}from"oslo/jwt";import{z as U}from"zod";var qe=g("/forget-password",{method:"POST",body:U.object({email:U.string().email(),redirectTo:U.string()})},async e=>{if(!e.context.options.emailAndPassword?.sendResetPassword)return e.context.logger.error("Reset password isn't enabled.Please pass an emailAndPassword.sendResetPasswordToken function to your auth config!"),e.json(null,{status:400,statusText:"RESET_PASSWORD_EMAIL_NOT_SENT",body:{message:"Reset password isn't enabled"}});let{email:t}=e.body,o=await e.context.internalAdapter.findUserByEmail(t);if(!o)return e.json({status:!1},{body:{status:!0}});let r=await Mt("HS256",Buffer.from(e.context.secret),{email:o.user.email,redirectTo:e.body.redirectTo},{expiresIn:new Vt(1,"h"),issuer:"better-auth",subject:"forget-password",audiences:[o.user.email],includeIssuedTimestamp:!0}),n=`${e.context.baseURL}/reset-password/${r}`;return await e.context.options.emailAndPassword.sendResetPassword(n,o.user),e.json({status:!0})}),je=g("/reset-password/:token",{method:"GET"},async e=>{let{token:t}=e.params,o,r=U.object({email:U.string(),redirectTo:U.string()});try{if(o=await Be("HS256",Buffer.from(e.context.secret),t),!o.expiresAt||o.expiresAt<new Date)throw Error("Token expired")}catch{let i=Ht(t),a=r.safeParse(i?.payload);throw a.success?e.redirect(`${a.data?.redirectTo}?error=invalid_token`):e.redirect(`${e.context.baseURL}/error?error=invalid_token`)}let{redirectTo:n}=r.parse(o.payload);throw e.redirect(`${n}?token=${t}`)}),De=g("/reset-password",{method:"POST",query:U.object({currentURL:U.string()}).optional(),body:U.object({newPassword:U.string(),callbackURL:U.string().optional()})},async e=>{let t=e.query?.currentURL.split("?token=")[1];if(!t)return e.json({error:"Invalid token",data:null},{status:400,statusText:"INVALID_TOKEN",body:{message:"Invalid token"}});let{newPassword:o}=e.body;try{let r=await Be("HS256",Buffer.from(e.context.secret),t),n=U.string().email().parse(r.payload.email),s=await e.context.internalAdapter.findUserByEmail(n);if(!s)return e.json({error:"User not found",data:null},{status:400,body:{message:"failed to reset password"}});if(o.length<(e.context.options.emailAndPassword?.minPasswordLength||8)||o.length>(e.context.options.emailAndPassword?.maxPasswordLength||32))return e.json({data:null,error:"password is too short or too long"},{status:400,statusText:"INVALID_PASSWORD_LENGTH",body:{message:"password is too short or too long"}});let i=await e.context.password.hash(o);return await e.context.internalAdapter.updatePassword(s.user.id,i)?e.json({error:null,data:{status:!0,url:e.body.callbackURL,redirect:!!e.body.callbackURL}},{body:{status:!0,url:e.body.callbackURL,redirect:!!e.body.callbackURL}}):e.json(null,{status:400,statusText:"USER_NOT_FOUND",body:{message:"User doesn't have a credential account"}})}catch(r){return console.log(r),e.json({error:"Invalid token",data:null},{status:400,statusText:"INVALID_TOKEN",body:{message:"Invalid token"}})}});import{TimeSpan as Gt}from"oslo";import{createJWT as Kt,validateJWT as Wt}from"oslo/jwt";import{z as v}from"zod";async function ne(e,t){return await Kt("HS256",Buffer.from(e),{email:t.toLowerCase()},{expiresIn:new Gt(1,"h"),issuer:"better-auth",subject:"verify-email",audiences:[t],includeIssuedTimestamp:!0})}var Ne=g("/send-verification-email",{method:"POST",query:v.object({currentURL:v.string().optional()}).optional(),body:v.object({email:v.string().email(),callbackURL:v.string().optional()})},async e=>{if(!e.context.options.emailAndPassword?.sendVerificationEmail)return e.context.logger.error("Verification email isn't enabled. Pass `sendVerificationEmail` in `emailAndPassword` options to enable it."),e.json(null,{status:400,statusText:"VERIFICATION_EMAIL_NOT_SENT",body:{message:"Verification email isn't enabled"}});let{email:t}=e.body,o=await ne(e.context.secret,t),r=`${e.context.baseURL}/verify-email?token=${o}&callbackURL=${e.body.callbackURL||e.query?.currentURL||"/"}`;return await e.context.options.emailAndPassword.sendVerificationEmail(t,r,o),e.json({status:!0})}),$e=g("/verify-email",{method:"GET",query:v.object({token:v.string(),callbackURL:v.string().optional()})},async e=>{let{token:t}=e.query,o;try{o=await Wt("HS256",Buffer.from(e.context.secret),t)}catch(a){return e.context.logger.error("Failed to verify email",a),e.json(null,{status:400,statusText:"INVALID_TOKEN",body:{message:"Invalid token"}})}let n=v.object({email:v.string().email()}).parse(o.payload),s=await e.context.internalAdapter.findUserByEmail(n.email);if(!s)return e.json(null,{status:400,statusText:"USER_NOT_FOUND",body:{message:"User not found"}});if(!s.accounts.find(a=>a.providerId==="credential"))throw e.redirect;if(await e.context.internalAdapter.updateUserByEmail(n.email,{emailVerified:!0}),e.query.callbackURL)throw console.log("Redirecting to",e.query.callbackURL),e.redirect("/");return e.json({status:!0})});import{z as S}from"zod";import{alphabet as Jt,generateRandomString as Zt}from"oslo/crypto";import"better-call";var Fe=g("/user/update",{method:"POST",body:S.object({name:S.string().optional(),image:S.string().optional()}),use:[_]},async e=>{let{name:t,image:o}=e.body,r=e.context.session;if(!o&&!t)return e.json(r.user);let n=await e.context.internalAdapter.updateUserByEmail(r.user.email,{name:t,image:o});return e.json(n)}),ze=g("/user/change-password",{method:"POST",body:S.object({newPassword:S.string(),currentPassword:S.string(),revokeOtherSessions:S.boolean().optional()}),use:[_]},async e=>{let{newPassword:t,currentPassword:o,revokeOtherSessions:r}=e.body,n=e.context.session,s=e.context.password.config.minPasswordLength;if(t.length<s)return e.context.logger.error("Password is too short"),e.json(null,{status:400,body:{message:"Password is too short"}});let i=e.context.password.config.maxPasswordLength;if(t.length>i)return e.context.logger.error("Password is too long"),e.json(null,{status:400,body:{message:"Password is too long"}});let d=(await e.context.internalAdapter.findAccounts(n.user.id)).find(u=>u.providerId==="credential"&&u.password);if(!d||!d.password)return e.json(null,{status:400,body:{message:"User does not have a password"}});let c=await e.context.password.hash(t);if(!await e.context.password.verify(d.password,o))return e.json(null,{status:400,body:{message:"Invalid password"}});if(await e.context.internalAdapter.updateAccount(d.id,{password:c}),r){await e.context.internalAdapter.deleteSessions(n.user.id);let u=await e.context.internalAdapter.createSession(n.user.id,e.headers);if(!u)return e.json(null,{status:500,body:{message:"Failed to create session"}});await P(e,u.id)}return e.json(n.user)}),Ve=g("/user/set-password",{method:"POST",body:S.object({newPassword:S.string()}),use:[_]},async e=>{let{newPassword:t}=e.body,o=e.context.session,r=e.context.password.config.minPasswordLength;if(t.length<r)return e.context.logger.error("Password is too short"),e.json(null,{status:400,body:{message:"Password is too short"}});let n=e.context.password.config.maxPasswordLength;if(t.length>n)return e.context.logger.error("Password is too long"),e.json(null,{status:400,body:{message:"Password is too long"}});let i=(await e.context.internalAdapter.findAccounts(o.user.id)).find(d=>d.providerId==="credential"&&d.password),a=await e.context.password.hash(t);return i?e.json(null,{status:400,body:{message:"User already has a password"}}):(await e.context.internalAdapter.linkAccount({id:Zt(32,Jt("a-z","0-9","A-Z")),userId:o.user.id,providerId:"credential",accountId:o.user.id,password:a}),e.json(o.user))});import{alphabet as Qt,generateRandomString as Xt}from"oslo/crypto";var Me=g("/csrf",{method:"GET",metadata:L},async e=>{let t=await e.getSignedCookie(e.context.authCookies.csrfToken.name,e.context.secret);if(t)return{csrfToken:t};let o=Xt(32,Qt("a-z","0-9","A-Z")),r=await F(e.context.secret,o),n=`${o}!${r}`;return await e.setSignedCookie(e.context.authCookies.csrfToken.name,n,e.context.secret,e.context.authCookies.csrfToken.options),{csrfToken:o}});var Yt=(e="Unknown")=>`<!DOCTYPE html>
|
|
1
|
+
// src/api/index.ts
|
|
2
|
+
import {
|
|
3
|
+
APIError as APIError6,
|
|
4
|
+
createRouter
|
|
5
|
+
} from "better-call";
|
|
6
|
+
|
|
7
|
+
// src/api/middlewares/csrf.ts
|
|
8
|
+
import { APIError } from "better-call";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// src/crypto/index.ts
|
|
12
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
|
13
|
+
import { bytesToHex, hexToBytes, utf8ToBytes } from "@noble/ciphers/utils";
|
|
14
|
+
import { managedNonce } from "@noble/ciphers/webcrypto";
|
|
15
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
16
|
+
async function hs256(secretKey, message) {
|
|
17
|
+
const enc = new TextEncoder();
|
|
18
|
+
const algorithm = { name: "HMAC", hash: "SHA-256" };
|
|
19
|
+
const key = await crypto.subtle.importKey(
|
|
20
|
+
"raw",
|
|
21
|
+
enc.encode(secretKey),
|
|
22
|
+
algorithm,
|
|
23
|
+
false,
|
|
24
|
+
["sign", "verify"]
|
|
25
|
+
);
|
|
26
|
+
const signature = await crypto.subtle.sign(
|
|
27
|
+
algorithm.name,
|
|
28
|
+
key,
|
|
29
|
+
enc.encode(message)
|
|
30
|
+
);
|
|
31
|
+
return btoa(String.fromCharCode(...new Uint8Array(signature)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/api/call.ts
|
|
35
|
+
import {
|
|
36
|
+
createEndpointCreator,
|
|
37
|
+
createMiddleware,
|
|
38
|
+
createMiddlewareCreator
|
|
39
|
+
} from "better-call";
|
|
40
|
+
var optionsMiddleware = createMiddleware(async () => {
|
|
41
|
+
return {};
|
|
42
|
+
});
|
|
43
|
+
var createAuthMiddleware = createMiddlewareCreator({
|
|
44
|
+
use: [
|
|
45
|
+
optionsMiddleware,
|
|
46
|
+
/**
|
|
47
|
+
* Only use for post hooks
|
|
48
|
+
*/
|
|
49
|
+
createMiddleware(async () => {
|
|
50
|
+
return {};
|
|
51
|
+
})
|
|
52
|
+
]
|
|
53
|
+
});
|
|
54
|
+
var createAuthEndpoint = createEndpointCreator({
|
|
55
|
+
use: [optionsMiddleware]
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// src/api/middlewares/csrf.ts
|
|
59
|
+
var csrfMiddleware = createAuthMiddleware(
|
|
60
|
+
{
|
|
61
|
+
body: z.object({
|
|
62
|
+
csrfToken: z.string().optional()
|
|
63
|
+
}).optional()
|
|
64
|
+
},
|
|
65
|
+
async (ctx) => {
|
|
66
|
+
if (ctx.request?.method !== "POST" || ctx.context.options.advanced?.disableCSRFCheck) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const url = new URL(ctx.request.url);
|
|
70
|
+
if (url.origin === new URL(ctx.context.baseURL).origin || ctx.context.options.trustedOrigins?.includes(url.origin)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const csrfToken = ctx.body?.csrfToken;
|
|
74
|
+
const csrfCookie = await ctx.getSignedCookie(
|
|
75
|
+
ctx.context.authCookies.csrfToken.name,
|
|
76
|
+
ctx.context.secret
|
|
77
|
+
);
|
|
78
|
+
const [token, hash] = csrfCookie?.split("!") || [null, null];
|
|
79
|
+
if (!csrfToken || !csrfCookie || !token || !hash || csrfCookie !== csrfToken) {
|
|
80
|
+
ctx.setCookie(ctx.context.authCookies.csrfToken.name, "", {
|
|
81
|
+
maxAge: 0
|
|
82
|
+
});
|
|
83
|
+
throw new APIError("UNAUTHORIZED", {
|
|
84
|
+
message: "Invalid CSRF Token"
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const expectedHash = await hs256(ctx.context.secret, token);
|
|
88
|
+
if (hash !== expectedHash) {
|
|
89
|
+
ctx.setCookie(ctx.context.authCookies.csrfToken.name, "", {
|
|
90
|
+
maxAge: 0
|
|
91
|
+
});
|
|
92
|
+
throw new APIError("UNAUTHORIZED", {
|
|
93
|
+
message: "Invalid CSRF Token"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// src/api/routes/sign-in.ts
|
|
100
|
+
import { APIError as APIError3 } from "better-call";
|
|
101
|
+
import { generateCodeVerifier } from "oslo/oauth2";
|
|
102
|
+
import { z as z4 } from "zod";
|
|
103
|
+
|
|
104
|
+
// src/social-providers/apple.ts
|
|
105
|
+
import "arctic";
|
|
106
|
+
import { parseJWT } from "oslo/jwt";
|
|
107
|
+
import "@better-fetch/fetch";
|
|
108
|
+
|
|
109
|
+
// src/error/better-auth-error.ts
|
|
110
|
+
var BetterAuthError = class extends Error {
|
|
111
|
+
constructor(message, cause, docsLink) {
|
|
112
|
+
super(message);
|
|
113
|
+
this.name = "BetterAuthError";
|
|
114
|
+
this.message = message;
|
|
115
|
+
this.cause = cause;
|
|
116
|
+
this.stack = "";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var MissingDependencyError = class extends BetterAuthError {
|
|
120
|
+
constructor(pkgName) {
|
|
121
|
+
super(
|
|
122
|
+
`The package "${pkgName}" is required. Make sure it is installed.`,
|
|
123
|
+
pkgName
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// src/social-providers/utils.ts
|
|
129
|
+
import { OAuth2Tokens } from "arctic";
|
|
130
|
+
|
|
131
|
+
// src/utils/base-url.ts
|
|
132
|
+
function checkHasPath(url) {
|
|
133
|
+
try {
|
|
134
|
+
const parsedUrl = new URL(url);
|
|
135
|
+
return parsedUrl.pathname !== "/";
|
|
136
|
+
} catch (error2) {
|
|
137
|
+
throw new BetterAuthError(
|
|
138
|
+
`Invalid base URL: ${url}. Please provide a valid base URL.`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function withPath(url, path = "/api/auth") {
|
|
143
|
+
const hasPath = checkHasPath(url);
|
|
144
|
+
if (hasPath) {
|
|
145
|
+
return url;
|
|
146
|
+
}
|
|
147
|
+
path = path.startsWith("/") ? path : `/${path}`;
|
|
148
|
+
return `${url}${path}`;
|
|
149
|
+
}
|
|
150
|
+
function getBaseURL(url, path) {
|
|
151
|
+
if (url) {
|
|
152
|
+
return withPath(url, path);
|
|
153
|
+
}
|
|
154
|
+
const env = process?.env || {};
|
|
155
|
+
const fromEnv = env.BETTER_AUTH_URL || env.NEXT_PUBLIC_BETTER_AUTH_URL || env.PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_BETTER_AUTH_URL || env.NUXT_PUBLIC_AUTH_URL || (env.BASE_URL !== "/" ? env.BASE_URL : void 0);
|
|
156
|
+
if (fromEnv) {
|
|
157
|
+
return withPath(fromEnv, path);
|
|
158
|
+
}
|
|
159
|
+
if (typeof window !== "undefined") {
|
|
160
|
+
return withPath(window.location.origin, path);
|
|
161
|
+
}
|
|
162
|
+
return void 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// src/social-providers/utils.ts
|
|
166
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
167
|
+
function getRedirectURI(providerId, redirectURI) {
|
|
168
|
+
return redirectURI || `${getBaseURL()}/callback/${providerId}`;
|
|
169
|
+
}
|
|
170
|
+
async function validateAuthorizationCode({
|
|
171
|
+
code,
|
|
172
|
+
codeVerifier,
|
|
173
|
+
redirectURI,
|
|
174
|
+
options,
|
|
175
|
+
tokenEndpoint
|
|
176
|
+
}) {
|
|
177
|
+
const body = new URLSearchParams();
|
|
178
|
+
body.set("grant_type", "authorization_code");
|
|
179
|
+
body.set("code", code);
|
|
180
|
+
codeVerifier && body.set("code_verifier", codeVerifier);
|
|
181
|
+
body.set("redirect_uri", redirectURI);
|
|
182
|
+
body.set("client_id", options.clientId);
|
|
183
|
+
body.set("client_secret", options.clientSecret);
|
|
184
|
+
const { data, error: error2 } = await betterFetch(tokenEndpoint, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
body,
|
|
187
|
+
headers: {
|
|
188
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
189
|
+
accept: "application/json",
|
|
190
|
+
"user-agent": "better-auth"
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
if (error2) {
|
|
194
|
+
throw error2;
|
|
195
|
+
}
|
|
196
|
+
const tokens = new OAuth2Tokens(data);
|
|
197
|
+
return tokens;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/social-providers/apple.ts
|
|
201
|
+
var apple = (options) => {
|
|
202
|
+
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
203
|
+
return {
|
|
204
|
+
id: "apple",
|
|
205
|
+
name: "Apple",
|
|
206
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
207
|
+
const _scope = scopes || ["email", "name", "openid"];
|
|
208
|
+
return new URL(
|
|
209
|
+
`https://appleid.apple.com/auth/authorize?client_id=${options.clientId}&response_type=code&redirect_uri=${redirectURI || options.redirectURI}&scope=${_scope.join(" ")}&state=${state}`
|
|
210
|
+
);
|
|
211
|
+
},
|
|
212
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
213
|
+
return validateAuthorizationCode({
|
|
214
|
+
code,
|
|
215
|
+
codeVerifier,
|
|
216
|
+
redirectURI: redirectURI || getRedirectURI("apple", options.redirectURI),
|
|
217
|
+
options,
|
|
218
|
+
tokenEndpoint
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
async getUserInfo(token) {
|
|
222
|
+
const data = parseJWT(token.idToken())?.payload;
|
|
223
|
+
if (!data) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
user: {
|
|
228
|
+
id: data.sub,
|
|
229
|
+
name: data.name,
|
|
230
|
+
email: data.email,
|
|
231
|
+
emailVerified: data.email_verified === "true"
|
|
232
|
+
},
|
|
233
|
+
data
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/social-providers/discord.ts
|
|
240
|
+
import { betterFetch as betterFetch3 } from "@better-fetch/fetch";
|
|
241
|
+
import { Discord } from "arctic";
|
|
242
|
+
var discord = (options) => {
|
|
243
|
+
const discordArctic = new Discord(
|
|
244
|
+
options.clientId,
|
|
245
|
+
options.clientSecret,
|
|
246
|
+
getRedirectURI("discord", options.redirectURI)
|
|
247
|
+
);
|
|
248
|
+
return {
|
|
249
|
+
id: "discord",
|
|
250
|
+
name: "Discord",
|
|
251
|
+
createAuthorizationURL({ state, scopes }) {
|
|
252
|
+
const _scope = scopes || ["email"];
|
|
253
|
+
return discordArctic.createAuthorizationURL(state, _scope);
|
|
254
|
+
},
|
|
255
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
256
|
+
return validateAuthorizationCode({
|
|
257
|
+
code,
|
|
258
|
+
codeVerifier,
|
|
259
|
+
redirectURI: redirectURI || getRedirectURI("discord", options.redirectURI),
|
|
260
|
+
options,
|
|
261
|
+
tokenEndpoint: "https://discord.com/api/oauth2/token"
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
async getUserInfo(token) {
|
|
265
|
+
const { data: profile, error: error2 } = await betterFetch3(
|
|
266
|
+
"https://discord.com/api/users/@me",
|
|
267
|
+
{
|
|
268
|
+
auth: {
|
|
269
|
+
type: "Bearer",
|
|
270
|
+
token: token.accessToken()
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
if (error2) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
user: {
|
|
279
|
+
id: profile.id,
|
|
280
|
+
name: profile.display_name || profile.username || "",
|
|
281
|
+
email: profile.email,
|
|
282
|
+
emailVerified: profile.verified
|
|
283
|
+
},
|
|
284
|
+
data: profile
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/social-providers/facebook.ts
|
|
291
|
+
import { betterFetch as betterFetch4 } from "@better-fetch/fetch";
|
|
292
|
+
import { Facebook } from "arctic";
|
|
293
|
+
var facebook = (options) => {
|
|
294
|
+
const facebookArctic = new Facebook(
|
|
295
|
+
options.clientId,
|
|
296
|
+
options.clientSecret,
|
|
297
|
+
getRedirectURI("facebook", options.redirectURI)
|
|
298
|
+
);
|
|
299
|
+
return {
|
|
300
|
+
id: "facebook",
|
|
301
|
+
name: "Facebook",
|
|
302
|
+
createAuthorizationURL({ state, scopes }) {
|
|
303
|
+
const _scopes = scopes || ["email", "public_profile"];
|
|
304
|
+
return facebookArctic.createAuthorizationURL(state, _scopes);
|
|
305
|
+
},
|
|
306
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
307
|
+
return validateAuthorizationCode({
|
|
308
|
+
code,
|
|
309
|
+
codeVerifier,
|
|
310
|
+
redirectURI: redirectURI || getRedirectURI("facebook", options.redirectURI),
|
|
311
|
+
options,
|
|
312
|
+
tokenEndpoint: "https://graph.facebook.com/v16.0/oauth/access_token"
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
async getUserInfo(token) {
|
|
316
|
+
const { data: profile, error: error2 } = await betterFetch4(
|
|
317
|
+
"https://graph.facebook.com/me",
|
|
318
|
+
{
|
|
319
|
+
auth: {
|
|
320
|
+
type: "Bearer",
|
|
321
|
+
token: token.accessToken()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
if (error2) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
user: {
|
|
330
|
+
id: profile.id,
|
|
331
|
+
name: profile.name,
|
|
332
|
+
email: profile.email,
|
|
333
|
+
emailVerified: profile.email_verified
|
|
334
|
+
},
|
|
335
|
+
data: profile
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// src/social-providers/github.ts
|
|
342
|
+
import { betterFetch as betterFetch5 } from "@better-fetch/fetch";
|
|
343
|
+
import { GitHub } from "arctic";
|
|
344
|
+
var github = ({
|
|
345
|
+
clientId,
|
|
346
|
+
clientSecret,
|
|
347
|
+
redirectURI
|
|
348
|
+
}) => {
|
|
349
|
+
const githubArctic = new GitHub(
|
|
350
|
+
clientId,
|
|
351
|
+
clientSecret,
|
|
352
|
+
getRedirectURI("github", redirectURI)
|
|
353
|
+
);
|
|
354
|
+
return {
|
|
355
|
+
id: "github",
|
|
356
|
+
name: "Github",
|
|
357
|
+
createAuthorizationURL({ state, scopes }) {
|
|
358
|
+
const _scopes = scopes || ["user:email"];
|
|
359
|
+
return githubArctic.createAuthorizationURL(state, _scopes);
|
|
360
|
+
},
|
|
361
|
+
validateAuthorizationCode: async (state) => {
|
|
362
|
+
return await githubArctic.validateAuthorizationCode(state);
|
|
363
|
+
},
|
|
364
|
+
async getUserInfo(token) {
|
|
365
|
+
const { data: profile, error: error2 } = await betterFetch5(
|
|
366
|
+
"https://api.github.com/user",
|
|
367
|
+
{
|
|
368
|
+
auth: {
|
|
369
|
+
type: "Bearer",
|
|
370
|
+
token: token.accessToken()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
if (error2) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
let emailVerified = false;
|
|
378
|
+
if (!profile.email) {
|
|
379
|
+
const { data, error: error3 } = await betterFetch5("https://api.github.com/user/emails", {
|
|
380
|
+
auth: {
|
|
381
|
+
type: "Bearer",
|
|
382
|
+
token: token.accessToken()
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
if (!error3) {
|
|
386
|
+
profile.email = (data.find((e) => e.primary) ?? data[0])?.email;
|
|
387
|
+
emailVerified = data.find((e) => e.email === profile.email)?.verified ?? false;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
user: {
|
|
392
|
+
id: profile.id,
|
|
393
|
+
name: profile.name,
|
|
394
|
+
email: profile.email,
|
|
395
|
+
image: profile.avatar_url,
|
|
396
|
+
emailVerified,
|
|
397
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
398
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
399
|
+
},
|
|
400
|
+
data: profile
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// src/social-providers/google.ts
|
|
407
|
+
import { Google } from "arctic";
|
|
408
|
+
import { parseJWT as parseJWT2 } from "oslo/jwt";
|
|
409
|
+
|
|
410
|
+
// src/utils/logger.ts
|
|
411
|
+
import { createConsola } from "consola";
|
|
412
|
+
var consola = createConsola({
|
|
413
|
+
formatOptions: {
|
|
414
|
+
date: false,
|
|
415
|
+
colors: true,
|
|
416
|
+
compact: true
|
|
417
|
+
},
|
|
418
|
+
defaults: {
|
|
419
|
+
tag: "Better Auth"
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
var createLogger = (options) => {
|
|
423
|
+
return {
|
|
424
|
+
log: (...args) => {
|
|
425
|
+
!options?.disabled && consola.log("", ...args);
|
|
426
|
+
},
|
|
427
|
+
error: (...args) => {
|
|
428
|
+
!options?.disabled && consola.error("", ...args);
|
|
429
|
+
},
|
|
430
|
+
warn: (...args) => {
|
|
431
|
+
!options?.disabled && consola.warn("", ...args);
|
|
432
|
+
},
|
|
433
|
+
info: (...args) => {
|
|
434
|
+
!options?.disabled && consola.info("", ...args);
|
|
435
|
+
},
|
|
436
|
+
debug: (...args) => {
|
|
437
|
+
!options?.disabled && consola.debug("", ...args);
|
|
438
|
+
},
|
|
439
|
+
box: (...args) => {
|
|
440
|
+
!options?.disabled && consola.box("", ...args);
|
|
441
|
+
},
|
|
442
|
+
success: (...args) => {
|
|
443
|
+
!options?.disabled && consola.success("", ...args);
|
|
444
|
+
},
|
|
445
|
+
break: (...args) => {
|
|
446
|
+
!options?.disabled && console.log("\n");
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
var logger = createLogger();
|
|
451
|
+
|
|
452
|
+
// src/social-providers/google.ts
|
|
453
|
+
var google = (options) => {
|
|
454
|
+
const googleArctic = new Google(
|
|
455
|
+
options.clientId,
|
|
456
|
+
options.clientSecret,
|
|
457
|
+
getRedirectURI("google", options.redirectURI)
|
|
458
|
+
);
|
|
459
|
+
return {
|
|
460
|
+
id: "google",
|
|
461
|
+
name: "Google",
|
|
462
|
+
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
463
|
+
if (!options.clientId || !options.clientSecret) {
|
|
464
|
+
logger.error(
|
|
465
|
+
"Client Id and Client Secret is required for Google. Make sure to provide them in the options."
|
|
466
|
+
);
|
|
467
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
468
|
+
}
|
|
469
|
+
if (!codeVerifier) {
|
|
470
|
+
throw new BetterAuthError("codeVerifier is required for Google");
|
|
471
|
+
}
|
|
472
|
+
const _scopes = scopes || ["email", "profile"];
|
|
473
|
+
const url = googleArctic.createAuthorizationURL(
|
|
474
|
+
state,
|
|
475
|
+
codeVerifier,
|
|
476
|
+
_scopes
|
|
477
|
+
);
|
|
478
|
+
return url;
|
|
479
|
+
},
|
|
480
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
481
|
+
return validateAuthorizationCode({
|
|
482
|
+
code,
|
|
483
|
+
codeVerifier,
|
|
484
|
+
redirectURI: redirectURI || getRedirectURI("google", options.redirectURI),
|
|
485
|
+
options,
|
|
486
|
+
tokenEndpoint: "https://oauth2.googleapis.com/token"
|
|
487
|
+
});
|
|
488
|
+
},
|
|
489
|
+
async getUserInfo(token) {
|
|
490
|
+
if (!token.idToken) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
const user = parseJWT2(token.idToken())?.payload;
|
|
494
|
+
return {
|
|
495
|
+
user: {
|
|
496
|
+
id: user.sub,
|
|
497
|
+
name: user.name,
|
|
498
|
+
email: user.email,
|
|
499
|
+
image: user.picture,
|
|
500
|
+
emailVerified: user.email_verified
|
|
501
|
+
},
|
|
502
|
+
data: user
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// src/social-providers/spotify.ts
|
|
509
|
+
import { betterFetch as betterFetch6 } from "@better-fetch/fetch";
|
|
510
|
+
import { Spotify } from "arctic";
|
|
511
|
+
var spotify = (options) => {
|
|
512
|
+
const spotifyArctic = new Spotify(
|
|
513
|
+
options.clientId,
|
|
514
|
+
options.clientSecret,
|
|
515
|
+
getRedirectURI("spotify", options.redirectURI)
|
|
516
|
+
);
|
|
517
|
+
return {
|
|
518
|
+
id: "spotify",
|
|
519
|
+
name: "Spotify",
|
|
520
|
+
createAuthorizationURL({ state, scopes }) {
|
|
521
|
+
const _scopes = scopes || ["user-read-email"];
|
|
522
|
+
return spotifyArctic.createAuthorizationURL(state, _scopes);
|
|
523
|
+
},
|
|
524
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
525
|
+
return validateAuthorizationCode({
|
|
526
|
+
code,
|
|
527
|
+
codeVerifier,
|
|
528
|
+
redirectURI: redirectURI || getRedirectURI("spotify", options.redirectURI),
|
|
529
|
+
options,
|
|
530
|
+
tokenEndpoint: "https://accounts.spotify.com/api/token"
|
|
531
|
+
});
|
|
532
|
+
},
|
|
533
|
+
async getUserInfo(token) {
|
|
534
|
+
const { data: profile, error: error2 } = await betterFetch6(
|
|
535
|
+
"https://api.spotify.com/v1/me",
|
|
536
|
+
{
|
|
537
|
+
method: "GET",
|
|
538
|
+
headers: {
|
|
539
|
+
Authorization: `Bearer ${token.accessToken()}`
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
if (error2) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
user: {
|
|
548
|
+
id: profile.id,
|
|
549
|
+
name: profile.display_name,
|
|
550
|
+
email: profile.email,
|
|
551
|
+
image: profile.images[0]?.url,
|
|
552
|
+
emailVerified: false
|
|
553
|
+
},
|
|
554
|
+
data: profile
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// src/social-providers/twitch.ts
|
|
561
|
+
import { betterFetch as betterFetch7 } from "@better-fetch/fetch";
|
|
562
|
+
import { Twitch } from "arctic";
|
|
563
|
+
var twitch = (options) => {
|
|
564
|
+
const twitchArctic = new Twitch(
|
|
565
|
+
options.clientId,
|
|
566
|
+
options.clientSecret,
|
|
567
|
+
getRedirectURI("twitch", options.redirectURI)
|
|
568
|
+
);
|
|
569
|
+
return {
|
|
570
|
+
id: "twitch",
|
|
571
|
+
name: "Twitch",
|
|
572
|
+
createAuthorizationURL({ state, scopes }) {
|
|
573
|
+
const _scopes = scopes || ["activity:write", "read"];
|
|
574
|
+
return twitchArctic.createAuthorizationURL(state, _scopes);
|
|
575
|
+
},
|
|
576
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
577
|
+
return validateAuthorizationCode({
|
|
578
|
+
code,
|
|
579
|
+
codeVerifier,
|
|
580
|
+
redirectURI: redirectURI || getRedirectURI("twitch", options.redirectURI),
|
|
581
|
+
options,
|
|
582
|
+
tokenEndpoint: "https://id.twitch.tv/oauth2/token"
|
|
583
|
+
});
|
|
584
|
+
},
|
|
585
|
+
async getUserInfo(token) {
|
|
586
|
+
const { data: profile, error: error2 } = await betterFetch7(
|
|
587
|
+
"https://api.twitch.tv/helix/users",
|
|
588
|
+
{
|
|
589
|
+
method: "GET",
|
|
590
|
+
headers: {
|
|
591
|
+
Authorization: `Bearer ${token.accessToken()}`
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
);
|
|
595
|
+
if (error2) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
user: {
|
|
600
|
+
id: profile.sub,
|
|
601
|
+
name: profile.preferred_username,
|
|
602
|
+
email: profile.email,
|
|
603
|
+
image: profile.picture,
|
|
604
|
+
emailVerified: false
|
|
605
|
+
},
|
|
606
|
+
data: profile
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// src/social-providers/twitter.ts
|
|
613
|
+
import { betterFetch as betterFetch8 } from "@better-fetch/fetch";
|
|
614
|
+
import { Twitter } from "arctic";
|
|
615
|
+
var twitter = (options) => {
|
|
616
|
+
const twitterArctic = new Twitter(
|
|
617
|
+
options.clientId,
|
|
618
|
+
options.clientSecret,
|
|
619
|
+
getRedirectURI("twitter", options.redirectURI)
|
|
620
|
+
);
|
|
621
|
+
return {
|
|
622
|
+
id: "twitter",
|
|
623
|
+
name: "Twitter",
|
|
624
|
+
createAuthorizationURL(data) {
|
|
625
|
+
const _scopes = data.scopes || ["account_info.read"];
|
|
626
|
+
return twitterArctic.createAuthorizationURL(
|
|
627
|
+
data.state,
|
|
628
|
+
data.codeVerifier,
|
|
629
|
+
_scopes
|
|
630
|
+
);
|
|
631
|
+
},
|
|
632
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
633
|
+
return validateAuthorizationCode({
|
|
634
|
+
code,
|
|
635
|
+
codeVerifier,
|
|
636
|
+
redirectURI: redirectURI || getRedirectURI("twitch", options.redirectURI),
|
|
637
|
+
options,
|
|
638
|
+
tokenEndpoint: "https://id.twitch.tv/oauth2/token"
|
|
639
|
+
});
|
|
640
|
+
},
|
|
641
|
+
async getUserInfo(token) {
|
|
642
|
+
const { data: profile, error: error2 } = await betterFetch8(
|
|
643
|
+
"https://api.x.com/2/users/me?user.fields=profile_image_url",
|
|
644
|
+
{
|
|
645
|
+
method: "GET",
|
|
646
|
+
headers: {
|
|
647
|
+
Authorization: `Bearer ${token.accessToken()}`
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
);
|
|
651
|
+
if (error2) {
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
if (!profile.data.email) {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
user: {
|
|
659
|
+
id: profile.data.id,
|
|
660
|
+
name: profile.data.name,
|
|
661
|
+
email: profile.data.email,
|
|
662
|
+
image: profile.data.profile_image_url,
|
|
663
|
+
emailVerified: profile.data.verified || false
|
|
664
|
+
},
|
|
665
|
+
data: profile
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
// src/types/provider.ts
|
|
672
|
+
import "arctic";
|
|
673
|
+
|
|
674
|
+
// src/social-providers/index.ts
|
|
675
|
+
var oAuthProviders = {
|
|
676
|
+
apple,
|
|
677
|
+
discord,
|
|
678
|
+
facebook,
|
|
679
|
+
github,
|
|
680
|
+
google,
|
|
681
|
+
spotify,
|
|
682
|
+
twitch,
|
|
683
|
+
twitter
|
|
684
|
+
};
|
|
685
|
+
var oAuthProviderList = Object.keys(oAuthProviders);
|
|
686
|
+
|
|
687
|
+
// src/utils/state.ts
|
|
688
|
+
import { generateState as generateStateOAuth } from "oslo/oauth2";
|
|
689
|
+
import { z as z2 } from "zod";
|
|
690
|
+
function generateState(callbackURL, currentURL, dontRememberMe) {
|
|
691
|
+
const code = generateStateOAuth();
|
|
692
|
+
const state = JSON.stringify({
|
|
693
|
+
code,
|
|
694
|
+
callbackURL,
|
|
695
|
+
currentURL,
|
|
696
|
+
dontRememberMe
|
|
697
|
+
});
|
|
698
|
+
return { state, code };
|
|
699
|
+
}
|
|
700
|
+
function parseState(state) {
|
|
701
|
+
const data = z2.object({
|
|
702
|
+
code: z2.string(),
|
|
703
|
+
callbackURL: z2.string().optional(),
|
|
704
|
+
currentURL: z2.string().optional(),
|
|
705
|
+
dontRememberMe: z2.boolean().optional()
|
|
706
|
+
}).safeParse(JSON.parse(state));
|
|
707
|
+
return data;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/api/routes/session.ts
|
|
711
|
+
import { APIError as APIError2 } from "better-call";
|
|
712
|
+
|
|
713
|
+
// src/utils/date.ts
|
|
714
|
+
var getDate = (span, isSeconds = false) => {
|
|
715
|
+
const date = /* @__PURE__ */ new Date();
|
|
716
|
+
return new Date(date.getTime() + (isSeconds ? span * 1e3 : span));
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/utils/cookies.ts
|
|
720
|
+
import { TimeSpan } from "oslo";
|
|
721
|
+
function getCookies(options) {
|
|
722
|
+
const secure = !!options.advanced?.useSecureCookies || process.env.NODE_ENV !== "development" && process.env.NODE_ENV !== "test";
|
|
723
|
+
const secureCookiePrefix = secure ? "__Secure-" : "";
|
|
724
|
+
const cookiePrefix = "better-auth";
|
|
725
|
+
const sessionMaxAge = new TimeSpan(7, "d").seconds();
|
|
726
|
+
return {
|
|
727
|
+
sessionToken: {
|
|
728
|
+
name: `${secureCookiePrefix}${cookiePrefix}.session_token`,
|
|
729
|
+
options: {
|
|
730
|
+
httpOnly: true,
|
|
731
|
+
sameSite: "lax",
|
|
732
|
+
path: "/",
|
|
733
|
+
secure: !!secureCookiePrefix,
|
|
734
|
+
maxAge: sessionMaxAge
|
|
735
|
+
}
|
|
736
|
+
},
|
|
737
|
+
csrfToken: {
|
|
738
|
+
name: `${secureCookiePrefix ? "__Host-" : ""}${cookiePrefix}.csrf_token`,
|
|
739
|
+
options: {
|
|
740
|
+
httpOnly: true,
|
|
741
|
+
sameSite: "lax",
|
|
742
|
+
path: "/",
|
|
743
|
+
secure: !!secureCookiePrefix,
|
|
744
|
+
maxAge: 60 * 60 * 24 * 7
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
state: {
|
|
748
|
+
name: `${secureCookiePrefix}${cookiePrefix}.state`,
|
|
749
|
+
options: {
|
|
750
|
+
httpOnly: true,
|
|
751
|
+
sameSite: "lax",
|
|
752
|
+
path: "/",
|
|
753
|
+
secure: !!secureCookiePrefix,
|
|
754
|
+
maxAge: 60 * 15
|
|
755
|
+
// 15 minutes in seconds
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
pkCodeVerifier: {
|
|
759
|
+
name: `${secureCookiePrefix}${cookiePrefix}.pk_code_verifier`,
|
|
760
|
+
options: {
|
|
761
|
+
httpOnly: true,
|
|
762
|
+
sameSite: "lax",
|
|
763
|
+
path: "/",
|
|
764
|
+
secure: !!secureCookiePrefix,
|
|
765
|
+
maxAge: 60 * 15
|
|
766
|
+
// 15 minutes in seconds
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
dontRememberToken: {
|
|
770
|
+
name: `${secureCookiePrefix}${cookiePrefix}.dont_remember`,
|
|
771
|
+
options: {
|
|
772
|
+
httpOnly: true,
|
|
773
|
+
sameSite: "lax",
|
|
774
|
+
path: "/",
|
|
775
|
+
secure: !!secureCookiePrefix
|
|
776
|
+
//no max age so it expires when the browser closes
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
nonce: {
|
|
780
|
+
name: `${secureCookiePrefix}${cookiePrefix}.nonce`,
|
|
781
|
+
options: {
|
|
782
|
+
httpOnly: true,
|
|
783
|
+
sameSite: "lax",
|
|
784
|
+
path: "/",
|
|
785
|
+
secure: !!secureCookiePrefix,
|
|
786
|
+
maxAge: 60 * 15
|
|
787
|
+
// 15 minutes in seconds
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
function createCookieGetter(options) {
|
|
793
|
+
const secure = !!options.advanced?.useSecureCookies || process.env.NODE_ENV === "production";
|
|
794
|
+
const secureCookiePrefix = secure ? "__Secure-" : "";
|
|
795
|
+
const cookiePrefix = "better-auth";
|
|
796
|
+
function getCookie(cookieName, options2) {
|
|
797
|
+
return {
|
|
798
|
+
name: process.env.NODE_ENV === "production" ? `${secureCookiePrefix}${cookiePrefix}.${cookieName}` : `${cookiePrefix}.${cookieName}`,
|
|
799
|
+
options: {
|
|
800
|
+
secure: !!secureCookiePrefix,
|
|
801
|
+
sameSite: "lax",
|
|
802
|
+
path: "/",
|
|
803
|
+
maxAge: 60 * 15,
|
|
804
|
+
// 15 minutes in seconds
|
|
805
|
+
...options2
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
return getCookie;
|
|
810
|
+
}
|
|
811
|
+
async function setSessionCookie(ctx, sessionToken, dontRememberMe, overrides) {
|
|
812
|
+
const options = ctx.context.authCookies.sessionToken.options;
|
|
813
|
+
options.maxAge = dontRememberMe ? void 0 : options.maxAge;
|
|
814
|
+
await ctx.setSignedCookie(
|
|
815
|
+
ctx.context.authCookies.sessionToken.name,
|
|
816
|
+
sessionToken,
|
|
817
|
+
ctx.context.secret,
|
|
818
|
+
options
|
|
819
|
+
);
|
|
820
|
+
if (dontRememberMe) {
|
|
821
|
+
await ctx.setSignedCookie(
|
|
822
|
+
ctx.context.authCookies.dontRememberToken.name,
|
|
823
|
+
"true",
|
|
824
|
+
ctx.context.secret,
|
|
825
|
+
ctx.context.authCookies.dontRememberToken.options
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function deleteSessionCookie(ctx) {
|
|
830
|
+
ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
|
|
831
|
+
maxAge: 0
|
|
832
|
+
});
|
|
833
|
+
ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", {
|
|
834
|
+
maxAge: 0
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// src/api/routes/session.ts
|
|
839
|
+
import { z as z3 } from "zod";
|
|
840
|
+
|
|
841
|
+
// src/utils/get-request-ip.ts
|
|
842
|
+
function getIp(req) {
|
|
843
|
+
const testIP = "127.0.0.1";
|
|
844
|
+
if (process.env.NODE_ENV === "test") {
|
|
845
|
+
return testIP;
|
|
846
|
+
}
|
|
847
|
+
const headers = [
|
|
848
|
+
"x-client-ip",
|
|
849
|
+
"x-forwarded-for",
|
|
850
|
+
"cf-connecting-ip",
|
|
851
|
+
"fastly-client-ip",
|
|
852
|
+
"x-real-ip",
|
|
853
|
+
"x-cluster-client-ip",
|
|
854
|
+
"x-forwarded",
|
|
855
|
+
"forwarded-for",
|
|
856
|
+
"forwarded"
|
|
857
|
+
];
|
|
858
|
+
for (const header of headers) {
|
|
859
|
+
const value = req.headers.get(header);
|
|
860
|
+
if (typeof value === "string") {
|
|
861
|
+
const ip = value.split(",")[0].trim();
|
|
862
|
+
if (ip) return ip;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/api/routes/session.ts
|
|
869
|
+
var sessionCache = /* @__PURE__ */ new Map();
|
|
870
|
+
function getRequestUniqueKey(ctx, token) {
|
|
871
|
+
if (!ctx.request) {
|
|
872
|
+
return "";
|
|
873
|
+
}
|
|
874
|
+
const { method, url, headers } = ctx.request;
|
|
875
|
+
const userAgent = ctx.request.headers.get("User-Agent") || "";
|
|
876
|
+
const ip = getIp(ctx.request) || "";
|
|
877
|
+
const headerString = JSON.stringify(headers);
|
|
878
|
+
const uniqueString = `${method}:${url}:${headerString}:${userAgent}:${ip}:${token}`;
|
|
879
|
+
return uniqueString;
|
|
880
|
+
}
|
|
881
|
+
var getSession = () => createAuthEndpoint(
|
|
882
|
+
"/session",
|
|
883
|
+
{
|
|
884
|
+
method: "GET",
|
|
885
|
+
requireHeaders: true
|
|
886
|
+
},
|
|
887
|
+
async (ctx) => {
|
|
888
|
+
try {
|
|
889
|
+
const sessionCookieToken = await ctx.getSignedCookie(
|
|
890
|
+
ctx.context.authCookies.sessionToken.name,
|
|
891
|
+
ctx.context.secret
|
|
892
|
+
);
|
|
893
|
+
if (!sessionCookieToken) {
|
|
894
|
+
return ctx.json(null, {
|
|
895
|
+
status: 401
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
const key = getRequestUniqueKey(ctx, sessionCookieToken);
|
|
899
|
+
const cachedSession = sessionCache.get(key);
|
|
900
|
+
if (cachedSession) {
|
|
901
|
+
if (cachedSession.expiresAt > Date.now()) {
|
|
902
|
+
return ctx.json(
|
|
903
|
+
cachedSession.data
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
sessionCache.delete(key);
|
|
907
|
+
}
|
|
908
|
+
const session = await ctx.context.internalAdapter.findSession(sessionCookieToken);
|
|
909
|
+
if (!session || session.session.expiresAt < /* @__PURE__ */ new Date()) {
|
|
910
|
+
deleteSessionCookie(ctx);
|
|
911
|
+
if (session) {
|
|
912
|
+
await ctx.context.internalAdapter.deleteSession(session.session.id);
|
|
913
|
+
}
|
|
914
|
+
return ctx.json(null, {
|
|
915
|
+
status: 401
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
const dontRememberMe = await ctx.getSignedCookie(
|
|
919
|
+
ctx.context.authCookies.dontRememberToken.name,
|
|
920
|
+
ctx.context.secret
|
|
921
|
+
);
|
|
922
|
+
if (dontRememberMe) {
|
|
923
|
+
return ctx.json(
|
|
924
|
+
session
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const expiresIn = ctx.context.sessionConfig.expiresIn;
|
|
928
|
+
const updateAge = ctx.context.sessionConfig.updateAge;
|
|
929
|
+
const sessionIsDueToBeUpdatedDate = session.session.expiresAt.valueOf() - expiresIn * 1e3 + updateAge * 1e3;
|
|
930
|
+
const shouldBeUpdated = sessionIsDueToBeUpdatedDate <= Date.now();
|
|
931
|
+
if (shouldBeUpdated) {
|
|
932
|
+
const updatedSession = await ctx.context.internalAdapter.updateSession(
|
|
933
|
+
session.session.id,
|
|
934
|
+
{
|
|
935
|
+
expiresAt: getDate(ctx.context.sessionConfig.expiresIn, true)
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
if (!updatedSession) {
|
|
939
|
+
deleteSessionCookie(ctx);
|
|
940
|
+
return ctx.json(null, { status: 401 });
|
|
941
|
+
}
|
|
942
|
+
const maxAge = (updatedSession.expiresAt.valueOf() - Date.now()) / 1e3;
|
|
943
|
+
await setSessionCookie(ctx, updatedSession.id, false, {
|
|
944
|
+
maxAge
|
|
945
|
+
});
|
|
946
|
+
return ctx.json({
|
|
947
|
+
session: updatedSession,
|
|
948
|
+
user: session.user
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
sessionCache.set(key, {
|
|
952
|
+
data: session,
|
|
953
|
+
expiresAt: Date.now() + 5e3
|
|
954
|
+
});
|
|
955
|
+
return ctx.json(
|
|
956
|
+
session
|
|
957
|
+
);
|
|
958
|
+
} catch (error2) {
|
|
959
|
+
ctx.context.logger.error(error2);
|
|
960
|
+
return ctx.json(null, { status: 500 });
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
);
|
|
964
|
+
var getSessionFromCtx = async (ctx) => {
|
|
965
|
+
const session = await getSession()({
|
|
966
|
+
...ctx,
|
|
967
|
+
//@ts-expect-error: By default since this request context comes from a router it'll have a `router` flag which force it to be a request object
|
|
968
|
+
_flag: void 0
|
|
969
|
+
});
|
|
970
|
+
return session;
|
|
971
|
+
};
|
|
972
|
+
var sessionMiddleware = createAuthMiddleware(async (ctx) => {
|
|
973
|
+
const session = await getSessionFromCtx(ctx);
|
|
974
|
+
if (!session?.session) {
|
|
975
|
+
throw new APIError2("UNAUTHORIZED");
|
|
976
|
+
}
|
|
977
|
+
return {
|
|
978
|
+
session
|
|
979
|
+
};
|
|
980
|
+
});
|
|
981
|
+
var listSessions = () => createAuthEndpoint(
|
|
982
|
+
"/user/list-sessions",
|
|
983
|
+
{
|
|
984
|
+
method: "GET",
|
|
985
|
+
use: [sessionMiddleware],
|
|
986
|
+
requireHeaders: true
|
|
987
|
+
},
|
|
988
|
+
async (ctx) => {
|
|
989
|
+
const sessions = await ctx.context.adapter.findMany({
|
|
990
|
+
model: ctx.context.tables.session.tableName,
|
|
991
|
+
where: [
|
|
992
|
+
{
|
|
993
|
+
field: "userId",
|
|
994
|
+
value: ctx.context.session.user.id
|
|
995
|
+
}
|
|
996
|
+
]
|
|
997
|
+
});
|
|
998
|
+
const activeSessions = sessions.filter((session) => {
|
|
999
|
+
return session.expiresAt > /* @__PURE__ */ new Date();
|
|
1000
|
+
});
|
|
1001
|
+
return ctx.json(
|
|
1002
|
+
activeSessions
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
);
|
|
1006
|
+
var revokeSession = createAuthEndpoint(
|
|
1007
|
+
"/user/revoke-session",
|
|
1008
|
+
{
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
body: z3.object({
|
|
1011
|
+
id: z3.string()
|
|
1012
|
+
}),
|
|
1013
|
+
use: [sessionMiddleware],
|
|
1014
|
+
requireHeaders: true
|
|
1015
|
+
},
|
|
1016
|
+
async (ctx) => {
|
|
1017
|
+
const id = ctx.body.id;
|
|
1018
|
+
const findSession = await ctx.context.internalAdapter.findSession(id);
|
|
1019
|
+
if (!findSession) {
|
|
1020
|
+
return ctx.json(null, { status: 400 });
|
|
1021
|
+
}
|
|
1022
|
+
if (findSession.session.userId !== ctx.context.session.user.id) {
|
|
1023
|
+
return ctx.json(null, { status: 403 });
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
await ctx.context.internalAdapter.deleteSession(id);
|
|
1027
|
+
} catch (error2) {
|
|
1028
|
+
ctx.context.logger.error(error2);
|
|
1029
|
+
return ctx.json(null, { status: 500 });
|
|
1030
|
+
}
|
|
1031
|
+
return ctx.json({
|
|
1032
|
+
status: true
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
var revokeSessions = createAuthEndpoint(
|
|
1037
|
+
"/user/revoke-sessions",
|
|
1038
|
+
{
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
use: [sessionMiddleware],
|
|
1041
|
+
requireHeaders: true
|
|
1042
|
+
},
|
|
1043
|
+
async (ctx) => {
|
|
1044
|
+
try {
|
|
1045
|
+
await ctx.context.internalAdapter.deleteSessions(
|
|
1046
|
+
ctx.context.session.user.id
|
|
1047
|
+
);
|
|
1048
|
+
} catch (error2) {
|
|
1049
|
+
ctx.context.logger.error(error2);
|
|
1050
|
+
return ctx.json(null, { status: 500 });
|
|
1051
|
+
}
|
|
1052
|
+
return ctx.json({
|
|
1053
|
+
status: true
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
// src/api/routes/sign-in.ts
|
|
1059
|
+
var signInOAuth = createAuthEndpoint(
|
|
1060
|
+
"/sign-in/social",
|
|
1061
|
+
{
|
|
1062
|
+
method: "POST",
|
|
1063
|
+
requireHeaders: true,
|
|
1064
|
+
query: z4.object({
|
|
1065
|
+
/**
|
|
1066
|
+
* Redirect to the current URL after the
|
|
1067
|
+
* user has signed in.
|
|
1068
|
+
*/
|
|
1069
|
+
currentURL: z4.string().optional()
|
|
1070
|
+
}).optional(),
|
|
1071
|
+
body: z4.object({
|
|
1072
|
+
/**
|
|
1073
|
+
* Callback URL to redirect to after the user has signed in.
|
|
1074
|
+
*/
|
|
1075
|
+
callbackURL: z4.string().optional(),
|
|
1076
|
+
/**
|
|
1077
|
+
* OAuth2 provider to use`
|
|
1078
|
+
*/
|
|
1079
|
+
provider: z4.enum(oAuthProviderList),
|
|
1080
|
+
/**
|
|
1081
|
+
* If this is true the session will only be valid for the current browser session
|
|
1082
|
+
*/
|
|
1083
|
+
dontRememberMe: z4.boolean().default(false).optional()
|
|
1084
|
+
})
|
|
1085
|
+
},
|
|
1086
|
+
async (c) => {
|
|
1087
|
+
const provider = c.context.socialProviders.find(
|
|
1088
|
+
(p) => p.id === c.body.provider
|
|
1089
|
+
);
|
|
1090
|
+
if (!provider) {
|
|
1091
|
+
c.context.logger.error(
|
|
1092
|
+
"Provider not found. Make sure to add the provider to your auth config",
|
|
1093
|
+
{
|
|
1094
|
+
provider: c.body.provider
|
|
1095
|
+
}
|
|
1096
|
+
);
|
|
1097
|
+
throw new APIError3("NOT_FOUND", {
|
|
1098
|
+
message: "Provider not found"
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
const cookie = c.context.authCookies;
|
|
1102
|
+
const currentURL = c.query?.currentURL ? new URL(c.query?.currentURL) : null;
|
|
1103
|
+
const callbackURL = c.body.callbackURL?.startsWith("http") ? c.body.callbackURL : `${currentURL?.origin}${c.body.callbackURL || ""}`;
|
|
1104
|
+
const state = generateState(
|
|
1105
|
+
callbackURL || currentURL?.origin || c.context.baseURL,
|
|
1106
|
+
c.query?.currentURL
|
|
1107
|
+
);
|
|
1108
|
+
try {
|
|
1109
|
+
await c.setSignedCookie(
|
|
1110
|
+
cookie.state.name,
|
|
1111
|
+
state.code,
|
|
1112
|
+
c.context.secret,
|
|
1113
|
+
cookie.state.options
|
|
1114
|
+
);
|
|
1115
|
+
const codeVerifier = generateCodeVerifier();
|
|
1116
|
+
await c.setSignedCookie(
|
|
1117
|
+
cookie.pkCodeVerifier.name,
|
|
1118
|
+
codeVerifier,
|
|
1119
|
+
c.context.secret,
|
|
1120
|
+
cookie.pkCodeVerifier.options
|
|
1121
|
+
);
|
|
1122
|
+
const url = provider.createAuthorizationURL({
|
|
1123
|
+
state: state.state,
|
|
1124
|
+
codeVerifier
|
|
1125
|
+
});
|
|
1126
|
+
url.searchParams.set(
|
|
1127
|
+
"redirect_uri",
|
|
1128
|
+
`${c.context.baseURL}/callback/${c.body.provider}`
|
|
1129
|
+
);
|
|
1130
|
+
return {
|
|
1131
|
+
url: url.toString(),
|
|
1132
|
+
state: state.state,
|
|
1133
|
+
codeVerifier,
|
|
1134
|
+
redirect: true
|
|
1135
|
+
};
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
throw new APIError3("INTERNAL_SERVER_ERROR");
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
);
|
|
1141
|
+
var signInEmail = createAuthEndpoint(
|
|
1142
|
+
"/sign-in/email",
|
|
1143
|
+
{
|
|
1144
|
+
method: "POST",
|
|
1145
|
+
body: z4.object({
|
|
1146
|
+
email: z4.string().email(),
|
|
1147
|
+
password: z4.string(),
|
|
1148
|
+
callbackURL: z4.string().optional(),
|
|
1149
|
+
/**
|
|
1150
|
+
* If this is true the session will only be valid for the current browser session
|
|
1151
|
+
* @default false
|
|
1152
|
+
*/
|
|
1153
|
+
dontRememberMe: z4.boolean().default(false).optional()
|
|
1154
|
+
})
|
|
1155
|
+
},
|
|
1156
|
+
async (ctx) => {
|
|
1157
|
+
if (!ctx.context.options?.emailAndPassword?.enabled) {
|
|
1158
|
+
ctx.context.logger.error(
|
|
1159
|
+
"Email and password is not enabled. Make sure to enable it in the options on you `auth.ts` file. Check `https://better-auth.com/docs/authentication/email-password` for more!"
|
|
1160
|
+
);
|
|
1161
|
+
throw new APIError3("BAD_REQUEST", {
|
|
1162
|
+
message: "Email and password is not enabled"
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
const currentSession = await getSessionFromCtx(ctx);
|
|
1166
|
+
if (currentSession) {
|
|
1167
|
+
await ctx.context.internalAdapter.deleteSession(
|
|
1168
|
+
currentSession.session.id
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const { email, password } = ctx.body;
|
|
1172
|
+
const checkEmail = z4.string().email().safeParse(email);
|
|
1173
|
+
if (!checkEmail.success) {
|
|
1174
|
+
throw new APIError3("BAD_REQUEST", {
|
|
1175
|
+
message: "Invalid email"
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
1179
|
+
if (!user) {
|
|
1180
|
+
await ctx.context.password.hash(password);
|
|
1181
|
+
ctx.context.logger.error("User not found", { email });
|
|
1182
|
+
throw new APIError3("UNAUTHORIZED", {
|
|
1183
|
+
message: "Invalid email or password"
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
const credentialAccount = user.accounts.find(
|
|
1187
|
+
(a) => a.providerId === "credential"
|
|
1188
|
+
);
|
|
1189
|
+
if (!credentialAccount) {
|
|
1190
|
+
ctx.context.logger.error("Credential account not found", { email });
|
|
1191
|
+
throw new APIError3("UNAUTHORIZED", {
|
|
1192
|
+
message: "Invalid email or password"
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
const currentPassword = credentialAccount?.password;
|
|
1196
|
+
if (!currentPassword) {
|
|
1197
|
+
ctx.context.logger.error("Password not found", { email });
|
|
1198
|
+
throw new APIError3("UNAUTHORIZED", {
|
|
1199
|
+
message: "Unexpected error"
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
const validPassword = await ctx.context.password.verify(
|
|
1203
|
+
currentPassword,
|
|
1204
|
+
password
|
|
1205
|
+
);
|
|
1206
|
+
if (!validPassword) {
|
|
1207
|
+
ctx.context.logger.error("Invalid password");
|
|
1208
|
+
throw new APIError3("UNAUTHORIZED", {
|
|
1209
|
+
message: "Invalid email or password"
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
const session = await ctx.context.internalAdapter.createSession(
|
|
1213
|
+
user.user.id,
|
|
1214
|
+
ctx.headers,
|
|
1215
|
+
ctx.body.dontRememberMe
|
|
1216
|
+
);
|
|
1217
|
+
if (!session) {
|
|
1218
|
+
ctx.context.logger.error("Failed to create session");
|
|
1219
|
+
throw new APIError3("INTERNAL_SERVER_ERROR");
|
|
1220
|
+
}
|
|
1221
|
+
await setSessionCookie(ctx, session.id, ctx.body.dontRememberMe);
|
|
1222
|
+
return ctx.json({
|
|
1223
|
+
user: user.user,
|
|
1224
|
+
session,
|
|
1225
|
+
redirect: !!ctx.body.callbackURL,
|
|
1226
|
+
url: ctx.body.callbackURL
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
1230
|
+
|
|
1231
|
+
// src/api/routes/callback.ts
|
|
1232
|
+
import { APIError as APIError4 } from "better-call";
|
|
1233
|
+
import { z as z6 } from "zod";
|
|
1234
|
+
|
|
1235
|
+
// src/db/schema.ts
|
|
1236
|
+
import { z as z5 } from "zod";
|
|
1237
|
+
var accountSchema = z5.object({
|
|
1238
|
+
id: z5.string(),
|
|
1239
|
+
providerId: z5.string(),
|
|
1240
|
+
accountId: z5.string(),
|
|
1241
|
+
userId: z5.string(),
|
|
1242
|
+
accessToken: z5.string().nullable().optional(),
|
|
1243
|
+
refreshToken: z5.string().nullable().optional(),
|
|
1244
|
+
idToken: z5.string().nullable().optional(),
|
|
1245
|
+
/**
|
|
1246
|
+
* Access token expires at
|
|
1247
|
+
*/
|
|
1248
|
+
expiresAt: z5.date().nullable().optional(),
|
|
1249
|
+
/**
|
|
1250
|
+
* Password is only stored in the credential provider
|
|
1251
|
+
*/
|
|
1252
|
+
password: z5.string().optional().nullable()
|
|
1253
|
+
});
|
|
1254
|
+
var userSchema = z5.object({
|
|
1255
|
+
id: z5.string(),
|
|
1256
|
+
email: z5.string().transform((val) => val.toLowerCase()),
|
|
1257
|
+
emailVerified: z5.boolean().default(false),
|
|
1258
|
+
name: z5.string(),
|
|
1259
|
+
image: z5.string().optional(),
|
|
1260
|
+
createdAt: z5.date().default(/* @__PURE__ */ new Date()),
|
|
1261
|
+
updatedAt: z5.date().default(/* @__PURE__ */ new Date())
|
|
1262
|
+
});
|
|
1263
|
+
var sessionSchema = z5.object({
|
|
1264
|
+
id: z5.string(),
|
|
1265
|
+
userId: z5.string(),
|
|
1266
|
+
expiresAt: z5.date(),
|
|
1267
|
+
ipAddress: z5.string().optional(),
|
|
1268
|
+
userAgent: z5.string().optional()
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
// src/utils/id.ts
|
|
1272
|
+
import { alphabet, generateRandomString } from "oslo/crypto";
|
|
1273
|
+
var generateId = () => {
|
|
1274
|
+
return generateRandomString(36, alphabet("a-z", "0-9"));
|
|
1275
|
+
};
|
|
1276
|
+
|
|
1277
|
+
// src/utils/hide-metadata.ts
|
|
1278
|
+
var HIDE_METADATA = {
|
|
1279
|
+
isAction: false
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
// src/utils/getAccount.ts
|
|
1283
|
+
function getAccountTokens(tokens) {
|
|
1284
|
+
const accessToken = tokens.accessToken();
|
|
1285
|
+
let refreshToken = tokens.hasRefreshToken() ? tokens.refreshToken() : void 0;
|
|
1286
|
+
let accessTokenExpiresAt = void 0;
|
|
1287
|
+
try {
|
|
1288
|
+
accessTokenExpiresAt = tokens.accessTokenExpiresAt();
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
accessToken,
|
|
1293
|
+
refreshToken,
|
|
1294
|
+
expiresAt: accessTokenExpiresAt
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// src/api/routes/callback.ts
|
|
1299
|
+
var callbackOAuth = createAuthEndpoint(
|
|
1300
|
+
"/callback/:id",
|
|
1301
|
+
{
|
|
1302
|
+
method: "GET",
|
|
1303
|
+
query: z6.object({
|
|
1304
|
+
state: z6.string(),
|
|
1305
|
+
code: z6.string().optional(),
|
|
1306
|
+
error: z6.string().optional()
|
|
1307
|
+
}),
|
|
1308
|
+
metadata: HIDE_METADATA
|
|
1309
|
+
},
|
|
1310
|
+
async (c) => {
|
|
1311
|
+
if (c.query.error || !c.query.code) {
|
|
1312
|
+
const parsedState2 = parseState(c.query.state);
|
|
1313
|
+
const callbackURL2 = parsedState2.data?.callbackURL || `${c.context.baseURL}/error`;
|
|
1314
|
+
c.context.logger.error(c.query.error, c.params.id);
|
|
1315
|
+
throw c.redirect(
|
|
1316
|
+
`${callbackURL2}?error=${c.query.error || "oAuth_code_missing"}`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
const provider = c.context.socialProviders.find(
|
|
1320
|
+
(p) => p.id === c.params.id
|
|
1321
|
+
);
|
|
1322
|
+
if (!provider) {
|
|
1323
|
+
c.context.logger.error(
|
|
1324
|
+
"Oauth provider with id",
|
|
1325
|
+
c.params.id,
|
|
1326
|
+
"not found"
|
|
1327
|
+
);
|
|
1328
|
+
throw c.redirect(
|
|
1329
|
+
`${c.context.baseURL}/error?error=oauth_provider_not_found`
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const codeVerifier = await c.getSignedCookie(
|
|
1333
|
+
c.context.authCookies.pkCodeVerifier.name,
|
|
1334
|
+
c.context.secret
|
|
1335
|
+
);
|
|
1336
|
+
let tokens;
|
|
1337
|
+
try {
|
|
1338
|
+
tokens = await provider.validateAuthorizationCode(
|
|
1339
|
+
c.query.code,
|
|
1340
|
+
codeVerifier,
|
|
1341
|
+
`${c.context.baseURL}/callback/${provider.id}`
|
|
1342
|
+
);
|
|
1343
|
+
} catch (e) {
|
|
1344
|
+
c.context.logger.error(e);
|
|
1345
|
+
throw c.redirect(
|
|
1346
|
+
`${c.context.baseURL}/error?error=oauth_code_verification_failed`
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
const user = await provider.getUserInfo(tokens).then((res) => res?.user);
|
|
1350
|
+
const id = generateId();
|
|
1351
|
+
const data = userSchema.safeParse({
|
|
1352
|
+
...user,
|
|
1353
|
+
id
|
|
1354
|
+
});
|
|
1355
|
+
const parsedState = parseState(c.query.state);
|
|
1356
|
+
if (!parsedState.success) {
|
|
1357
|
+
c.context.logger.error("Unable to parse state");
|
|
1358
|
+
throw c.redirect(
|
|
1359
|
+
`${c.context.baseURL}/error?error=invalid_state_parameter`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const { callbackURL, currentURL, dontRememberMe } = parsedState.data;
|
|
1363
|
+
if (!user || data.success === false) {
|
|
1364
|
+
throw c.redirect(
|
|
1365
|
+
`${c.context.baseURL}/error?error=oauth_validation_failed`
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
if (!callbackURL) {
|
|
1369
|
+
throw c.redirect(
|
|
1370
|
+
`${c.context.baseURL}/error?error=oauth_callback_url_not_found`
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
const dbUser = await c.context.internalAdapter.findUserByEmail(user.email);
|
|
1374
|
+
const userId = dbUser?.user.id;
|
|
1375
|
+
if (dbUser) {
|
|
1376
|
+
const hasBeenLinked = dbUser.accounts.find(
|
|
1377
|
+
(a) => a.providerId === provider.id
|
|
1378
|
+
);
|
|
1379
|
+
const trustedProviders = c.context.options.account?.accountLinking?.trustedProviders;
|
|
1380
|
+
const isTrustedProvider = trustedProviders ? trustedProviders.includes(provider.id) : true;
|
|
1381
|
+
if (!hasBeenLinked && (!user.emailVerified || !isTrustedProvider)) {
|
|
1382
|
+
let url;
|
|
1383
|
+
try {
|
|
1384
|
+
url = new URL(currentURL || callbackURL);
|
|
1385
|
+
url.searchParams.set("error", "account_not_linked");
|
|
1386
|
+
} catch (e) {
|
|
1387
|
+
throw c.redirect(
|
|
1388
|
+
`${c.context.baseURL}/error?error=account_not_linked`
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
throw c.redirect(url.toString());
|
|
1392
|
+
}
|
|
1393
|
+
if (!hasBeenLinked) {
|
|
1394
|
+
try {
|
|
1395
|
+
await c.context.internalAdapter.linkAccount({
|
|
1396
|
+
providerId: provider.id,
|
|
1397
|
+
accountId: user.id,
|
|
1398
|
+
id: `${provider.id}:${user.id}`,
|
|
1399
|
+
userId: dbUser.user.id,
|
|
1400
|
+
...getAccountTokens(tokens)
|
|
1401
|
+
});
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
console.log(e);
|
|
1404
|
+
throw c.redirect(
|
|
1405
|
+
`${c.context.baseURL}/error?error=failed_linking_account`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
try {
|
|
1411
|
+
await c.context.internalAdapter.createOAuthUser(data.data, {
|
|
1412
|
+
...getAccountTokens(tokens),
|
|
1413
|
+
id: `${provider.id}:${user.id}`,
|
|
1414
|
+
providerId: provider.id,
|
|
1415
|
+
accountId: user.id,
|
|
1416
|
+
userId: id
|
|
1417
|
+
});
|
|
1418
|
+
} catch (e) {
|
|
1419
|
+
const url = new URL(currentURL || callbackURL);
|
|
1420
|
+
url.searchParams.set("error", "unable_to_create_user");
|
|
1421
|
+
c.setHeader("Location", url.toString());
|
|
1422
|
+
throw c.redirect(url.toString());
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (!userId && !id)
|
|
1426
|
+
throw new APIError4("INTERNAL_SERVER_ERROR", {
|
|
1427
|
+
message: "Unable to create user"
|
|
1428
|
+
});
|
|
1429
|
+
const session = await c.context.internalAdapter.createSession(
|
|
1430
|
+
userId || id,
|
|
1431
|
+
c.request,
|
|
1432
|
+
dontRememberMe
|
|
1433
|
+
);
|
|
1434
|
+
if (!session) {
|
|
1435
|
+
const url = new URL(currentURL || callbackURL);
|
|
1436
|
+
url.searchParams.set("error", "unable_to_create_session");
|
|
1437
|
+
throw c.redirect(url.toString());
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
await setSessionCookie(c, session.id, dontRememberMe);
|
|
1441
|
+
} catch (e) {
|
|
1442
|
+
c.context.logger.error("Unable to set session cookie", e);
|
|
1443
|
+
const url = new URL(currentURL || callbackURL);
|
|
1444
|
+
url.searchParams.set("error", "unable_to_create_session");
|
|
1445
|
+
throw c.redirect(url.toString());
|
|
1446
|
+
}
|
|
1447
|
+
throw c.redirect(callbackURL);
|
|
1448
|
+
}
|
|
1449
|
+
);
|
|
1450
|
+
|
|
1451
|
+
// src/api/routes/sign-out.ts
|
|
1452
|
+
import { z as z7 } from "zod";
|
|
1453
|
+
var signOut = createAuthEndpoint(
|
|
1454
|
+
"/sign-out",
|
|
1455
|
+
{
|
|
1456
|
+
method: "POST",
|
|
1457
|
+
body: z7.optional(
|
|
1458
|
+
z7.object({
|
|
1459
|
+
callbackURL: z7.string().optional()
|
|
1460
|
+
})
|
|
1461
|
+
)
|
|
1462
|
+
},
|
|
1463
|
+
async (ctx) => {
|
|
1464
|
+
const sessionCookieToken = await ctx.getSignedCookie(
|
|
1465
|
+
ctx.context.authCookies.sessionToken.name,
|
|
1466
|
+
ctx.context.secret
|
|
1467
|
+
);
|
|
1468
|
+
if (!sessionCookieToken) {
|
|
1469
|
+
return ctx.json(null);
|
|
1470
|
+
}
|
|
1471
|
+
await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
|
|
1472
|
+
deleteSessionCookie(ctx);
|
|
1473
|
+
return ctx.json(null, {
|
|
1474
|
+
body: {
|
|
1475
|
+
redirect: !!ctx.body?.callbackURL,
|
|
1476
|
+
url: ctx.body?.callbackURL
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
// src/api/routes/forget-password.ts
|
|
1483
|
+
import { TimeSpan as TimeSpan2 } from "oslo";
|
|
1484
|
+
import { createJWT, parseJWT as parseJWT3 } from "oslo/jwt";
|
|
1485
|
+
import { validateJWT } from "oslo/jwt";
|
|
1486
|
+
import { z as z8 } from "zod";
|
|
1487
|
+
var forgetPassword = createAuthEndpoint(
|
|
1488
|
+
"/forget-password",
|
|
1489
|
+
{
|
|
1490
|
+
method: "POST",
|
|
1491
|
+
body: z8.object({
|
|
1492
|
+
/**
|
|
1493
|
+
* The email address of the user to send a password reset email to.
|
|
1494
|
+
*/
|
|
1495
|
+
email: z8.string().email(),
|
|
1496
|
+
/**
|
|
1497
|
+
* The URL to redirect the user to reset their password.
|
|
1498
|
+
* If the token isn't valid or expired, it'll be redirected with a query parameter `?
|
|
1499
|
+
* error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?
|
|
1500
|
+
* token=VALID_TOKEN
|
|
1501
|
+
*/
|
|
1502
|
+
redirectTo: z8.string()
|
|
1503
|
+
})
|
|
1504
|
+
},
|
|
1505
|
+
async (ctx) => {
|
|
1506
|
+
if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
|
|
1507
|
+
ctx.context.logger.error(
|
|
1508
|
+
"Reset password isn't enabled.Please pass an emailAndPassword.sendResetPasswordToken function to your auth config!"
|
|
1509
|
+
);
|
|
1510
|
+
return ctx.json(null, {
|
|
1511
|
+
status: 400,
|
|
1512
|
+
statusText: "RESET_PASSWORD_EMAIL_NOT_SENT",
|
|
1513
|
+
body: {
|
|
1514
|
+
message: "Reset password isn't enabled"
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
const { email } = ctx.body;
|
|
1519
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
1520
|
+
if (!user) {
|
|
1521
|
+
return ctx.json(
|
|
1522
|
+
{
|
|
1523
|
+
status: false
|
|
1524
|
+
},
|
|
1525
|
+
{
|
|
1526
|
+
body: {
|
|
1527
|
+
status: true
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
const token = await createJWT(
|
|
1533
|
+
"HS256",
|
|
1534
|
+
Buffer.from(ctx.context.secret),
|
|
1535
|
+
{
|
|
1536
|
+
email: user.user.email,
|
|
1537
|
+
redirectTo: ctx.body.redirectTo
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
expiresIn: new TimeSpan2(1, "h"),
|
|
1541
|
+
issuer: "better-auth",
|
|
1542
|
+
subject: "forget-password",
|
|
1543
|
+
audiences: [user.user.email],
|
|
1544
|
+
includeIssuedTimestamp: true
|
|
1545
|
+
}
|
|
1546
|
+
);
|
|
1547
|
+
const url = `${ctx.context.baseURL}/reset-password/${token}`;
|
|
1548
|
+
await ctx.context.options.emailAndPassword.sendResetPassword(
|
|
1549
|
+
url,
|
|
1550
|
+
user.user
|
|
1551
|
+
);
|
|
1552
|
+
return ctx.json({
|
|
1553
|
+
status: true
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1557
|
+
var forgetPasswordCallback = createAuthEndpoint(
|
|
1558
|
+
"/reset-password/:token",
|
|
1559
|
+
{
|
|
1560
|
+
method: "GET"
|
|
1561
|
+
},
|
|
1562
|
+
async (ctx) => {
|
|
1563
|
+
const { token } = ctx.params;
|
|
1564
|
+
let decodedToken;
|
|
1565
|
+
const schema = z8.object({
|
|
1566
|
+
email: z8.string(),
|
|
1567
|
+
redirectTo: z8.string()
|
|
1568
|
+
});
|
|
1569
|
+
try {
|
|
1570
|
+
decodedToken = await validateJWT(
|
|
1571
|
+
"HS256",
|
|
1572
|
+
Buffer.from(ctx.context.secret),
|
|
1573
|
+
token
|
|
1574
|
+
);
|
|
1575
|
+
if (!decodedToken.expiresAt || decodedToken.expiresAt < /* @__PURE__ */ new Date()) {
|
|
1576
|
+
throw Error("Token expired");
|
|
1577
|
+
}
|
|
1578
|
+
} catch (e) {
|
|
1579
|
+
const decoded = parseJWT3(token);
|
|
1580
|
+
const jwt = schema.safeParse(decoded?.payload);
|
|
1581
|
+
if (jwt.success) {
|
|
1582
|
+
throw ctx.redirect(`${jwt.data?.redirectTo}?error=invalid_token`);
|
|
1583
|
+
} else {
|
|
1584
|
+
throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_token`);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const { redirectTo } = schema.parse(decodedToken.payload);
|
|
1588
|
+
throw ctx.redirect(`${redirectTo}?token=${token}`);
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1591
|
+
var resetPassword = createAuthEndpoint(
|
|
1592
|
+
"/reset-password",
|
|
1593
|
+
{
|
|
1594
|
+
method: "POST",
|
|
1595
|
+
query: z8.object({
|
|
1596
|
+
currentURL: z8.string()
|
|
1597
|
+
}).optional(),
|
|
1598
|
+
body: z8.object({
|
|
1599
|
+
newPassword: z8.string(),
|
|
1600
|
+
callbackURL: z8.string().optional()
|
|
1601
|
+
})
|
|
1602
|
+
},
|
|
1603
|
+
async (ctx) => {
|
|
1604
|
+
const token = ctx.query?.currentURL.split("?token=")[1];
|
|
1605
|
+
if (!token) {
|
|
1606
|
+
return ctx.json(
|
|
1607
|
+
{
|
|
1608
|
+
error: "Invalid token",
|
|
1609
|
+
data: null
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
status: 400,
|
|
1613
|
+
statusText: "INVALID_TOKEN",
|
|
1614
|
+
body: {
|
|
1615
|
+
message: "Invalid token"
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
const { newPassword } = ctx.body;
|
|
1621
|
+
try {
|
|
1622
|
+
const jwt = await validateJWT(
|
|
1623
|
+
"HS256",
|
|
1624
|
+
Buffer.from(ctx.context.secret),
|
|
1625
|
+
token
|
|
1626
|
+
);
|
|
1627
|
+
const email = z8.string().email().parse(jwt.payload.email);
|
|
1628
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
1629
|
+
if (!user) {
|
|
1630
|
+
return ctx.json(
|
|
1631
|
+
{
|
|
1632
|
+
error: "User not found",
|
|
1633
|
+
data: null
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
status: 400,
|
|
1637
|
+
body: {
|
|
1638
|
+
message: "failed to reset password"
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
if (newPassword.length < (ctx.context.options.emailAndPassword?.minPasswordLength || 8) || newPassword.length > (ctx.context.options.emailAndPassword?.maxPasswordLength || 32)) {
|
|
1644
|
+
return ctx.json(
|
|
1645
|
+
{
|
|
1646
|
+
data: null,
|
|
1647
|
+
error: "password is too short or too long"
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
status: 400,
|
|
1651
|
+
statusText: "INVALID_PASSWORD_LENGTH",
|
|
1652
|
+
body: {
|
|
1653
|
+
message: "password is too short or too long"
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
1659
|
+
const updatedUser = await ctx.context.internalAdapter.updatePassword(
|
|
1660
|
+
user.user.id,
|
|
1661
|
+
hashedPassword
|
|
1662
|
+
);
|
|
1663
|
+
if (!updatedUser) {
|
|
1664
|
+
return ctx.json(null, {
|
|
1665
|
+
status: 400,
|
|
1666
|
+
statusText: "USER_NOT_FOUND",
|
|
1667
|
+
body: {
|
|
1668
|
+
message: "User doesn't have a credential account"
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
return ctx.json(
|
|
1673
|
+
{
|
|
1674
|
+
error: null,
|
|
1675
|
+
data: {
|
|
1676
|
+
status: true,
|
|
1677
|
+
url: ctx.body.callbackURL,
|
|
1678
|
+
redirect: !!ctx.body.callbackURL
|
|
1679
|
+
}
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
body: {
|
|
1683
|
+
status: true,
|
|
1684
|
+
url: ctx.body.callbackURL,
|
|
1685
|
+
redirect: !!ctx.body.callbackURL
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
);
|
|
1689
|
+
} catch (e) {
|
|
1690
|
+
console.log(e);
|
|
1691
|
+
return ctx.json(
|
|
1692
|
+
{
|
|
1693
|
+
error: "Invalid token",
|
|
1694
|
+
data: null
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
status: 400,
|
|
1698
|
+
statusText: "INVALID_TOKEN",
|
|
1699
|
+
body: {
|
|
1700
|
+
message: "Invalid token"
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
);
|
|
1707
|
+
|
|
1708
|
+
// src/api/routes/verify-email.ts
|
|
1709
|
+
import { TimeSpan as TimeSpan3 } from "oslo";
|
|
1710
|
+
import { createJWT as createJWT2, validateJWT as validateJWT2 } from "oslo/jwt";
|
|
1711
|
+
import { z as z9 } from "zod";
|
|
1712
|
+
async function createEmailVerificationToken(secret, email) {
|
|
1713
|
+
const token = await createJWT2(
|
|
1714
|
+
"HS256",
|
|
1715
|
+
Buffer.from(secret),
|
|
1716
|
+
{
|
|
1717
|
+
email: email.toLowerCase()
|
|
1718
|
+
},
|
|
1719
|
+
{
|
|
1720
|
+
expiresIn: new TimeSpan3(1, "h"),
|
|
1721
|
+
issuer: "better-auth",
|
|
1722
|
+
subject: "verify-email",
|
|
1723
|
+
audiences: [email],
|
|
1724
|
+
includeIssuedTimestamp: true
|
|
1725
|
+
}
|
|
1726
|
+
);
|
|
1727
|
+
return token;
|
|
1728
|
+
}
|
|
1729
|
+
var sendVerificationEmail = createAuthEndpoint(
|
|
1730
|
+
"/send-verification-email",
|
|
1731
|
+
{
|
|
1732
|
+
method: "POST",
|
|
1733
|
+
query: z9.object({
|
|
1734
|
+
currentURL: z9.string().optional()
|
|
1735
|
+
}).optional(),
|
|
1736
|
+
body: z9.object({
|
|
1737
|
+
email: z9.string().email(),
|
|
1738
|
+
callbackURL: z9.string().optional()
|
|
1739
|
+
})
|
|
1740
|
+
},
|
|
1741
|
+
async (ctx) => {
|
|
1742
|
+
if (!ctx.context.options.emailAndPassword?.sendVerificationEmail) {
|
|
1743
|
+
ctx.context.logger.error(
|
|
1744
|
+
"Verification email isn't enabled. Pass `sendVerificationEmail` in `emailAndPassword` options to enable it."
|
|
1745
|
+
);
|
|
1746
|
+
return ctx.json(null, {
|
|
1747
|
+
status: 400,
|
|
1748
|
+
statusText: "VERIFICATION_EMAIL_NOT_SENT",
|
|
1749
|
+
body: {
|
|
1750
|
+
message: "Verification email isn't enabled"
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
const { email } = ctx.body;
|
|
1755
|
+
const token = await createEmailVerificationToken(ctx.context.secret, email);
|
|
1756
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || ctx.query?.currentURL || "/"}`;
|
|
1757
|
+
await ctx.context.options.emailAndPassword.sendVerificationEmail(
|
|
1758
|
+
email,
|
|
1759
|
+
url,
|
|
1760
|
+
token
|
|
1761
|
+
);
|
|
1762
|
+
return ctx.json({
|
|
1763
|
+
status: true
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
);
|
|
1767
|
+
var verifyEmail = createAuthEndpoint(
|
|
1768
|
+
"/verify-email",
|
|
1769
|
+
{
|
|
1770
|
+
method: "GET",
|
|
1771
|
+
query: z9.object({
|
|
1772
|
+
token: z9.string(),
|
|
1773
|
+
callbackURL: z9.string().optional()
|
|
1774
|
+
})
|
|
1775
|
+
},
|
|
1776
|
+
async (ctx) => {
|
|
1777
|
+
const { token } = ctx.query;
|
|
1778
|
+
let jwt;
|
|
1779
|
+
try {
|
|
1780
|
+
jwt = await validateJWT2("HS256", Buffer.from(ctx.context.secret), token);
|
|
1781
|
+
} catch (e) {
|
|
1782
|
+
ctx.context.logger.error("Failed to verify email", e);
|
|
1783
|
+
return ctx.json(null, {
|
|
1784
|
+
status: 400,
|
|
1785
|
+
statusText: "INVALID_TOKEN",
|
|
1786
|
+
body: {
|
|
1787
|
+
message: "Invalid token"
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
const schema = z9.object({
|
|
1792
|
+
email: z9.string().email()
|
|
1793
|
+
});
|
|
1794
|
+
const parsed = schema.parse(jwt.payload);
|
|
1795
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(
|
|
1796
|
+
parsed.email
|
|
1797
|
+
);
|
|
1798
|
+
if (!user) {
|
|
1799
|
+
return ctx.json(null, {
|
|
1800
|
+
status: 400,
|
|
1801
|
+
statusText: "USER_NOT_FOUND",
|
|
1802
|
+
body: {
|
|
1803
|
+
message: "User not found"
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
const account = user.accounts.find((a) => a.providerId === "credential");
|
|
1808
|
+
if (!account) {
|
|
1809
|
+
throw ctx.redirect;
|
|
1810
|
+
}
|
|
1811
|
+
await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
|
|
1812
|
+
emailVerified: true
|
|
1813
|
+
});
|
|
1814
|
+
if (ctx.query.callbackURL) {
|
|
1815
|
+
console.log("Redirecting to", ctx.query.callbackURL);
|
|
1816
|
+
throw ctx.redirect("/");
|
|
1817
|
+
}
|
|
1818
|
+
return ctx.json({
|
|
1819
|
+
status: true
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
);
|
|
1823
|
+
|
|
1824
|
+
// src/api/routes/update-user.ts
|
|
1825
|
+
import { z as z10 } from "zod";
|
|
1826
|
+
import { alphabet as alphabet2, generateRandomString as generateRandomString2 } from "oslo/crypto";
|
|
1827
|
+
import "better-call";
|
|
1828
|
+
var updateUser = createAuthEndpoint(
|
|
1829
|
+
"/user/update",
|
|
1830
|
+
{
|
|
1831
|
+
method: "POST",
|
|
1832
|
+
body: z10.object({
|
|
1833
|
+
name: z10.string().optional(),
|
|
1834
|
+
image: z10.string().optional()
|
|
1835
|
+
}),
|
|
1836
|
+
use: [sessionMiddleware]
|
|
1837
|
+
},
|
|
1838
|
+
async (ctx) => {
|
|
1839
|
+
const { name, image } = ctx.body;
|
|
1840
|
+
const session = ctx.context.session;
|
|
1841
|
+
if (!image && !name) {
|
|
1842
|
+
return ctx.json(session.user);
|
|
1843
|
+
}
|
|
1844
|
+
const user = await ctx.context.internalAdapter.updateUserByEmail(
|
|
1845
|
+
session.user.email,
|
|
1846
|
+
{
|
|
1847
|
+
name,
|
|
1848
|
+
image
|
|
1849
|
+
}
|
|
1850
|
+
);
|
|
1851
|
+
return ctx.json(user);
|
|
1852
|
+
}
|
|
1853
|
+
);
|
|
1854
|
+
var changePassword = createAuthEndpoint(
|
|
1855
|
+
"/user/change-password",
|
|
1856
|
+
{
|
|
1857
|
+
method: "POST",
|
|
1858
|
+
body: z10.object({
|
|
1859
|
+
/**
|
|
1860
|
+
* The new password to set
|
|
1861
|
+
*/
|
|
1862
|
+
newPassword: z10.string(),
|
|
1863
|
+
/**
|
|
1864
|
+
* The current password of the user
|
|
1865
|
+
*/
|
|
1866
|
+
currentPassword: z10.string(),
|
|
1867
|
+
/**
|
|
1868
|
+
* revoke all sessions that are not the
|
|
1869
|
+
* current one logged in by the user
|
|
1870
|
+
*/
|
|
1871
|
+
revokeOtherSessions: z10.boolean().optional()
|
|
1872
|
+
}),
|
|
1873
|
+
use: [sessionMiddleware]
|
|
1874
|
+
},
|
|
1875
|
+
async (ctx) => {
|
|
1876
|
+
const { newPassword, currentPassword, revokeOtherSessions } = ctx.body;
|
|
1877
|
+
const session = ctx.context.session;
|
|
1878
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
1879
|
+
if (newPassword.length < minPasswordLength) {
|
|
1880
|
+
ctx.context.logger.error("Password is too short");
|
|
1881
|
+
return ctx.json(null, {
|
|
1882
|
+
status: 400,
|
|
1883
|
+
body: { message: "Password is too short" }
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
1887
|
+
if (newPassword.length > maxPasswordLength) {
|
|
1888
|
+
ctx.context.logger.error("Password is too long");
|
|
1889
|
+
return ctx.json(null, {
|
|
1890
|
+
status: 400,
|
|
1891
|
+
body: { message: "Password is too long" }
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(
|
|
1895
|
+
session.user.id
|
|
1896
|
+
);
|
|
1897
|
+
const account = accounts.find(
|
|
1898
|
+
(account2) => account2.providerId === "credential" && account2.password
|
|
1899
|
+
);
|
|
1900
|
+
if (!account || !account.password) {
|
|
1901
|
+
return ctx.json(null, {
|
|
1902
|
+
status: 400,
|
|
1903
|
+
body: { message: "User does not have a password" }
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
1907
|
+
const verify = await ctx.context.password.verify(
|
|
1908
|
+
account.password,
|
|
1909
|
+
currentPassword
|
|
1910
|
+
);
|
|
1911
|
+
if (!verify) {
|
|
1912
|
+
return ctx.json(null, {
|
|
1913
|
+
status: 400,
|
|
1914
|
+
body: { message: "Invalid password" }
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
await ctx.context.internalAdapter.updateAccount(account.id, {
|
|
1918
|
+
password: passwordHash
|
|
1919
|
+
});
|
|
1920
|
+
if (revokeOtherSessions) {
|
|
1921
|
+
await ctx.context.internalAdapter.deleteSessions(session.user.id);
|
|
1922
|
+
const newSession = await ctx.context.internalAdapter.createSession(
|
|
1923
|
+
session.user.id,
|
|
1924
|
+
ctx.headers
|
|
1925
|
+
);
|
|
1926
|
+
if (!newSession) {
|
|
1927
|
+
return ctx.json(null, {
|
|
1928
|
+
status: 500,
|
|
1929
|
+
body: { message: "Failed to create session" }
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
await setSessionCookie(ctx, newSession.id);
|
|
1933
|
+
}
|
|
1934
|
+
return ctx.json(session.user);
|
|
1935
|
+
}
|
|
1936
|
+
);
|
|
1937
|
+
var setPassword = createAuthEndpoint(
|
|
1938
|
+
"/user/set-password",
|
|
1939
|
+
{
|
|
1940
|
+
method: "POST",
|
|
1941
|
+
body: z10.object({
|
|
1942
|
+
/**
|
|
1943
|
+
* The new password to set
|
|
1944
|
+
*/
|
|
1945
|
+
newPassword: z10.string()
|
|
1946
|
+
}),
|
|
1947
|
+
use: [sessionMiddleware]
|
|
1948
|
+
},
|
|
1949
|
+
async (ctx) => {
|
|
1950
|
+
const { newPassword } = ctx.body;
|
|
1951
|
+
const session = ctx.context.session;
|
|
1952
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
1953
|
+
if (newPassword.length < minPasswordLength) {
|
|
1954
|
+
ctx.context.logger.error("Password is too short");
|
|
1955
|
+
return ctx.json(null, {
|
|
1956
|
+
status: 400,
|
|
1957
|
+
body: { message: "Password is too short" }
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
1961
|
+
if (newPassword.length > maxPasswordLength) {
|
|
1962
|
+
ctx.context.logger.error("Password is too long");
|
|
1963
|
+
return ctx.json(null, {
|
|
1964
|
+
status: 400,
|
|
1965
|
+
body: { message: "Password is too long" }
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(
|
|
1969
|
+
session.user.id
|
|
1970
|
+
);
|
|
1971
|
+
const account = accounts.find(
|
|
1972
|
+
(account2) => account2.providerId === "credential" && account2.password
|
|
1973
|
+
);
|
|
1974
|
+
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
1975
|
+
if (!account) {
|
|
1976
|
+
await ctx.context.internalAdapter.linkAccount({
|
|
1977
|
+
id: generateRandomString2(32, alphabet2("a-z", "0-9", "A-Z")),
|
|
1978
|
+
userId: session.user.id,
|
|
1979
|
+
providerId: "credential",
|
|
1980
|
+
accountId: session.user.id,
|
|
1981
|
+
password: passwordHash
|
|
1982
|
+
});
|
|
1983
|
+
return ctx.json(session.user);
|
|
1984
|
+
}
|
|
1985
|
+
return ctx.json(null, {
|
|
1986
|
+
status: 400,
|
|
1987
|
+
body: { message: "User already has a password" }
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
);
|
|
1991
|
+
|
|
1992
|
+
// src/api/routes/csrf.ts
|
|
1993
|
+
import { alphabet as alphabet3, generateRandomString as generateRandomString3 } from "oslo/crypto";
|
|
1994
|
+
var getCSRFToken = createAuthEndpoint(
|
|
1995
|
+
"/csrf",
|
|
1996
|
+
{
|
|
1997
|
+
method: "GET",
|
|
1998
|
+
metadata: HIDE_METADATA
|
|
1999
|
+
},
|
|
2000
|
+
async (ctx) => {
|
|
2001
|
+
const csrfToken = await ctx.getSignedCookie(
|
|
2002
|
+
ctx.context.authCookies.csrfToken.name,
|
|
2003
|
+
ctx.context.secret
|
|
2004
|
+
);
|
|
2005
|
+
if (csrfToken) {
|
|
2006
|
+
return {
|
|
2007
|
+
csrfToken
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
const token = generateRandomString3(32, alphabet3("a-z", "0-9", "A-Z"));
|
|
2011
|
+
const hash = await hs256(ctx.context.secret, token);
|
|
2012
|
+
const cookie = `${token}!${hash}`;
|
|
2013
|
+
await ctx.setSignedCookie(
|
|
2014
|
+
ctx.context.authCookies.csrfToken.name,
|
|
2015
|
+
cookie,
|
|
2016
|
+
ctx.context.secret,
|
|
2017
|
+
ctx.context.authCookies.csrfToken.options
|
|
2018
|
+
);
|
|
2019
|
+
return {
|
|
2020
|
+
csrfToken: token
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
);
|
|
2024
|
+
|
|
2025
|
+
// src/api/routes/error.ts
|
|
2026
|
+
var html = (errorCode = "Unknown") => `<!DOCTYPE html>
|
|
3
2027
|
<html lang="en">
|
|
4
2028
|
<head>
|
|
5
2029
|
<meta charset="UTF-8">
|
|
@@ -76,9 +2100,1650 @@ import{APIError as ir,createRouter as ar}from"better-call";import{APIError as ce
|
|
|
76
2100
|
<h1>Better Auth Error</h1>
|
|
77
2101
|
<p>We encountered an issue while processing your request. Please try again or contact the application owner if the problem persists.</p>
|
|
78
2102
|
<a href="/" id="returnLink" class="btn">Return to Application</a>
|
|
79
|
-
<div class="error-code">Error Code: <span id="errorCode">${
|
|
2103
|
+
<div class="error-code">Error Code: <span id="errorCode">${errorCode}</span></div>
|
|
80
2104
|
</div>
|
|
81
2105
|
</body>
|
|
82
|
-
</html>`,He=g("/error",{method:"GET",metadata:L},async e=>{let t=new URL(e.request?.url||"").searchParams.get("error")||"Unknown";return new Response(Yt(t),{headers:{"Content-Type":"text/html"}})});var Ge=g("/ok",{method:"GET",metadata:L},async e=>e.json({ok:!0}));import{alphabet as Ke,generateRandomString as We}from"oslo/crypto";import{z as O}from"zod";var Je=g("/sign-up/email",{method:"POST",query:O.object({currentURL:O.string().optional()}).optional(),body:O.object({name:O.string(),email:O.string(),password:O.string(),image:O.string().optional(),callbackURL:O.string().optional()})},async e=>{if(!e.context.options.emailAndPassword?.enabled)return e.json(null,{status:400,body:{message:"Email and password is not enabled"}});let{name:t,email:o,password:r,image:n}=e.body;if(!O.string().email().safeParse(o).success)return e.json(null,{status:400,body:{message:"Invalid email address"}});let i=e.context.password.config.minPasswordLength;if(r.length<i)return e.context.logger.error("Password is too short"),e.json(null,{status:400,body:{message:"Password is too short"}});let a=e.context.password.config.maxPasswordLength;if(r.length>a)return e.context.logger.error("Password is too long"),e.json(null,{status:400,body:{message:"Password is too long"}});let d=await e.context.internalAdapter.findUserByEmail(o),c=await e.context.password.hash(r);if(d?.user)return e.json(null,{status:400,body:{message:"User already exists"}});let l=await e.context.internalAdapter.createUser({id:We(32,Ke("a-z","0-9","A-Z")),email:o.toLowerCase(),name:t,image:n,emailVerified:!1,createdAt:new Date,updatedAt:new Date});if(!l)return e.json(null,{status:400,body:{message:"Could not create user"}});await e.context.internalAdapter.linkAccount({id:We(32,Ke("a-z","0-9","A-Z")),userId:l.id,providerId:"credential",accountId:l.id,password:c});let u=await e.context.internalAdapter.createSession(l.id,e.request);if(!u)return e.json(null,{status:400,body:{message:"Could not create session"}});if(await P(e,u.id),e.context.options.emailAndPassword.sendEmailVerificationOnSignUp){let p=await ne(e.context.secret,l.email),m=`${e.context.baseURL}/verify-email?token=${p}&callbackURL=${e.body.callbackURL||e.query?.currentURL||"/"}`;await e.context.options.emailAndPassword.sendVerificationEmail?.(l.email,m,p)}return e.json({user:l,session:u},{body:e.body.callbackURL?{url:e.body.callbackURL,redirect:!0}:{user:l,session:u}})});import se from"chalk";function er(e,t,o){let r=Date.now(),n=t*1e3;return r-o.lastRequest<n&&o.count>=e}function tr(e){return new Response(JSON.stringify({message:"Too many requests. Please try again later."}),{status:429,statusText:"Too Many Requests",headers:{"X-Retry-After":e.toString()}})}function rr(e,t){let o=Date.now(),r=t*1e3;return Math.ceil((e+r-o)/1e3)}function or(e,t){let o=t??"rateLimit",r=e.adapter;return{get:async n=>await r.findOne({model:o,where:[{field:"key",value:n}]}),set:async(n,s,i)=>{try{i?await r.update({model:t??"rateLimit",where:[{field:"key",value:n}],update:{count:s.count,lastRequest:s.lastRequest}}):await r.create({model:t??"rateLimit",data:{key:n,count:s.count,lastRequest:s.lastRequest}})}catch(a){T.error("Error setting rate limit",a)}}}}var Ze=new Map;function nr(e){return e.rateLimit.customStorage?e.rateLimit.customStorage:e.rateLimit.storage==="memory"?{async get(o){return Ze.get(o)},async set(o,r,n){Ze.set(o,r)}}:or(e,e.rateLimit.tableName)}async function Qe(e,t){if(!t.rateLimit.enabled)return;let o=t.baseURL,r=e.url.replace(o,""),n=t.rateLimit.window,s=t.rateLimit.max,i=M(e)+r,d=sr().find(p=>p.pathMatcher(r));d&&(n=d.window,s=d.max);for(let p of t.options.plugins||[])if(p.rateLimit){let m=p.rateLimit.find(f=>f.pathMatcher(r));if(m){n=m.window,s=m.max;break}}if(t.rateLimit.customRules){let p=t.rateLimit.customRules[r];p&&(n=p.window,s=p.max)}let c=nr(t),l=await c.get(i),u=Date.now();if(!l)await c.set(i,{key:i,count:1,lastRequest:u});else{let p=u-l.lastRequest;if(er(s,n,l)){let m=rr(l.lastRequest,n);return tr(m)}else p>n*1e3?await c.set(i,{...l,count:1,lastRequest:u}):await c.set(i,{...l,count:l.count+1,lastRequest:u})}}function sr(){return[{pathMatcher(t){return t.startsWith("/sign-in")||t.startsWith("/sign-up")},window:10,max:7}]}function ie(e,t){let o=t.plugins?.reduce((a,d)=>({...a,...d.endpoints}),{}),r=t.plugins?.map(a=>a.middlewares?.map(d=>{let c=async l=>d.middleware({...l,context:{...e,...l.context}});return c.path=d.path,c.options=d.middleware.options,c.headers=d.middleware.headers,{path:d.path,middleware:c}})).filter(a=>a!==void 0).flat()||[],s={...{signInOAuth:Se,callbackOAuth:Ee,getCSRFToken:Me,getSession:ee(),signOut:_e,signUpEmail:Je,signInEmail:Oe,forgetPassword:qe,resetPassword:De,verifyEmail:$e,sendVerificationEmail:Ne,changePassword:ze,setPassword:Ve,updateUser:Fe,forgetPasswordCallback:je,listSessions:Pe(),revokeSession:ve,revokeSessions:Ie},...o,ok:Ge,error:He},i={};for(let[a,d]of Object.entries(s))i[a]=async c=>{let l=await e,p=await d({...c,context:{...l,...c.context}});for(let m of t.plugins||[])if(m.hooks?.after){for(let f of m.hooks.after)if(f.matcher(c)){let h=Object.assign(c,{context:{...e,returned:p}}),A=await f.handler(h);A&&"response"in A&&(p=A.response)}}return p},i[a].path=d.path,i[a].method=d.method,i[a].options=d.options,i[a].headers=d.headers;return{api:i,middlewares:r}}var Xe=(e,t)=>{let{api:o,middlewares:r}=ie(e,t),n=new URL(e.baseURL).pathname;return ar(o,{extraContext:e,basePath:n,routerMiddleware:[{path:"/**",middleware:ue},...r],async onRequest(s){for(let i of e.options.plugins||[])if(i.onRequest){let a=await i.onRequest(s,e);if(a)return a}return Qe(s,e)},async onResponse(s){for(let i of e.options.plugins||[])if(i.onResponse){let a=await i.onResponse(s,e);if(a)return a.response}return s},onError(s){let i=t.logger?.verboseLogging?T:void 0;if(t.logger?.disabled!==!0)if(s instanceof ir)i?.warn(s);else if(typeof s=="object"&&s!==null&&"message"in s){let a=s.message;if(!a||typeof a!="string"){i?.error(s);return}a.includes("no such table")?T?.error(`Please run ${se.green("npx better-auth migrate")} to create the tables. There are missing tables in your SQLite database.`):a.includes("relation")&&a.includes("does not exist")?T.error(`Please run ${se.green("npx better-auth migrate")} to create the tables. There are missing tables in your PostgreSQL database.`):a.includes("Table")&&a.includes("doesn't exist")?T?.error(`Please run ${se.green("npx better-auth migrate")} to create the tables. There are missing tables in your MySQL database.`):i?.error(s)}else i?.error(s)}})};var C=e=>{let t=e.plugins?.reduce((d,c)=>{let l=c.schema;if(!l)return d;for(let[u,p]of Object.entries(l))d[u]={fields:{...d[u]?.fields,...p.fields},tableName:u};return d},{}),o=e.rateLimit?.storage==="database",r={rateLimit:{tableName:e.rateLimit?.tableName||"rateLimit",fields:{key:{type:"string"},count:{type:"number"},lastRequest:{type:"number"}}}},{user:n,session:s,account:i,...a}=t||{};return{user:{tableName:e.user?.modelName||"user",fields:{name:{type:"string",required:!0},email:{type:"string",unique:!0,required:!0},emailVerified:{type:"boolean",defaultValue:()=>!1,required:!0},image:{type:"string",required:!1},createdAt:{type:"date",defaultValue:()=>new Date,required:!0},updatedAt:{type:"date",defaultValue:()=>new Date,required:!0},...n?.fields},order:0},session:{tableName:e.session?.modelName||"session",fields:{expiresAt:{type:"date",required:!0},ipAddress:{type:"string",required:!1},userAgent:{type:"string",required:!1},userId:{type:"string",references:{model:"user",field:"id",onDelete:"cascade"},required:!0},...s?.fields},order:1},account:{tableName:e.account?.modelName||"account",fields:{accountId:{type:"string",required:!0},providerId:{type:"string",required:!0},userId:{type:"string",references:{model:"user",field:"id",onDelete:"cascade"},required:!0},accessToken:{type:"string",required:!1},refreshToken:{type:"string",required:!1},idToken:{type:"string",required:!1},expiresAt:{type:"date",required:!1},password:{type:"string",required:!1},...i?.fields},order:2},...a,...o?r:{}}};import{Kysely as dr}from"kysely";import{MysqlDialect as Ye,PostgresDialect as et,SqliteDialect as tt}from"kysely";var cr=async e=>{if(!e.database)return;if("createDriver"in e.database)return e.database;let t;if("provider"in e.database){let o=e.database.provider,r=e.database?.url?.trim();if(o==="postgres"){let s=(await import("pg").catch(i=>{throw new k("Please install `pg` to use postgres database")})).Pool;t=new et({pool:new s({connectionString:r})})}if(o==="mysql")try{let{createPool:n}=await import("mysql2/promise").catch(a=>{throw new k("Please install `mysql2` to use mysql database")}),s=new URL(r),i=n({host:s.hostname,user:s.username,password:s.password,database:s.pathname.split("/")[1],port:Number(s.port)});t=new Ye({pool:i})}catch(n){if(n instanceof TypeError)throw new k("Invalid database URL")}if(o==="sqlite")try{let n=await import("better-sqlite3").catch(a=>{throw new k("Please install `better-sqlite3` to use sqlite database")}),s=n.default||n;if(!s)throw new k("Failed to import better-sqlite3. Please ensure `better-sqlite3` is properly installed.");let i=new s(r);t=new tt({database:i})}catch(n){throw console.error(n),new k("Failed to initialize SQLite. Please ensure `better-sqlite3` is properly installed.")}}return t},q=async e=>{let t=await cr(e);return t&&new dr({dialect:t})},G=e=>{if("provider"in e.database)return e.database.provider;if("dialect"in e.database){if(e.database.dialect instanceof et)return"postgres";if(e.database.dialect instanceof Ye)return"mysql";if(e.database.dialect instanceof tt)return"sqlite"}return"sqlite"};import"kysely";function lr(e){return e.plugins?.flatMap(o=>Object.keys(o.schema||{}).map(r=>{let s=(o.schema||{})[r];if(!s?.disableMigration)return{tableName:r,fields:s?.fields}}).filter(r=>r!==void 0))||[]}function rt(e){let t=C(e),o=lr(e);return[t.user,t.session,t.account,...o].reduce((n,s)=>(n[s.tableName]={fields:{...n[s.tableName]?.fields,...s.fields}},n),{})}var ur={string:["character varying","text"],number:["int4","integer","bigint","smallint","numeric","real","double precision"],boolean:["bool","boolean"],date:["timestamp","date"]},pr={string:["varchar","text"],number:["integer","int","bigint","smallint","decimal","float","double"],boolean:["boolean"],date:["date","datetime"]},mr={string:["TEXT"],number:["INTEGER","REAL"],boolean:["INTEGER","BOOLEAN"],date:["DATE","INTEGER"]},fr={postgres:ur,mysql:pr,sqlite:mr};function gr(e,t,o){return fr[o][t].map(i=>i.toLowerCase()).includes(e.toLowerCase())}async function ot(e){let t=rt(e),o=G(e),r=await q(e);r||(T.error("Invalid database configuration."),process.exit(1));let n=await r.introspection.getTables(),s=[],i=[];for(let[u,p]of Object.entries(t)){let m=n.find(y=>y.name===u);if(!m){let y=s.findIndex(B=>B.table===u),h={table:u,fields:p.fields,order:p.order||1/0},A=s.findIndex(B=>(B.order||1/0)>h.order);A===-1?y===-1?s.push(h):s[y].fields={...s[y].fields,...p.fields}:s.splice(A,0,h);continue}let f={};for(let[y,h]of Object.entries(p.fields)){let A=m.columns.find(B=>B.name===y);if(!A){f[y]=h;continue}gr(A.dataType,h.type,o)||T.warn(`Field ${y} in table ${u} has a different type in the database. Expected ${h.type} but got ${A.dataType}.`)}Object.keys(f).length>0&&i.push({table:u,fields:f,order:p.order||1/0})}let a=[];function d(u){let p={string:"text",boolean:"boolean",number:"integer",date:"date"};return o==="mysql"&&u==="string"?"varchar(255)":p[u]}if(i.length)for(let u of i)for(let[p,m]of Object.entries(u.fields)){let f=d(m.type),y=r.schema.alterTable(u.table).addColumn(p,f,h=>(h=m.required!==!1?h.notNull():h,m.references&&(h=h.references(`${m.references.model}.${m.references.field}`)),h));a.push(y)}if(s.length)for(let u of s){let p=r.schema.createTable(u.table).addColumn("id",d("string"),m=>m.primaryKey());for(let[m,f]of Object.entries(u.fields)){let y=d(f.type);p=p.addColumn(m,y,h=>(h=f.required!==!1?h.notNull():h,f.references&&(h=h.references(`${f.references.model}.${f.references.field}`)),f.unique&&(h=h.unique()),h))}a.push(p)}async function c(){for(let u of a)await u.execute()}async function l(){return a.map(p=>p.compile().sql).join(`;
|
|
2106
|
+
</html>`;
|
|
2107
|
+
var error = createAuthEndpoint(
|
|
2108
|
+
"/error",
|
|
2109
|
+
{
|
|
2110
|
+
method: "GET",
|
|
2111
|
+
metadata: HIDE_METADATA
|
|
2112
|
+
},
|
|
2113
|
+
async (c) => {
|
|
2114
|
+
const query = new URL(c.request?.url || "").searchParams.get("error") || "Unknown";
|
|
2115
|
+
return new Response(html(query), {
|
|
2116
|
+
headers: {
|
|
2117
|
+
"Content-Type": "text/html"
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
);
|
|
2122
|
+
|
|
2123
|
+
// src/api/routes/ok.ts
|
|
2124
|
+
var ok = createAuthEndpoint(
|
|
2125
|
+
"/ok",
|
|
2126
|
+
{
|
|
2127
|
+
method: "GET",
|
|
2128
|
+
metadata: HIDE_METADATA
|
|
2129
|
+
},
|
|
2130
|
+
async (ctx) => {
|
|
2131
|
+
return ctx.json({
|
|
2132
|
+
ok: true
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
);
|
|
2136
|
+
|
|
2137
|
+
// src/api/routes/sign-up.ts
|
|
2138
|
+
import { alphabet as alphabet4, generateRandomString as generateRandomString4 } from "oslo/crypto";
|
|
2139
|
+
import { z as z11 } from "zod";
|
|
2140
|
+
var signUpEmail = createAuthEndpoint(
|
|
2141
|
+
"/sign-up/email",
|
|
2142
|
+
{
|
|
2143
|
+
method: "POST",
|
|
2144
|
+
query: z11.object({
|
|
2145
|
+
currentURL: z11.string().optional()
|
|
2146
|
+
}).optional(),
|
|
2147
|
+
body: z11.object({
|
|
2148
|
+
name: z11.string(),
|
|
2149
|
+
email: z11.string(),
|
|
2150
|
+
password: z11.string(),
|
|
2151
|
+
image: z11.string().optional(),
|
|
2152
|
+
callbackURL: z11.string().optional()
|
|
2153
|
+
})
|
|
2154
|
+
},
|
|
2155
|
+
async (ctx) => {
|
|
2156
|
+
if (!ctx.context.options.emailAndPassword?.enabled) {
|
|
2157
|
+
return ctx.json(null, {
|
|
2158
|
+
status: 400,
|
|
2159
|
+
body: {
|
|
2160
|
+
message: "Email and password is not enabled"
|
|
2161
|
+
}
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
const { name, email, password, image } = ctx.body;
|
|
2165
|
+
const isValidEmail = z11.string().email().safeParse(email);
|
|
2166
|
+
if (!isValidEmail.success) {
|
|
2167
|
+
return ctx.json(null, {
|
|
2168
|
+
status: 400,
|
|
2169
|
+
body: {
|
|
2170
|
+
message: "Invalid email address"
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
2175
|
+
if (password.length < minPasswordLength) {
|
|
2176
|
+
ctx.context.logger.error("Password is too short");
|
|
2177
|
+
return ctx.json(null, {
|
|
2178
|
+
status: 400,
|
|
2179
|
+
body: { message: "Password is too short" }
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
2183
|
+
if (password.length > maxPasswordLength) {
|
|
2184
|
+
ctx.context.logger.error("Password is too long");
|
|
2185
|
+
return ctx.json(null, {
|
|
2186
|
+
status: 400,
|
|
2187
|
+
body: { message: "Password is too long" }
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
2191
|
+
const hash = await ctx.context.password.hash(password);
|
|
2192
|
+
if (dbUser?.user) {
|
|
2193
|
+
return ctx.json(null, {
|
|
2194
|
+
status: 400,
|
|
2195
|
+
body: {
|
|
2196
|
+
message: "User already exists"
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
const createdUser = await ctx.context.internalAdapter.createUser({
|
|
2201
|
+
id: generateRandomString4(32, alphabet4("a-z", "0-9", "A-Z")),
|
|
2202
|
+
email: email.toLowerCase(),
|
|
2203
|
+
name,
|
|
2204
|
+
image,
|
|
2205
|
+
emailVerified: false,
|
|
2206
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2207
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2208
|
+
});
|
|
2209
|
+
if (!createdUser) {
|
|
2210
|
+
return ctx.json(null, {
|
|
2211
|
+
status: 400,
|
|
2212
|
+
body: {
|
|
2213
|
+
message: "Could not create user"
|
|
2214
|
+
}
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
await ctx.context.internalAdapter.linkAccount({
|
|
2218
|
+
id: generateRandomString4(32, alphabet4("a-z", "0-9", "A-Z")),
|
|
2219
|
+
userId: createdUser.id,
|
|
2220
|
+
providerId: "credential",
|
|
2221
|
+
accountId: createdUser.id,
|
|
2222
|
+
password: hash
|
|
2223
|
+
});
|
|
2224
|
+
const session = await ctx.context.internalAdapter.createSession(
|
|
2225
|
+
createdUser.id,
|
|
2226
|
+
ctx.request
|
|
2227
|
+
);
|
|
2228
|
+
if (!session) {
|
|
2229
|
+
return ctx.json(null, {
|
|
2230
|
+
status: 400,
|
|
2231
|
+
body: {
|
|
2232
|
+
message: "Could not create session"
|
|
2233
|
+
}
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
await setSessionCookie(ctx, session.id);
|
|
2237
|
+
if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) {
|
|
2238
|
+
const token = await createEmailVerificationToken(
|
|
2239
|
+
ctx.context.secret,
|
|
2240
|
+
createdUser.email
|
|
2241
|
+
);
|
|
2242
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || ctx.query?.currentURL || "/"}`;
|
|
2243
|
+
await ctx.context.options.emailAndPassword.sendVerificationEmail?.(
|
|
2244
|
+
createdUser.email,
|
|
2245
|
+
url,
|
|
2246
|
+
token
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
return ctx.json(
|
|
2250
|
+
{
|
|
2251
|
+
user: createdUser,
|
|
2252
|
+
session
|
|
2253
|
+
},
|
|
2254
|
+
{
|
|
2255
|
+
body: ctx.body.callbackURL ? {
|
|
2256
|
+
url: ctx.body.callbackURL,
|
|
2257
|
+
redirect: true
|
|
2258
|
+
} : {
|
|
2259
|
+
user: createdUser,
|
|
2260
|
+
session
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
);
|
|
2266
|
+
|
|
2267
|
+
// src/api/index.ts
|
|
2268
|
+
import chalk from "chalk";
|
|
2269
|
+
|
|
2270
|
+
// src/api/rate-limiter.ts
|
|
2271
|
+
function shouldRateLimit(max, window2, rateLimitData) {
|
|
2272
|
+
const now = Date.now();
|
|
2273
|
+
const windowInMs = window2 * 1e3;
|
|
2274
|
+
const timeSinceLastRequest = now - rateLimitData.lastRequest;
|
|
2275
|
+
return timeSinceLastRequest < windowInMs && rateLimitData.count >= max;
|
|
2276
|
+
}
|
|
2277
|
+
function rateLimitResponse(retryAfter) {
|
|
2278
|
+
return new Response(
|
|
2279
|
+
JSON.stringify({
|
|
2280
|
+
message: "Too many requests. Please try again later."
|
|
2281
|
+
}),
|
|
2282
|
+
{
|
|
2283
|
+
status: 429,
|
|
2284
|
+
statusText: "Too Many Requests",
|
|
2285
|
+
headers: {
|
|
2286
|
+
"X-Retry-After": retryAfter.toString()
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
function getRetryAfter(lastRequest, window2) {
|
|
2292
|
+
const now = Date.now();
|
|
2293
|
+
const windowInMs = window2 * 1e3;
|
|
2294
|
+
return Math.ceil((lastRequest + windowInMs - now) / 1e3);
|
|
2295
|
+
}
|
|
2296
|
+
function createDBStorage(ctx, tableName) {
|
|
2297
|
+
const model = tableName ?? "rateLimit";
|
|
2298
|
+
const db = ctx.adapter;
|
|
2299
|
+
return {
|
|
2300
|
+
get: async (key) => {
|
|
2301
|
+
const res = await db.findOne({
|
|
2302
|
+
model,
|
|
2303
|
+
where: [{ field: "key", value: key }]
|
|
2304
|
+
});
|
|
2305
|
+
return res;
|
|
2306
|
+
},
|
|
2307
|
+
set: async (key, value, _update) => {
|
|
2308
|
+
try {
|
|
2309
|
+
if (_update) {
|
|
2310
|
+
await db.update({
|
|
2311
|
+
model: tableName ?? "rateLimit",
|
|
2312
|
+
where: [{ field: "key", value: key }],
|
|
2313
|
+
update: {
|
|
2314
|
+
count: value.count,
|
|
2315
|
+
lastRequest: value.lastRequest
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
} else {
|
|
2319
|
+
await db.create({
|
|
2320
|
+
model: tableName ?? "rateLimit",
|
|
2321
|
+
data: {
|
|
2322
|
+
key,
|
|
2323
|
+
count: value.count,
|
|
2324
|
+
lastRequest: value.lastRequest
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
} catch (e) {
|
|
2329
|
+
logger.error("Error setting rate limit", e);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
var memory = /* @__PURE__ */ new Map();
|
|
2335
|
+
function getRateLimitStorage(ctx) {
|
|
2336
|
+
if (ctx.rateLimit.customStorage) {
|
|
2337
|
+
return ctx.rateLimit.customStorage;
|
|
2338
|
+
}
|
|
2339
|
+
const storage = ctx.rateLimit.storage;
|
|
2340
|
+
if (storage === "memory") {
|
|
2341
|
+
return {
|
|
2342
|
+
async get(key) {
|
|
2343
|
+
return memory.get(key);
|
|
2344
|
+
},
|
|
2345
|
+
async set(key, value, _update) {
|
|
2346
|
+
memory.set(key, value);
|
|
2347
|
+
}
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
return createDBStorage(ctx, ctx.rateLimit.tableName);
|
|
2351
|
+
}
|
|
2352
|
+
async function onRequestRateLimit(req, ctx) {
|
|
2353
|
+
if (!ctx.rateLimit.enabled) {
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
const baseURL = ctx.baseURL;
|
|
2357
|
+
const path = req.url.replace(baseURL, "");
|
|
2358
|
+
let window2 = ctx.rateLimit.window;
|
|
2359
|
+
let max = ctx.rateLimit.max;
|
|
2360
|
+
const key = getIp(req) + path;
|
|
2361
|
+
const specialRules = getDefaultSpecialRules();
|
|
2362
|
+
const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
|
|
2363
|
+
if (specialRule) {
|
|
2364
|
+
window2 = specialRule.window;
|
|
2365
|
+
max = specialRule.max;
|
|
2366
|
+
}
|
|
2367
|
+
for (const plugin of ctx.options.plugins || []) {
|
|
2368
|
+
if (plugin.rateLimit) {
|
|
2369
|
+
const matchedRule = plugin.rateLimit.find(
|
|
2370
|
+
(rule) => rule.pathMatcher(path)
|
|
2371
|
+
);
|
|
2372
|
+
if (matchedRule) {
|
|
2373
|
+
window2 = matchedRule.window;
|
|
2374
|
+
max = matchedRule.max;
|
|
2375
|
+
break;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
if (ctx.rateLimit.customRules) {
|
|
2380
|
+
const customRule = ctx.rateLimit.customRules[path];
|
|
2381
|
+
if (customRule) {
|
|
2382
|
+
window2 = customRule.window;
|
|
2383
|
+
max = customRule.max;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
const storage = getRateLimitStorage(ctx);
|
|
2387
|
+
const data = await storage.get(key);
|
|
2388
|
+
const now = Date.now();
|
|
2389
|
+
if (!data) {
|
|
2390
|
+
await storage.set(key, {
|
|
2391
|
+
key,
|
|
2392
|
+
count: 1,
|
|
2393
|
+
lastRequest: now
|
|
2394
|
+
});
|
|
2395
|
+
} else {
|
|
2396
|
+
const timeSinceLastRequest = now - data.lastRequest;
|
|
2397
|
+
if (shouldRateLimit(max, window2, data)) {
|
|
2398
|
+
const retryAfter = getRetryAfter(data.lastRequest, window2);
|
|
2399
|
+
return rateLimitResponse(retryAfter);
|
|
2400
|
+
} else if (timeSinceLastRequest > window2 * 1e3) {
|
|
2401
|
+
await storage.set(key, {
|
|
2402
|
+
...data,
|
|
2403
|
+
count: 1,
|
|
2404
|
+
lastRequest: now
|
|
2405
|
+
});
|
|
2406
|
+
} else {
|
|
2407
|
+
await storage.set(key, {
|
|
2408
|
+
...data,
|
|
2409
|
+
count: data.count + 1,
|
|
2410
|
+
lastRequest: now
|
|
2411
|
+
});
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
function getDefaultSpecialRules() {
|
|
2416
|
+
const specialRules = [
|
|
2417
|
+
{
|
|
2418
|
+
pathMatcher(path) {
|
|
2419
|
+
return path.startsWith("/sign-in") || path.startsWith("/sign-up");
|
|
2420
|
+
},
|
|
2421
|
+
window: 10,
|
|
2422
|
+
max: 7
|
|
2423
|
+
}
|
|
2424
|
+
];
|
|
2425
|
+
return specialRules;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/api/index.ts
|
|
2429
|
+
function getEndpoints(ctx, options) {
|
|
2430
|
+
const pluginEndpoints = options.plugins?.reduce(
|
|
2431
|
+
(acc, plugin) => {
|
|
2432
|
+
return {
|
|
2433
|
+
...acc,
|
|
2434
|
+
...plugin.endpoints
|
|
2435
|
+
};
|
|
2436
|
+
},
|
|
2437
|
+
{}
|
|
2438
|
+
);
|
|
2439
|
+
const middlewares = options.plugins?.map(
|
|
2440
|
+
(plugin) => plugin.middlewares?.map((m) => {
|
|
2441
|
+
const middleware = async (context) => {
|
|
2442
|
+
return m.middleware({
|
|
2443
|
+
...context,
|
|
2444
|
+
context: {
|
|
2445
|
+
...ctx,
|
|
2446
|
+
...context.context
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
};
|
|
2450
|
+
middleware.path = m.path;
|
|
2451
|
+
middleware.options = m.middleware.options;
|
|
2452
|
+
middleware.headers = m.middleware.headers;
|
|
2453
|
+
return {
|
|
2454
|
+
path: m.path,
|
|
2455
|
+
middleware
|
|
2456
|
+
};
|
|
2457
|
+
})
|
|
2458
|
+
).filter((plugin) => plugin !== void 0).flat() || [];
|
|
2459
|
+
const baseEndpoints = {
|
|
2460
|
+
signInOAuth,
|
|
2461
|
+
callbackOAuth,
|
|
2462
|
+
getCSRFToken,
|
|
2463
|
+
getSession: getSession(),
|
|
2464
|
+
signOut,
|
|
2465
|
+
signUpEmail,
|
|
2466
|
+
signInEmail,
|
|
2467
|
+
forgetPassword,
|
|
2468
|
+
resetPassword,
|
|
2469
|
+
verifyEmail,
|
|
2470
|
+
sendVerificationEmail,
|
|
2471
|
+
changePassword,
|
|
2472
|
+
setPassword,
|
|
2473
|
+
updateUser,
|
|
2474
|
+
forgetPasswordCallback,
|
|
2475
|
+
listSessions: listSessions(),
|
|
2476
|
+
revokeSession,
|
|
2477
|
+
revokeSessions
|
|
2478
|
+
};
|
|
2479
|
+
const endpoints = {
|
|
2480
|
+
...baseEndpoints,
|
|
2481
|
+
...pluginEndpoints,
|
|
2482
|
+
ok,
|
|
2483
|
+
error
|
|
2484
|
+
};
|
|
2485
|
+
let api = {};
|
|
2486
|
+
for (const [key, value] of Object.entries(endpoints)) {
|
|
2487
|
+
api[key] = async (context) => {
|
|
2488
|
+
const c = await ctx;
|
|
2489
|
+
const endpointRes = await value({
|
|
2490
|
+
...context,
|
|
2491
|
+
context: {
|
|
2492
|
+
...c,
|
|
2493
|
+
...context.context
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
let response = endpointRes;
|
|
2497
|
+
for (const plugin of options.plugins || []) {
|
|
2498
|
+
if (plugin.hooks?.after) {
|
|
2499
|
+
for (const hook of plugin.hooks.after) {
|
|
2500
|
+
const match = hook.matcher(context);
|
|
2501
|
+
if (match) {
|
|
2502
|
+
const obj = Object.assign(context, {
|
|
2503
|
+
context: {
|
|
2504
|
+
...ctx,
|
|
2505
|
+
returned: response
|
|
2506
|
+
}
|
|
2507
|
+
});
|
|
2508
|
+
const hookRes = await hook.handler(obj);
|
|
2509
|
+
if (hookRes && "response" in hookRes) {
|
|
2510
|
+
response = hookRes.response;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
return response;
|
|
2517
|
+
};
|
|
2518
|
+
api[key].path = value.path;
|
|
2519
|
+
api[key].method = value.method;
|
|
2520
|
+
api[key].options = value.options;
|
|
2521
|
+
api[key].headers = value.headers;
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
api,
|
|
2525
|
+
middlewares
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
var router = (ctx, options) => {
|
|
2529
|
+
const { api, middlewares } = getEndpoints(ctx, options);
|
|
2530
|
+
const basePath = new URL(ctx.baseURL).pathname;
|
|
2531
|
+
return createRouter(api, {
|
|
2532
|
+
extraContext: ctx,
|
|
2533
|
+
basePath,
|
|
2534
|
+
routerMiddleware: [
|
|
2535
|
+
{
|
|
2536
|
+
path: "/**",
|
|
2537
|
+
middleware: csrfMiddleware
|
|
2538
|
+
},
|
|
2539
|
+
...middlewares
|
|
2540
|
+
],
|
|
2541
|
+
async onRequest(req) {
|
|
2542
|
+
for (const plugin of ctx.options.plugins || []) {
|
|
2543
|
+
if (plugin.onRequest) {
|
|
2544
|
+
const response = await plugin.onRequest(req, ctx);
|
|
2545
|
+
if (response) {
|
|
2546
|
+
return response;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return onRequestRateLimit(req, ctx);
|
|
2551
|
+
},
|
|
2552
|
+
async onResponse(res) {
|
|
2553
|
+
for (const plugin of ctx.options.plugins || []) {
|
|
2554
|
+
if (plugin.onResponse) {
|
|
2555
|
+
const response = await plugin.onResponse(res, ctx);
|
|
2556
|
+
if (response) {
|
|
2557
|
+
return response.response;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return res;
|
|
2562
|
+
},
|
|
2563
|
+
onError(e) {
|
|
2564
|
+
const log = options.logger?.verboseLogging ? logger : void 0;
|
|
2565
|
+
if (options.logger?.disabled !== true) {
|
|
2566
|
+
if (e instanceof APIError6) {
|
|
2567
|
+
log?.warn(e);
|
|
2568
|
+
} else {
|
|
2569
|
+
if (typeof e === "object" && e !== null && "message" in e) {
|
|
2570
|
+
const errorMessage = e.message;
|
|
2571
|
+
if (!errorMessage || typeof errorMessage !== "string") {
|
|
2572
|
+
log?.error(e);
|
|
2573
|
+
return;
|
|
2574
|
+
}
|
|
2575
|
+
if (errorMessage.includes("no such table")) {
|
|
2576
|
+
logger?.error(
|
|
2577
|
+
`Please run ${chalk.green(
|
|
2578
|
+
"npx better-auth migrate"
|
|
2579
|
+
)} to create the tables. There are missing tables in your SQLite database.`
|
|
2580
|
+
);
|
|
2581
|
+
} else if (errorMessage.includes("relation") && errorMessage.includes("does not exist")) {
|
|
2582
|
+
logger.error(
|
|
2583
|
+
`Please run ${chalk.green(
|
|
2584
|
+
"npx better-auth migrate"
|
|
2585
|
+
)} to create the tables. There are missing tables in your PostgreSQL database.`
|
|
2586
|
+
);
|
|
2587
|
+
} else if (errorMessage.includes("Table") && errorMessage.includes("doesn't exist")) {
|
|
2588
|
+
logger?.error(
|
|
2589
|
+
`Please run ${chalk.green(
|
|
2590
|
+
"npx better-auth migrate"
|
|
2591
|
+
)} to create the tables. There are missing tables in your MySQL database.`
|
|
2592
|
+
);
|
|
2593
|
+
} else {
|
|
2594
|
+
log?.error(e);
|
|
2595
|
+
}
|
|
2596
|
+
} else {
|
|
2597
|
+
log?.error(e);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
};
|
|
2604
|
+
|
|
2605
|
+
// src/db/get-tables.ts
|
|
2606
|
+
var getAuthTables = (options) => {
|
|
2607
|
+
const pluginSchema = options.plugins?.reduce(
|
|
2608
|
+
(acc, plugin) => {
|
|
2609
|
+
const schema = plugin.schema;
|
|
2610
|
+
if (!schema) return acc;
|
|
2611
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
2612
|
+
acc[key] = {
|
|
2613
|
+
fields: {
|
|
2614
|
+
...acc[key]?.fields,
|
|
2615
|
+
...value.fields
|
|
2616
|
+
},
|
|
2617
|
+
tableName: key
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
return acc;
|
|
2621
|
+
},
|
|
2622
|
+
{}
|
|
2623
|
+
);
|
|
2624
|
+
const shouldAddRateLimitTable = options.rateLimit?.storage === "database";
|
|
2625
|
+
const rateLimitTable = {
|
|
2626
|
+
rateLimit: {
|
|
2627
|
+
tableName: options.rateLimit?.tableName || "rateLimit",
|
|
2628
|
+
fields: {
|
|
2629
|
+
key: {
|
|
2630
|
+
type: "string"
|
|
2631
|
+
},
|
|
2632
|
+
count: {
|
|
2633
|
+
type: "number"
|
|
2634
|
+
},
|
|
2635
|
+
lastRequest: {
|
|
2636
|
+
type: "number"
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
};
|
|
2641
|
+
const { user, session, account, ...pluginTables } = pluginSchema || {};
|
|
2642
|
+
return {
|
|
2643
|
+
user: {
|
|
2644
|
+
tableName: options.user?.modelName || "user",
|
|
2645
|
+
fields: {
|
|
2646
|
+
name: {
|
|
2647
|
+
type: "string",
|
|
2648
|
+
required: true
|
|
2649
|
+
},
|
|
2650
|
+
email: {
|
|
2651
|
+
type: "string",
|
|
2652
|
+
unique: true,
|
|
2653
|
+
required: true
|
|
2654
|
+
},
|
|
2655
|
+
emailVerified: {
|
|
2656
|
+
type: "boolean",
|
|
2657
|
+
defaultValue: () => false,
|
|
2658
|
+
required: true
|
|
2659
|
+
},
|
|
2660
|
+
image: {
|
|
2661
|
+
type: "string",
|
|
2662
|
+
required: false
|
|
2663
|
+
},
|
|
2664
|
+
createdAt: {
|
|
2665
|
+
type: "date",
|
|
2666
|
+
defaultValue: () => /* @__PURE__ */ new Date(),
|
|
2667
|
+
required: true
|
|
2668
|
+
},
|
|
2669
|
+
updatedAt: {
|
|
2670
|
+
type: "date",
|
|
2671
|
+
defaultValue: () => /* @__PURE__ */ new Date(),
|
|
2672
|
+
required: true
|
|
2673
|
+
},
|
|
2674
|
+
...user?.fields
|
|
2675
|
+
},
|
|
2676
|
+
order: 0
|
|
2677
|
+
},
|
|
2678
|
+
session: {
|
|
2679
|
+
tableName: options.session?.modelName || "session",
|
|
2680
|
+
fields: {
|
|
2681
|
+
expiresAt: {
|
|
2682
|
+
type: "date",
|
|
2683
|
+
required: true
|
|
2684
|
+
},
|
|
2685
|
+
ipAddress: {
|
|
2686
|
+
type: "string",
|
|
2687
|
+
required: false
|
|
2688
|
+
},
|
|
2689
|
+
userAgent: {
|
|
2690
|
+
type: "string",
|
|
2691
|
+
required: false
|
|
2692
|
+
},
|
|
2693
|
+
userId: {
|
|
2694
|
+
type: "string",
|
|
2695
|
+
references: {
|
|
2696
|
+
model: "user",
|
|
2697
|
+
field: "id",
|
|
2698
|
+
onDelete: "cascade"
|
|
2699
|
+
},
|
|
2700
|
+
required: true
|
|
2701
|
+
},
|
|
2702
|
+
...session?.fields
|
|
2703
|
+
},
|
|
2704
|
+
order: 1
|
|
2705
|
+
},
|
|
2706
|
+
account: {
|
|
2707
|
+
tableName: options.account?.modelName || "account",
|
|
2708
|
+
fields: {
|
|
2709
|
+
accountId: {
|
|
2710
|
+
type: "string",
|
|
2711
|
+
required: true
|
|
2712
|
+
},
|
|
2713
|
+
providerId: {
|
|
2714
|
+
type: "string",
|
|
2715
|
+
required: true
|
|
2716
|
+
},
|
|
2717
|
+
userId: {
|
|
2718
|
+
type: "string",
|
|
2719
|
+
references: {
|
|
2720
|
+
model: "user",
|
|
2721
|
+
field: "id",
|
|
2722
|
+
onDelete: "cascade"
|
|
2723
|
+
},
|
|
2724
|
+
required: true
|
|
2725
|
+
},
|
|
2726
|
+
accessToken: {
|
|
2727
|
+
type: "string",
|
|
2728
|
+
required: false
|
|
2729
|
+
},
|
|
2730
|
+
refreshToken: {
|
|
2731
|
+
type: "string",
|
|
2732
|
+
required: false
|
|
2733
|
+
},
|
|
2734
|
+
idToken: {
|
|
2735
|
+
type: "string",
|
|
2736
|
+
required: false
|
|
2737
|
+
},
|
|
2738
|
+
expiresAt: {
|
|
2739
|
+
type: "date",
|
|
2740
|
+
required: false
|
|
2741
|
+
},
|
|
2742
|
+
password: {
|
|
2743
|
+
type: "string",
|
|
2744
|
+
required: false
|
|
2745
|
+
},
|
|
2746
|
+
...account?.fields
|
|
2747
|
+
},
|
|
2748
|
+
order: 2
|
|
2749
|
+
},
|
|
2750
|
+
...pluginTables,
|
|
2751
|
+
...shouldAddRateLimitTable ? rateLimitTable : {}
|
|
2752
|
+
};
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2755
|
+
// src/adapters/kysely-adapter/dialect.ts
|
|
2756
|
+
import { Kysely } from "kysely";
|
|
2757
|
+
import {
|
|
2758
|
+
MysqlDialect,
|
|
2759
|
+
PostgresDialect,
|
|
2760
|
+
SqliteDialect
|
|
2761
|
+
} from "kysely";
|
|
2762
|
+
import "execa";
|
|
2763
|
+
import "prompts";
|
|
2764
|
+
|
|
2765
|
+
// src/cli/utils/get-package-manager.ts
|
|
2766
|
+
import { detect } from "@antfu/ni";
|
|
2767
|
+
|
|
2768
|
+
// src/adapters/kysely-adapter/dialect.ts
|
|
2769
|
+
import "ora";
|
|
2770
|
+
var getDialect = async (config2, isCli) => {
|
|
2771
|
+
if (!config2.database) {
|
|
2772
|
+
return void 0;
|
|
2773
|
+
}
|
|
2774
|
+
if ("createDriver" in config2.database) {
|
|
2775
|
+
return config2.database;
|
|
2776
|
+
}
|
|
2777
|
+
let dialect = void 0;
|
|
2778
|
+
if ("provider" in config2.database) {
|
|
2779
|
+
const provider = config2.database.provider;
|
|
2780
|
+
const connectionString = config2.database?.url?.trim();
|
|
2781
|
+
if (provider === "postgres") {
|
|
2782
|
+
const pg = await import("pg").catch(async (e) => {
|
|
2783
|
+
throw new MissingDependencyError("pg");
|
|
2784
|
+
});
|
|
2785
|
+
const Pool = pg.default?.Pool || pg.Pool;
|
|
2786
|
+
const pool = new Pool({
|
|
2787
|
+
connectionString
|
|
2788
|
+
});
|
|
2789
|
+
dialect = new PostgresDialect({
|
|
2790
|
+
pool
|
|
2791
|
+
});
|
|
2792
|
+
}
|
|
2793
|
+
if (provider === "mysql") {
|
|
2794
|
+
try {
|
|
2795
|
+
const { createPool } = await import("mysql2/promise").catch(
|
|
2796
|
+
async (e) => {
|
|
2797
|
+
throw new MissingDependencyError("mysql2");
|
|
2798
|
+
}
|
|
2799
|
+
);
|
|
2800
|
+
const params = new URL(connectionString);
|
|
2801
|
+
const pool = createPool({
|
|
2802
|
+
host: params.hostname,
|
|
2803
|
+
user: params.username,
|
|
2804
|
+
password: params.password,
|
|
2805
|
+
database: params.pathname.split("/")[1],
|
|
2806
|
+
port: Number(params.port)
|
|
2807
|
+
});
|
|
2808
|
+
dialect = new MysqlDialect({ pool });
|
|
2809
|
+
} catch (e) {
|
|
2810
|
+
if (e instanceof TypeError) {
|
|
2811
|
+
throw new BetterAuthError("Invalid database URL");
|
|
2812
|
+
}
|
|
2813
|
+
throw e;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
if (provider === "sqlite") {
|
|
2817
|
+
try {
|
|
2818
|
+
const database = await import("better-sqlite3").catch(async (e) => {
|
|
2819
|
+
throw new MissingDependencyError("better-sqlite3");
|
|
2820
|
+
});
|
|
2821
|
+
const Database = database.default || database;
|
|
2822
|
+
const db = new Database(connectionString);
|
|
2823
|
+
dialect = new SqliteDialect({
|
|
2824
|
+
database: db
|
|
2825
|
+
});
|
|
2826
|
+
} catch (e) {
|
|
2827
|
+
console.error(e);
|
|
2828
|
+
throw new BetterAuthError(
|
|
2829
|
+
"Failed to initialize SQLite. Make sure `better-sqlite3` is properly installed."
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
return dialect;
|
|
2835
|
+
};
|
|
2836
|
+
var createKyselyAdapter = async (config2, isCli) => {
|
|
2837
|
+
const dialect = await getDialect(config2, isCli);
|
|
2838
|
+
if (!dialect) {
|
|
2839
|
+
return dialect;
|
|
2840
|
+
}
|
|
2841
|
+
const db = new Kysely({
|
|
2842
|
+
dialect
|
|
2843
|
+
});
|
|
2844
|
+
return db;
|
|
2845
|
+
};
|
|
2846
|
+
var getDatabaseType = (config2) => {
|
|
2847
|
+
if ("provider" in config2.database) {
|
|
2848
|
+
return config2.database.provider;
|
|
2849
|
+
}
|
|
2850
|
+
if ("dialect" in config2.database) {
|
|
2851
|
+
if (config2.database.dialect instanceof PostgresDialect) {
|
|
2852
|
+
return "postgres";
|
|
2853
|
+
}
|
|
2854
|
+
if (config2.database.dialect instanceof MysqlDialect) {
|
|
2855
|
+
return "mysql";
|
|
2856
|
+
}
|
|
2857
|
+
if (config2.database.dialect instanceof SqliteDialect) {
|
|
2858
|
+
return "sqlite";
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
return "sqlite";
|
|
2862
|
+
};
|
|
2863
|
+
|
|
2864
|
+
// src/cli/utils/get-migration.ts
|
|
2865
|
+
import "kysely";
|
|
2866
|
+
|
|
2867
|
+
// src/cli/utils/get-schema.ts
|
|
2868
|
+
function getPluginTable(config2) {
|
|
2869
|
+
const pluginsMigrations = config2.plugins?.flatMap(
|
|
2870
|
+
(plugin) => Object.keys(plugin.schema || {}).map((key) => {
|
|
2871
|
+
const schema = plugin.schema || {};
|
|
2872
|
+
const table = schema[key];
|
|
2873
|
+
if (table?.disableMigration) {
|
|
2874
|
+
return;
|
|
2875
|
+
}
|
|
2876
|
+
return {
|
|
2877
|
+
tableName: key,
|
|
2878
|
+
fields: table?.fields
|
|
2879
|
+
};
|
|
2880
|
+
}).filter((value) => value !== void 0)
|
|
2881
|
+
) || [];
|
|
2882
|
+
return pluginsMigrations;
|
|
2883
|
+
}
|
|
2884
|
+
function getSchema(config2) {
|
|
2885
|
+
const baseSchema = getAuthTables(config2);
|
|
2886
|
+
const pluginSchema = getPluginTable(config2);
|
|
2887
|
+
const schema = [
|
|
2888
|
+
baseSchema.user,
|
|
2889
|
+
baseSchema.session,
|
|
2890
|
+
baseSchema.account,
|
|
2891
|
+
...pluginSchema
|
|
2892
|
+
].reduce((acc, curr) => {
|
|
2893
|
+
acc[curr.tableName] = {
|
|
2894
|
+
fields: {
|
|
2895
|
+
...acc[curr.tableName]?.fields,
|
|
2896
|
+
...curr.fields
|
|
2897
|
+
}
|
|
2898
|
+
};
|
|
2899
|
+
return acc;
|
|
2900
|
+
}, {});
|
|
2901
|
+
return schema;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// src/cli/utils/get-migration.ts
|
|
2905
|
+
var postgresMap = {
|
|
2906
|
+
string: ["character varying", "text"],
|
|
2907
|
+
number: [
|
|
2908
|
+
"int4",
|
|
2909
|
+
"integer",
|
|
2910
|
+
"bigint",
|
|
2911
|
+
"smallint",
|
|
2912
|
+
"numeric",
|
|
2913
|
+
"real",
|
|
2914
|
+
"double precision"
|
|
2915
|
+
],
|
|
2916
|
+
boolean: ["bool", "boolean"],
|
|
2917
|
+
date: ["timestamp", "date"]
|
|
2918
|
+
};
|
|
2919
|
+
var mysqlMap = {
|
|
2920
|
+
string: ["varchar", "text"],
|
|
2921
|
+
number: [
|
|
2922
|
+
"integer",
|
|
2923
|
+
"int",
|
|
2924
|
+
"bigint",
|
|
2925
|
+
"smallint",
|
|
2926
|
+
"decimal",
|
|
2927
|
+
"float",
|
|
2928
|
+
"double"
|
|
2929
|
+
],
|
|
2930
|
+
boolean: ["boolean"],
|
|
2931
|
+
date: ["date", "datetime"]
|
|
2932
|
+
};
|
|
2933
|
+
var sqliteMap = {
|
|
2934
|
+
string: ["TEXT"],
|
|
2935
|
+
number: ["INTEGER", "REAL"],
|
|
2936
|
+
boolean: ["INTEGER", "BOOLEAN"],
|
|
2937
|
+
// 0 or 1
|
|
2938
|
+
date: ["DATE", "INTEGER"]
|
|
2939
|
+
};
|
|
2940
|
+
var map = {
|
|
2941
|
+
postgres: postgresMap,
|
|
2942
|
+
mysql: mysqlMap,
|
|
2943
|
+
sqlite: sqliteMap
|
|
2944
|
+
};
|
|
2945
|
+
function matchType(columnDataType, fieldType, dbType) {
|
|
2946
|
+
const types = map[dbType];
|
|
2947
|
+
const type = types[fieldType].map((t) => t.toLowerCase());
|
|
2948
|
+
const matches = type.includes(columnDataType.toLowerCase());
|
|
2949
|
+
return matches;
|
|
2950
|
+
}
|
|
2951
|
+
async function getMigrations(config2) {
|
|
2952
|
+
const betterAuthSchema = getSchema(config2);
|
|
2953
|
+
const dbType = getDatabaseType(config2);
|
|
2954
|
+
const db = await createKyselyAdapter(config2);
|
|
2955
|
+
if (!db) {
|
|
2956
|
+
logger.error("Invalid database configuration.");
|
|
2957
|
+
process.exit(1);
|
|
2958
|
+
}
|
|
2959
|
+
const tableMetadata = await db.introspection.getTables();
|
|
2960
|
+
const toBeCreated = [];
|
|
2961
|
+
const toBeAdded = [];
|
|
2962
|
+
for (const [key, value] of Object.entries(betterAuthSchema)) {
|
|
2963
|
+
const table = tableMetadata.find((t) => t.name === key);
|
|
2964
|
+
if (!table) {
|
|
2965
|
+
const tIndex = toBeCreated.findIndex((t) => t.table === key);
|
|
2966
|
+
const tableData = {
|
|
2967
|
+
table: key,
|
|
2968
|
+
fields: value.fields,
|
|
2969
|
+
order: value.order || Infinity
|
|
2970
|
+
};
|
|
2971
|
+
const insertIndex = toBeCreated.findIndex(
|
|
2972
|
+
(t) => (t.order || Infinity) > tableData.order
|
|
2973
|
+
);
|
|
2974
|
+
if (insertIndex === -1) {
|
|
2975
|
+
if (tIndex === -1) {
|
|
2976
|
+
toBeCreated.push(tableData);
|
|
2977
|
+
} else {
|
|
2978
|
+
toBeCreated[tIndex].fields = {
|
|
2979
|
+
...toBeCreated[tIndex].fields,
|
|
2980
|
+
...value.fields
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
} else {
|
|
2984
|
+
toBeCreated.splice(insertIndex, 0, tableData);
|
|
2985
|
+
}
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
let toBeAddedFields = {};
|
|
2989
|
+
for (const [fieldName, field] of Object.entries(value.fields)) {
|
|
2990
|
+
const column = table.columns.find((c) => c.name === fieldName);
|
|
2991
|
+
if (!column) {
|
|
2992
|
+
toBeAddedFields[fieldName] = field;
|
|
2993
|
+
continue;
|
|
2994
|
+
}
|
|
2995
|
+
if (matchType(column.dataType, field.type, dbType)) {
|
|
2996
|
+
continue;
|
|
2997
|
+
} else {
|
|
2998
|
+
logger.warn(
|
|
2999
|
+
`Field ${fieldName} in table ${key} has a different type in the database. Expected ${field.type} but got ${column.dataType}.`
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
if (Object.keys(toBeAddedFields).length > 0) {
|
|
3004
|
+
toBeAdded.push({
|
|
3005
|
+
table: key,
|
|
3006
|
+
fields: toBeAddedFields,
|
|
3007
|
+
order: value.order || Infinity
|
|
3008
|
+
});
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
const migrations = [];
|
|
3012
|
+
function getType(type) {
|
|
3013
|
+
const typeMap = {
|
|
3014
|
+
string: "text",
|
|
3015
|
+
boolean: "boolean",
|
|
3016
|
+
number: "integer",
|
|
3017
|
+
date: "date"
|
|
3018
|
+
};
|
|
3019
|
+
if (dbType === "mysql" && type === "string") {
|
|
3020
|
+
return "varchar(255)";
|
|
3021
|
+
}
|
|
3022
|
+
return typeMap[type];
|
|
3023
|
+
}
|
|
3024
|
+
if (toBeAdded.length) {
|
|
3025
|
+
for (const table of toBeAdded) {
|
|
3026
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
3027
|
+
const type = getType(field.type);
|
|
3028
|
+
const exec = db.schema.alterTable(table.table).addColumn(fieldName, type, (col) => {
|
|
3029
|
+
col = field.required !== false ? col.notNull() : col;
|
|
3030
|
+
if (field.references) {
|
|
3031
|
+
col = col.references(
|
|
3032
|
+
`${field.references.model}.${field.references.field}`
|
|
3033
|
+
);
|
|
3034
|
+
}
|
|
3035
|
+
return col;
|
|
3036
|
+
});
|
|
3037
|
+
migrations.push(exec);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
if (toBeCreated.length) {
|
|
3042
|
+
for (const table of toBeCreated) {
|
|
3043
|
+
let dbT = db.schema.createTable(table.table).addColumn("id", getType("string"), (col) => col.primaryKey());
|
|
3044
|
+
for (const [fieldName, field] of Object.entries(table.fields)) {
|
|
3045
|
+
const type = getType(field.type);
|
|
3046
|
+
dbT = dbT.addColumn(fieldName, type, (col) => {
|
|
3047
|
+
col = field.required !== false ? col.notNull() : col;
|
|
3048
|
+
if (field.references) {
|
|
3049
|
+
col = col.references(
|
|
3050
|
+
`${field.references.model}.${field.references.field}`
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
if (field.unique) {
|
|
3054
|
+
col = col.unique();
|
|
3055
|
+
}
|
|
3056
|
+
return col;
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
migrations.push(dbT);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
async function runMigrations() {
|
|
3063
|
+
for (const migration of migrations) {
|
|
3064
|
+
await migration.execute();
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
async function compileMigrations() {
|
|
3068
|
+
const compiled = migrations.map((m) => m.compile().sql);
|
|
3069
|
+
return compiled.join(";\n\n");
|
|
3070
|
+
}
|
|
3071
|
+
return { toBeCreated, toBeAdded, runMigrations, compileMigrations };
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// src/adapters/kysely-adapter/index.ts
|
|
3075
|
+
function convertWhere(w) {
|
|
3076
|
+
if (!w)
|
|
3077
|
+
return {
|
|
3078
|
+
and: null,
|
|
3079
|
+
or: null
|
|
3080
|
+
};
|
|
3081
|
+
const and = w?.filter((w2) => w2.connector === "AND" || !w2.connector).reduce(
|
|
3082
|
+
(acc, w2) => ({
|
|
3083
|
+
...acc,
|
|
3084
|
+
[w2.field]: w2.value
|
|
3085
|
+
}),
|
|
3086
|
+
{}
|
|
3087
|
+
);
|
|
3088
|
+
const or = w?.filter((w2) => w2.connector === "OR").reduce(
|
|
3089
|
+
(acc, w2) => ({
|
|
3090
|
+
...acc,
|
|
3091
|
+
[w2.field]: w2.value
|
|
3092
|
+
}),
|
|
3093
|
+
{}
|
|
3094
|
+
);
|
|
3095
|
+
return {
|
|
3096
|
+
and: Object.keys(and).length ? and : null,
|
|
3097
|
+
or: Object.keys(or).length ? or : null
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
function transformTo(val, fields, transform) {
|
|
3101
|
+
for (const key in val) {
|
|
3102
|
+
if (val[key] === 0 && fields[key]?.type === "boolean" && transform?.boolean) {
|
|
3103
|
+
val[key] = false;
|
|
3104
|
+
}
|
|
3105
|
+
if (val[key] === 1 && fields[key]?.type === "boolean" && transform?.boolean) {
|
|
3106
|
+
val[key] = true;
|
|
3107
|
+
}
|
|
3108
|
+
if (fields[key]?.type === "date") {
|
|
3109
|
+
if (!(val[key] instanceof Date)) {
|
|
3110
|
+
val[key] = new Date(val[key]);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
return val;
|
|
3115
|
+
}
|
|
3116
|
+
function transformFrom(val, transform) {
|
|
3117
|
+
for (const key in val) {
|
|
3118
|
+
if (typeof val[key] === "boolean" && transform?.boolean) {
|
|
3119
|
+
val[key] = val[key] ? 1 : 0;
|
|
3120
|
+
}
|
|
3121
|
+
if (val[key] instanceof Date) {
|
|
3122
|
+
val[key] = val[key].toISOString();
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
return val;
|
|
3126
|
+
}
|
|
3127
|
+
var kyselyAdapter = (db, config2) => {
|
|
3128
|
+
return {
|
|
3129
|
+
id: "kysely",
|
|
3130
|
+
async create(data) {
|
|
3131
|
+
let { model, data: val, select } = data;
|
|
3132
|
+
if (config2?.transform) {
|
|
3133
|
+
val = transformFrom(val, config2.transform);
|
|
3134
|
+
}
|
|
3135
|
+
let res = await db.insertInto(model).values(val).returningAll().executeTakeFirst();
|
|
3136
|
+
if (config2?.transform) {
|
|
3137
|
+
const schema = config2.transform.schema[model];
|
|
3138
|
+
res = schema ? transformTo(val, schema, config2.transform) : res;
|
|
3139
|
+
}
|
|
3140
|
+
if (select?.length) {
|
|
3141
|
+
const data2 = res ? select.reduce((acc, cur) => {
|
|
3142
|
+
if (res?.[cur]) {
|
|
3143
|
+
return {
|
|
3144
|
+
...acc,
|
|
3145
|
+
[cur]: res[cur]
|
|
3146
|
+
};
|
|
3147
|
+
}
|
|
3148
|
+
return acc;
|
|
3149
|
+
}, {}) : null;
|
|
3150
|
+
res = data2;
|
|
3151
|
+
}
|
|
3152
|
+
return res;
|
|
3153
|
+
},
|
|
3154
|
+
async findOne(data) {
|
|
3155
|
+
const { model, where, select } = data;
|
|
3156
|
+
const { and, or } = convertWhere(where);
|
|
3157
|
+
let query = db.selectFrom(model).selectAll();
|
|
3158
|
+
if (or) {
|
|
3159
|
+
query = query.where((eb) => eb.or(or));
|
|
3160
|
+
}
|
|
3161
|
+
if (and) {
|
|
3162
|
+
query = query.where((eb) => eb.and(and));
|
|
3163
|
+
}
|
|
3164
|
+
let res = await query.executeTakeFirst();
|
|
3165
|
+
if (select?.length) {
|
|
3166
|
+
const data2 = res ? select.reduce((acc, cur) => {
|
|
3167
|
+
if (res?.[cur]) {
|
|
3168
|
+
return {
|
|
3169
|
+
...acc,
|
|
3170
|
+
[cur]: res[cur]
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
return acc;
|
|
3174
|
+
}, {}) : null;
|
|
3175
|
+
res = data2;
|
|
3176
|
+
}
|
|
3177
|
+
if (config2?.transform) {
|
|
3178
|
+
const schema = config2.transform.schema[model];
|
|
3179
|
+
res = res && schema ? transformTo(res, schema, config2.transform) : res;
|
|
3180
|
+
return res || null;
|
|
3181
|
+
}
|
|
3182
|
+
return res || null;
|
|
3183
|
+
},
|
|
3184
|
+
async findMany(data) {
|
|
3185
|
+
const { model, where } = data;
|
|
3186
|
+
let query = db.selectFrom(model);
|
|
3187
|
+
const { and, or } = convertWhere(where);
|
|
3188
|
+
if (and) {
|
|
3189
|
+
query = query.where((eb) => eb.and(and));
|
|
3190
|
+
}
|
|
3191
|
+
if (or) {
|
|
3192
|
+
query = query.where((eb) => eb.or(or));
|
|
3193
|
+
}
|
|
3194
|
+
const res = await query.selectAll().execute();
|
|
3195
|
+
if (config2?.transform) {
|
|
3196
|
+
const schema = config2.transform.schema[model];
|
|
3197
|
+
return schema ? res.map((v) => transformTo(v, schema, config2.transform)) : res;
|
|
3198
|
+
}
|
|
3199
|
+
return res;
|
|
3200
|
+
},
|
|
3201
|
+
async update(data) {
|
|
3202
|
+
let { model, where, update: val } = data;
|
|
3203
|
+
const { and, or } = convertWhere(where);
|
|
3204
|
+
if (config2?.transform) {
|
|
3205
|
+
val = transformFrom(val, config2.transform);
|
|
3206
|
+
}
|
|
3207
|
+
let query = db.updateTable(model).set(val);
|
|
3208
|
+
if (and) {
|
|
3209
|
+
query = query.where((eb) => eb.and(and));
|
|
3210
|
+
}
|
|
3211
|
+
if (or) {
|
|
3212
|
+
query = query.where((eb) => eb.or(or));
|
|
3213
|
+
}
|
|
3214
|
+
const res = await query.returningAll().executeTakeFirst() || null;
|
|
3215
|
+
if (config2?.transform) {
|
|
3216
|
+
const schema = config2.transform.schema[model];
|
|
3217
|
+
return schema ? transformTo(res, schema, config2.transform) : res;
|
|
3218
|
+
}
|
|
3219
|
+
return res;
|
|
3220
|
+
},
|
|
3221
|
+
async delete(data) {
|
|
3222
|
+
const { model, where } = data;
|
|
3223
|
+
const { and, or } = convertWhere(where);
|
|
3224
|
+
let query = db.deleteFrom(model);
|
|
3225
|
+
if (and) {
|
|
3226
|
+
query = query.where((eb) => eb.and(and));
|
|
3227
|
+
}
|
|
3228
|
+
if (or) {
|
|
3229
|
+
query = query.where((eb) => eb.or(or));
|
|
3230
|
+
}
|
|
3231
|
+
await query.execute();
|
|
3232
|
+
},
|
|
3233
|
+
async createSchema(options) {
|
|
3234
|
+
const { compileMigrations } = await getMigrations(options);
|
|
3235
|
+
const migrations = await compileMigrations();
|
|
3236
|
+
return {
|
|
3237
|
+
code: migrations,
|
|
3238
|
+
fileName: `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString()}.sql`
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
};
|
|
3242
|
+
};
|
|
3243
|
+
|
|
3244
|
+
// src/db/utils.ts
|
|
3245
|
+
async function getAdapter(options, isCli) {
|
|
3246
|
+
if (!options.database) {
|
|
3247
|
+
throw new BetterAuthError("Database configuration is required");
|
|
3248
|
+
}
|
|
3249
|
+
if ("create" in options.database) {
|
|
3250
|
+
return options.database;
|
|
3251
|
+
}
|
|
3252
|
+
const db = await createKyselyAdapter(options, isCli);
|
|
3253
|
+
if (!db) {
|
|
3254
|
+
throw new BetterAuthError("Failed to initialize database adapter");
|
|
3255
|
+
}
|
|
3256
|
+
const tables = getAuthTables(options);
|
|
3257
|
+
let schema = {};
|
|
3258
|
+
for (const table of Object.values(tables)) {
|
|
3259
|
+
schema[table.tableName] = table.fields;
|
|
3260
|
+
}
|
|
3261
|
+
return kyselyAdapter(db, {
|
|
3262
|
+
transform: {
|
|
3263
|
+
schema,
|
|
3264
|
+
date: true,
|
|
3265
|
+
boolean: getDatabaseType(options) === "sqlite"
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// src/crypto/password.ts
|
|
3271
|
+
import { scrypt } from "node:crypto";
|
|
3272
|
+
import { decodeHex, encodeHex } from "oslo/encoding";
|
|
3273
|
+
import { constantTimeEqual } from "oslo/crypto";
|
|
3274
|
+
var config = {
|
|
3275
|
+
N: 16384,
|
|
3276
|
+
r: 16,
|
|
3277
|
+
p: 1,
|
|
3278
|
+
dkLen: 64
|
|
3279
|
+
};
|
|
3280
|
+
async function generateKey(password, salt) {
|
|
3281
|
+
return await new Promise((resolve, reject) => {
|
|
3282
|
+
scrypt(
|
|
3283
|
+
password.normalize("NFKC"),
|
|
3284
|
+
salt,
|
|
3285
|
+
config.dkLen,
|
|
3286
|
+
{
|
|
3287
|
+
N: config.N,
|
|
3288
|
+
p: config.p,
|
|
3289
|
+
r: config.r,
|
|
3290
|
+
// errors when 128 * N * r > `maxmem` (approximately)
|
|
3291
|
+
maxmem: 128 * config.N * config.r * 2
|
|
3292
|
+
},
|
|
3293
|
+
(err, buff) => {
|
|
3294
|
+
if (err) return reject(err);
|
|
3295
|
+
return resolve(buff);
|
|
3296
|
+
}
|
|
3297
|
+
);
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
var hashPassword = async (password) => {
|
|
3301
|
+
const salt = encodeHex(crypto.getRandomValues(new Uint8Array(16)));
|
|
3302
|
+
const key = await generateKey(password, salt);
|
|
3303
|
+
return `${salt}:${encodeHex(key)}`;
|
|
3304
|
+
};
|
|
3305
|
+
var verifyPassword = async (hash, password) => {
|
|
3306
|
+
const [salt, key] = hash.split(":");
|
|
3307
|
+
const targetKey = await generateKey(password, salt);
|
|
3308
|
+
return constantTimeEqual(targetKey, decodeHex(key));
|
|
3309
|
+
};
|
|
3310
|
+
|
|
3311
|
+
// src/db/internal-adapter.ts
|
|
3312
|
+
import { alphabet as alphabet5, generateRandomString as generateRandomString5 } from "oslo/crypto";
|
|
3313
|
+
var createInternalAdapter = (adapter, options) => {
|
|
3314
|
+
const sessionExpiration = options.session?.expiresIn || 60 * 60 * 24 * 7;
|
|
3315
|
+
const tables = getAuthTables(options);
|
|
3316
|
+
const hooks = options.databaseHooks;
|
|
3317
|
+
async function createWithHooks(data, model) {
|
|
3318
|
+
let actualData = data;
|
|
3319
|
+
if (hooks?.[model]?.create?.before) {
|
|
3320
|
+
const result = await hooks[model].create.before(data);
|
|
3321
|
+
if (result === false) {
|
|
3322
|
+
return null;
|
|
3323
|
+
}
|
|
3324
|
+
const isObject = typeof result === "object";
|
|
3325
|
+
actualData = isObject ? result.data : result;
|
|
3326
|
+
}
|
|
3327
|
+
const created = await adapter.create({
|
|
3328
|
+
model,
|
|
3329
|
+
data
|
|
3330
|
+
});
|
|
3331
|
+
if (hooks?.[model]?.create?.after && created) {
|
|
3332
|
+
await hooks[model].create.after(created);
|
|
3333
|
+
}
|
|
3334
|
+
return created;
|
|
3335
|
+
}
|
|
3336
|
+
return {
|
|
3337
|
+
createOAuthUser: async (user, account) => {
|
|
3338
|
+
try {
|
|
3339
|
+
const createdUser = await createWithHooks(user, "user");
|
|
3340
|
+
const createdAccount = await createWithHooks(account, "account");
|
|
3341
|
+
return {
|
|
3342
|
+
user: createdUser,
|
|
3343
|
+
account: createdAccount
|
|
3344
|
+
};
|
|
3345
|
+
} catch (e) {
|
|
3346
|
+
console.log(e);
|
|
3347
|
+
return null;
|
|
3348
|
+
}
|
|
3349
|
+
},
|
|
3350
|
+
createUser: async (user) => {
|
|
3351
|
+
const createdUser = await createWithHooks(user, "user");
|
|
3352
|
+
return createdUser;
|
|
3353
|
+
},
|
|
3354
|
+
createSession: async (userId, request, dontRememberMe) => {
|
|
3355
|
+
const headers = request instanceof Request ? request.headers : request;
|
|
3356
|
+
const data = {
|
|
3357
|
+
id: generateRandomString5(32, alphabet5("a-z", "0-9", "A-Z")),
|
|
3358
|
+
userId,
|
|
3359
|
+
/**
|
|
3360
|
+
* If the user doesn't want to be remembered
|
|
3361
|
+
* set the session to expire in 1 day.
|
|
3362
|
+
* The cookie will be set to expire at the end of the session
|
|
3363
|
+
*/
|
|
3364
|
+
expiresAt: dontRememberMe ? getDate(1e3 * 60 * 60 * 24) : getDate(sessionExpiration, true),
|
|
3365
|
+
ipAddress: headers?.get("x-forwarded-for") || "",
|
|
3366
|
+
userAgent: headers?.get("user-agent") || ""
|
|
3367
|
+
};
|
|
3368
|
+
const session = await createWithHooks(data, "session");
|
|
3369
|
+
return session;
|
|
3370
|
+
},
|
|
3371
|
+
findSession: async (sessionId) => {
|
|
3372
|
+
const session = await adapter.findOne({
|
|
3373
|
+
model: tables.session.tableName,
|
|
3374
|
+
where: [
|
|
3375
|
+
{
|
|
3376
|
+
value: sessionId,
|
|
3377
|
+
field: "id"
|
|
3378
|
+
}
|
|
3379
|
+
]
|
|
3380
|
+
});
|
|
3381
|
+
if (!session) {
|
|
3382
|
+
return null;
|
|
3383
|
+
}
|
|
3384
|
+
const user = await adapter.findOne({
|
|
3385
|
+
model: tables.user.tableName,
|
|
3386
|
+
where: [
|
|
3387
|
+
{
|
|
3388
|
+
value: session.userId,
|
|
3389
|
+
field: "id"
|
|
3390
|
+
}
|
|
3391
|
+
]
|
|
3392
|
+
});
|
|
3393
|
+
if (!user) {
|
|
3394
|
+
return null;
|
|
3395
|
+
}
|
|
3396
|
+
return {
|
|
3397
|
+
session,
|
|
3398
|
+
user
|
|
3399
|
+
};
|
|
3400
|
+
},
|
|
3401
|
+
updateSession: async (sessionId, session) => {
|
|
3402
|
+
if (hooks?.session?.update?.before) {
|
|
3403
|
+
const result = await hooks.session.update.before(session);
|
|
3404
|
+
if (result === false) {
|
|
3405
|
+
return null;
|
|
3406
|
+
}
|
|
3407
|
+
session = typeof result === "object" ? result.data : result;
|
|
3408
|
+
}
|
|
3409
|
+
const updatedSession = await adapter.update({
|
|
3410
|
+
model: tables.session.tableName,
|
|
3411
|
+
where: [
|
|
3412
|
+
{
|
|
3413
|
+
field: "id",
|
|
3414
|
+
value: sessionId
|
|
3415
|
+
}
|
|
3416
|
+
],
|
|
3417
|
+
update: session
|
|
3418
|
+
});
|
|
3419
|
+
if (hooks?.session?.update?.after && updatedSession) {
|
|
3420
|
+
await hooks.session.update.after(updatedSession);
|
|
3421
|
+
}
|
|
3422
|
+
return updatedSession;
|
|
3423
|
+
},
|
|
3424
|
+
deleteSession: async (id) => {
|
|
3425
|
+
const session = await adapter.delete({
|
|
3426
|
+
model: tables.session.tableName,
|
|
3427
|
+
where: [
|
|
3428
|
+
{
|
|
3429
|
+
field: "id",
|
|
3430
|
+
value: id
|
|
3431
|
+
}
|
|
3432
|
+
]
|
|
3433
|
+
});
|
|
3434
|
+
return session;
|
|
3435
|
+
},
|
|
3436
|
+
deleteSessions: async (userId) => {
|
|
3437
|
+
return await adapter.delete({
|
|
3438
|
+
model: tables.session.tableName,
|
|
3439
|
+
where: [
|
|
3440
|
+
{
|
|
3441
|
+
field: "userId",
|
|
3442
|
+
value: userId
|
|
3443
|
+
}
|
|
3444
|
+
]
|
|
3445
|
+
});
|
|
3446
|
+
},
|
|
3447
|
+
findUserByEmail: async (email) => {
|
|
3448
|
+
const user = await adapter.findOne({
|
|
3449
|
+
model: tables.user.tableName,
|
|
3450
|
+
where: [
|
|
3451
|
+
{
|
|
3452
|
+
value: email.toLowerCase(),
|
|
3453
|
+
field: "email"
|
|
3454
|
+
}
|
|
3455
|
+
]
|
|
3456
|
+
});
|
|
3457
|
+
if (!user) return null;
|
|
3458
|
+
const accounts = await adapter.findMany({
|
|
3459
|
+
model: tables.account.tableName,
|
|
3460
|
+
where: [
|
|
3461
|
+
{
|
|
3462
|
+
value: user.id,
|
|
3463
|
+
field: "userId"
|
|
3464
|
+
}
|
|
3465
|
+
]
|
|
3466
|
+
});
|
|
3467
|
+
return {
|
|
3468
|
+
user,
|
|
3469
|
+
accounts
|
|
3470
|
+
};
|
|
3471
|
+
},
|
|
3472
|
+
findUserById: async (userId) => {
|
|
3473
|
+
const user = await adapter.findOne({
|
|
3474
|
+
model: tables.user.tableName,
|
|
3475
|
+
where: [
|
|
3476
|
+
{
|
|
3477
|
+
field: "id",
|
|
3478
|
+
value: userId
|
|
3479
|
+
}
|
|
3480
|
+
]
|
|
3481
|
+
});
|
|
3482
|
+
return user;
|
|
3483
|
+
},
|
|
3484
|
+
linkAccount: async (account) => {
|
|
3485
|
+
const _account = await createWithHooks(account, "account");
|
|
3486
|
+
return _account;
|
|
3487
|
+
},
|
|
3488
|
+
updateUserByEmail: async (email, data) => {
|
|
3489
|
+
if (hooks?.user?.update?.before) {
|
|
3490
|
+
const result = await hooks.user.update.before(data);
|
|
3491
|
+
if (result === false) {
|
|
3492
|
+
return null;
|
|
3493
|
+
}
|
|
3494
|
+
data = typeof result === "object" ? result.data : result;
|
|
3495
|
+
}
|
|
3496
|
+
const user = await adapter.update({
|
|
3497
|
+
model: tables.user.tableName,
|
|
3498
|
+
where: [
|
|
3499
|
+
{
|
|
3500
|
+
value: email,
|
|
3501
|
+
field: "email"
|
|
3502
|
+
}
|
|
3503
|
+
],
|
|
3504
|
+
update: data
|
|
3505
|
+
});
|
|
3506
|
+
if (hooks?.user?.update?.after && user) {
|
|
3507
|
+
await hooks.user.update.after(user);
|
|
3508
|
+
}
|
|
3509
|
+
return user;
|
|
3510
|
+
},
|
|
3511
|
+
updatePassword: async (userId, password) => {
|
|
3512
|
+
const account = await adapter.update({
|
|
3513
|
+
model: tables.account.tableName,
|
|
3514
|
+
where: [
|
|
3515
|
+
{
|
|
3516
|
+
value: userId,
|
|
3517
|
+
field: "userId"
|
|
3518
|
+
},
|
|
3519
|
+
{
|
|
3520
|
+
field: "providerId",
|
|
3521
|
+
value: "credential"
|
|
3522
|
+
}
|
|
3523
|
+
],
|
|
3524
|
+
update: {
|
|
3525
|
+
password
|
|
3526
|
+
}
|
|
3527
|
+
});
|
|
3528
|
+
return account;
|
|
3529
|
+
},
|
|
3530
|
+
findAccounts: async (userId) => {
|
|
3531
|
+
const accounts = await adapter.findMany({
|
|
3532
|
+
model: tables.account.tableName,
|
|
3533
|
+
where: [
|
|
3534
|
+
{
|
|
3535
|
+
field: "userId",
|
|
3536
|
+
value: userId
|
|
3537
|
+
}
|
|
3538
|
+
]
|
|
3539
|
+
});
|
|
3540
|
+
return accounts;
|
|
3541
|
+
},
|
|
3542
|
+
updateAccount: async (accountId, data) => {
|
|
3543
|
+
if (hooks?.account?.update?.before) {
|
|
3544
|
+
const result = await hooks.account.update.before(data);
|
|
3545
|
+
if (result === false) {
|
|
3546
|
+
return null;
|
|
3547
|
+
}
|
|
3548
|
+
data = typeof result === "object" ? result.data : result;
|
|
3549
|
+
}
|
|
3550
|
+
const account = await adapter.update({
|
|
3551
|
+
model: tables.account.tableName,
|
|
3552
|
+
where: [
|
|
3553
|
+
{
|
|
3554
|
+
field: "id",
|
|
3555
|
+
value: accountId
|
|
3556
|
+
}
|
|
3557
|
+
],
|
|
3558
|
+
update: data
|
|
3559
|
+
});
|
|
3560
|
+
if (hooks?.account?.update?.after && account) {
|
|
3561
|
+
await hooks.account.update.after(account);
|
|
3562
|
+
}
|
|
3563
|
+
return account;
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
};
|
|
3567
|
+
|
|
3568
|
+
// src/utils/constants.ts
|
|
3569
|
+
var DEFAULT_SECRET = "better-auth-secret-123456789";
|
|
3570
|
+
|
|
3571
|
+
// src/internal-plugins/cross-subdomain/index.ts
|
|
3572
|
+
var crossSubdomainCookies = (options) => {
|
|
3573
|
+
return {
|
|
3574
|
+
id: "cross-subdomain-cookies",
|
|
3575
|
+
async onResponse(response, ctx) {
|
|
3576
|
+
const setCookie = response.headers.get("set-cookie");
|
|
3577
|
+
if (!setCookie) return;
|
|
3578
|
+
const baseURL = ctx.baseURL;
|
|
3579
|
+
const cookieParts = setCookie.split(";");
|
|
3580
|
+
const domain = options?.domainName || new URL(baseURL).hostname;
|
|
3581
|
+
const authCookies = ctx.authCookies;
|
|
3582
|
+
const cookieNamesEligibleForDomain = [
|
|
3583
|
+
authCookies.sessionToken.name,
|
|
3584
|
+
authCookies.csrfToken.name,
|
|
3585
|
+
authCookies.dontRememberToken.name
|
|
3586
|
+
];
|
|
3587
|
+
if (!cookieNamesEligibleForDomain.some((name) => setCookie.includes(name))) {
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
const updatedCookies = cookieParts.map((part) => {
|
|
3591
|
+
if (!cookieNamesEligibleForDomain.some(
|
|
3592
|
+
(name) => part.toLowerCase().includes(name.toLowerCase())
|
|
3593
|
+
)) {
|
|
3594
|
+
return part;
|
|
3595
|
+
}
|
|
3596
|
+
const trimmedPart = part.trim();
|
|
3597
|
+
if (trimmedPart.toLowerCase().startsWith("domain=")) {
|
|
3598
|
+
return `Domain=${domain}`;
|
|
3599
|
+
}
|
|
3600
|
+
if (!trimmedPart.toLowerCase().includes("domain=")) {
|
|
3601
|
+
return `${trimmedPart}; Domain=${domain}`;
|
|
3602
|
+
}
|
|
3603
|
+
return trimmedPart;
|
|
3604
|
+
}).filter(
|
|
3605
|
+
(part, index, self) => index === self.findIndex((p) => p.split(";")[0] === part.split(";")[0])
|
|
3606
|
+
).join("; ");
|
|
3607
|
+
response.headers.set("set-cookie", updatedCookies);
|
|
3608
|
+
return {
|
|
3609
|
+
response
|
|
3610
|
+
};
|
|
3611
|
+
}
|
|
3612
|
+
};
|
|
3613
|
+
};
|
|
3614
|
+
|
|
3615
|
+
// src/init.ts
|
|
3616
|
+
var init = async (opts) => {
|
|
3617
|
+
const { options, context } = runPluginInit(opts);
|
|
3618
|
+
const plugins = options.plugins || [];
|
|
3619
|
+
const internalPlugins = getInternalPlugins(options);
|
|
3620
|
+
const adapter = await getAdapter(options);
|
|
3621
|
+
const db = await createKyselyAdapter(options);
|
|
3622
|
+
const baseURL = getBaseURL(options.baseURL, options.basePath) || "";
|
|
3623
|
+
const secret = options.secret || process.env.BETTER_AUTH_SECRET || process.env.AUTH_SECRET || DEFAULT_SECRET;
|
|
3624
|
+
const cookies = getCookies(options);
|
|
3625
|
+
const tables = getAuthTables(options);
|
|
3626
|
+
const socialProviders = Object.keys(options.socialProviders || {}).map((key) => {
|
|
3627
|
+
const value = options.socialProviders?.[key];
|
|
3628
|
+
if (value.enabled === false) {
|
|
3629
|
+
return null;
|
|
3630
|
+
}
|
|
3631
|
+
if (!value.clientId || !value.clientSecret) {
|
|
3632
|
+
logger.warn(
|
|
3633
|
+
`Social provider ${key} is missing clientId or clientSecret`
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3636
|
+
return oAuthProviders[key](value);
|
|
3637
|
+
}).filter((x) => x !== null);
|
|
3638
|
+
return {
|
|
3639
|
+
appName: options.appName || "Better Auth",
|
|
3640
|
+
socialProviders,
|
|
3641
|
+
options: {
|
|
3642
|
+
...options,
|
|
3643
|
+
baseURL: baseURL ? new URL(baseURL).origin : "",
|
|
3644
|
+
basePath: options.basePath || "/api/auth",
|
|
3645
|
+
plugins: plugins.concat(internalPlugins)
|
|
3646
|
+
},
|
|
3647
|
+
tables,
|
|
3648
|
+
baseURL,
|
|
3649
|
+
sessionConfig: {
|
|
3650
|
+
updateAge: options.session?.updateAge || 24 * 60 * 60,
|
|
3651
|
+
// 24 hours
|
|
3652
|
+
expiresIn: options.session?.expiresIn || 60 * 60 * 24 * 7
|
|
3653
|
+
// 7 days
|
|
3654
|
+
},
|
|
3655
|
+
secret,
|
|
3656
|
+
rateLimit: {
|
|
3657
|
+
...options.rateLimit,
|
|
3658
|
+
enabled: options.rateLimit?.enabled ?? process.env.NODE_ENV !== "development",
|
|
3659
|
+
window: options.rateLimit?.window || 60,
|
|
3660
|
+
max: options.rateLimit?.max || 100,
|
|
3661
|
+
storage: options.rateLimit?.storage || "memory"
|
|
3662
|
+
},
|
|
3663
|
+
authCookies: cookies,
|
|
3664
|
+
logger: createLogger({
|
|
3665
|
+
disabled: options.logger?.disabled || false
|
|
3666
|
+
}),
|
|
3667
|
+
db,
|
|
3668
|
+
password: {
|
|
3669
|
+
hash: options.emailAndPassword?.password?.hash || hashPassword,
|
|
3670
|
+
verify: options.emailAndPassword?.password?.verify || verifyPassword,
|
|
3671
|
+
config: {
|
|
3672
|
+
minPasswordLength: options.emailAndPassword?.minPasswordLength || 8,
|
|
3673
|
+
maxPasswordLength: options.emailAndPassword?.maxPasswordLength || 128
|
|
3674
|
+
}
|
|
3675
|
+
},
|
|
3676
|
+
adapter,
|
|
3677
|
+
internalAdapter: createInternalAdapter(adapter, options),
|
|
3678
|
+
createAuthCookie: createCookieGetter(options),
|
|
3679
|
+
...context
|
|
3680
|
+
};
|
|
3681
|
+
};
|
|
3682
|
+
function runPluginInit(options) {
|
|
3683
|
+
const plugins = options.plugins || [];
|
|
3684
|
+
let context = {};
|
|
3685
|
+
for (const plugin of plugins) {
|
|
3686
|
+
if (plugin.init) {
|
|
3687
|
+
const result = plugin.init(options);
|
|
3688
|
+
if (typeof result === "object") {
|
|
3689
|
+
if (result.options) {
|
|
3690
|
+
options = {
|
|
3691
|
+
...options,
|
|
3692
|
+
...result.options
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
context = {
|
|
3696
|
+
...result
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
const { options: _, ...rest } = context;
|
|
3702
|
+
return {
|
|
3703
|
+
options,
|
|
3704
|
+
context: rest
|
|
3705
|
+
};
|
|
3706
|
+
}
|
|
3707
|
+
function getInternalPlugins(options) {
|
|
3708
|
+
const plugins = [];
|
|
3709
|
+
if (options.advanced?.crossSubDomainCookies?.enabled) {
|
|
3710
|
+
plugins.push(
|
|
3711
|
+
crossSubdomainCookies({
|
|
3712
|
+
eligibleCookies: options.advanced.crossSubDomainCookies.eligibleCookies
|
|
3713
|
+
})
|
|
3714
|
+
);
|
|
3715
|
+
}
|
|
3716
|
+
return plugins;
|
|
3717
|
+
}
|
|
83
3718
|
|
|
84
|
-
|
|
3719
|
+
// src/auth.ts
|
|
3720
|
+
var betterAuth = (options) => {
|
|
3721
|
+
const authContext = init(options);
|
|
3722
|
+
const { api } = getEndpoints(authContext, options);
|
|
3723
|
+
return {
|
|
3724
|
+
handler: async (request) => {
|
|
3725
|
+
const ctx = await authContext;
|
|
3726
|
+
const basePath = ctx.options.basePath;
|
|
3727
|
+
const url = new URL(request.url);
|
|
3728
|
+
if (!ctx.options.baseURL) {
|
|
3729
|
+
const baseURL = `${url.origin}/api/auth`;
|
|
3730
|
+
ctx.options.baseURL = baseURL;
|
|
3731
|
+
ctx.baseURL = baseURL;
|
|
3732
|
+
}
|
|
3733
|
+
if (!ctx.options.baseURL) {
|
|
3734
|
+
return new Response("Base URL not set", { status: 400 });
|
|
3735
|
+
}
|
|
3736
|
+
if (url.pathname === basePath || url.pathname === `${basePath}/`) {
|
|
3737
|
+
return new Response("Welcome to BetterAuth", { status: 200 });
|
|
3738
|
+
}
|
|
3739
|
+
const { handler } = router(ctx, options);
|
|
3740
|
+
return handler(request);
|
|
3741
|
+
},
|
|
3742
|
+
api,
|
|
3743
|
+
options,
|
|
3744
|
+
$Infer: {}
|
|
3745
|
+
};
|
|
3746
|
+
};
|
|
3747
|
+
export {
|
|
3748
|
+
betterAuth
|
|
3749
|
+
};
|