Accepting organization invitation flow

Do you guys have any recommendations for using better-auth's organization invitation with users who haven't got an account / aren't signed in? I check on the frontend's page for accepting the invitation if there isn't a session, and if there isn't, I redirect them to the sign in page with the email already set and the input disabled Then once they log in (e.g. with OTP), then I use the hook to check if the user is new, and if so, it accepts the invite automatically if there is one, and if they already had an account and are logging in, I want to redirect them to the invitation page again Problem is, my backend and frontend are on separate domains so I'm getting CORS errors if I do the redirect
6 Replies
Amos
AmosOP2mo ago
For context, here is the code:
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/sign-up") || ctx.path.startsWith("/sign-in")) {
const newSession = ctx.context.newSession;

if (!newSession) return;

// Check if the user is new (created within the last minute)
// For context this is used to automatically accept invitations when a user signs up for the first time
const isNewUser = newSession.user.createdAt && Date.now() - newSession.user.createdAt.getTime() < 60_000;

const invitation = await db
.select()
.from(invitationTable)
.where(eq(invitationTable.email, newSession.user.email))
.then((rows) => rows.at(0));

if (!invitation) return;

if (!isNewUser) {
throw ctx.redirect(`${process.env["FRONTEND_URL"]}/invitation/${invitation.id}`);
}

await db.transaction(async (tx) => {
await tx
.insert(memberTable)
.values({
userId: newSession.user.id,
organizationId: invitation.organizationId,
role: invitation.role,
})
.onConflictDoNothing();

await tx.update(invitationTable).set({ status: "accepted" }).where(eq(invitationTable.id, invitation.id));

await tx
.update(sessionTable)
.set({ activeOrganizationId: invitation.organizationId })
.where(eq(sessionTable.id, newSession.session.id));

await setSessionCookie(ctx, {
session: {
...newSession.session,
activeOrganizationId: invitation.organizationId,
},
user: newSession.user
});
});
}
}),
},
hooks: {
after: createAuthMiddleware(async (ctx) => {
if (ctx.path.startsWith("/sign-up") || ctx.path.startsWith("/sign-in")) {
const newSession = ctx.context.newSession;

if (!newSession) return;

// Check if the user is new (created within the last minute)
// For context this is used to automatically accept invitations when a user signs up for the first time
const isNewUser = newSession.user.createdAt && Date.now() - newSession.user.createdAt.getTime() < 60_000;

const invitation = await db
.select()
.from(invitationTable)
.where(eq(invitationTable.email, newSession.user.email))
.then((rows) => rows.at(0));

if (!invitation) return;

if (!isNewUser) {
throw ctx.redirect(`${process.env["FRONTEND_URL"]}/invitation/${invitation.id}`);
}

await db.transaction(async (tx) => {
await tx
.insert(memberTable)
.values({
userId: newSession.user.id,
organizationId: invitation.organizationId,
role: invitation.role,
})
.onConflictDoNothing();

await tx.update(invitationTable).set({ status: "accepted" }).where(eq(invitationTable.id, invitation.id));

await tx
.update(sessionTable)
.set({ activeOrganizationId: invitation.organizationId })
.where(eq(sessionTable.id, newSession.session.id));

await setSessionCookie(ctx, {
session: {
...newSession.session,
activeOrganizationId: invitation.organizationId,
},
user: newSession.user
});
});
}
}),
},
I guess I can do the same check on the frontend, but I'd like to do it all on the backend tbh
bekacru
bekacru2mo ago
if they aren't signed in and the server returned 401 you should redirect them to the sign in page and once they are authenticated you should redirect them back to the page where they can accept the invitation.
Amos
AmosOP2mo ago
Yes but how? I get CORS errors when doing a redirect
bekacru
bekacru2mo ago
first you should be redirect from your spa to your spa to accept invitation and even if you need to redirect to your server endpoint there is no cors issues that is unique to redirecting.
Amos
AmosOP2mo ago
Apparently there is because I’m getting one when doing the ctx.redirect in the above code. Not sure if there is a better way of knowing that they need to be redirected to the invitation page or doing it somewhere else than the hook
bekacru
bekacru2mo ago
If you're using OTP login, it's better to keep the from= query in the SPA url so that after the user signs in, you can redirect them from the frontend instead. This way, you wouldn't need this hook. Ideally, before accepting the invitation, the user should be asked whether they want to accept it or not

Did you find this page helpful?