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/api.js
CHANGED
|
@@ -1,5 +1,1931 @@
|
|
|
1
|
-
|
|
2
|
-
`)}}),P=Xe();var ie=e=>{let t=new Ye(e.clientId,e.clientSecret,f("google",e.redirectURI));return{id:"google",name:"Google",createAuthorizationURL({state:o,scopes:r,codeVerifier:n,redirectURI:s}){if(!e.clientId||!e.clientSecret)throw P.error("Client Id and Client Secret is required for Google. Make sure to provide them in the options."),new I("CLIENT_ID_AND_SECRET_REQUIRED");if(!n)throw new I("codeVerifier is required for Google");let i=r||["email","profile"];return t.createAuthorizationURL(o,n,i)},validateAuthorizationCode:async(o,r,n)=>y({code:o,codeVerifier:r,redirectURI:n||f("google",e.redirectURI),options:e,tokenEndpoint:"https://oauth2.googleapis.com/token"}),async getUserInfo(o){if(!o.idToken)return null;let r=et(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 tt}from"@better-fetch/fetch";import{Spotify as rt}from"arctic";var ae=e=>{let t=new rt(e.clientId,e.clientSecret,f("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)=>y({code:o,codeVerifier:r,redirectURI:n||f("spotify",e.redirectURI),options:e,tokenEndpoint:"https://accounts.spotify.com/api/token"}),async getUserInfo(o){let{data:r,error:n}=await tt("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 ot}from"@better-fetch/fetch";import{Twitch as nt}from"arctic";var de=e=>{let t=new nt(e.clientId,e.clientSecret,f("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)=>y({code:o,codeVerifier:r,redirectURI:n||f("twitch",e.redirectURI),options:e,tokenEndpoint:"https://id.twitch.tv/oauth2/token"}),async getUserInfo(o){let{data:r,error:n}=await ot("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 st}from"@better-fetch/fetch";import{Twitter as it}from"arctic";var ce=e=>{let t=new it(e.clientId,e.clientSecret,f("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)=>y({code:o,codeVerifier:r,redirectURI:n||f("twitch",e.redirectURI),options:e,tokenEndpoint:"https://id.twitch.tv/oauth2/token"}),async getUserInfo(o){let{data:r,error:n}=await st("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 at={apple:te,discord:re,facebook:oe,github:se,google:ie,spotify:ae,twitch:de,twitter:ce},le=Object.keys(at);import{generateState as dt}from"oslo/oauth2";import{z as O}from"zod";function ue(e,t,o){let r=dt();return{state:JSON.stringify({code:r,callbackURL:e,currentURL:t,dontRememberMe:o}),code:r}}function V(e){return O.object({code:O.string(),callbackURL:O.string().optional(),currentURL:O.string().optional(),dontRememberMe:O.boolean().optional()}).safeParse(JSON.parse(e))}import{APIError as ct}from"better-call";var pe=(e,t=!1)=>{let o=new Date;return new Date(o.getTime()+(t?e*1e3:e))};import{TimeSpan as Ro}from"oslo";async function U(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 C(e){e.setCookie(e.context.authCookies.sessionToken.name,"",{maxAge:0}),e.setCookie(e.context.authCookies.dontRememberToken.name,"",{maxAge:0})}import{z as me}from"zod";function $(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 N=new Map;function lt(e,t){if(!e.request)return"";let{method:o,url:r,headers:n}=e.request,s=e.request.headers.get("User-Agent")||"",i=$(e.request)||"",a=JSON.stringify(n);return`${o}:${r}:${a}:${s}:${i}:${t}`}var M=()=>l("/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=lt(e,t),r=N.get(o);if(r){if(r.expiresAt>Date.now())return e.json(r.data);N.delete(o)}let n=await e.context.internalAdapter.findSession(t);if(!n||n.session.expiresAt<new Date)return C(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 c=await e.context.internalAdapter.updateSession(n.session.id,{expiresAt:pe(e.context.sessionConfig.expiresIn,!0)});if(!c)return C(e),e.json(null,{status:401});let m=(c.expiresAt.valueOf()-Date.now())/1e3;return await U(e,c.id,!1,{maxAge:m}),e.json({session:c,user:n.user})}return N.set(o,{data:n,expiresAt:Date.now()+5e3}),e.json(n)}catch(t){return e.context.logger.error(t),e.json(null,{status:500})}}),H=async e=>await M()({...e,_flag:void 0}),E=D(async e=>{let t=await H(e);if(!t?.session)throw new ct("UNAUTHORIZED");return{session:t}}),fe=()=>l("/user/list-sessions",{method:"GET",use:[E],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)}),ge=l("/user/revoke-session",{method:"POST",body:me.object({id:me.string()}),use:[E],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})}),he=l("/user/revoke-sessions",{method:"POST",use:[E],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 ye=l("/sign-in/social",{method:"POST",requireHeaders:!0,query:A.object({currentURL:A.string().optional()}).optional(),body:A.object({callbackURL:A.string().optional(),provider:A.enum(le),dontRememberMe:A.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 v("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=ue(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=ut();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 v("INTERNAL_SERVER_ERROR")}}),we=l("/sign-in/email",{method:"POST",body:A.object({email:A.string().email(),password:A.string(),callbackURL:A.string().optional(),dontRememberMe:A.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 v("BAD_REQUEST",{message:"Email and password is not enabled"});let t=await H(e);t&&await e.context.internalAdapter.deleteSession(t.session.id);let{email:o,password:r}=e.body;if(!A.string().email().safeParse(o).success)throw new v("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 v("UNAUTHORIZED",{message:"Invalid email or password"});let i=s.accounts.find(c=>c.providerId==="credential");if(!i)throw e.context.logger.error("Credential account not found",{email:o}),new v("UNAUTHORIZED",{message:"Invalid email or password"});let a=i?.password;if(!a)throw e.context.logger.error("Password not found",{email:o}),new v("UNAUTHORIZED",{message:"Unexpected error"});if(!await e.context.password.verify(a,r))throw e.context.logger.error("Invalid password"),new v("UNAUTHORIZED",{message:"Invalid email or password"});let u=await e.context.internalAdapter.createSession(s.user.id,e.headers,e.body.dontRememberMe);if(!u)throw e.context.logger.error("Failed to create session"),new v("INTERNAL_SERVER_ERROR");return await U(e,u.id,e.body.dontRememberMe),e.json({user:s.user,session:u,redirect:!!e.body.callbackURL,url:e.body.callbackURL})});import{APIError as ft}from"better-call";import{z as B}from"zod";import{z as p}from"zod";var No=p.object({id:p.string(),providerId:p.string(),accountId:p.string(),userId:p.string(),accessToken:p.string().nullable().optional(),refreshToken:p.string().nullable().optional(),idToken:p.string().nullable().optional(),expiresAt:p.date().nullable().optional(),password:p.string().optional().nullable()}),be=p.object({id:p.string(),email:p.string().transform(e=>e.toLowerCase()),emailVerified:p.boolean().default(!1),name:p.string(),image:p.string().optional(),createdAt:p.date().default(new Date),updatedAt:p.date().default(new Date)}),Mo=p.object({id:p.string(),userId:p.string(),expiresAt:p.date(),ipAddress:p.string().optional(),userAgent:p.string().optional()});import{alphabet as pt,generateRandomString as mt}from"oslo/crypto";var Ae=()=>mt(36,pt("a-z","0-9"));var _={isAction:!1};function F(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 ke=l("/callback/:id",{method:"GET",query:B.object({state:B.string(),code:B.string().optional(),error:B.string().optional()}),metadata:_},async e=>{if(e.query.error||!e.query.code){let b=V(e.query.state).data?.callbackURL||`${e.context.baseURL}/error`;throw e.context.logger.error(e.query.error,e.params.id),e.redirect(`${b}?error=${e.query.error||"oAuth_code_missing"}`)}let t=e.context.socialProviders.find(h=>h.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(h){throw e.context.logger.error(h),e.redirect(`${e.context.baseURL}/error?error=oauth_code_verification_failed`)}let n=await t.getUserInfo(r).then(h=>h?.user),s=Ae(),i=be.safeParse({...n,id:s}),a=V(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:u,dontRememberMe:c}=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 m=await e.context.internalAdapter.findUserByEmail(n.email),g=m?.user.id;if(m){let h=m.accounts.find(R=>R.providerId===t.id),b=e.context.options.account?.accountLinking?.trustedProviders,z=b?b.includes(t.id):!0;if(!h&&(!n.emailVerified||!z)){let R;try{R=new URL(u||d),R.searchParams.set("error","account_not_linked")}catch{throw e.redirect(`${e.context.baseURL}/error?error=account_not_linked`)}throw e.redirect(R.toString())}if(!h)try{await e.context.internalAdapter.linkAccount({providerId:t.id,accountId:n.id,id:`${t.id}:${n.id}`,userId:m.user.id,...F(r)})}catch(R){throw console.log(R),e.redirect(`${e.context.baseURL}/error?error=failed_linking_account`)}}else try{await e.context.internalAdapter.createOAuthUser(i.data,{...F(r),id:`${t.id}:${n.id}`,providerId:t.id,accountId:n.id,userId:s})}catch{let b=new URL(u||d);throw b.searchParams.set("error","unable_to_create_user"),e.setHeader("Location",b.toString()),e.redirect(b.toString())}if(!g&&!s)throw new ft("INTERNAL_SERVER_ERROR",{message:"Unable to create user"});let w=await e.context.internalAdapter.createSession(g||s,e.request,c);if(!w){let h=new URL(u||d);throw h.searchParams.set("error","unable_to_create_session"),e.redirect(h.toString())}try{await U(e,w.id,c)}catch(h){e.context.logger.error("Unable to set session cookie",h);let b=new URL(u||d);throw b.searchParams.set("error","unable_to_create_session"),e.redirect(b.toString())}throw e.redirect(d)});import{z as G}from"zod";var Re=l("/sign-out",{method:"POST",body:G.optional(G.object({callbackURL:G.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),C(e),e.json(null,{body:{redirect:!!e.body?.callbackURL,url:e.body?.callbackURL}})):e.json(null)});import{TimeSpan as gt}from"oslo";import{createJWT as ht,parseJWT as yt}from"oslo/jwt";import{validateJWT as Ue}from"oslo/jwt";import{z as k}from"zod";var Te=l("/forget-password",{method:"POST",body:k.object({email:k.string().email(),redirectTo:k.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 ht("HS256",Buffer.from(e.context.secret),{email:o.user.email,redirectTo:e.body.redirectTo},{expiresIn:new gt(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})}),Pe=l("/reset-password/:token",{method:"GET"},async e=>{let{token:t}=e.params,o,r=k.object({email:k.string(),redirectTo:k.string()});try{if(o=await Ue("HS256",Buffer.from(e.context.secret),t),!o.expiresAt||o.expiresAt<new Date)throw Error("Token expired")}catch{let i=yt(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}`)}),ve=l("/reset-password",{method:"POST",query:k.object({currentURL:k.string()}).optional(),body:k.object({newPassword:k.string(),callbackURL:k.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 Ue("HS256",Buffer.from(e.context.secret),t),n=k.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 wt}from"oslo";import{createJWT as bt,validateJWT as At}from"oslo/jwt";import{z as T}from"zod";async function W(e,t){return await bt("HS256",Buffer.from(e),{email:t.toLowerCase()},{expiresIn:new wt(1,"h"),issuer:"better-auth",subject:"verify-email",audiences:[t],includeIssuedTimestamp:!0})}var xe=l("/send-verification-email",{method:"POST",query:T.object({currentURL:T.string().optional()}).optional(),body:T.object({email:T.string().email(),callbackURL:T.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 W(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})}),Se=l("/verify-email",{method:"GET",query:T.object({token:T.string(),callbackURL:T.string().optional()})},async e=>{let{token:t}=e.query,o;try{o=await At("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=T.object({email:T.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 x}from"zod";import{alphabet as kt,generateRandomString as Rt}from"oslo/crypto";import"better-call";var _e=l("/user/update",{method:"POST",body:x.object({name:x.string().optional(),image:x.string().optional()}),use:[E]},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)}),Ie=l("/user/change-password",{method:"POST",body:x.object({newPassword:x.string(),currentPassword:x.string(),revokeOtherSessions:x.boolean().optional()}),use:[E]},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(m=>m.providerId==="credential"&&m.password);if(!d||!d.password)return e.json(null,{status:400,body:{message:"User does not have a password"}});let u=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:u}),r){await e.context.internalAdapter.deleteSessions(n.user.id);let m=await e.context.internalAdapter.createSession(n.user.id,e.headers);if(!m)return e.json(null,{status:500,body:{message:"Failed to create session"}});await U(e,m.id)}return e.json(n.user)}),Le=l("/user/set-password",{method:"POST",body:x.object({newPassword:x.string()}),use:[E]},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:Rt(32,kt("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 Ut,generateRandomString as Tt}from"oslo/crypto";var Ee=l("/csrf",{method:"GET",metadata:_},async e=>{let t=await e.getSignedCookie(e.context.authCookies.csrfToken.name,e.context.secret);if(t)return{csrfToken:t};let o=Tt(32,Ut("a-z","0-9","A-Z")),r=await j(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 Pt=(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
|
+
|
|
120
|
+
// src/social-providers/utils.ts
|
|
121
|
+
import { OAuth2Tokens } from "arctic";
|
|
122
|
+
|
|
123
|
+
// src/utils/base-url.ts
|
|
124
|
+
function checkHasPath(url) {
|
|
125
|
+
try {
|
|
126
|
+
const parsedUrl = new URL(url);
|
|
127
|
+
return parsedUrl.pathname !== "/";
|
|
128
|
+
} catch (error2) {
|
|
129
|
+
throw new BetterAuthError(
|
|
130
|
+
`Invalid base URL: ${url}. Please provide a valid base URL.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function withPath(url, path = "/api/auth") {
|
|
135
|
+
const hasPath = checkHasPath(url);
|
|
136
|
+
if (hasPath) {
|
|
137
|
+
return url;
|
|
138
|
+
}
|
|
139
|
+
path = path.startsWith("/") ? path : `/${path}`;
|
|
140
|
+
return `${url}${path}`;
|
|
141
|
+
}
|
|
142
|
+
function getBaseURL(url, path) {
|
|
143
|
+
if (url) {
|
|
144
|
+
return withPath(url, path);
|
|
145
|
+
}
|
|
146
|
+
const env = process?.env || {};
|
|
147
|
+
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);
|
|
148
|
+
if (fromEnv) {
|
|
149
|
+
return withPath(fromEnv, path);
|
|
150
|
+
}
|
|
151
|
+
if (typeof window !== "undefined") {
|
|
152
|
+
return withPath(window.location.origin, path);
|
|
153
|
+
}
|
|
154
|
+
return void 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/social-providers/utils.ts
|
|
158
|
+
import { betterFetch } from "@better-fetch/fetch";
|
|
159
|
+
function getRedirectURI(providerId, redirectURI) {
|
|
160
|
+
return redirectURI || `${getBaseURL()}/callback/${providerId}`;
|
|
161
|
+
}
|
|
162
|
+
async function validateAuthorizationCode({
|
|
163
|
+
code,
|
|
164
|
+
codeVerifier,
|
|
165
|
+
redirectURI,
|
|
166
|
+
options,
|
|
167
|
+
tokenEndpoint
|
|
168
|
+
}) {
|
|
169
|
+
const body = new URLSearchParams();
|
|
170
|
+
body.set("grant_type", "authorization_code");
|
|
171
|
+
body.set("code", code);
|
|
172
|
+
codeVerifier && body.set("code_verifier", codeVerifier);
|
|
173
|
+
body.set("redirect_uri", redirectURI);
|
|
174
|
+
body.set("client_id", options.clientId);
|
|
175
|
+
body.set("client_secret", options.clientSecret);
|
|
176
|
+
const { data, error: error2 } = await betterFetch(tokenEndpoint, {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body,
|
|
179
|
+
headers: {
|
|
180
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
181
|
+
accept: "application/json",
|
|
182
|
+
"user-agent": "better-auth"
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
if (error2) {
|
|
186
|
+
throw error2;
|
|
187
|
+
}
|
|
188
|
+
const tokens = new OAuth2Tokens(data);
|
|
189
|
+
return tokens;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/social-providers/apple.ts
|
|
193
|
+
var apple = (options) => {
|
|
194
|
+
const tokenEndpoint = "https://appleid.apple.com/auth/token";
|
|
195
|
+
return {
|
|
196
|
+
id: "apple",
|
|
197
|
+
name: "Apple",
|
|
198
|
+
createAuthorizationURL({ state, scopes, redirectURI }) {
|
|
199
|
+
const _scope = scopes || ["email", "name", "openid"];
|
|
200
|
+
return new URL(
|
|
201
|
+
`https://appleid.apple.com/auth/authorize?client_id=${options.clientId}&response_type=code&redirect_uri=${redirectURI || options.redirectURI}&scope=${_scope.join(" ")}&state=${state}`
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
205
|
+
return validateAuthorizationCode({
|
|
206
|
+
code,
|
|
207
|
+
codeVerifier,
|
|
208
|
+
redirectURI: redirectURI || getRedirectURI("apple", options.redirectURI),
|
|
209
|
+
options,
|
|
210
|
+
tokenEndpoint
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
async getUserInfo(token) {
|
|
214
|
+
const data = parseJWT(token.idToken())?.payload;
|
|
215
|
+
if (!data) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
user: {
|
|
220
|
+
id: data.sub,
|
|
221
|
+
name: data.name,
|
|
222
|
+
email: data.email,
|
|
223
|
+
emailVerified: data.email_verified === "true"
|
|
224
|
+
},
|
|
225
|
+
data
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/social-providers/discord.ts
|
|
232
|
+
import { betterFetch as betterFetch3 } from "@better-fetch/fetch";
|
|
233
|
+
import { Discord } from "arctic";
|
|
234
|
+
var discord = (options) => {
|
|
235
|
+
const discordArctic = new Discord(
|
|
236
|
+
options.clientId,
|
|
237
|
+
options.clientSecret,
|
|
238
|
+
getRedirectURI("discord", options.redirectURI)
|
|
239
|
+
);
|
|
240
|
+
return {
|
|
241
|
+
id: "discord",
|
|
242
|
+
name: "Discord",
|
|
243
|
+
createAuthorizationURL({ state, scopes }) {
|
|
244
|
+
const _scope = scopes || ["email"];
|
|
245
|
+
return discordArctic.createAuthorizationURL(state, _scope);
|
|
246
|
+
},
|
|
247
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
248
|
+
return validateAuthorizationCode({
|
|
249
|
+
code,
|
|
250
|
+
codeVerifier,
|
|
251
|
+
redirectURI: redirectURI || getRedirectURI("discord", options.redirectURI),
|
|
252
|
+
options,
|
|
253
|
+
tokenEndpoint: "https://discord.com/api/oauth2/token"
|
|
254
|
+
});
|
|
255
|
+
},
|
|
256
|
+
async getUserInfo(token) {
|
|
257
|
+
const { data: profile, error: error2 } = await betterFetch3(
|
|
258
|
+
"https://discord.com/api/users/@me",
|
|
259
|
+
{
|
|
260
|
+
auth: {
|
|
261
|
+
type: "Bearer",
|
|
262
|
+
token: token.accessToken()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
if (error2) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
user: {
|
|
271
|
+
id: profile.id,
|
|
272
|
+
name: profile.display_name || profile.username || "",
|
|
273
|
+
email: profile.email,
|
|
274
|
+
emailVerified: profile.verified
|
|
275
|
+
},
|
|
276
|
+
data: profile
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/social-providers/facebook.ts
|
|
283
|
+
import { betterFetch as betterFetch4 } from "@better-fetch/fetch";
|
|
284
|
+
import { Facebook } from "arctic";
|
|
285
|
+
var facebook = (options) => {
|
|
286
|
+
const facebookArctic = new Facebook(
|
|
287
|
+
options.clientId,
|
|
288
|
+
options.clientSecret,
|
|
289
|
+
getRedirectURI("facebook", options.redirectURI)
|
|
290
|
+
);
|
|
291
|
+
return {
|
|
292
|
+
id: "facebook",
|
|
293
|
+
name: "Facebook",
|
|
294
|
+
createAuthorizationURL({ state, scopes }) {
|
|
295
|
+
const _scopes = scopes || ["email", "public_profile"];
|
|
296
|
+
return facebookArctic.createAuthorizationURL(state, _scopes);
|
|
297
|
+
},
|
|
298
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
299
|
+
return validateAuthorizationCode({
|
|
300
|
+
code,
|
|
301
|
+
codeVerifier,
|
|
302
|
+
redirectURI: redirectURI || getRedirectURI("facebook", options.redirectURI),
|
|
303
|
+
options,
|
|
304
|
+
tokenEndpoint: "https://graph.facebook.com/v16.0/oauth/access_token"
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
async getUserInfo(token) {
|
|
308
|
+
const { data: profile, error: error2 } = await betterFetch4(
|
|
309
|
+
"https://graph.facebook.com/me",
|
|
310
|
+
{
|
|
311
|
+
auth: {
|
|
312
|
+
type: "Bearer",
|
|
313
|
+
token: token.accessToken()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
if (error2) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
user: {
|
|
322
|
+
id: profile.id,
|
|
323
|
+
name: profile.name,
|
|
324
|
+
email: profile.email,
|
|
325
|
+
emailVerified: profile.email_verified
|
|
326
|
+
},
|
|
327
|
+
data: profile
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/social-providers/github.ts
|
|
334
|
+
import { betterFetch as betterFetch5 } from "@better-fetch/fetch";
|
|
335
|
+
import { GitHub } from "arctic";
|
|
336
|
+
var github = ({
|
|
337
|
+
clientId,
|
|
338
|
+
clientSecret,
|
|
339
|
+
redirectURI
|
|
340
|
+
}) => {
|
|
341
|
+
const githubArctic = new GitHub(
|
|
342
|
+
clientId,
|
|
343
|
+
clientSecret,
|
|
344
|
+
getRedirectURI("github", redirectURI)
|
|
345
|
+
);
|
|
346
|
+
return {
|
|
347
|
+
id: "github",
|
|
348
|
+
name: "Github",
|
|
349
|
+
createAuthorizationURL({ state, scopes }) {
|
|
350
|
+
const _scopes = scopes || ["user:email"];
|
|
351
|
+
return githubArctic.createAuthorizationURL(state, _scopes);
|
|
352
|
+
},
|
|
353
|
+
validateAuthorizationCode: async (state) => {
|
|
354
|
+
return await githubArctic.validateAuthorizationCode(state);
|
|
355
|
+
},
|
|
356
|
+
async getUserInfo(token) {
|
|
357
|
+
const { data: profile, error: error2 } = await betterFetch5(
|
|
358
|
+
"https://api.github.com/user",
|
|
359
|
+
{
|
|
360
|
+
auth: {
|
|
361
|
+
type: "Bearer",
|
|
362
|
+
token: token.accessToken()
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
if (error2) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
let emailVerified = false;
|
|
370
|
+
if (!profile.email) {
|
|
371
|
+
const { data, error: error3 } = await betterFetch5("https://api.github.com/user/emails", {
|
|
372
|
+
auth: {
|
|
373
|
+
type: "Bearer",
|
|
374
|
+
token: token.accessToken()
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
if (!error3) {
|
|
378
|
+
profile.email = (data.find((e) => e.primary) ?? data[0])?.email;
|
|
379
|
+
emailVerified = data.find((e) => e.email === profile.email)?.verified ?? false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
user: {
|
|
384
|
+
id: profile.id,
|
|
385
|
+
name: profile.name,
|
|
386
|
+
email: profile.email,
|
|
387
|
+
image: profile.avatar_url,
|
|
388
|
+
emailVerified,
|
|
389
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
390
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
391
|
+
},
|
|
392
|
+
data: profile
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// src/social-providers/google.ts
|
|
399
|
+
import { Google } from "arctic";
|
|
400
|
+
import { parseJWT as parseJWT2 } from "oslo/jwt";
|
|
401
|
+
|
|
402
|
+
// src/utils/logger.ts
|
|
403
|
+
import { createConsola } from "consola";
|
|
404
|
+
var consola = createConsola({
|
|
405
|
+
formatOptions: {
|
|
406
|
+
date: false,
|
|
407
|
+
colors: true,
|
|
408
|
+
compact: true
|
|
409
|
+
},
|
|
410
|
+
defaults: {
|
|
411
|
+
tag: "Better Auth"
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
var createLogger = (options) => {
|
|
415
|
+
return {
|
|
416
|
+
log: (...args) => {
|
|
417
|
+
!options?.disabled && consola.log("", ...args);
|
|
418
|
+
},
|
|
419
|
+
error: (...args) => {
|
|
420
|
+
!options?.disabled && consola.error("", ...args);
|
|
421
|
+
},
|
|
422
|
+
warn: (...args) => {
|
|
423
|
+
!options?.disabled && consola.warn("", ...args);
|
|
424
|
+
},
|
|
425
|
+
info: (...args) => {
|
|
426
|
+
!options?.disabled && consola.info("", ...args);
|
|
427
|
+
},
|
|
428
|
+
debug: (...args) => {
|
|
429
|
+
!options?.disabled && consola.debug("", ...args);
|
|
430
|
+
},
|
|
431
|
+
box: (...args) => {
|
|
432
|
+
!options?.disabled && consola.box("", ...args);
|
|
433
|
+
},
|
|
434
|
+
success: (...args) => {
|
|
435
|
+
!options?.disabled && consola.success("", ...args);
|
|
436
|
+
},
|
|
437
|
+
break: (...args) => {
|
|
438
|
+
!options?.disabled && console.log("\n");
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
var logger = createLogger();
|
|
443
|
+
|
|
444
|
+
// src/social-providers/google.ts
|
|
445
|
+
var google = (options) => {
|
|
446
|
+
const googleArctic = new Google(
|
|
447
|
+
options.clientId,
|
|
448
|
+
options.clientSecret,
|
|
449
|
+
getRedirectURI("google", options.redirectURI)
|
|
450
|
+
);
|
|
451
|
+
return {
|
|
452
|
+
id: "google",
|
|
453
|
+
name: "Google",
|
|
454
|
+
createAuthorizationURL({ state, scopes, codeVerifier, redirectURI }) {
|
|
455
|
+
if (!options.clientId || !options.clientSecret) {
|
|
456
|
+
logger.error(
|
|
457
|
+
"Client Id and Client Secret is required for Google. Make sure to provide them in the options."
|
|
458
|
+
);
|
|
459
|
+
throw new BetterAuthError("CLIENT_ID_AND_SECRET_REQUIRED");
|
|
460
|
+
}
|
|
461
|
+
if (!codeVerifier) {
|
|
462
|
+
throw new BetterAuthError("codeVerifier is required for Google");
|
|
463
|
+
}
|
|
464
|
+
const _scopes = scopes || ["email", "profile"];
|
|
465
|
+
const url = googleArctic.createAuthorizationURL(
|
|
466
|
+
state,
|
|
467
|
+
codeVerifier,
|
|
468
|
+
_scopes
|
|
469
|
+
);
|
|
470
|
+
return url;
|
|
471
|
+
},
|
|
472
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
473
|
+
return validateAuthorizationCode({
|
|
474
|
+
code,
|
|
475
|
+
codeVerifier,
|
|
476
|
+
redirectURI: redirectURI || getRedirectURI("google", options.redirectURI),
|
|
477
|
+
options,
|
|
478
|
+
tokenEndpoint: "https://oauth2.googleapis.com/token"
|
|
479
|
+
});
|
|
480
|
+
},
|
|
481
|
+
async getUserInfo(token) {
|
|
482
|
+
if (!token.idToken) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const user = parseJWT2(token.idToken())?.payload;
|
|
486
|
+
return {
|
|
487
|
+
user: {
|
|
488
|
+
id: user.sub,
|
|
489
|
+
name: user.name,
|
|
490
|
+
email: user.email,
|
|
491
|
+
image: user.picture,
|
|
492
|
+
emailVerified: user.email_verified
|
|
493
|
+
},
|
|
494
|
+
data: user
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// src/social-providers/spotify.ts
|
|
501
|
+
import { betterFetch as betterFetch6 } from "@better-fetch/fetch";
|
|
502
|
+
import { Spotify } from "arctic";
|
|
503
|
+
var spotify = (options) => {
|
|
504
|
+
const spotifyArctic = new Spotify(
|
|
505
|
+
options.clientId,
|
|
506
|
+
options.clientSecret,
|
|
507
|
+
getRedirectURI("spotify", options.redirectURI)
|
|
508
|
+
);
|
|
509
|
+
return {
|
|
510
|
+
id: "spotify",
|
|
511
|
+
name: "Spotify",
|
|
512
|
+
createAuthorizationURL({ state, scopes }) {
|
|
513
|
+
const _scopes = scopes || ["user-read-email"];
|
|
514
|
+
return spotifyArctic.createAuthorizationURL(state, _scopes);
|
|
515
|
+
},
|
|
516
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
517
|
+
return validateAuthorizationCode({
|
|
518
|
+
code,
|
|
519
|
+
codeVerifier,
|
|
520
|
+
redirectURI: redirectURI || getRedirectURI("spotify", options.redirectURI),
|
|
521
|
+
options,
|
|
522
|
+
tokenEndpoint: "https://accounts.spotify.com/api/token"
|
|
523
|
+
});
|
|
524
|
+
},
|
|
525
|
+
async getUserInfo(token) {
|
|
526
|
+
const { data: profile, error: error2 } = await betterFetch6(
|
|
527
|
+
"https://api.spotify.com/v1/me",
|
|
528
|
+
{
|
|
529
|
+
method: "GET",
|
|
530
|
+
headers: {
|
|
531
|
+
Authorization: `Bearer ${token.accessToken()}`
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
);
|
|
535
|
+
if (error2) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
user: {
|
|
540
|
+
id: profile.id,
|
|
541
|
+
name: profile.display_name,
|
|
542
|
+
email: profile.email,
|
|
543
|
+
image: profile.images[0]?.url,
|
|
544
|
+
emailVerified: false
|
|
545
|
+
},
|
|
546
|
+
data: profile
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/social-providers/twitch.ts
|
|
553
|
+
import { betterFetch as betterFetch7 } from "@better-fetch/fetch";
|
|
554
|
+
import { Twitch } from "arctic";
|
|
555
|
+
var twitch = (options) => {
|
|
556
|
+
const twitchArctic = new Twitch(
|
|
557
|
+
options.clientId,
|
|
558
|
+
options.clientSecret,
|
|
559
|
+
getRedirectURI("twitch", options.redirectURI)
|
|
560
|
+
);
|
|
561
|
+
return {
|
|
562
|
+
id: "twitch",
|
|
563
|
+
name: "Twitch",
|
|
564
|
+
createAuthorizationURL({ state, scopes }) {
|
|
565
|
+
const _scopes = scopes || ["activity:write", "read"];
|
|
566
|
+
return twitchArctic.createAuthorizationURL(state, _scopes);
|
|
567
|
+
},
|
|
568
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
569
|
+
return validateAuthorizationCode({
|
|
570
|
+
code,
|
|
571
|
+
codeVerifier,
|
|
572
|
+
redirectURI: redirectURI || getRedirectURI("twitch", options.redirectURI),
|
|
573
|
+
options,
|
|
574
|
+
tokenEndpoint: "https://id.twitch.tv/oauth2/token"
|
|
575
|
+
});
|
|
576
|
+
},
|
|
577
|
+
async getUserInfo(token) {
|
|
578
|
+
const { data: profile, error: error2 } = await betterFetch7(
|
|
579
|
+
"https://api.twitch.tv/helix/users",
|
|
580
|
+
{
|
|
581
|
+
method: "GET",
|
|
582
|
+
headers: {
|
|
583
|
+
Authorization: `Bearer ${token.accessToken()}`
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
if (error2) {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
return {
|
|
591
|
+
user: {
|
|
592
|
+
id: profile.sub,
|
|
593
|
+
name: profile.preferred_username,
|
|
594
|
+
email: profile.email,
|
|
595
|
+
image: profile.picture,
|
|
596
|
+
emailVerified: false
|
|
597
|
+
},
|
|
598
|
+
data: profile
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// src/social-providers/twitter.ts
|
|
605
|
+
import { betterFetch as betterFetch8 } from "@better-fetch/fetch";
|
|
606
|
+
import { Twitter } from "arctic";
|
|
607
|
+
var twitter = (options) => {
|
|
608
|
+
const twitterArctic = new Twitter(
|
|
609
|
+
options.clientId,
|
|
610
|
+
options.clientSecret,
|
|
611
|
+
getRedirectURI("twitter", options.redirectURI)
|
|
612
|
+
);
|
|
613
|
+
return {
|
|
614
|
+
id: "twitter",
|
|
615
|
+
name: "Twitter",
|
|
616
|
+
createAuthorizationURL(data) {
|
|
617
|
+
const _scopes = data.scopes || ["account_info.read"];
|
|
618
|
+
return twitterArctic.createAuthorizationURL(
|
|
619
|
+
data.state,
|
|
620
|
+
data.codeVerifier,
|
|
621
|
+
_scopes
|
|
622
|
+
);
|
|
623
|
+
},
|
|
624
|
+
validateAuthorizationCode: async (code, codeVerifier, redirectURI) => {
|
|
625
|
+
return validateAuthorizationCode({
|
|
626
|
+
code,
|
|
627
|
+
codeVerifier,
|
|
628
|
+
redirectURI: redirectURI || getRedirectURI("twitch", options.redirectURI),
|
|
629
|
+
options,
|
|
630
|
+
tokenEndpoint: "https://id.twitch.tv/oauth2/token"
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
async getUserInfo(token) {
|
|
634
|
+
const { data: profile, error: error2 } = await betterFetch8(
|
|
635
|
+
"https://api.x.com/2/users/me?user.fields=profile_image_url",
|
|
636
|
+
{
|
|
637
|
+
method: "GET",
|
|
638
|
+
headers: {
|
|
639
|
+
Authorization: `Bearer ${token.accessToken()}`
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
if (error2) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
if (!profile.data.email) {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
user: {
|
|
651
|
+
id: profile.data.id,
|
|
652
|
+
name: profile.data.name,
|
|
653
|
+
email: profile.data.email,
|
|
654
|
+
image: profile.data.profile_image_url,
|
|
655
|
+
emailVerified: profile.data.verified || false
|
|
656
|
+
},
|
|
657
|
+
data: profile
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// src/types/provider.ts
|
|
664
|
+
import "arctic";
|
|
665
|
+
|
|
666
|
+
// src/social-providers/index.ts
|
|
667
|
+
var oAuthProviders = {
|
|
668
|
+
apple,
|
|
669
|
+
discord,
|
|
670
|
+
facebook,
|
|
671
|
+
github,
|
|
672
|
+
google,
|
|
673
|
+
spotify,
|
|
674
|
+
twitch,
|
|
675
|
+
twitter
|
|
676
|
+
};
|
|
677
|
+
var oAuthProviderList = Object.keys(oAuthProviders);
|
|
678
|
+
|
|
679
|
+
// src/utils/state.ts
|
|
680
|
+
import { generateState as generateStateOAuth } from "oslo/oauth2";
|
|
681
|
+
import { z as z2 } from "zod";
|
|
682
|
+
function generateState(callbackURL, currentURL, dontRememberMe) {
|
|
683
|
+
const code = generateStateOAuth();
|
|
684
|
+
const state = JSON.stringify({
|
|
685
|
+
code,
|
|
686
|
+
callbackURL,
|
|
687
|
+
currentURL,
|
|
688
|
+
dontRememberMe
|
|
689
|
+
});
|
|
690
|
+
return { state, code };
|
|
691
|
+
}
|
|
692
|
+
function parseState(state) {
|
|
693
|
+
const data = z2.object({
|
|
694
|
+
code: z2.string(),
|
|
695
|
+
callbackURL: z2.string().optional(),
|
|
696
|
+
currentURL: z2.string().optional(),
|
|
697
|
+
dontRememberMe: z2.boolean().optional()
|
|
698
|
+
}).safeParse(JSON.parse(state));
|
|
699
|
+
return data;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/api/routes/session.ts
|
|
703
|
+
import { APIError as APIError2 } from "better-call";
|
|
704
|
+
|
|
705
|
+
// src/utils/date.ts
|
|
706
|
+
var getDate = (span, isSeconds = false) => {
|
|
707
|
+
const date = /* @__PURE__ */ new Date();
|
|
708
|
+
return new Date(date.getTime() + (isSeconds ? span * 1e3 : span));
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// src/utils/cookies.ts
|
|
712
|
+
import { TimeSpan } from "oslo";
|
|
713
|
+
async function setSessionCookie(ctx, sessionToken, dontRememberMe, overrides) {
|
|
714
|
+
const options = ctx.context.authCookies.sessionToken.options;
|
|
715
|
+
options.maxAge = dontRememberMe ? void 0 : options.maxAge;
|
|
716
|
+
await ctx.setSignedCookie(
|
|
717
|
+
ctx.context.authCookies.sessionToken.name,
|
|
718
|
+
sessionToken,
|
|
719
|
+
ctx.context.secret,
|
|
720
|
+
options
|
|
721
|
+
);
|
|
722
|
+
if (dontRememberMe) {
|
|
723
|
+
await ctx.setSignedCookie(
|
|
724
|
+
ctx.context.authCookies.dontRememberToken.name,
|
|
725
|
+
"true",
|
|
726
|
+
ctx.context.secret,
|
|
727
|
+
ctx.context.authCookies.dontRememberToken.options
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function deleteSessionCookie(ctx) {
|
|
732
|
+
ctx.setCookie(ctx.context.authCookies.sessionToken.name, "", {
|
|
733
|
+
maxAge: 0
|
|
734
|
+
});
|
|
735
|
+
ctx.setCookie(ctx.context.authCookies.dontRememberToken.name, "", {
|
|
736
|
+
maxAge: 0
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/api/routes/session.ts
|
|
741
|
+
import { z as z3 } from "zod";
|
|
742
|
+
|
|
743
|
+
// src/utils/get-request-ip.ts
|
|
744
|
+
function getIp(req) {
|
|
745
|
+
const testIP = "127.0.0.1";
|
|
746
|
+
if (process.env.NODE_ENV === "test") {
|
|
747
|
+
return testIP;
|
|
748
|
+
}
|
|
749
|
+
const headers = [
|
|
750
|
+
"x-client-ip",
|
|
751
|
+
"x-forwarded-for",
|
|
752
|
+
"cf-connecting-ip",
|
|
753
|
+
"fastly-client-ip",
|
|
754
|
+
"x-real-ip",
|
|
755
|
+
"x-cluster-client-ip",
|
|
756
|
+
"x-forwarded",
|
|
757
|
+
"forwarded-for",
|
|
758
|
+
"forwarded"
|
|
759
|
+
];
|
|
760
|
+
for (const header of headers) {
|
|
761
|
+
const value = req.headers.get(header);
|
|
762
|
+
if (typeof value === "string") {
|
|
763
|
+
const ip = value.split(",")[0].trim();
|
|
764
|
+
if (ip) return ip;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/api/routes/session.ts
|
|
771
|
+
var sessionCache = /* @__PURE__ */ new Map();
|
|
772
|
+
function getRequestUniqueKey(ctx, token) {
|
|
773
|
+
if (!ctx.request) {
|
|
774
|
+
return "";
|
|
775
|
+
}
|
|
776
|
+
const { method, url, headers } = ctx.request;
|
|
777
|
+
const userAgent = ctx.request.headers.get("User-Agent") || "";
|
|
778
|
+
const ip = getIp(ctx.request) || "";
|
|
779
|
+
const headerString = JSON.stringify(headers);
|
|
780
|
+
const uniqueString = `${method}:${url}:${headerString}:${userAgent}:${ip}:${token}`;
|
|
781
|
+
return uniqueString;
|
|
782
|
+
}
|
|
783
|
+
var getSession = () => createAuthEndpoint(
|
|
784
|
+
"/session",
|
|
785
|
+
{
|
|
786
|
+
method: "GET",
|
|
787
|
+
requireHeaders: true
|
|
788
|
+
},
|
|
789
|
+
async (ctx) => {
|
|
790
|
+
try {
|
|
791
|
+
const sessionCookieToken = await ctx.getSignedCookie(
|
|
792
|
+
ctx.context.authCookies.sessionToken.name,
|
|
793
|
+
ctx.context.secret
|
|
794
|
+
);
|
|
795
|
+
if (!sessionCookieToken) {
|
|
796
|
+
return ctx.json(null, {
|
|
797
|
+
status: 401
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
const key = getRequestUniqueKey(ctx, sessionCookieToken);
|
|
801
|
+
const cachedSession = sessionCache.get(key);
|
|
802
|
+
if (cachedSession) {
|
|
803
|
+
if (cachedSession.expiresAt > Date.now()) {
|
|
804
|
+
return ctx.json(
|
|
805
|
+
cachedSession.data
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
sessionCache.delete(key);
|
|
809
|
+
}
|
|
810
|
+
const session = await ctx.context.internalAdapter.findSession(sessionCookieToken);
|
|
811
|
+
if (!session || session.session.expiresAt < /* @__PURE__ */ new Date()) {
|
|
812
|
+
deleteSessionCookie(ctx);
|
|
813
|
+
if (session) {
|
|
814
|
+
await ctx.context.internalAdapter.deleteSession(session.session.id);
|
|
815
|
+
}
|
|
816
|
+
return ctx.json(null, {
|
|
817
|
+
status: 401
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
const dontRememberMe = await ctx.getSignedCookie(
|
|
821
|
+
ctx.context.authCookies.dontRememberToken.name,
|
|
822
|
+
ctx.context.secret
|
|
823
|
+
);
|
|
824
|
+
if (dontRememberMe) {
|
|
825
|
+
return ctx.json(
|
|
826
|
+
session
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
const expiresIn = ctx.context.sessionConfig.expiresIn;
|
|
830
|
+
const updateAge = ctx.context.sessionConfig.updateAge;
|
|
831
|
+
const sessionIsDueToBeUpdatedDate = session.session.expiresAt.valueOf() - expiresIn * 1e3 + updateAge * 1e3;
|
|
832
|
+
const shouldBeUpdated = sessionIsDueToBeUpdatedDate <= Date.now();
|
|
833
|
+
if (shouldBeUpdated) {
|
|
834
|
+
const updatedSession = await ctx.context.internalAdapter.updateSession(
|
|
835
|
+
session.session.id,
|
|
836
|
+
{
|
|
837
|
+
expiresAt: getDate(ctx.context.sessionConfig.expiresIn, true)
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
if (!updatedSession) {
|
|
841
|
+
deleteSessionCookie(ctx);
|
|
842
|
+
return ctx.json(null, { status: 401 });
|
|
843
|
+
}
|
|
844
|
+
const maxAge = (updatedSession.expiresAt.valueOf() - Date.now()) / 1e3;
|
|
845
|
+
await setSessionCookie(ctx, updatedSession.id, false, {
|
|
846
|
+
maxAge
|
|
847
|
+
});
|
|
848
|
+
return ctx.json({
|
|
849
|
+
session: updatedSession,
|
|
850
|
+
user: session.user
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
sessionCache.set(key, {
|
|
854
|
+
data: session,
|
|
855
|
+
expiresAt: Date.now() + 5e3
|
|
856
|
+
});
|
|
857
|
+
return ctx.json(
|
|
858
|
+
session
|
|
859
|
+
);
|
|
860
|
+
} catch (error2) {
|
|
861
|
+
ctx.context.logger.error(error2);
|
|
862
|
+
return ctx.json(null, { status: 500 });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
);
|
|
866
|
+
var getSessionFromCtx = async (ctx) => {
|
|
867
|
+
const session = await getSession()({
|
|
868
|
+
...ctx,
|
|
869
|
+
//@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
|
|
870
|
+
_flag: void 0
|
|
871
|
+
});
|
|
872
|
+
return session;
|
|
873
|
+
};
|
|
874
|
+
var sessionMiddleware = createAuthMiddleware(async (ctx) => {
|
|
875
|
+
const session = await getSessionFromCtx(ctx);
|
|
876
|
+
if (!session?.session) {
|
|
877
|
+
throw new APIError2("UNAUTHORIZED");
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
session
|
|
881
|
+
};
|
|
882
|
+
});
|
|
883
|
+
var listSessions = () => createAuthEndpoint(
|
|
884
|
+
"/user/list-sessions",
|
|
885
|
+
{
|
|
886
|
+
method: "GET",
|
|
887
|
+
use: [sessionMiddleware],
|
|
888
|
+
requireHeaders: true
|
|
889
|
+
},
|
|
890
|
+
async (ctx) => {
|
|
891
|
+
const sessions = await ctx.context.adapter.findMany({
|
|
892
|
+
model: ctx.context.tables.session.tableName,
|
|
893
|
+
where: [
|
|
894
|
+
{
|
|
895
|
+
field: "userId",
|
|
896
|
+
value: ctx.context.session.user.id
|
|
897
|
+
}
|
|
898
|
+
]
|
|
899
|
+
});
|
|
900
|
+
const activeSessions = sessions.filter((session) => {
|
|
901
|
+
return session.expiresAt > /* @__PURE__ */ new Date();
|
|
902
|
+
});
|
|
903
|
+
return ctx.json(
|
|
904
|
+
activeSessions
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
);
|
|
908
|
+
var revokeSession = createAuthEndpoint(
|
|
909
|
+
"/user/revoke-session",
|
|
910
|
+
{
|
|
911
|
+
method: "POST",
|
|
912
|
+
body: z3.object({
|
|
913
|
+
id: z3.string()
|
|
914
|
+
}),
|
|
915
|
+
use: [sessionMiddleware],
|
|
916
|
+
requireHeaders: true
|
|
917
|
+
},
|
|
918
|
+
async (ctx) => {
|
|
919
|
+
const id = ctx.body.id;
|
|
920
|
+
const findSession = await ctx.context.internalAdapter.findSession(id);
|
|
921
|
+
if (!findSession) {
|
|
922
|
+
return ctx.json(null, { status: 400 });
|
|
923
|
+
}
|
|
924
|
+
if (findSession.session.userId !== ctx.context.session.user.id) {
|
|
925
|
+
return ctx.json(null, { status: 403 });
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
await ctx.context.internalAdapter.deleteSession(id);
|
|
929
|
+
} catch (error2) {
|
|
930
|
+
ctx.context.logger.error(error2);
|
|
931
|
+
return ctx.json(null, { status: 500 });
|
|
932
|
+
}
|
|
933
|
+
return ctx.json({
|
|
934
|
+
status: true
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
var revokeSessions = createAuthEndpoint(
|
|
939
|
+
"/user/revoke-sessions",
|
|
940
|
+
{
|
|
941
|
+
method: "POST",
|
|
942
|
+
use: [sessionMiddleware],
|
|
943
|
+
requireHeaders: true
|
|
944
|
+
},
|
|
945
|
+
async (ctx) => {
|
|
946
|
+
try {
|
|
947
|
+
await ctx.context.internalAdapter.deleteSessions(
|
|
948
|
+
ctx.context.session.user.id
|
|
949
|
+
);
|
|
950
|
+
} catch (error2) {
|
|
951
|
+
ctx.context.logger.error(error2);
|
|
952
|
+
return ctx.json(null, { status: 500 });
|
|
953
|
+
}
|
|
954
|
+
return ctx.json({
|
|
955
|
+
status: true
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
// src/api/routes/sign-in.ts
|
|
961
|
+
var signInOAuth = createAuthEndpoint(
|
|
962
|
+
"/sign-in/social",
|
|
963
|
+
{
|
|
964
|
+
method: "POST",
|
|
965
|
+
requireHeaders: true,
|
|
966
|
+
query: z4.object({
|
|
967
|
+
/**
|
|
968
|
+
* Redirect to the current URL after the
|
|
969
|
+
* user has signed in.
|
|
970
|
+
*/
|
|
971
|
+
currentURL: z4.string().optional()
|
|
972
|
+
}).optional(),
|
|
973
|
+
body: z4.object({
|
|
974
|
+
/**
|
|
975
|
+
* Callback URL to redirect to after the user has signed in.
|
|
976
|
+
*/
|
|
977
|
+
callbackURL: z4.string().optional(),
|
|
978
|
+
/**
|
|
979
|
+
* OAuth2 provider to use`
|
|
980
|
+
*/
|
|
981
|
+
provider: z4.enum(oAuthProviderList),
|
|
982
|
+
/**
|
|
983
|
+
* If this is true the session will only be valid for the current browser session
|
|
984
|
+
*/
|
|
985
|
+
dontRememberMe: z4.boolean().default(false).optional()
|
|
986
|
+
})
|
|
987
|
+
},
|
|
988
|
+
async (c) => {
|
|
989
|
+
const provider = c.context.socialProviders.find(
|
|
990
|
+
(p) => p.id === c.body.provider
|
|
991
|
+
);
|
|
992
|
+
if (!provider) {
|
|
993
|
+
c.context.logger.error(
|
|
994
|
+
"Provider not found. Make sure to add the provider to your auth config",
|
|
995
|
+
{
|
|
996
|
+
provider: c.body.provider
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
throw new APIError3("NOT_FOUND", {
|
|
1000
|
+
message: "Provider not found"
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
const cookie = c.context.authCookies;
|
|
1004
|
+
const currentURL = c.query?.currentURL ? new URL(c.query?.currentURL) : null;
|
|
1005
|
+
const callbackURL = c.body.callbackURL?.startsWith("http") ? c.body.callbackURL : `${currentURL?.origin}${c.body.callbackURL || ""}`;
|
|
1006
|
+
const state = generateState(
|
|
1007
|
+
callbackURL || currentURL?.origin || c.context.baseURL,
|
|
1008
|
+
c.query?.currentURL
|
|
1009
|
+
);
|
|
1010
|
+
try {
|
|
1011
|
+
await c.setSignedCookie(
|
|
1012
|
+
cookie.state.name,
|
|
1013
|
+
state.code,
|
|
1014
|
+
c.context.secret,
|
|
1015
|
+
cookie.state.options
|
|
1016
|
+
);
|
|
1017
|
+
const codeVerifier = generateCodeVerifier();
|
|
1018
|
+
await c.setSignedCookie(
|
|
1019
|
+
cookie.pkCodeVerifier.name,
|
|
1020
|
+
codeVerifier,
|
|
1021
|
+
c.context.secret,
|
|
1022
|
+
cookie.pkCodeVerifier.options
|
|
1023
|
+
);
|
|
1024
|
+
const url = provider.createAuthorizationURL({
|
|
1025
|
+
state: state.state,
|
|
1026
|
+
codeVerifier
|
|
1027
|
+
});
|
|
1028
|
+
url.searchParams.set(
|
|
1029
|
+
"redirect_uri",
|
|
1030
|
+
`${c.context.baseURL}/callback/${c.body.provider}`
|
|
1031
|
+
);
|
|
1032
|
+
return {
|
|
1033
|
+
url: url.toString(),
|
|
1034
|
+
state: state.state,
|
|
1035
|
+
codeVerifier,
|
|
1036
|
+
redirect: true
|
|
1037
|
+
};
|
|
1038
|
+
} catch (e) {
|
|
1039
|
+
throw new APIError3("INTERNAL_SERVER_ERROR");
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
);
|
|
1043
|
+
var signInEmail = createAuthEndpoint(
|
|
1044
|
+
"/sign-in/email",
|
|
1045
|
+
{
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
body: z4.object({
|
|
1048
|
+
email: z4.string().email(),
|
|
1049
|
+
password: z4.string(),
|
|
1050
|
+
callbackURL: z4.string().optional(),
|
|
1051
|
+
/**
|
|
1052
|
+
* If this is true the session will only be valid for the current browser session
|
|
1053
|
+
* @default false
|
|
1054
|
+
*/
|
|
1055
|
+
dontRememberMe: z4.boolean().default(false).optional()
|
|
1056
|
+
})
|
|
1057
|
+
},
|
|
1058
|
+
async (ctx) => {
|
|
1059
|
+
if (!ctx.context.options?.emailAndPassword?.enabled) {
|
|
1060
|
+
ctx.context.logger.error(
|
|
1061
|
+
"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!"
|
|
1062
|
+
);
|
|
1063
|
+
throw new APIError3("BAD_REQUEST", {
|
|
1064
|
+
message: "Email and password is not enabled"
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
const currentSession = await getSessionFromCtx(ctx);
|
|
1068
|
+
if (currentSession) {
|
|
1069
|
+
await ctx.context.internalAdapter.deleteSession(
|
|
1070
|
+
currentSession.session.id
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
const { email, password } = ctx.body;
|
|
1074
|
+
const checkEmail = z4.string().email().safeParse(email);
|
|
1075
|
+
if (!checkEmail.success) {
|
|
1076
|
+
throw new APIError3("BAD_REQUEST", {
|
|
1077
|
+
message: "Invalid email"
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
1081
|
+
if (!user) {
|
|
1082
|
+
await ctx.context.password.hash(password);
|
|
1083
|
+
ctx.context.logger.error("User not found", { email });
|
|
1084
|
+
throw new APIError3("UNAUTHORIZED", {
|
|
1085
|
+
message: "Invalid email or password"
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
const credentialAccount = user.accounts.find(
|
|
1089
|
+
(a) => a.providerId === "credential"
|
|
1090
|
+
);
|
|
1091
|
+
if (!credentialAccount) {
|
|
1092
|
+
ctx.context.logger.error("Credential account not found", { email });
|
|
1093
|
+
throw new APIError3("UNAUTHORIZED", {
|
|
1094
|
+
message: "Invalid email or password"
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
const currentPassword = credentialAccount?.password;
|
|
1098
|
+
if (!currentPassword) {
|
|
1099
|
+
ctx.context.logger.error("Password not found", { email });
|
|
1100
|
+
throw new APIError3("UNAUTHORIZED", {
|
|
1101
|
+
message: "Unexpected error"
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
const validPassword = await ctx.context.password.verify(
|
|
1105
|
+
currentPassword,
|
|
1106
|
+
password
|
|
1107
|
+
);
|
|
1108
|
+
if (!validPassword) {
|
|
1109
|
+
ctx.context.logger.error("Invalid password");
|
|
1110
|
+
throw new APIError3("UNAUTHORIZED", {
|
|
1111
|
+
message: "Invalid email or password"
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
const session = await ctx.context.internalAdapter.createSession(
|
|
1115
|
+
user.user.id,
|
|
1116
|
+
ctx.headers,
|
|
1117
|
+
ctx.body.dontRememberMe
|
|
1118
|
+
);
|
|
1119
|
+
if (!session) {
|
|
1120
|
+
ctx.context.logger.error("Failed to create session");
|
|
1121
|
+
throw new APIError3("INTERNAL_SERVER_ERROR");
|
|
1122
|
+
}
|
|
1123
|
+
await setSessionCookie(ctx, session.id, ctx.body.dontRememberMe);
|
|
1124
|
+
return ctx.json({
|
|
1125
|
+
user: user.user,
|
|
1126
|
+
session,
|
|
1127
|
+
redirect: !!ctx.body.callbackURL,
|
|
1128
|
+
url: ctx.body.callbackURL
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
);
|
|
1132
|
+
|
|
1133
|
+
// src/api/routes/callback.ts
|
|
1134
|
+
import { APIError as APIError4 } from "better-call";
|
|
1135
|
+
import { z as z6 } from "zod";
|
|
1136
|
+
|
|
1137
|
+
// src/db/schema.ts
|
|
1138
|
+
import { z as z5 } from "zod";
|
|
1139
|
+
var accountSchema = z5.object({
|
|
1140
|
+
id: z5.string(),
|
|
1141
|
+
providerId: z5.string(),
|
|
1142
|
+
accountId: z5.string(),
|
|
1143
|
+
userId: z5.string(),
|
|
1144
|
+
accessToken: z5.string().nullable().optional(),
|
|
1145
|
+
refreshToken: z5.string().nullable().optional(),
|
|
1146
|
+
idToken: z5.string().nullable().optional(),
|
|
1147
|
+
/**
|
|
1148
|
+
* Access token expires at
|
|
1149
|
+
*/
|
|
1150
|
+
expiresAt: z5.date().nullable().optional(),
|
|
1151
|
+
/**
|
|
1152
|
+
* Password is only stored in the credential provider
|
|
1153
|
+
*/
|
|
1154
|
+
password: z5.string().optional().nullable()
|
|
1155
|
+
});
|
|
1156
|
+
var userSchema = z5.object({
|
|
1157
|
+
id: z5.string(),
|
|
1158
|
+
email: z5.string().transform((val) => val.toLowerCase()),
|
|
1159
|
+
emailVerified: z5.boolean().default(false),
|
|
1160
|
+
name: z5.string(),
|
|
1161
|
+
image: z5.string().optional(),
|
|
1162
|
+
createdAt: z5.date().default(/* @__PURE__ */ new Date()),
|
|
1163
|
+
updatedAt: z5.date().default(/* @__PURE__ */ new Date())
|
|
1164
|
+
});
|
|
1165
|
+
var sessionSchema = z5.object({
|
|
1166
|
+
id: z5.string(),
|
|
1167
|
+
userId: z5.string(),
|
|
1168
|
+
expiresAt: z5.date(),
|
|
1169
|
+
ipAddress: z5.string().optional(),
|
|
1170
|
+
userAgent: z5.string().optional()
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// src/utils/id.ts
|
|
1174
|
+
import { alphabet, generateRandomString } from "oslo/crypto";
|
|
1175
|
+
var generateId = () => {
|
|
1176
|
+
return generateRandomString(36, alphabet("a-z", "0-9"));
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
// src/utils/hide-metadata.ts
|
|
1180
|
+
var HIDE_METADATA = {
|
|
1181
|
+
isAction: false
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
// src/utils/getAccount.ts
|
|
1185
|
+
function getAccountTokens(tokens) {
|
|
1186
|
+
const accessToken = tokens.accessToken();
|
|
1187
|
+
let refreshToken = tokens.hasRefreshToken() ? tokens.refreshToken() : void 0;
|
|
1188
|
+
let accessTokenExpiresAt = void 0;
|
|
1189
|
+
try {
|
|
1190
|
+
accessTokenExpiresAt = tokens.accessTokenExpiresAt();
|
|
1191
|
+
} catch {
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
accessToken,
|
|
1195
|
+
refreshToken,
|
|
1196
|
+
expiresAt: accessTokenExpiresAt
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/api/routes/callback.ts
|
|
1201
|
+
var callbackOAuth = createAuthEndpoint(
|
|
1202
|
+
"/callback/:id",
|
|
1203
|
+
{
|
|
1204
|
+
method: "GET",
|
|
1205
|
+
query: z6.object({
|
|
1206
|
+
state: z6.string(),
|
|
1207
|
+
code: z6.string().optional(),
|
|
1208
|
+
error: z6.string().optional()
|
|
1209
|
+
}),
|
|
1210
|
+
metadata: HIDE_METADATA
|
|
1211
|
+
},
|
|
1212
|
+
async (c) => {
|
|
1213
|
+
if (c.query.error || !c.query.code) {
|
|
1214
|
+
const parsedState2 = parseState(c.query.state);
|
|
1215
|
+
const callbackURL2 = parsedState2.data?.callbackURL || `${c.context.baseURL}/error`;
|
|
1216
|
+
c.context.logger.error(c.query.error, c.params.id);
|
|
1217
|
+
throw c.redirect(
|
|
1218
|
+
`${callbackURL2}?error=${c.query.error || "oAuth_code_missing"}`
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
const provider = c.context.socialProviders.find(
|
|
1222
|
+
(p) => p.id === c.params.id
|
|
1223
|
+
);
|
|
1224
|
+
if (!provider) {
|
|
1225
|
+
c.context.logger.error(
|
|
1226
|
+
"Oauth provider with id",
|
|
1227
|
+
c.params.id,
|
|
1228
|
+
"not found"
|
|
1229
|
+
);
|
|
1230
|
+
throw c.redirect(
|
|
1231
|
+
`${c.context.baseURL}/error?error=oauth_provider_not_found`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
const codeVerifier = await c.getSignedCookie(
|
|
1235
|
+
c.context.authCookies.pkCodeVerifier.name,
|
|
1236
|
+
c.context.secret
|
|
1237
|
+
);
|
|
1238
|
+
let tokens;
|
|
1239
|
+
try {
|
|
1240
|
+
tokens = await provider.validateAuthorizationCode(
|
|
1241
|
+
c.query.code,
|
|
1242
|
+
codeVerifier,
|
|
1243
|
+
`${c.context.baseURL}/callback/${provider.id}`
|
|
1244
|
+
);
|
|
1245
|
+
} catch (e) {
|
|
1246
|
+
c.context.logger.error(e);
|
|
1247
|
+
throw c.redirect(
|
|
1248
|
+
`${c.context.baseURL}/error?error=oauth_code_verification_failed`
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
const user = await provider.getUserInfo(tokens).then((res) => res?.user);
|
|
1252
|
+
const id = generateId();
|
|
1253
|
+
const data = userSchema.safeParse({
|
|
1254
|
+
...user,
|
|
1255
|
+
id
|
|
1256
|
+
});
|
|
1257
|
+
const parsedState = parseState(c.query.state);
|
|
1258
|
+
if (!parsedState.success) {
|
|
1259
|
+
c.context.logger.error("Unable to parse state");
|
|
1260
|
+
throw c.redirect(
|
|
1261
|
+
`${c.context.baseURL}/error?error=invalid_state_parameter`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const { callbackURL, currentURL, dontRememberMe } = parsedState.data;
|
|
1265
|
+
if (!user || data.success === false) {
|
|
1266
|
+
throw c.redirect(
|
|
1267
|
+
`${c.context.baseURL}/error?error=oauth_validation_failed`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
if (!callbackURL) {
|
|
1271
|
+
throw c.redirect(
|
|
1272
|
+
`${c.context.baseURL}/error?error=oauth_callback_url_not_found`
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
const dbUser = await c.context.internalAdapter.findUserByEmail(user.email);
|
|
1276
|
+
const userId = dbUser?.user.id;
|
|
1277
|
+
if (dbUser) {
|
|
1278
|
+
const hasBeenLinked = dbUser.accounts.find(
|
|
1279
|
+
(a) => a.providerId === provider.id
|
|
1280
|
+
);
|
|
1281
|
+
const trustedProviders = c.context.options.account?.accountLinking?.trustedProviders;
|
|
1282
|
+
const isTrustedProvider = trustedProviders ? trustedProviders.includes(provider.id) : true;
|
|
1283
|
+
if (!hasBeenLinked && (!user.emailVerified || !isTrustedProvider)) {
|
|
1284
|
+
let url;
|
|
1285
|
+
try {
|
|
1286
|
+
url = new URL(currentURL || callbackURL);
|
|
1287
|
+
url.searchParams.set("error", "account_not_linked");
|
|
1288
|
+
} catch (e) {
|
|
1289
|
+
throw c.redirect(
|
|
1290
|
+
`${c.context.baseURL}/error?error=account_not_linked`
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
throw c.redirect(url.toString());
|
|
1294
|
+
}
|
|
1295
|
+
if (!hasBeenLinked) {
|
|
1296
|
+
try {
|
|
1297
|
+
await c.context.internalAdapter.linkAccount({
|
|
1298
|
+
providerId: provider.id,
|
|
1299
|
+
accountId: user.id,
|
|
1300
|
+
id: `${provider.id}:${user.id}`,
|
|
1301
|
+
userId: dbUser.user.id,
|
|
1302
|
+
...getAccountTokens(tokens)
|
|
1303
|
+
});
|
|
1304
|
+
} catch (e) {
|
|
1305
|
+
console.log(e);
|
|
1306
|
+
throw c.redirect(
|
|
1307
|
+
`${c.context.baseURL}/error?error=failed_linking_account`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
} else {
|
|
1312
|
+
try {
|
|
1313
|
+
await c.context.internalAdapter.createOAuthUser(data.data, {
|
|
1314
|
+
...getAccountTokens(tokens),
|
|
1315
|
+
id: `${provider.id}:${user.id}`,
|
|
1316
|
+
providerId: provider.id,
|
|
1317
|
+
accountId: user.id,
|
|
1318
|
+
userId: id
|
|
1319
|
+
});
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
const url = new URL(currentURL || callbackURL);
|
|
1322
|
+
url.searchParams.set("error", "unable_to_create_user");
|
|
1323
|
+
c.setHeader("Location", url.toString());
|
|
1324
|
+
throw c.redirect(url.toString());
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (!userId && !id)
|
|
1328
|
+
throw new APIError4("INTERNAL_SERVER_ERROR", {
|
|
1329
|
+
message: "Unable to create user"
|
|
1330
|
+
});
|
|
1331
|
+
const session = await c.context.internalAdapter.createSession(
|
|
1332
|
+
userId || id,
|
|
1333
|
+
c.request,
|
|
1334
|
+
dontRememberMe
|
|
1335
|
+
);
|
|
1336
|
+
if (!session) {
|
|
1337
|
+
const url = new URL(currentURL || callbackURL);
|
|
1338
|
+
url.searchParams.set("error", "unable_to_create_session");
|
|
1339
|
+
throw c.redirect(url.toString());
|
|
1340
|
+
}
|
|
1341
|
+
try {
|
|
1342
|
+
await setSessionCookie(c, session.id, dontRememberMe);
|
|
1343
|
+
} catch (e) {
|
|
1344
|
+
c.context.logger.error("Unable to set session cookie", e);
|
|
1345
|
+
const url = new URL(currentURL || callbackURL);
|
|
1346
|
+
url.searchParams.set("error", "unable_to_create_session");
|
|
1347
|
+
throw c.redirect(url.toString());
|
|
1348
|
+
}
|
|
1349
|
+
throw c.redirect(callbackURL);
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
|
|
1353
|
+
// src/api/routes/sign-out.ts
|
|
1354
|
+
import { z as z7 } from "zod";
|
|
1355
|
+
var signOut = createAuthEndpoint(
|
|
1356
|
+
"/sign-out",
|
|
1357
|
+
{
|
|
1358
|
+
method: "POST",
|
|
1359
|
+
body: z7.optional(
|
|
1360
|
+
z7.object({
|
|
1361
|
+
callbackURL: z7.string().optional()
|
|
1362
|
+
})
|
|
1363
|
+
)
|
|
1364
|
+
},
|
|
1365
|
+
async (ctx) => {
|
|
1366
|
+
const sessionCookieToken = await ctx.getSignedCookie(
|
|
1367
|
+
ctx.context.authCookies.sessionToken.name,
|
|
1368
|
+
ctx.context.secret
|
|
1369
|
+
);
|
|
1370
|
+
if (!sessionCookieToken) {
|
|
1371
|
+
return ctx.json(null);
|
|
1372
|
+
}
|
|
1373
|
+
await ctx.context.internalAdapter.deleteSession(sessionCookieToken);
|
|
1374
|
+
deleteSessionCookie(ctx);
|
|
1375
|
+
return ctx.json(null, {
|
|
1376
|
+
body: {
|
|
1377
|
+
redirect: !!ctx.body?.callbackURL,
|
|
1378
|
+
url: ctx.body?.callbackURL
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
|
|
1384
|
+
// src/api/routes/forget-password.ts
|
|
1385
|
+
import { TimeSpan as TimeSpan2 } from "oslo";
|
|
1386
|
+
import { createJWT, parseJWT as parseJWT3 } from "oslo/jwt";
|
|
1387
|
+
import { validateJWT } from "oslo/jwt";
|
|
1388
|
+
import { z as z8 } from "zod";
|
|
1389
|
+
var forgetPassword = createAuthEndpoint(
|
|
1390
|
+
"/forget-password",
|
|
1391
|
+
{
|
|
1392
|
+
method: "POST",
|
|
1393
|
+
body: z8.object({
|
|
1394
|
+
/**
|
|
1395
|
+
* The email address of the user to send a password reset email to.
|
|
1396
|
+
*/
|
|
1397
|
+
email: z8.string().email(),
|
|
1398
|
+
/**
|
|
1399
|
+
* The URL to redirect the user to reset their password.
|
|
1400
|
+
* If the token isn't valid or expired, it'll be redirected with a query parameter `?
|
|
1401
|
+
* error=INVALID_TOKEN`. If the token is valid, it'll be redirected with a query parameter `?
|
|
1402
|
+
* token=VALID_TOKEN
|
|
1403
|
+
*/
|
|
1404
|
+
redirectTo: z8.string()
|
|
1405
|
+
})
|
|
1406
|
+
},
|
|
1407
|
+
async (ctx) => {
|
|
1408
|
+
if (!ctx.context.options.emailAndPassword?.sendResetPassword) {
|
|
1409
|
+
ctx.context.logger.error(
|
|
1410
|
+
"Reset password isn't enabled.Please pass an emailAndPassword.sendResetPasswordToken function to your auth config!"
|
|
1411
|
+
);
|
|
1412
|
+
return ctx.json(null, {
|
|
1413
|
+
status: 400,
|
|
1414
|
+
statusText: "RESET_PASSWORD_EMAIL_NOT_SENT",
|
|
1415
|
+
body: {
|
|
1416
|
+
message: "Reset password isn't enabled"
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
const { email } = ctx.body;
|
|
1421
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
1422
|
+
if (!user) {
|
|
1423
|
+
return ctx.json(
|
|
1424
|
+
{
|
|
1425
|
+
status: false
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
body: {
|
|
1429
|
+
status: true
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
const token = await createJWT(
|
|
1435
|
+
"HS256",
|
|
1436
|
+
Buffer.from(ctx.context.secret),
|
|
1437
|
+
{
|
|
1438
|
+
email: user.user.email,
|
|
1439
|
+
redirectTo: ctx.body.redirectTo
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
expiresIn: new TimeSpan2(1, "h"),
|
|
1443
|
+
issuer: "better-auth",
|
|
1444
|
+
subject: "forget-password",
|
|
1445
|
+
audiences: [user.user.email],
|
|
1446
|
+
includeIssuedTimestamp: true
|
|
1447
|
+
}
|
|
1448
|
+
);
|
|
1449
|
+
const url = `${ctx.context.baseURL}/reset-password/${token}`;
|
|
1450
|
+
await ctx.context.options.emailAndPassword.sendResetPassword(
|
|
1451
|
+
url,
|
|
1452
|
+
user.user
|
|
1453
|
+
);
|
|
1454
|
+
return ctx.json({
|
|
1455
|
+
status: true
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
);
|
|
1459
|
+
var forgetPasswordCallback = createAuthEndpoint(
|
|
1460
|
+
"/reset-password/:token",
|
|
1461
|
+
{
|
|
1462
|
+
method: "GET"
|
|
1463
|
+
},
|
|
1464
|
+
async (ctx) => {
|
|
1465
|
+
const { token } = ctx.params;
|
|
1466
|
+
let decodedToken;
|
|
1467
|
+
const schema = z8.object({
|
|
1468
|
+
email: z8.string(),
|
|
1469
|
+
redirectTo: z8.string()
|
|
1470
|
+
});
|
|
1471
|
+
try {
|
|
1472
|
+
decodedToken = await validateJWT(
|
|
1473
|
+
"HS256",
|
|
1474
|
+
Buffer.from(ctx.context.secret),
|
|
1475
|
+
token
|
|
1476
|
+
);
|
|
1477
|
+
if (!decodedToken.expiresAt || decodedToken.expiresAt < /* @__PURE__ */ new Date()) {
|
|
1478
|
+
throw Error("Token expired");
|
|
1479
|
+
}
|
|
1480
|
+
} catch (e) {
|
|
1481
|
+
const decoded = parseJWT3(token);
|
|
1482
|
+
const jwt = schema.safeParse(decoded?.payload);
|
|
1483
|
+
if (jwt.success) {
|
|
1484
|
+
throw ctx.redirect(`${jwt.data?.redirectTo}?error=invalid_token`);
|
|
1485
|
+
} else {
|
|
1486
|
+
throw ctx.redirect(`${ctx.context.baseURL}/error?error=invalid_token`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
const { redirectTo } = schema.parse(decodedToken.payload);
|
|
1490
|
+
throw ctx.redirect(`${redirectTo}?token=${token}`);
|
|
1491
|
+
}
|
|
1492
|
+
);
|
|
1493
|
+
var resetPassword = createAuthEndpoint(
|
|
1494
|
+
"/reset-password",
|
|
1495
|
+
{
|
|
1496
|
+
method: "POST",
|
|
1497
|
+
query: z8.object({
|
|
1498
|
+
currentURL: z8.string()
|
|
1499
|
+
}).optional(),
|
|
1500
|
+
body: z8.object({
|
|
1501
|
+
newPassword: z8.string(),
|
|
1502
|
+
callbackURL: z8.string().optional()
|
|
1503
|
+
})
|
|
1504
|
+
},
|
|
1505
|
+
async (ctx) => {
|
|
1506
|
+
const token = ctx.query?.currentURL.split("?token=")[1];
|
|
1507
|
+
if (!token) {
|
|
1508
|
+
return ctx.json(
|
|
1509
|
+
{
|
|
1510
|
+
error: "Invalid token",
|
|
1511
|
+
data: null
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
status: 400,
|
|
1515
|
+
statusText: "INVALID_TOKEN",
|
|
1516
|
+
body: {
|
|
1517
|
+
message: "Invalid token"
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
const { newPassword } = ctx.body;
|
|
1523
|
+
try {
|
|
1524
|
+
const jwt = await validateJWT(
|
|
1525
|
+
"HS256",
|
|
1526
|
+
Buffer.from(ctx.context.secret),
|
|
1527
|
+
token
|
|
1528
|
+
);
|
|
1529
|
+
const email = z8.string().email().parse(jwt.payload.email);
|
|
1530
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
1531
|
+
if (!user) {
|
|
1532
|
+
return ctx.json(
|
|
1533
|
+
{
|
|
1534
|
+
error: "User not found",
|
|
1535
|
+
data: null
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
status: 400,
|
|
1539
|
+
body: {
|
|
1540
|
+
message: "failed to reset password"
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
if (newPassword.length < (ctx.context.options.emailAndPassword?.minPasswordLength || 8) || newPassword.length > (ctx.context.options.emailAndPassword?.maxPasswordLength || 32)) {
|
|
1546
|
+
return ctx.json(
|
|
1547
|
+
{
|
|
1548
|
+
data: null,
|
|
1549
|
+
error: "password is too short or too long"
|
|
1550
|
+
},
|
|
1551
|
+
{
|
|
1552
|
+
status: 400,
|
|
1553
|
+
statusText: "INVALID_PASSWORD_LENGTH",
|
|
1554
|
+
body: {
|
|
1555
|
+
message: "password is too short or too long"
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
const hashedPassword = await ctx.context.password.hash(newPassword);
|
|
1561
|
+
const updatedUser = await ctx.context.internalAdapter.updatePassword(
|
|
1562
|
+
user.user.id,
|
|
1563
|
+
hashedPassword
|
|
1564
|
+
);
|
|
1565
|
+
if (!updatedUser) {
|
|
1566
|
+
return ctx.json(null, {
|
|
1567
|
+
status: 400,
|
|
1568
|
+
statusText: "USER_NOT_FOUND",
|
|
1569
|
+
body: {
|
|
1570
|
+
message: "User doesn't have a credential account"
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
return ctx.json(
|
|
1575
|
+
{
|
|
1576
|
+
error: null,
|
|
1577
|
+
data: {
|
|
1578
|
+
status: true,
|
|
1579
|
+
url: ctx.body.callbackURL,
|
|
1580
|
+
redirect: !!ctx.body.callbackURL
|
|
1581
|
+
}
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
body: {
|
|
1585
|
+
status: true,
|
|
1586
|
+
url: ctx.body.callbackURL,
|
|
1587
|
+
redirect: !!ctx.body.callbackURL
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1591
|
+
} catch (e) {
|
|
1592
|
+
console.log(e);
|
|
1593
|
+
return ctx.json(
|
|
1594
|
+
{
|
|
1595
|
+
error: "Invalid token",
|
|
1596
|
+
data: null
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
status: 400,
|
|
1600
|
+
statusText: "INVALID_TOKEN",
|
|
1601
|
+
body: {
|
|
1602
|
+
message: "Invalid token"
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
);
|
|
1609
|
+
|
|
1610
|
+
// src/api/routes/verify-email.ts
|
|
1611
|
+
import { TimeSpan as TimeSpan3 } from "oslo";
|
|
1612
|
+
import { createJWT as createJWT2, validateJWT as validateJWT2 } from "oslo/jwt";
|
|
1613
|
+
import { z as z9 } from "zod";
|
|
1614
|
+
async function createEmailVerificationToken(secret, email) {
|
|
1615
|
+
const token = await createJWT2(
|
|
1616
|
+
"HS256",
|
|
1617
|
+
Buffer.from(secret),
|
|
1618
|
+
{
|
|
1619
|
+
email: email.toLowerCase()
|
|
1620
|
+
},
|
|
1621
|
+
{
|
|
1622
|
+
expiresIn: new TimeSpan3(1, "h"),
|
|
1623
|
+
issuer: "better-auth",
|
|
1624
|
+
subject: "verify-email",
|
|
1625
|
+
audiences: [email],
|
|
1626
|
+
includeIssuedTimestamp: true
|
|
1627
|
+
}
|
|
1628
|
+
);
|
|
1629
|
+
return token;
|
|
1630
|
+
}
|
|
1631
|
+
var sendVerificationEmail = createAuthEndpoint(
|
|
1632
|
+
"/send-verification-email",
|
|
1633
|
+
{
|
|
1634
|
+
method: "POST",
|
|
1635
|
+
query: z9.object({
|
|
1636
|
+
currentURL: z9.string().optional()
|
|
1637
|
+
}).optional(),
|
|
1638
|
+
body: z9.object({
|
|
1639
|
+
email: z9.string().email(),
|
|
1640
|
+
callbackURL: z9.string().optional()
|
|
1641
|
+
})
|
|
1642
|
+
},
|
|
1643
|
+
async (ctx) => {
|
|
1644
|
+
if (!ctx.context.options.emailAndPassword?.sendVerificationEmail) {
|
|
1645
|
+
ctx.context.logger.error(
|
|
1646
|
+
"Verification email isn't enabled. Pass `sendVerificationEmail` in `emailAndPassword` options to enable it."
|
|
1647
|
+
);
|
|
1648
|
+
return ctx.json(null, {
|
|
1649
|
+
status: 400,
|
|
1650
|
+
statusText: "VERIFICATION_EMAIL_NOT_SENT",
|
|
1651
|
+
body: {
|
|
1652
|
+
message: "Verification email isn't enabled"
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
const { email } = ctx.body;
|
|
1657
|
+
const token = await createEmailVerificationToken(ctx.context.secret, email);
|
|
1658
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || ctx.query?.currentURL || "/"}`;
|
|
1659
|
+
await ctx.context.options.emailAndPassword.sendVerificationEmail(
|
|
1660
|
+
email,
|
|
1661
|
+
url,
|
|
1662
|
+
token
|
|
1663
|
+
);
|
|
1664
|
+
return ctx.json({
|
|
1665
|
+
status: true
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
);
|
|
1669
|
+
var verifyEmail = createAuthEndpoint(
|
|
1670
|
+
"/verify-email",
|
|
1671
|
+
{
|
|
1672
|
+
method: "GET",
|
|
1673
|
+
query: z9.object({
|
|
1674
|
+
token: z9.string(),
|
|
1675
|
+
callbackURL: z9.string().optional()
|
|
1676
|
+
})
|
|
1677
|
+
},
|
|
1678
|
+
async (ctx) => {
|
|
1679
|
+
const { token } = ctx.query;
|
|
1680
|
+
let jwt;
|
|
1681
|
+
try {
|
|
1682
|
+
jwt = await validateJWT2("HS256", Buffer.from(ctx.context.secret), token);
|
|
1683
|
+
} catch (e) {
|
|
1684
|
+
ctx.context.logger.error("Failed to verify email", e);
|
|
1685
|
+
return ctx.json(null, {
|
|
1686
|
+
status: 400,
|
|
1687
|
+
statusText: "INVALID_TOKEN",
|
|
1688
|
+
body: {
|
|
1689
|
+
message: "Invalid token"
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
const schema = z9.object({
|
|
1694
|
+
email: z9.string().email()
|
|
1695
|
+
});
|
|
1696
|
+
const parsed = schema.parse(jwt.payload);
|
|
1697
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(
|
|
1698
|
+
parsed.email
|
|
1699
|
+
);
|
|
1700
|
+
if (!user) {
|
|
1701
|
+
return ctx.json(null, {
|
|
1702
|
+
status: 400,
|
|
1703
|
+
statusText: "USER_NOT_FOUND",
|
|
1704
|
+
body: {
|
|
1705
|
+
message: "User not found"
|
|
1706
|
+
}
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
const account = user.accounts.find((a) => a.providerId === "credential");
|
|
1710
|
+
if (!account) {
|
|
1711
|
+
throw ctx.redirect;
|
|
1712
|
+
}
|
|
1713
|
+
await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
|
|
1714
|
+
emailVerified: true
|
|
1715
|
+
});
|
|
1716
|
+
if (ctx.query.callbackURL) {
|
|
1717
|
+
console.log("Redirecting to", ctx.query.callbackURL);
|
|
1718
|
+
throw ctx.redirect("/");
|
|
1719
|
+
}
|
|
1720
|
+
return ctx.json({
|
|
1721
|
+
status: true
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
);
|
|
1725
|
+
|
|
1726
|
+
// src/api/routes/update-user.ts
|
|
1727
|
+
import { z as z10 } from "zod";
|
|
1728
|
+
import { alphabet as alphabet2, generateRandomString as generateRandomString2 } from "oslo/crypto";
|
|
1729
|
+
import "better-call";
|
|
1730
|
+
var updateUser = createAuthEndpoint(
|
|
1731
|
+
"/user/update",
|
|
1732
|
+
{
|
|
1733
|
+
method: "POST",
|
|
1734
|
+
body: z10.object({
|
|
1735
|
+
name: z10.string().optional(),
|
|
1736
|
+
image: z10.string().optional()
|
|
1737
|
+
}),
|
|
1738
|
+
use: [sessionMiddleware]
|
|
1739
|
+
},
|
|
1740
|
+
async (ctx) => {
|
|
1741
|
+
const { name, image } = ctx.body;
|
|
1742
|
+
const session = ctx.context.session;
|
|
1743
|
+
if (!image && !name) {
|
|
1744
|
+
return ctx.json(session.user);
|
|
1745
|
+
}
|
|
1746
|
+
const user = await ctx.context.internalAdapter.updateUserByEmail(
|
|
1747
|
+
session.user.email,
|
|
1748
|
+
{
|
|
1749
|
+
name,
|
|
1750
|
+
image
|
|
1751
|
+
}
|
|
1752
|
+
);
|
|
1753
|
+
return ctx.json(user);
|
|
1754
|
+
}
|
|
1755
|
+
);
|
|
1756
|
+
var changePassword = createAuthEndpoint(
|
|
1757
|
+
"/user/change-password",
|
|
1758
|
+
{
|
|
1759
|
+
method: "POST",
|
|
1760
|
+
body: z10.object({
|
|
1761
|
+
/**
|
|
1762
|
+
* The new password to set
|
|
1763
|
+
*/
|
|
1764
|
+
newPassword: z10.string(),
|
|
1765
|
+
/**
|
|
1766
|
+
* The current password of the user
|
|
1767
|
+
*/
|
|
1768
|
+
currentPassword: z10.string(),
|
|
1769
|
+
/**
|
|
1770
|
+
* revoke all sessions that are not the
|
|
1771
|
+
* current one logged in by the user
|
|
1772
|
+
*/
|
|
1773
|
+
revokeOtherSessions: z10.boolean().optional()
|
|
1774
|
+
}),
|
|
1775
|
+
use: [sessionMiddleware]
|
|
1776
|
+
},
|
|
1777
|
+
async (ctx) => {
|
|
1778
|
+
const { newPassword, currentPassword, revokeOtherSessions } = ctx.body;
|
|
1779
|
+
const session = ctx.context.session;
|
|
1780
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
1781
|
+
if (newPassword.length < minPasswordLength) {
|
|
1782
|
+
ctx.context.logger.error("Password is too short");
|
|
1783
|
+
return ctx.json(null, {
|
|
1784
|
+
status: 400,
|
|
1785
|
+
body: { message: "Password is too short" }
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
1789
|
+
if (newPassword.length > maxPasswordLength) {
|
|
1790
|
+
ctx.context.logger.error("Password is too long");
|
|
1791
|
+
return ctx.json(null, {
|
|
1792
|
+
status: 400,
|
|
1793
|
+
body: { message: "Password is too long" }
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(
|
|
1797
|
+
session.user.id
|
|
1798
|
+
);
|
|
1799
|
+
const account = accounts.find(
|
|
1800
|
+
(account2) => account2.providerId === "credential" && account2.password
|
|
1801
|
+
);
|
|
1802
|
+
if (!account || !account.password) {
|
|
1803
|
+
return ctx.json(null, {
|
|
1804
|
+
status: 400,
|
|
1805
|
+
body: { message: "User does not have a password" }
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
1809
|
+
const verify = await ctx.context.password.verify(
|
|
1810
|
+
account.password,
|
|
1811
|
+
currentPassword
|
|
1812
|
+
);
|
|
1813
|
+
if (!verify) {
|
|
1814
|
+
return ctx.json(null, {
|
|
1815
|
+
status: 400,
|
|
1816
|
+
body: { message: "Invalid password" }
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
await ctx.context.internalAdapter.updateAccount(account.id, {
|
|
1820
|
+
password: passwordHash
|
|
1821
|
+
});
|
|
1822
|
+
if (revokeOtherSessions) {
|
|
1823
|
+
await ctx.context.internalAdapter.deleteSessions(session.user.id);
|
|
1824
|
+
const newSession = await ctx.context.internalAdapter.createSession(
|
|
1825
|
+
session.user.id,
|
|
1826
|
+
ctx.headers
|
|
1827
|
+
);
|
|
1828
|
+
if (!newSession) {
|
|
1829
|
+
return ctx.json(null, {
|
|
1830
|
+
status: 500,
|
|
1831
|
+
body: { message: "Failed to create session" }
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
await setSessionCookie(ctx, newSession.id);
|
|
1835
|
+
}
|
|
1836
|
+
return ctx.json(session.user);
|
|
1837
|
+
}
|
|
1838
|
+
);
|
|
1839
|
+
var setPassword = createAuthEndpoint(
|
|
1840
|
+
"/user/set-password",
|
|
1841
|
+
{
|
|
1842
|
+
method: "POST",
|
|
1843
|
+
body: z10.object({
|
|
1844
|
+
/**
|
|
1845
|
+
* The new password to set
|
|
1846
|
+
*/
|
|
1847
|
+
newPassword: z10.string()
|
|
1848
|
+
}),
|
|
1849
|
+
use: [sessionMiddleware]
|
|
1850
|
+
},
|
|
1851
|
+
async (ctx) => {
|
|
1852
|
+
const { newPassword } = ctx.body;
|
|
1853
|
+
const session = ctx.context.session;
|
|
1854
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
1855
|
+
if (newPassword.length < minPasswordLength) {
|
|
1856
|
+
ctx.context.logger.error("Password is too short");
|
|
1857
|
+
return ctx.json(null, {
|
|
1858
|
+
status: 400,
|
|
1859
|
+
body: { message: "Password is too short" }
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
1863
|
+
if (newPassword.length > maxPasswordLength) {
|
|
1864
|
+
ctx.context.logger.error("Password is too long");
|
|
1865
|
+
return ctx.json(null, {
|
|
1866
|
+
status: 400,
|
|
1867
|
+
body: { message: "Password is too long" }
|
|
1868
|
+
});
|
|
1869
|
+
}
|
|
1870
|
+
const accounts = await ctx.context.internalAdapter.findAccounts(
|
|
1871
|
+
session.user.id
|
|
1872
|
+
);
|
|
1873
|
+
const account = accounts.find(
|
|
1874
|
+
(account2) => account2.providerId === "credential" && account2.password
|
|
1875
|
+
);
|
|
1876
|
+
const passwordHash = await ctx.context.password.hash(newPassword);
|
|
1877
|
+
if (!account) {
|
|
1878
|
+
await ctx.context.internalAdapter.linkAccount({
|
|
1879
|
+
id: generateRandomString2(32, alphabet2("a-z", "0-9", "A-Z")),
|
|
1880
|
+
userId: session.user.id,
|
|
1881
|
+
providerId: "credential",
|
|
1882
|
+
accountId: session.user.id,
|
|
1883
|
+
password: passwordHash
|
|
1884
|
+
});
|
|
1885
|
+
return ctx.json(session.user);
|
|
1886
|
+
}
|
|
1887
|
+
return ctx.json(null, {
|
|
1888
|
+
status: 400,
|
|
1889
|
+
body: { message: "User already has a password" }
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
);
|
|
1893
|
+
|
|
1894
|
+
// src/api/routes/csrf.ts
|
|
1895
|
+
import { alphabet as alphabet3, generateRandomString as generateRandomString3 } from "oslo/crypto";
|
|
1896
|
+
var getCSRFToken = createAuthEndpoint(
|
|
1897
|
+
"/csrf",
|
|
1898
|
+
{
|
|
1899
|
+
method: "GET",
|
|
1900
|
+
metadata: HIDE_METADATA
|
|
1901
|
+
},
|
|
1902
|
+
async (ctx) => {
|
|
1903
|
+
const csrfToken = await ctx.getSignedCookie(
|
|
1904
|
+
ctx.context.authCookies.csrfToken.name,
|
|
1905
|
+
ctx.context.secret
|
|
1906
|
+
);
|
|
1907
|
+
if (csrfToken) {
|
|
1908
|
+
return {
|
|
1909
|
+
csrfToken
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
const token = generateRandomString3(32, alphabet3("a-z", "0-9", "A-Z"));
|
|
1913
|
+
const hash = await hs256(ctx.context.secret, token);
|
|
1914
|
+
const cookie = `${token}!${hash}`;
|
|
1915
|
+
await ctx.setSignedCookie(
|
|
1916
|
+
ctx.context.authCookies.csrfToken.name,
|
|
1917
|
+
cookie,
|
|
1918
|
+
ctx.context.secret,
|
|
1919
|
+
ctx.context.authCookies.csrfToken.options
|
|
1920
|
+
);
|
|
1921
|
+
return {
|
|
1922
|
+
csrfToken: token
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
);
|
|
1926
|
+
|
|
1927
|
+
// src/api/routes/error.ts
|
|
1928
|
+
var html = (errorCode = "Unknown") => `<!DOCTYPE html>
|
|
3
1929
|
<html lang="en">
|
|
4
1930
|
<head>
|
|
5
1931
|
<meta charset="UTF-8">
|
|
@@ -76,7 +2002,535 @@ import{APIError as Et,createRouter as Ot}from"better-call";import{APIError as Q}
|
|
|
76
2002
|
<h1>Better Auth Error</h1>
|
|
77
2003
|
<p>We encountered an issue while processing your request. Please try again or contact the application owner if the problem persists.</p>
|
|
78
2004
|
<a href="/" id="returnLink" class="btn">Return to Application</a>
|
|
79
|
-
<div class="error-code">Error Code: <span id="errorCode">${
|
|
2005
|
+
<div class="error-code">Error Code: <span id="errorCode">${errorCode}</span></div>
|
|
80
2006
|
</div>
|
|
81
2007
|
</body>
|
|
82
|
-
</html
|
|
2008
|
+
</html>`;
|
|
2009
|
+
var error = createAuthEndpoint(
|
|
2010
|
+
"/error",
|
|
2011
|
+
{
|
|
2012
|
+
method: "GET",
|
|
2013
|
+
metadata: HIDE_METADATA
|
|
2014
|
+
},
|
|
2015
|
+
async (c) => {
|
|
2016
|
+
const query = new URL(c.request?.url || "").searchParams.get("error") || "Unknown";
|
|
2017
|
+
return new Response(html(query), {
|
|
2018
|
+
headers: {
|
|
2019
|
+
"Content-Type": "text/html"
|
|
2020
|
+
}
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
);
|
|
2024
|
+
|
|
2025
|
+
// src/api/routes/ok.ts
|
|
2026
|
+
var ok = createAuthEndpoint(
|
|
2027
|
+
"/ok",
|
|
2028
|
+
{
|
|
2029
|
+
method: "GET",
|
|
2030
|
+
metadata: HIDE_METADATA
|
|
2031
|
+
},
|
|
2032
|
+
async (ctx) => {
|
|
2033
|
+
return ctx.json({
|
|
2034
|
+
ok: true
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
);
|
|
2038
|
+
|
|
2039
|
+
// src/api/routes/sign-up.ts
|
|
2040
|
+
import { alphabet as alphabet4, generateRandomString as generateRandomString4 } from "oslo/crypto";
|
|
2041
|
+
import { z as z11 } from "zod";
|
|
2042
|
+
var signUpEmail = createAuthEndpoint(
|
|
2043
|
+
"/sign-up/email",
|
|
2044
|
+
{
|
|
2045
|
+
method: "POST",
|
|
2046
|
+
query: z11.object({
|
|
2047
|
+
currentURL: z11.string().optional()
|
|
2048
|
+
}).optional(),
|
|
2049
|
+
body: z11.object({
|
|
2050
|
+
name: z11.string(),
|
|
2051
|
+
email: z11.string(),
|
|
2052
|
+
password: z11.string(),
|
|
2053
|
+
image: z11.string().optional(),
|
|
2054
|
+
callbackURL: z11.string().optional()
|
|
2055
|
+
})
|
|
2056
|
+
},
|
|
2057
|
+
async (ctx) => {
|
|
2058
|
+
if (!ctx.context.options.emailAndPassword?.enabled) {
|
|
2059
|
+
return ctx.json(null, {
|
|
2060
|
+
status: 400,
|
|
2061
|
+
body: {
|
|
2062
|
+
message: "Email and password is not enabled"
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
const { name, email, password, image } = ctx.body;
|
|
2067
|
+
const isValidEmail = z11.string().email().safeParse(email);
|
|
2068
|
+
if (!isValidEmail.success) {
|
|
2069
|
+
return ctx.json(null, {
|
|
2070
|
+
status: 400,
|
|
2071
|
+
body: {
|
|
2072
|
+
message: "Invalid email address"
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
const minPasswordLength = ctx.context.password.config.minPasswordLength;
|
|
2077
|
+
if (password.length < minPasswordLength) {
|
|
2078
|
+
ctx.context.logger.error("Password is too short");
|
|
2079
|
+
return ctx.json(null, {
|
|
2080
|
+
status: 400,
|
|
2081
|
+
body: { message: "Password is too short" }
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
const maxPasswordLength = ctx.context.password.config.maxPasswordLength;
|
|
2085
|
+
if (password.length > maxPasswordLength) {
|
|
2086
|
+
ctx.context.logger.error("Password is too long");
|
|
2087
|
+
return ctx.json(null, {
|
|
2088
|
+
status: 400,
|
|
2089
|
+
body: { message: "Password is too long" }
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
const dbUser = await ctx.context.internalAdapter.findUserByEmail(email);
|
|
2093
|
+
const hash = await ctx.context.password.hash(password);
|
|
2094
|
+
if (dbUser?.user) {
|
|
2095
|
+
return ctx.json(null, {
|
|
2096
|
+
status: 400,
|
|
2097
|
+
body: {
|
|
2098
|
+
message: "User already exists"
|
|
2099
|
+
}
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
const createdUser = await ctx.context.internalAdapter.createUser({
|
|
2103
|
+
id: generateRandomString4(32, alphabet4("a-z", "0-9", "A-Z")),
|
|
2104
|
+
email: email.toLowerCase(),
|
|
2105
|
+
name,
|
|
2106
|
+
image,
|
|
2107
|
+
emailVerified: false,
|
|
2108
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
2109
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
2110
|
+
});
|
|
2111
|
+
if (!createdUser) {
|
|
2112
|
+
return ctx.json(null, {
|
|
2113
|
+
status: 400,
|
|
2114
|
+
body: {
|
|
2115
|
+
message: "Could not create user"
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
await ctx.context.internalAdapter.linkAccount({
|
|
2120
|
+
id: generateRandomString4(32, alphabet4("a-z", "0-9", "A-Z")),
|
|
2121
|
+
userId: createdUser.id,
|
|
2122
|
+
providerId: "credential",
|
|
2123
|
+
accountId: createdUser.id,
|
|
2124
|
+
password: hash
|
|
2125
|
+
});
|
|
2126
|
+
const session = await ctx.context.internalAdapter.createSession(
|
|
2127
|
+
createdUser.id,
|
|
2128
|
+
ctx.request
|
|
2129
|
+
);
|
|
2130
|
+
if (!session) {
|
|
2131
|
+
return ctx.json(null, {
|
|
2132
|
+
status: 400,
|
|
2133
|
+
body: {
|
|
2134
|
+
message: "Could not create session"
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
await setSessionCookie(ctx, session.id);
|
|
2139
|
+
if (ctx.context.options.emailAndPassword.sendEmailVerificationOnSignUp) {
|
|
2140
|
+
const token = await createEmailVerificationToken(
|
|
2141
|
+
ctx.context.secret,
|
|
2142
|
+
createdUser.email
|
|
2143
|
+
);
|
|
2144
|
+
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${ctx.body.callbackURL || ctx.query?.currentURL || "/"}`;
|
|
2145
|
+
await ctx.context.options.emailAndPassword.sendVerificationEmail?.(
|
|
2146
|
+
createdUser.email,
|
|
2147
|
+
url,
|
|
2148
|
+
token
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
return ctx.json(
|
|
2152
|
+
{
|
|
2153
|
+
user: createdUser,
|
|
2154
|
+
session
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
body: ctx.body.callbackURL ? {
|
|
2158
|
+
url: ctx.body.callbackURL,
|
|
2159
|
+
redirect: true
|
|
2160
|
+
} : {
|
|
2161
|
+
user: createdUser,
|
|
2162
|
+
session
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
);
|
|
2168
|
+
|
|
2169
|
+
// src/api/index.ts
|
|
2170
|
+
import chalk from "chalk";
|
|
2171
|
+
|
|
2172
|
+
// src/api/rate-limiter.ts
|
|
2173
|
+
function shouldRateLimit(max, window2, rateLimitData) {
|
|
2174
|
+
const now = Date.now();
|
|
2175
|
+
const windowInMs = window2 * 1e3;
|
|
2176
|
+
const timeSinceLastRequest = now - rateLimitData.lastRequest;
|
|
2177
|
+
return timeSinceLastRequest < windowInMs && rateLimitData.count >= max;
|
|
2178
|
+
}
|
|
2179
|
+
function rateLimitResponse(retryAfter) {
|
|
2180
|
+
return new Response(
|
|
2181
|
+
JSON.stringify({
|
|
2182
|
+
message: "Too many requests. Please try again later."
|
|
2183
|
+
}),
|
|
2184
|
+
{
|
|
2185
|
+
status: 429,
|
|
2186
|
+
statusText: "Too Many Requests",
|
|
2187
|
+
headers: {
|
|
2188
|
+
"X-Retry-After": retryAfter.toString()
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
function getRetryAfter(lastRequest, window2) {
|
|
2194
|
+
const now = Date.now();
|
|
2195
|
+
const windowInMs = window2 * 1e3;
|
|
2196
|
+
return Math.ceil((lastRequest + windowInMs - now) / 1e3);
|
|
2197
|
+
}
|
|
2198
|
+
function createDBStorage(ctx, tableName) {
|
|
2199
|
+
const model = tableName ?? "rateLimit";
|
|
2200
|
+
const db = ctx.adapter;
|
|
2201
|
+
return {
|
|
2202
|
+
get: async (key) => {
|
|
2203
|
+
const res = await db.findOne({
|
|
2204
|
+
model,
|
|
2205
|
+
where: [{ field: "key", value: key }]
|
|
2206
|
+
});
|
|
2207
|
+
return res;
|
|
2208
|
+
},
|
|
2209
|
+
set: async (key, value, _update) => {
|
|
2210
|
+
try {
|
|
2211
|
+
if (_update) {
|
|
2212
|
+
await db.update({
|
|
2213
|
+
model: tableName ?? "rateLimit",
|
|
2214
|
+
where: [{ field: "key", value: key }],
|
|
2215
|
+
update: {
|
|
2216
|
+
count: value.count,
|
|
2217
|
+
lastRequest: value.lastRequest
|
|
2218
|
+
}
|
|
2219
|
+
});
|
|
2220
|
+
} else {
|
|
2221
|
+
await db.create({
|
|
2222
|
+
model: tableName ?? "rateLimit",
|
|
2223
|
+
data: {
|
|
2224
|
+
key,
|
|
2225
|
+
count: value.count,
|
|
2226
|
+
lastRequest: value.lastRequest
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
}
|
|
2230
|
+
} catch (e) {
|
|
2231
|
+
logger.error("Error setting rate limit", e);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
var memory = /* @__PURE__ */ new Map();
|
|
2237
|
+
function getRateLimitStorage(ctx) {
|
|
2238
|
+
if (ctx.rateLimit.customStorage) {
|
|
2239
|
+
return ctx.rateLimit.customStorage;
|
|
2240
|
+
}
|
|
2241
|
+
const storage = ctx.rateLimit.storage;
|
|
2242
|
+
if (storage === "memory") {
|
|
2243
|
+
return {
|
|
2244
|
+
async get(key) {
|
|
2245
|
+
return memory.get(key);
|
|
2246
|
+
},
|
|
2247
|
+
async set(key, value, _update) {
|
|
2248
|
+
memory.set(key, value);
|
|
2249
|
+
}
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
return createDBStorage(ctx, ctx.rateLimit.tableName);
|
|
2253
|
+
}
|
|
2254
|
+
async function onRequestRateLimit(req, ctx) {
|
|
2255
|
+
if (!ctx.rateLimit.enabled) {
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const baseURL = ctx.baseURL;
|
|
2259
|
+
const path = req.url.replace(baseURL, "");
|
|
2260
|
+
let window2 = ctx.rateLimit.window;
|
|
2261
|
+
let max = ctx.rateLimit.max;
|
|
2262
|
+
const key = getIp(req) + path;
|
|
2263
|
+
const specialRules = getDefaultSpecialRules();
|
|
2264
|
+
const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
|
|
2265
|
+
if (specialRule) {
|
|
2266
|
+
window2 = specialRule.window;
|
|
2267
|
+
max = specialRule.max;
|
|
2268
|
+
}
|
|
2269
|
+
for (const plugin of ctx.options.plugins || []) {
|
|
2270
|
+
if (plugin.rateLimit) {
|
|
2271
|
+
const matchedRule = plugin.rateLimit.find(
|
|
2272
|
+
(rule) => rule.pathMatcher(path)
|
|
2273
|
+
);
|
|
2274
|
+
if (matchedRule) {
|
|
2275
|
+
window2 = matchedRule.window;
|
|
2276
|
+
max = matchedRule.max;
|
|
2277
|
+
break;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
if (ctx.rateLimit.customRules) {
|
|
2282
|
+
const customRule = ctx.rateLimit.customRules[path];
|
|
2283
|
+
if (customRule) {
|
|
2284
|
+
window2 = customRule.window;
|
|
2285
|
+
max = customRule.max;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
const storage = getRateLimitStorage(ctx);
|
|
2289
|
+
const data = await storage.get(key);
|
|
2290
|
+
const now = Date.now();
|
|
2291
|
+
if (!data) {
|
|
2292
|
+
await storage.set(key, {
|
|
2293
|
+
key,
|
|
2294
|
+
count: 1,
|
|
2295
|
+
lastRequest: now
|
|
2296
|
+
});
|
|
2297
|
+
} else {
|
|
2298
|
+
const timeSinceLastRequest = now - data.lastRequest;
|
|
2299
|
+
if (shouldRateLimit(max, window2, data)) {
|
|
2300
|
+
const retryAfter = getRetryAfter(data.lastRequest, window2);
|
|
2301
|
+
return rateLimitResponse(retryAfter);
|
|
2302
|
+
} else if (timeSinceLastRequest > window2 * 1e3) {
|
|
2303
|
+
await storage.set(key, {
|
|
2304
|
+
...data,
|
|
2305
|
+
count: 1,
|
|
2306
|
+
lastRequest: now
|
|
2307
|
+
});
|
|
2308
|
+
} else {
|
|
2309
|
+
await storage.set(key, {
|
|
2310
|
+
...data,
|
|
2311
|
+
count: data.count + 1,
|
|
2312
|
+
lastRequest: now
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
function getDefaultSpecialRules() {
|
|
2318
|
+
const specialRules = [
|
|
2319
|
+
{
|
|
2320
|
+
pathMatcher(path) {
|
|
2321
|
+
return path.startsWith("/sign-in") || path.startsWith("/sign-up");
|
|
2322
|
+
},
|
|
2323
|
+
window: 10,
|
|
2324
|
+
max: 7
|
|
2325
|
+
}
|
|
2326
|
+
];
|
|
2327
|
+
return specialRules;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
// src/api/index.ts
|
|
2331
|
+
function getEndpoints(ctx, options) {
|
|
2332
|
+
const pluginEndpoints = options.plugins?.reduce(
|
|
2333
|
+
(acc, plugin) => {
|
|
2334
|
+
return {
|
|
2335
|
+
...acc,
|
|
2336
|
+
...plugin.endpoints
|
|
2337
|
+
};
|
|
2338
|
+
},
|
|
2339
|
+
{}
|
|
2340
|
+
);
|
|
2341
|
+
const middlewares = options.plugins?.map(
|
|
2342
|
+
(plugin) => plugin.middlewares?.map((m) => {
|
|
2343
|
+
const middleware = async (context) => {
|
|
2344
|
+
return m.middleware({
|
|
2345
|
+
...context,
|
|
2346
|
+
context: {
|
|
2347
|
+
...ctx,
|
|
2348
|
+
...context.context
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
};
|
|
2352
|
+
middleware.path = m.path;
|
|
2353
|
+
middleware.options = m.middleware.options;
|
|
2354
|
+
middleware.headers = m.middleware.headers;
|
|
2355
|
+
return {
|
|
2356
|
+
path: m.path,
|
|
2357
|
+
middleware
|
|
2358
|
+
};
|
|
2359
|
+
})
|
|
2360
|
+
).filter((plugin) => plugin !== void 0).flat() || [];
|
|
2361
|
+
const baseEndpoints = {
|
|
2362
|
+
signInOAuth,
|
|
2363
|
+
callbackOAuth,
|
|
2364
|
+
getCSRFToken,
|
|
2365
|
+
getSession: getSession(),
|
|
2366
|
+
signOut,
|
|
2367
|
+
signUpEmail,
|
|
2368
|
+
signInEmail,
|
|
2369
|
+
forgetPassword,
|
|
2370
|
+
resetPassword,
|
|
2371
|
+
verifyEmail,
|
|
2372
|
+
sendVerificationEmail,
|
|
2373
|
+
changePassword,
|
|
2374
|
+
setPassword,
|
|
2375
|
+
updateUser,
|
|
2376
|
+
forgetPasswordCallback,
|
|
2377
|
+
listSessions: listSessions(),
|
|
2378
|
+
revokeSession,
|
|
2379
|
+
revokeSessions
|
|
2380
|
+
};
|
|
2381
|
+
const endpoints = {
|
|
2382
|
+
...baseEndpoints,
|
|
2383
|
+
...pluginEndpoints,
|
|
2384
|
+
ok,
|
|
2385
|
+
error
|
|
2386
|
+
};
|
|
2387
|
+
let api = {};
|
|
2388
|
+
for (const [key, value] of Object.entries(endpoints)) {
|
|
2389
|
+
api[key] = async (context) => {
|
|
2390
|
+
const c = await ctx;
|
|
2391
|
+
const endpointRes = await value({
|
|
2392
|
+
...context,
|
|
2393
|
+
context: {
|
|
2394
|
+
...c,
|
|
2395
|
+
...context.context
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
let response = endpointRes;
|
|
2399
|
+
for (const plugin of options.plugins || []) {
|
|
2400
|
+
if (plugin.hooks?.after) {
|
|
2401
|
+
for (const hook of plugin.hooks.after) {
|
|
2402
|
+
const match = hook.matcher(context);
|
|
2403
|
+
if (match) {
|
|
2404
|
+
const obj = Object.assign(context, {
|
|
2405
|
+
context: {
|
|
2406
|
+
...ctx,
|
|
2407
|
+
returned: response
|
|
2408
|
+
}
|
|
2409
|
+
});
|
|
2410
|
+
const hookRes = await hook.handler(obj);
|
|
2411
|
+
if (hookRes && "response" in hookRes) {
|
|
2412
|
+
response = hookRes.response;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return response;
|
|
2419
|
+
};
|
|
2420
|
+
api[key].path = value.path;
|
|
2421
|
+
api[key].method = value.method;
|
|
2422
|
+
api[key].options = value.options;
|
|
2423
|
+
api[key].headers = value.headers;
|
|
2424
|
+
}
|
|
2425
|
+
return {
|
|
2426
|
+
api,
|
|
2427
|
+
middlewares
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
var router = (ctx, options) => {
|
|
2431
|
+
const { api, middlewares } = getEndpoints(ctx, options);
|
|
2432
|
+
const basePath = new URL(ctx.baseURL).pathname;
|
|
2433
|
+
return createRouter(api, {
|
|
2434
|
+
extraContext: ctx,
|
|
2435
|
+
basePath,
|
|
2436
|
+
routerMiddleware: [
|
|
2437
|
+
{
|
|
2438
|
+
path: "/**",
|
|
2439
|
+
middleware: csrfMiddleware
|
|
2440
|
+
},
|
|
2441
|
+
...middlewares
|
|
2442
|
+
],
|
|
2443
|
+
async onRequest(req) {
|
|
2444
|
+
for (const plugin of ctx.options.plugins || []) {
|
|
2445
|
+
if (plugin.onRequest) {
|
|
2446
|
+
const response = await plugin.onRequest(req, ctx);
|
|
2447
|
+
if (response) {
|
|
2448
|
+
return response;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return onRequestRateLimit(req, ctx);
|
|
2453
|
+
},
|
|
2454
|
+
async onResponse(res) {
|
|
2455
|
+
for (const plugin of ctx.options.plugins || []) {
|
|
2456
|
+
if (plugin.onResponse) {
|
|
2457
|
+
const response = await plugin.onResponse(res, ctx);
|
|
2458
|
+
if (response) {
|
|
2459
|
+
return response.response;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
return res;
|
|
2464
|
+
},
|
|
2465
|
+
onError(e) {
|
|
2466
|
+
const log = options.logger?.verboseLogging ? logger : void 0;
|
|
2467
|
+
if (options.logger?.disabled !== true) {
|
|
2468
|
+
if (e instanceof APIError6) {
|
|
2469
|
+
log?.warn(e);
|
|
2470
|
+
} else {
|
|
2471
|
+
if (typeof e === "object" && e !== null && "message" in e) {
|
|
2472
|
+
const errorMessage = e.message;
|
|
2473
|
+
if (!errorMessage || typeof errorMessage !== "string") {
|
|
2474
|
+
log?.error(e);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
if (errorMessage.includes("no such table")) {
|
|
2478
|
+
logger?.error(
|
|
2479
|
+
`Please run ${chalk.green(
|
|
2480
|
+
"npx better-auth migrate"
|
|
2481
|
+
)} to create the tables. There are missing tables in your SQLite database.`
|
|
2482
|
+
);
|
|
2483
|
+
} else if (errorMessage.includes("relation") && errorMessage.includes("does not exist")) {
|
|
2484
|
+
logger.error(
|
|
2485
|
+
`Please run ${chalk.green(
|
|
2486
|
+
"npx better-auth migrate"
|
|
2487
|
+
)} to create the tables. There are missing tables in your PostgreSQL database.`
|
|
2488
|
+
);
|
|
2489
|
+
} else if (errorMessage.includes("Table") && errorMessage.includes("doesn't exist")) {
|
|
2490
|
+
logger?.error(
|
|
2491
|
+
`Please run ${chalk.green(
|
|
2492
|
+
"npx better-auth migrate"
|
|
2493
|
+
)} to create the tables. There are missing tables in your MySQL database.`
|
|
2494
|
+
);
|
|
2495
|
+
} else {
|
|
2496
|
+
log?.error(e);
|
|
2497
|
+
}
|
|
2498
|
+
} else {
|
|
2499
|
+
log?.error(e);
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
});
|
|
2505
|
+
};
|
|
2506
|
+
export {
|
|
2507
|
+
callbackOAuth,
|
|
2508
|
+
changePassword,
|
|
2509
|
+
createAuthEndpoint,
|
|
2510
|
+
createAuthMiddleware,
|
|
2511
|
+
createEmailVerificationToken,
|
|
2512
|
+
csrfMiddleware,
|
|
2513
|
+
error,
|
|
2514
|
+
forgetPassword,
|
|
2515
|
+
forgetPasswordCallback,
|
|
2516
|
+
getCSRFToken,
|
|
2517
|
+
getEndpoints,
|
|
2518
|
+
getSession,
|
|
2519
|
+
getSessionFromCtx,
|
|
2520
|
+
listSessions,
|
|
2521
|
+
ok,
|
|
2522
|
+
optionsMiddleware,
|
|
2523
|
+
resetPassword,
|
|
2524
|
+
revokeSession,
|
|
2525
|
+
revokeSessions,
|
|
2526
|
+
router,
|
|
2527
|
+
sendVerificationEmail,
|
|
2528
|
+
sessionMiddleware,
|
|
2529
|
+
setPassword,
|
|
2530
|
+
signInEmail,
|
|
2531
|
+
signInOAuth,
|
|
2532
|
+
signOut,
|
|
2533
|
+
signUpEmail,
|
|
2534
|
+
updateUser,
|
|
2535
|
+
verifyEmail
|
|
2536
|
+
};
|