Add a business page for signed-in users

Create a workspace that handles sign-in, email verification, and 2FA correctly, starting with localized text and page routing.

This article creates a /workspace page. Visitors must complete the authentication steps required by the project before they can open it. Visitors who are not signed in, have not verified their email, or have not completed 2FA are sent to the page for the corresponding step.

First, make sure the local project starts with npm run dev and that you can sign up and sign in. This article creates only the page. The next article adds an API for business data.

1. Prepare the page text

Merge workspace into the existing pages object in each of locales/en.json, locales/zh-Hans.json, and locales/zh-Hant.json. Do not replace an entire locale file with the snippets below.

English:

"workspace": {
  "title": "My workspace",
  "description": "Manage your saved links here."
}

Simplified Chinese:

"workspace": {
  "title": "我的工作台",
  "description": "在这里管理你收藏的链接。"
}

Traditional Chinese:

"workspace": {
  "title": "我的工作區",
  "description": "在這裡管理你收藏的連結。"
}

Resource key types are inferred from the English file. Adding the resources before writing the page lets TypeScript check the key names. If your project has other languages enabled, add the same keys to those resources as well.

2. Create the page file

Create src/pages/workspace.tsx:

import { serverLocalized } from '@/components/server/Localized';
import { authenticatedGuard } from '@/core/services/auth/guards/authenticated';
import { createLayoutRenderer } from '@/layout';
import { createLayoutHandlers } from '@/libs/renderer/render';
import { getFullPath } from '@/libs/utils';
import type { Ctx } from '@/types';

function WorkspacePage({ c }: { c: Ctx }) {
    return (
        <main className="container mx-auto px-4 py-12">
            <h1 className="text-3xl font-bold">
                {serverLocalized({ c, id: 'pages.workspace.title' })}
            </h1>
            <p className="mt-4 text-muted-foreground">
                {serverLocalized({ c, id: 'pages.workspace.description' })}
            </p>
        </main>
    );
}

const workspacePageHandlers = createLayoutHandlers(
    createLayoutRenderer('page'),
    async (c: Ctx) => {
        const guard = authenticatedGuard(c);
        if (!guard.success) {
            const redirectUrl = guard.details?.redirectUrl;
            if (typeof redirectUrl !== 'string') {
                throw new Error('Authenticated guard returned no redirect URL');
            }
            return c.redirect(getFullPath(redirectUrl, c.var.getLocale()));
        }

        return <WorkspacePage c={c} />;
    },
    { pageRootClassName: 'flex flex-col' },
);

export default workspacePageHandlers;

authenticatedGuard reads the authentication state from the existing request context and checks sign-in, email verification, and 2FA according to the configuration. Use its returned redirectUrl to send the user to the step they currently need to complete.

Conditional rendering on the page cannot replace this check. Hiding content or redirecting after the client loads does not protect data returned by the server.

3. Register the page route

Import the new file in src/pages/routes.tsx:

import workspacePageHandlers from './workspace';

Add the following alongside the existing pageRouter route declarations:

pageRouter.get('/workspace', ...workspacePageHandlers);

Place it before pageRouter is mounted on the parent application. The internationalization entry point handles the locale prefix, so you do not need a separate /:locale/workspace route.

Start the development server and open /workspace. To add a workspace link to a page, follow the configuration pattern used by the existing navigation components, or use getFullPath('/workspace', locale) to generate the URL for the current language.

4. Require administrator access

If the page is an administration tool, add this import to the same file:

import { authz } from '@/authz';

Then add the following after successful authentication and before returning the page:

if (!(await authz.isAdmin(c))) {
    return c.notFound();
}

This variant returns 404 for regular users to avoid exposing the administration page. If your product needs to explicitly show that access is denied, you can design a dedicated 403 page and keep its text in the locale resources.

This code is a variant for administration pages. A regular user's workspace does not need an administrator check. For paid feature checks, see Add paid entitlements to a business feature.

5. Protect the page and API separately

When the workspace later reads data through an API, the API must check identity independently. Users can bypass the page and request the endpoint directly. A check on the page does not automatically protect another route.

Use the same guard in the API, but return JSON and a status code:

import { gResultCode } from '@/errors';

// Place this inside the API handler. Here, c is the current request context.
const guard = authenticatedGuard(c);
if (!guard.success) {
    return c.json(
        guard,
        guard.code === gResultCode.authLoginRequired ? 401 : 403,
    );
}

const { authContext } = guard.data;
const userId = authContext.userId;

The userId comes from the server-side session. When reading personal data, pass it to the business service. Do not accept a user ID submitted by the frontend as the current identity. The next article implements the full connection between the API and the subsequent layers.

Forms and lists that need browser interactions should use the existing client component registration and SSRWrapper hydration approach. Writing click handlers directly in a server-rendered page does not automatically enable client-side interaction.

6. Verify page behavior

Check each of the following cases:

ScenarioExpected result
Open the workspace directly while signed outEnter the sign-in flow
The project requires email verification, and the user has not verified their emailEnter the email verification flow
The user's 2FA state does not yet meet the requirementsEnter the setup or verification flow specified by the guard
Authentication is completeDisplay the workspace title and description
Switch to another enabled languageThe URL and text change to match the language
A regular user opens the administration page variantReturn 404

Email and 2FA test cases depend on your enabled configuration and account state. Do not disable existing authentication requirements just to make the page accessible.

When you are done, run npm run lint, npm run typecheck, and npm run build. These commands check code style, types, and the build, respectively. None replaces the others.

Common problems include an unregistered route, translation keys at the wrong nesting level, or redirecting every authentication failure to the sign-in page. Check these three areas first, then refer to the session documentation in Authentication and accounts.