Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
340 lines
11 KiB
Plaintext
340 lines
11 KiB
Plaintext
---
|
|
status: "published"
|
|
title: 'Disabling Personal Accounts in Next.js Supabase'
|
|
label: 'Disabling Personal Accounts'
|
|
order: 1
|
|
description: 'Learn how to disable personal accounts in the Next.js Supabase Turbo SaaS kit and only allow team accounts'
|
|
---
|
|
|
|
{% alert type="warning" title="v2 Recipe" %}
|
|
This recipe applies to **v2 only**. In v3, teams-only mode is built-in as a feature flag. Simply set `NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_ONLY=true` in your environment variables. See the [Feature Flags Configuration](/docs/next-supabase-turbo/configuration/feature-flags-configuration) for details.
|
|
{% /alert %}
|
|
|
|
The Next.js Supabase Turbo SaaS kit is designed to allow personal accounts by default. However, you can disable the personal account view, and only allow user to access team accounts.
|
|
|
|
Let's walk through the v2 steps to disable personal accounts in the Next.js Supabase Turbo SaaS kit:
|
|
|
|
1. **Store team slug in cookies**: When a user logs in, store the team slug in a cookie. If none is provided, redirect the user to the team selection page.
|
|
2. **Set up Redirect**: Redirect customers to the latest selected team account
|
|
3. **Create a Team Selection Page**: Create a page where users can select the team they want to log in to.
|
|
4. **Duplicate the user settings page**: Duplicate the user settings page so they can access their own settings from within the team workspace
|
|
|
|
## Storing the User Cookie and Redirecting to the Team Selection Page
|
|
|
|
To make sure that users are always redirected to the team selection page, you need to store the team slug in a cookie. If the team slug is not found, redirect the user to the team selection page. We will do all of this in the middleware.
|
|
|
|
First, let's create these functions in the `apps/web/proxy.ts` file (or `apps/web/middleware.ts` for versions prior to Next.js 16):
|
|
|
|
```tsx {% title="apps/web/proxy.ts" %}
|
|
const createTeamCookie = (userId: string) => `${userId}-selected-team-slug`;
|
|
|
|
function handleTeamAccountsOnly(request: NextRequest, userId: string) {
|
|
// always allow access to the teams page
|
|
if (request.nextUrl.pathname === '/home/teams') {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
if (request.nextUrl.pathname === '/home') {
|
|
return redirectToTeam(request, userId);
|
|
}
|
|
|
|
if (isTeamAccountRoute(request) && !isUserRoute(request)) {
|
|
return storeTeamSlug(request, userId);
|
|
}
|
|
|
|
if (isUserRoute(request)) {
|
|
return redirectToTeam(request, userId);
|
|
}
|
|
|
|
return NextResponse.next();
|
|
}
|
|
|
|
function isUserRoute(request: NextRequest) {
|
|
const pathName = request.nextUrl.pathname;
|
|
return ['settings', 'billing', 'members'].includes(pathName.split('/')[2]!);
|
|
}
|
|
|
|
function isTeamAccountRoute(request: NextRequest) {
|
|
const pathName = request.nextUrl.pathname;
|
|
|
|
return pathName.startsWith('/home/');
|
|
}
|
|
|
|
function storeTeamSlug(request: NextRequest, userId: string): NextResponse {
|
|
const accountSlug = request.nextUrl.pathname.split('/')[2];
|
|
|
|
if (!accountSlug) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
const cookieName = createTeamCookie(userId);
|
|
const existing = request.cookies.get(cookieName);
|
|
|
|
if (existing?.value === accountSlug) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
const response = NextResponse.next();
|
|
|
|
response.cookies.set({
|
|
name: createTeamCookie(userId),
|
|
value: accountSlug,
|
|
path: '/',
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
function redirectToTeam(request: NextRequest, userId: string): NextResponse {
|
|
const cookieName = createTeamCookie(userId);
|
|
const lastTeamSlug = request.cookies.get(cookieName);
|
|
|
|
if (lastTeamSlug) {
|
|
return NextResponse.redirect(
|
|
new URL(`/home/${lastTeamSlug.value}`, request.url),
|
|
);
|
|
}
|
|
|
|
return NextResponse.redirect(new URL('/home/teams', request.url));
|
|
}
|
|
```
|
|
|
|
We will now add the `handleTeamAccountsOnly` function to the middleware chain in the `apps/web/proxy.ts` file (or `apps/web/middleware.ts` for versions prior to Next.js 16). This function will check if the user is on a team account route and store the team slug in a cookie. If the user is on the home route, it will redirect them to the team selection page.
|
|
|
|
```tsx {% title="apps/web/proxy.ts" %}
|
|
{
|
|
pattern: new URLPattern({ pathname: '/home/*?' }),
|
|
handler: async (req: NextRequest, res: NextResponse) => {
|
|
const { data } = await getUser(req, res);
|
|
|
|
const origin = req.nextUrl.origin;
|
|
const next = req.nextUrl.pathname;
|
|
|
|
// If user is not logged in, redirect to sign in page.
|
|
if (!data?.claims) {
|
|
const signIn = pathsConfig.auth.signIn;
|
|
const redirectPath = `${signIn}?next=${next}`;
|
|
|
|
return NextResponse.redirect(new URL(redirectPath, origin).href);
|
|
}
|
|
|
|
const supabase = createMiddlewareClient(req, res);
|
|
|
|
const requiresMultiFactorAuthentication =
|
|
await checkRequiresMultiFactorAuthentication(supabase);
|
|
|
|
// If user requires multi-factor authentication, redirect to MFA page.
|
|
if (requiresMultiFactorAuthentication) {
|
|
return NextResponse.redirect(
|
|
new URL(pathsConfig.auth.verifyMfa, origin).href,
|
|
);
|
|
}
|
|
|
|
const userId = data.claims.sub;
|
|
|
|
return handleTeamAccountsOnly(req, userId);
|
|
},
|
|
}
|
|
```
|
|
|
|
In the above code snippet, we have added the `handleTeamAccountsOnly` function to the middleware chain.
|
|
|
|
## Creating the Team Selection Page
|
|
|
|
Next, we need to create a team selection page where users can select the team they want to log in to. We will create a new page at `apps/web/app/home/teams/page.tsx`:
|
|
|
|
```tsx {% title="apps/web/app/home/teams/page.tsx" %}
|
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
|
import { Trans } from '@kit/ui/trans';
|
|
|
|
import { getTranslations } from 'next-intl/server';
|
|
|
|
import { HomeAccountsList } from '~/home/(user)/_components/home-accounts-list';
|
|
|
|
export const generateMetadata = async () => {
|
|
const t = await getTranslations('account');
|
|
const title = t('homePage');
|
|
|
|
return {
|
|
title,
|
|
};
|
|
};
|
|
|
|
function TeamsPage() {
|
|
return (
|
|
<div className={'container flex flex-col flex-1 h-screen'}>
|
|
<PageHeader
|
|
title={<Trans i18nKey={'common.routes.home'} />}
|
|
description={<Trans i18nKey={'common.homeTabDescription'} />}
|
|
/>
|
|
|
|
<PageBody>
|
|
<HomeAccountsList />
|
|
</PageBody>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default TeamsPage;
|
|
```
|
|
|
|
The page is extremely minimal, it just displays a list of teams that the user can select from. You can customize this page to fit your application's design.
|
|
|
|
## Duplicating the User Settings Page
|
|
|
|
Finally, we need to duplicate the user settings page so that users can access their settings from within the team workspace.
|
|
|
|
We will create a new page called `user-settings.tsx` in the `apps/web/app/home/[account]` directory.
|
|
|
|
```tsx {% title="apps/web/app/home/[account]/user-settings/page.tsx" %}
|
|
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
|
import { PageHeader } from '@kit/ui/page';
|
|
|
|
import UserSettingsPage, { generateMetadata } from '../../(user)/settings/page';
|
|
|
|
export { generateMetadata };
|
|
|
|
export default function Page() {
|
|
return (
|
|
<>
|
|
<PageHeader title={'User Settings'} description={<AppBreadcrumbs />} />
|
|
|
|
<UserSettingsPage />
|
|
</>
|
|
);
|
|
}
|
|
```
|
|
|
|
Feel free to customize the path or the content of the user settings page.
|
|
|
|
### Adding the page to the Navigation Menu
|
|
|
|
Finally, you can add the `Teams` page to the navigation menu.
|
|
|
|
You can do this by updating the `apps/web/config/team-account-navigation.config.tsx` file:
|
|
|
|
```tsx {% title="apps/web/config/team-account-navigation.config.tsx" %} {26-30}
|
|
import { CreditCard, LayoutDashboard, Settings, User, Users } from 'lucide-react';
|
|
|
|
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
|
|
|
|
import featureFlagsConfig from '~/config/feature-flags.config';
|
|
import pathsConfig from '~/config/paths.config';
|
|
|
|
const iconClasses = 'w-4';
|
|
|
|
const getRoutes = (account: string) => [
|
|
{
|
|
label: 'common.routes.application',
|
|
collapsible: false,
|
|
children: [
|
|
{
|
|
label: 'common.routes.dashboard',
|
|
path: pathsConfig.app.accountHome.replace('[account]', account),
|
|
Icon: <LayoutDashboard className={iconClasses} />,
|
|
end: true,
|
|
}
|
|
],
|
|
},
|
|
{
|
|
label: 'common.routes.settings',
|
|
collapsible: false,
|
|
children: [
|
|
{
|
|
label: 'common.routes.settings',
|
|
path: createPath(pathsConfig.app.accountSettings, account),
|
|
Icon: <Settings className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common.routes.account',
|
|
path: createPath('/home/[account]/user-settings', account),
|
|
Icon: <User className={iconClasses} />,
|
|
},
|
|
{
|
|
label: 'common.routes.members',
|
|
path: createPath(pathsConfig.app.accountMembers, account),
|
|
Icon: <Users className={iconClasses} />,
|
|
},
|
|
featureFlagsConfig.enableTeamAccountBilling
|
|
? {
|
|
label: 'common.routes.billing',
|
|
path: createPath(pathsConfig.app.accountBilling, account),
|
|
Icon: <CreditCard className={iconClasses} />,
|
|
}
|
|
: undefined,
|
|
].filter(Boolean),
|
|
},
|
|
];
|
|
|
|
export function getTeamAccountSidebarConfig(account: string) {
|
|
return NavigationConfigSchema.parse({
|
|
routes: getRoutes(account),
|
|
style: process.env.NEXT_PUBLIC_TEAM_NAVIGATION_STYLE,
|
|
});
|
|
}
|
|
|
|
function createPath(path: string, account: string) {
|
|
return path.replace('[account]', account);
|
|
}
|
|
```
|
|
|
|
In the above code snippet, we have added the `User Settings` page to the navigation menu.
|
|
|
|
## Removing Personal Account menu item
|
|
|
|
To remove the personal account menu item, you can remove the personal account menu item:
|
|
|
|
```tsx {% title="packages/features/accounts/src/components/account-selector.tsx" %}
|
|
<CommandGroup>
|
|
<CommandItem
|
|
onSelect={() => onAccountChange(undefined)}
|
|
value={PERSONAL_ACCOUNT_SLUG}
|
|
>
|
|
<PersonalAccountAvatar />
|
|
|
|
<span className={'ml-2'}>
|
|
<Trans i18nKey={'teams.personalAccount'} />
|
|
</span>
|
|
|
|
<Icon item={PERSONAL_ACCOUNT_SLUG} />
|
|
</CommandItem>
|
|
</CommandGroup>
|
|
|
|
<CommandSeparator />
|
|
```
|
|
|
|
Once you remove the personal account menu item, users will only see the team accounts in the navigation menu.
|
|
|
|
## Change Redirect in Layout
|
|
|
|
We now need to change the redirect (in case of errors) from `/home` to `/home/teams`. This is to avoid infinite redirects in case of errors.
|
|
|
|
```tsx {% title="apps/web/app/home/[account]/_lib/server/team-account-workspace.loader.ts" %} {13}
|
|
async function workspaceLoader(accountSlug: string) {
|
|
const client = getSupabaseServerClient();
|
|
const api = createTeamAccountsApi(client);
|
|
|
|
const [workspace, user] = await Promise.all([
|
|
api.getAccountWorkspace(accountSlug),
|
|
requireUserInServerComponent(),
|
|
]);
|
|
|
|
// we cannot find any record for the selected account
|
|
// so we redirect the user to the home page
|
|
if (!workspace.data?.account) {
|
|
return redirect('/home/teams');
|
|
}
|
|
|
|
return {
|
|
...workspace.data,
|
|
user,
|
|
};
|
|
}
|
|
```
|
|
|
|
## Conclusion
|
|
|
|
By following these steps, you can disable personal accounts in the Next.js Supabase Turbo SaaS kit and only allow team accounts.
|
|
|
|
This can help you create a more focused and collaborative environment for your users. Feel free to customize the team selection page and user settings page to fit your application's design and requirements.
|