@better-auth-kit/app-invite
Version:
Invite users to your application and allow them to sign up.
4 lines (2 loc) • 15.7 kB
JavaScript
import{z as I}from"zod";import{APIError as L,createAuthEndpoint as S,originCheck as X,sessionMiddleware as i}from"better-auth/api";var m={USER_IS_ALREADY_A_MEMBER_OF_THIS_APPLICATION:"User is already a member of this application",USER_WAS_ALREADY_INVITED_TO_THIS_APPLICATION:"User was already invited to this application",INVITER_IS_NO_LONGER_A_MEMBER_OF_THIS_APPLICATION:"Inviter is no longer a member of this application",APP_INVITATION_NOT_FOUND:"App invitation not found",YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_APP_INVITATION:"You are not allowed to cancel this app invitation",YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_APPLICATION:"You are not allowed to invite users to this application",THIS_APP_INVITATION_CANT_BE_REJECTED:"This app invitation can't be rejected",EMAIL_DOMAIN_IS_NOT_IN_WHITELIST:"Email domain is not in whitelist"},F={USER_NOT_FOUND:"User not found",FAILED_TO_CREATE_USER:"Failed to create user",FAILED_TO_CREATE_SESSION:"Failed to create session",FAILED_TO_UPDATE_USER:"Failed to update user",FAILED_TO_GET_SESSION:"Failed to get session",INVALID_PASSWORD:"Invalid password",INVALID_EMAIL:"Invalid email",INVALID_EMAIL_OR_PASSWORD:"Invalid email or password",SOCIAL_ACCOUNT_ALREADY_LINKED:"Social account already linked",PROVIDER_NOT_FOUND:"Provider not found",INVALID_TOKEN:"invalid token",ID_TOKEN_NOT_SUPPORTED:"id_token not supported",FAILED_TO_GET_USER_INFO:"Failed to get user info",USER_EMAIL_NOT_FOUND:"User email not found",EMAIL_NOT_VERIFIED:"Email not verified",PASSWORD_TOO_SHORT:"Password too short",PASSWORD_TOO_LONG:"Password too long",USER_ALREADY_EXISTS:"User already exists",EMAIL_CAN_NOT_BE_UPDATED:"Email can not be updated",CREDENTIAL_ACCOUNT_NOT_FOUND:"Credential account not found",SESSION_EXPIRED:"Session expired. Re-authenticate to perform this action.",FAILED_TO_UNLINK_LAST_ACCOUNT:"You can't unlink your last account",ACCOUNT_NOT_FOUND:"Account not found",USER_ALREADY_HAS_PASSWORD:"User already has a password. Provide that to delete the account."};var g=(f,T="ms")=>{return new Date(Date.now()+(T==="sec"?f*1000:f))};var D=(f,T)=>{let _=f.adapter;return{createInvitation:async({invitation:l,user:u})=>{let O=g(T?.invitationExpiresIn||172800000);return await _.create({model:"appInvitation",data:{name:l.email?l.name:void 0,email:l.email,status:"pending",expiresAt:O,inviterId:u.id,domainWhitelist:!l.email?l.domainWhitelist:void 0}})},findInvitationById:async(l)=>{return await _.findOne({model:"appInvitation",where:[{field:"id",value:l}]})},findPendingInvitation:async(l)=>{let u=await _.findOne({model:"appInvitation",where:[{field:"email",value:l.email},{field:"status",value:"pending"}]});if(u&&new Date(u.expiresAt)>new Date)return u;return},listInvitationsByIssuer:async(l,u,n,O,A)=>{return await _.findMany({model:"appInvitation",where:[...A||[],{field:"inviterId",operator:"eq",value:l}],limit:u,offset:n,sortBy:O})},updateInvitation:async(l)=>{return await _.update({model:"appInvitation",where:[{field:"id",value:l.invitationId}],update:{status:l.status}})}}};import{parseUserInput as e}from"better-auth/db";var W=Object.create(null),U=(f)=>globalThis.process?.env||globalThis.Deno?.env.toObject()||globalThis.__env__||(f?W:globalThis),B=new Proxy(W,{get(f,T){return U()[T]??W[T]},has(f,T){let _=U();return T in _||T in W},set(f,T,_){let l=U(!0);return l[T]=_,!0},deleteProperty(f,T){if(!T)return!1;let _=U(!0);return delete _[T],!0},ownKeys(){let f=U(!0);return Object.keys(f)}});function V(f){return f?f!=="false":!1}var w=typeof process!=="undefined"&&process.env&&"development"||"";var C=w==="dev"||w==="development",z=w==="test"||V(B.TEST);import{setSessionCookie as h}from"better-auth/cookies";var u_=(f)=>{let T={allowUserToCreateInvitation:!0,allowUserToCancelInvitation:({user:_,invitation:l})=>{return l.inviterId===_.id},...f};return{id:"app-invite",endpoints:{createAppInvitation:S("/invite-user",{method:"POST",use:[i],body:I.object({name:I.string({description:"The name of the user to invite (Only for personal invitations)"}).optional(),email:I.string({description:"The email address of the user to invite (Only for personal invitations)"}).optional(),resend:I.boolean({description:"Resend the invitation email, if the user is already invited"}).optional(),domainWhitelist:I.string({description:"A comma separated list of domains that are allowed to accept the invitation. (Only for public invitations)"}).optional()}),metadata:{openapi:{operationId:"createAppInvitation",description:"Invite a user to the app",responses:{"200":{description:"Success",content:{"application/json":{schema:{$ref:"#/components/schemas/AppInvitation"}}}}}}}},async(_)=>{let l=_.body.email?"personal":"public";if(l==="personal"&&!T?.sendInvitationEmail)throw _.context.logger.warn("Invitation email is not enabled. Pass `sendInvitationEmail` to the plugin options to enable it."),new L("BAD_REQUEST",{message:"Invitation email is not enabled"});let u=_.context.session;if(!(typeof T.allowUserToCreateInvitation==="function"?await T.allowUserToCreateInvitation(u.user,l):T.allowUserToCreateInvitation))throw new L("FORBIDDEN",{message:m.YOU_ARE_NOT_ALLOWED_TO_INVITE_USERS_TO_THIS_APPLICATION});let O=D(_.context,T);if(_.body.email){if(await _.context.internalAdapter.findUserByEmail(_.body.email))throw new L("BAD_REQUEST",{message:m.USER_IS_ALREADY_A_MEMBER_OF_THIS_APPLICATION});if(!!await O.findPendingInvitation({email:_.body.email})&&!_.body.resend)throw new L("BAD_REQUEST",{message:m.USER_WAS_ALREADY_INVITED_TO_THIS_APPLICATION})}let A=await O.createInvitation({invitation:{name:_.body.name,email:_.body.email,domainWhitelist:_.body.domainWhitelist},user:u.user});if(A.email)await T.sendInvitationEmail?.({id:A.id,name:A.name,email:A.email,inviter:u.user},_.request);return _.json(A)}),getAppInvitation:S("/get-app-invitation",{method:"GET",query:I.object({id:I.string({description:"The ID of the invitation to get."})}),metadata:{openapi:{operationId:"getAppInvitation",description:"Get an invitation by ID",responses:{"200":{description:"Success",content:{"application/json":{schema:{$ref:"#/components/schemas/AppInvitation",type:"object",properties:{id:{type:"string"},name:{type:"string"},email:{type:"string"},inviterId:{type:"string"},status:{type:"string"},expiresAt:{type:"string"},inviterEmail:{type:"string"}},required:["id","inviterId","status","expiresAt","inviterEmail"]}}}}}}}},async(_)=>{let u=await D(_.context,T).findInvitationById(_.query.id);if(!u||u.status!=="pending"||u.expiresAt<new Date)throw new L("BAD_REQUEST",{message:m.APP_INVITATION_NOT_FOUND});let n=await _.context.internalAdapter.findUserById(u.inviterId);if(!n)throw new L("BAD_REQUEST",{message:m.INVITER_IS_NO_LONGER_A_MEMBER_OF_THIS_APPLICATION});return _.json({...u,inviterEmail:n.email})}),acceptAppInvitation:S("/accept-invitation",{method:"POST",query:I.object({callbackURL:I.string({description:"The URL to redirect to after accepting the invitation"}).optional()}).optional(),use:[X((_)=>_.query?.callbackURL)],body:I.object({invitationId:I.string({description:"The ID of the invitation"})}).and(I.record(I.string(),I.any())),metadata:{$Infer:{body:{}},openapi:{operationId:"acceptAppInvitation",description:"Accept an app invitation that has been issued by another user",requestBody:{content:{"application/json":{schema:{type:"object",properties:{name:{type:"string",description:"The name of the user"},email:{type:"string",description:"The email address of the user"},password:{type:"string",description:"The password of the user"}},required:["password"]}}}},responses:{"200":{description:"Success",content:{"application/json":{schema:{type:"object",properties:{token:{type:"string"},invitation:{type:"object"},user:{type:"object"}}}}}}}}}},async(_)=>{let l=D(_.context,T),{invitationId:u,...n}=_.body,O=await l.findInvitationById(u);if(!O||O.status!=="pending"||O.expiresAt<new Date)throw new L("BAD_REQUEST",{message:m.APP_INVITATION_NOT_FOUND});let A=O.email?"personal":"public";if(!await _.context.internalAdapter.findUserById(O.inviterId))throw new L("BAD_REQUEST",{message:m.INVITER_IS_NO_LONGER_A_MEMBER_OF_THIS_APPLICATION});let K={...n,email:O.email||n.email,name:O.name||n.name},{name:j,email:a,password:d,image:q,...J}=K;if(!I.string().email().safeParse(a).success)throw new L("BAD_REQUEST",{message:F.INVALID_EMAIL});if(A==="public"){let[H,b]=a.split("@",2),M=O.domainWhitelist?.split(",");if(M?.length&&M.length>0&&!M.some((k)=>k.trim().toLowerCase()===b.trim().toLowerCase()))throw new L("FORBIDDEN",{message:m.EMAIL_DOMAIN_IS_NOT_IN_WHITELIST})}let s=_.context.password.config.minPasswordLength;if(d.length<s)throw _.context.logger.error("Password is too short"),new L("BAD_REQUEST",{message:F.PASSWORD_TOO_SHORT});let Q=_.context.password.config.maxPasswordLength;if(d.length>Q)throw _.context.logger.error("Password is too long"),new L("BAD_REQUEST",{message:F.PASSWORD_TOO_LONG});if((await _.context.internalAdapter.findUserByEmail(a))?.user)throw _.context.logger.info(`Sign-up attempt for existing email: ${a}`),new L("UNPROCESSABLE_ENTITY",{message:F.USER_ALREADY_EXISTS});let Z=e(_.context.options,J),N;try{if(N=await _.context.internalAdapter.createUser({email:a.toLowerCase(),name:j,image:q,...Z,emailVerified:!0}),!N)throw new L("BAD_REQUEST",{message:F.FAILED_TO_CREATE_USER})}catch(H){if(C)_.context.logger.error("Failed to create user",H);throw new L("UNPROCESSABLE_ENTITY",{message:F.FAILED_TO_CREATE_USER,details:H})}if(!N)throw new L("UNPROCESSABLE_ENTITY",{message:F.FAILED_TO_CREATE_USER});let $=await _.context.password.hash(d);await _.context.internalAdapter.linkAccount({userId:N.id,providerId:"credential",accountId:N.id,password:$});let Y=O;if(A==="personal")Y=await l.updateInvitation({invitationId:u,status:"accepted"});if(!T?.autoSignIn)return _.json({token:null,user:{id:N.id,email:N.email,name:N.name,image:N.image,emailVerified:N.emailVerified,createdAt:N.createdAt,updatedAt:N.updatedAt},invitation:Y});let y=await _.context.internalAdapter.createSession(N.id,_);if(!y)throw new L("BAD_REQUEST",{message:F.FAILED_TO_CREATE_SESSION});if(await h(_,{session:y,user:N}),!_.query?.callbackURL)return _.json({token:y.token,user:{id:N.id,email:N.email,name:N.name,image:N.image,emailVerified:N.emailVerified,createdAt:N.createdAt,updatedAt:N.updatedAt},invitation:Y});throw _.redirect(_.query.callbackURL)}),rejectAppInvitation:S("/reject-invitation",{method:"POST",query:I.object({callbackURL:I.string({description:"The URL to redirect to after rejecting the invitation"}).optional()}).optional(),body:I.object({invitationId:I.string({description:"The ID of the invitation"})}),use:[X((_)=>_.query?.callbackURL)],metadata:{openapi:{operationId:"rejectAppInvitation",description:"Reject an app invitation",requestBody:{content:{"application/json":{schema:{type:"object",properties:{invitationId:{type:"string"}},required:["invitationId"]}}}},responses:{"200":{description:"Success",content:{"application/json":{schema:{type:"object",properties:{token:{type:"null"},invitation:{type:"object"},user:{type:"null"}}}}}}}}}},async(_)=>{let l=D(_.context,T),u=await l.findInvitationById(_.body.invitationId);if(!u||u.expiresAt<new Date||u.status!=="pending")throw new L("BAD_REQUEST",{message:m.APP_INVITATION_NOT_FOUND});if(!u.email)throw new L("BAD_REQUEST",{message:m.THIS_APP_INVITATION_CANT_BE_REJECTED});let n=await l.updateInvitation({invitationId:_.body.invitationId,status:"rejected"});return _.json({token:null,invitation:n,user:null})}),cancelAppInvitation:S("/cancel-invitation",{method:"POST",body:I.object({invitationId:I.string({description:"The ID of the app invitation to cancel"})}),use:[i],openapi:{operationId:"cancelAppInvitation",description:"Cancel an app invitation",responses:{"200":{description:"Success",content:{"application/json":{schema:{$ref:"#/components/schemas/AppInvitation"}}}}}}},async(_)=>{let l=_.context.session,u=D(_.context,T),n=await u.findInvitationById(_.body.invitationId);if(!n)throw new L("BAD_REQUEST",{message:m.APP_INVITATION_NOT_FOUND});if(!await T.allowUserToCancelInvitation?.({user:l.user,invitation:n}))throw new L("FORBIDDEN",{message:m.YOU_ARE_NOT_ALLOWED_TO_CANCEL_THIS_APP_INVITATION});let A=await u.updateInvitation({invitationId:_.body.invitationId,status:"canceled"});return _.json(A)}),listAppInvitations:S("/list-invitations",{method:"GET",use:[i],query:I.object({searchValue:I.string({description:"The value to search for"}).optional(),searchField:I.enum(["name","email","domainWhitelist"],{description:"The field to search in, defaults to email. Can be `email`, `name` or `domainWhitelist`"}).optional(),searchOperator:I.enum(["contains","starts_with","ends_with"],{description:"The operator to use for the search. Can be `contains`, `starts_with` or `ends_with`"}).optional(),limit:I.string({description:"The numbers of invitations to return"}).or(I.number()).optional(),offset:I.string({description:"The offset to start from"}).or(I.number()).optional(),sortBy:I.string({description:"The field to sort by"}).optional(),sortDirection:I.enum(["asc","desc"],{description:"The direction to sort by"}).optional(),filterField:I.string({description:"The field to filter by"}).optional(),filterValue:I.string({description:"The value to filter by"}).or(I.number()).optional(),filterOperator:I.enum(["eq","ne","lt","lte","gt","gte"],{description:"The operator to use for the filter"}).optional()}),metadata:{openapi:{operationId:"listAppInvitations",summary:"List issued invitations",description:"List issued invitations",responses:{200:{description:"List of issued invitations",content:{"application/json":{schema:{type:"object",properties:{invitations:{type:"array",items:{$ref:"#/components/schemas/AppInvitation"}},total:{type:"number"},limit:{type:["number","undefined"]},offset:{type:["number","undefined"]}}}}}}}}}},async(_)=>{let l=[];if(_.query?.searchValue)l.push({field:_.query.searchField||"email",operator:_.query.searchOperator||"contains",value:_.query.searchValue});if(_.query?.filterValue&&_.query.filterField!=="inviterId")l.push({field:_.query.filterField||"email",operator:_.query.filterOperator||"eq",value:_.query.filterValue});let u=await D(_.context,T);try{let n=Number(_.query?.limit)||void 0,O=Number(_.query?.offset)||void 0,A=await u.listInvitationsByIssuer(_.context.session.user.id,Number(_.query?.limit)||void 0,Number(_.query?.offset)||void 0,_.query?.sortBy?{field:_.query.sortBy,direction:_.query.sortDirection||"asc"}:void 0,l.length?l:void 0);return _.json({invitations:A,limit:n,offset:O})}catch(n){return _.json({invitations:[]})}})},rateLimit:[{pathMatcher(_){return _.startsWith("/accept-invitation")||_.startsWith("/reject-invitation")||_.startsWith("/cancel-invitation")||_.startsWith("/list-invitations")||_.startsWith("/invite-user")||_.startsWith("/get-app-invitation")},window:T?.rateLimit?.window||60,max:T?.rateLimit?.max||5}],schema:{appInvitation:{modelName:T?.schema?.appInvitation?.modelName,fields:{name:{type:"string",required:!1,sortable:!0,fieldName:T?.schema?.appInvitation?.fields?.name},email:{type:"string",required:!1,sortable:!0,fieldName:T?.schema?.appInvitation?.fields?.email},status:{type:"string",required:!0,sortable:!0,defaultValue:"pending",fieldName:T?.schema?.appInvitation?.fields?.status},expiresAt:{type:"date",required:!0,fieldName:T?.schema?.appInvitation?.fields?.expiresAt},inviterId:{type:"string",references:{model:"user",field:"id"},fieldName:T?.schema?.appInvitation?.fields?.inviterId,required:!0},domainWhitelist:{type:"string",required:!1,fieldName:T?.schema?.appInvitation?.fields?.domainWhitelist}}}},$Infer:{AppInvitation:{}},$ERROR_CODES:m}};export{u_ as appInvite};
//# debugId=674E4CBF841BCF8864756E2164756E21