vercel / platforms

A full-stack Next.js app with multi-tenancy and custom domain support. Built with Next.js App Router and the Vercel Domains API.
https://app.vercel.pub
5.38k stars 688 forks source link

Supporting tenant subdomain login #403

Closed imprakharshukla closed 1 month ago

imprakharshukla commented 1 month ago

Thank you for the amazing work on this repo!

I wanted to ask how can I support users logging on the tenant's website as well.

Here's what I wanted to do:

  1. Support users logging in on the admin dashboard (app.xxx.xx)[This is working fine with next-auth].
  2. Customers logging in on the tentant's website as well. (xxx.xxx.xxx) [I am not sure how will that work].

Since the /api can only have one auth route, I though of doing something like this firstly:

import { adminAuthOptions, customerAuthOptions } from "@gramflow/auth"; // Import both admin and user authentication options
import NextAuth from "next-auth";

export async function GET(request: Request) {
    const url = new URL(request.headers.get("referer") ?? "");
    // Check if the subdomain is 'app'
    const subdomain = url.hostname.split('.')[0];
    console.log({ subdomain });
    if (subdomain === 'app') {
        return NextAuth(adminAuthOptions);
    } else {
        return NextAuth(customerAuthOptions);
    }
}

export async function POST(request: Request) {
    const url = new URL(request.headers.get("referer") ?? "");

    // Check if the subdomain is 'app'
    const subdomain = url.hostname.split('.')[0];
    console.log({ subdomain });
    if (subdomain === 'app') {
        return NextAuth(adminAuthOptions);
    } else {
        return NextAuth(customerAuthOptions);
    }
}

but next complains that i am not returning a response here.

My auth-options look something like this:

import { CookiesOptions, DefaultSession, DefaultUser, Session, SessionStrategy, User, getServerSession, type NextAuthOptions } from "next-auth";
import { PrismaAdapter } from "next-auth-prisma-adapter";
import OtpGenerator from "otp-generator";
import { Resend } from "resend";

import { USER_ROLE, db } from "@gramflow/db";

import { env } from "../env.mjs";
import { JWT } from "next-auth/jwt";
import { AdapterUser } from "next-auth/adapters";
import { Provider } from "next-auth/providers";
const VERCEL_DEPLOYMENT = !!process.env.VERCEL_URL;

const sessionStrategy = {
  strategy: "jwt" as SessionStrategy,
};
const secret = process.env.NEXTAUTH_SECRET;
const callbacks = {
  async jwt({ token, user }: {
    token: JWT,
    user: AdapterUser | User
  }) {
    if (user) {
      token.role = user.role;
    }
    return token;
  },
  async session({ session, token }: {
    session: Session,
    token: JWT
  }) {
    if (token?.role) {
      //@ts-ignore
      session.user.role = token.role;
    }
    //add the user id to the session
    session.user.id = token.sub ?? "";
    return session;
  },
}
const pages = {
  signIn: "/login",
}
const cookies = {
  sessionToken: {
    name: `${VERCEL_DEPLOYMENT ? "__Secure-" : ""}next-auth.session-token`,
    options: {
      httpOnly: true,
      sameSite: "lax",
      path: "/",
      // When working on localhost, the cookie domain must be omitted entirely (https://stackoverflow.com/a/1188145)
      domain: VERCEL_DEPLOYMENT
        ? `.${process.env.NEXT_PUBLIC_ROOT_DOMAIN}`
        : undefined,
      secure: VERCEL_DEPLOYMENT,
    },
  },

} as Partial<CookiesOptions>
const providers: Provider[] = [
  {
    id: "email",
    type: "email",
    from: env.EMAIL_FROM,
    server: {},
    maxAge: 5 * 60,
    name: "Email",

    options: {},
    async generateVerificationToken() {
      const token = OtpGenerator.generate(6, {
        lowerCaseAlphabets: false,
        upperCaseAlphabets: false,
        digits: true,
        specialChars: false,
      });
      console.log("Generated OTP", token);
      return token;
    },

    async sendVerificationRequest({ identifier: email, url, token }: {
      identifier: string,
      url: string,
      token: string
    }) {
      console.log("Token", token);
      try {
        console.log("Sending email....", email);
        // const data = await resend.emails.send({
        //   from: `${AppConfig.StoreName} <${env.EMAIL_FROM}>`,
        //   to: [email],
        //   subject: "Login to your account",
        //   html: OtpEmailHtml({
        //     emailDetails: {
        //       otp: token,
        //       storeName: AppConfig.StoreName,
        //       storeInstagramUsername: AppConfig.InstagramUsername,
        //       baseOrderUrl: AppConfig.BaseOrderUrl,
        //       warehouseCity: AppConfig.WarehouseDetails.city,
        //       warehouseState: AppConfig.WarehouseDetails.state,
        //       warehouseCountry: AppConfig.WarehouseDetails.country,
        //     }
        //   })
        // });

        //console.log(data)
      } catch (e) {
        console.log(e);
        throw new Error(JSON.stringify(e));
      }
    },
  },
]

export const adminAuthOptions: NextAuthOptions = {
  session: sessionStrategy,
  secret: secret,
  callbacks: callbacks,
  cookies: cookies,
  adapter: PrismaAdapter(db, {
    userModel: "User",
    accountModel: "Account",
    sessionModel: "Session",
    verificationTokenModel: "VerificationToken",
  }),
  pages,
  providers,
};

export const customerAuthOptions: NextAuthOptions = {
  session: sessionStrategy,
  secret: secret,
  callbacks: callbacks,
  adapter: PrismaAdapter(db, {
    userModel: "Customer",
    accountModel: "AccountCustomer",
    sessionModel: "SessionCustomer",
    verificationTokenModel: "VerificationToken",
  }),
  pages,
  providers,
};

export function getSession() {
  return getServerSession(adminAuthOptions) as Promise<{
    user: {
      id: string;
      name: string;
      username: string;
      email: string;
      image: string;
    };
  } | null>;
}

Here: User-> Users who sign up on the admin panel Customer-> Customer who sign up on the tenant website.

bazylhorsey commented 1 month ago

Would also love to see this feature. Definitely a huge thing for multi-tenant. There may be unauthenticated pages (like their public blog), but they would also be able to make their own sites without exposing the rest of the users pages.

imprakharshukla commented 1 month ago

I rolled my own auth for this since NextJS (AuthJS) does not really support this yet.