# Web Authentication Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Implement frontend authentication for the Next.js 16 web app — route protection via `proxy.ts`, auth mutations, user state via React Query, and wiring existing form UI.

**Architecture:** `proxy.ts` acts as a gate — checks cookie presence, refreshes tokens server-side, enforces role-based routing. Client-side uses React Query (`useCurrentUser()`) as the single source of auth state, hydrated from SSR. Axios interceptor is patched to not redirect on `/auth/me` failures.

**Tech Stack:** Next.js 16 (`proxy.ts`), React Query 5, axios, TypeScript

**Spec:** `docs/superpowers/specs/2026-03-18-web-auth-design.md`

---

## File Map

| Action | File | Responsibility |
|--------|------|----------------|
| Create | `apps/web/src/proxy.ts` | Route protection, server-side token refresh, role-based redirects |
| Modify | `apps/web/src/shared/api/client.ts:65-69` | Skip `/login` redirect when `/auth/me` fails |
| Modify | `apps/web/src/shared/api/api-types.ts` | Add `User` type |
| Modify | `apps/web/src/shared/api/queries/auth.queries.ts` | Add `useCurrentUser()` hook, type `authQueries.me()` |
| Create | `apps/web/src/shared/api/mutations/auth.mutations.ts` | `useLogin()`, `useRegister()`, `useLogout()` |
| Modify | `apps/web/src/features/auth/ui/LoginForm.tsx:79,133-138` | Wire `useLogin()` mutation, error/loading states |
| Modify | `apps/web/src/features/auth/ui/RegisterForm.tsx` | Wire `useRegister()` mutation on step 2 submit |
| Modify | `apps/web/src/widgets/header/ui/Header.tsx` | Uncomment auth dropdown, wire `useCurrentUser()` + `useLogout()` |
| Modify | `apps/web/src/app/me/layout.tsx` | Refactor to Server Component + client sidebar, prefetch user |
| Modify | `apps/web/src/app/seller/layout.tsx` | Refactor to Server Component + client sidebar, prefetch user |

---

### Task 1: Add `User` type and fix axios interceptor

**Files:**
- Modify: `apps/web/src/shared/api/api-types.ts`
- Modify: `apps/web/src/shared/api/client.ts:65-69`

- [ ] **Step 1: Add `User` type to `api-types.ts`**

Append to end of file:

```ts
export type AccountType = 'BUYER' | 'SELLER' | 'SELLER_STAFF' | 'ADMIN';
export type AccountStatus = 'ACTIVE' | 'SUSPENDED' | 'DELETED';

export interface User {
  id: string;
  email: string;
  phone: string;
  type: AccountType;
  status: AccountStatus;
  createdAt: string;
}

export interface AuthResponse {
  user: User;
}

export interface LoginData {
  login: string;
  password: string;
}

export interface RegisterData {
  email: string;
  phone: string;
  password: string;
  type: 'BUYER' | 'SELLER';
}
```

- [ ] **Step 2: Fix axios interceptor to skip redirect on `/auth/me`**

In `apps/web/src/shared/api/client.ts`, replace lines 65-69:

```ts
// Before:
    } catch (refreshError) {
      processQueue(refreshError);
      if (typeof window !== 'undefined') {
        window.location.href = '/login';
      }
      return Promise.reject(toApiError(error));

// After:
    } catch (refreshError) {
      processQueue(refreshError);
      const isAuthMeRequest = originalRequest.url?.includes('/auth/me');
      if (typeof window !== 'undefined' && !isAuthMeRequest) {
        window.location.href = '/login';
      }
      return Promise.reject(toApiError(error));
```

- [ ] **Step 3: Verify build**

Run: `cd apps/web && npx next build --no-lint 2>&1 | tail -5`
Expected: Build succeeds (types are additive, interceptor change is minimal)

- [ ] **Step 4: Commit**

```bash
git add apps/web/src/shared/api/api-types.ts apps/web/src/shared/api/client.ts
git commit -m "feat(web): add User type and fix interceptor redirect for /auth/me"
```

---

### Task 2: Add `useCurrentUser()` hook and type auth queries

**Files:**
- Modify: `apps/web/src/shared/api/queries/auth.queries.ts`

- [ ] **Step 1: Update `auth.queries.ts` with typed query and `useCurrentUser` hook**

Replace entire file:

```ts
import { queryOptions, useQuery } from '@tanstack/react-query';
import { apiClient } from '../client';
import { authKeys } from '../keys/auth.keys';
import type { User } from '../api-types';

export const authQueries = {
  me: () =>
    queryOptions({
      queryKey: authKeys.me(),
      queryFn: () => apiClient.get<User>('/auth/me').then((r) => r.data),
      retry: false,
      staleTime: 5 * 60 * 1000,
    }),
};

export function useCurrentUser() {
  const { data, isLoading } = useQuery(authQueries.me());
  return {
    user: data ?? null,
    isLoading,
    isAuthenticated: !!data,
  };
}
```

- [ ] **Step 2: Verify build**

Run: `cd apps/web && npx next build --no-lint 2>&1 | tail -5`
Expected: Build succeeds

- [ ] **Step 3: Commit**

```bash
git add apps/web/src/shared/api/queries/auth.queries.ts
git commit -m "feat(web): add useCurrentUser hook with typed auth query"
```

---

### Task 3: Create auth mutations

**Files:**
- Create: `apps/web/src/shared/api/mutations/auth.mutations.ts`

- [ ] **Step 1: Create `auth.mutations.ts`**

Follow the pattern from `leads.mutations.ts`:

```ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useRouter, useSearchParams } from 'next/navigation';
import { apiClient } from '../client';
import { authKeys } from '../keys/auth.keys';
import type { AuthResponse, LoginData, RegisterData } from '../api-types';

const DEFAULT_ROUTES: Record<string, string> = {
  BUYER: '/me',
  SELLER: '/seller',
  SELLER_STAFF: '/seller',
  ADMIN: '/admin',
};

function getDefaultRoute(userType: string): string {
  return DEFAULT_ROUTES[userType] || '/me';
}

function isValidCallbackUrl(url: string | null): url is string {
  return !!url && url.startsWith('/') && !url.startsWith('//');
}

export function useLogin() {
  const queryClient = useQueryClient();
  const router = useRouter();
  const searchParams = useSearchParams();

  return useMutation({
    mutationFn: (data: LoginData) =>
      apiClient.post<AuthResponse>('/auth/login', data).then((r) => r.data),
    onSuccess: (data) => {
      queryClient.invalidateQueries({ queryKey: authKeys.all() });
      const callbackUrl = searchParams.get('callbackUrl');
      router.push(
        isValidCallbackUrl(callbackUrl)
          ? callbackUrl
          : getDefaultRoute(data.user.type),
      );
    },
  });
}

export function useRegister() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: RegisterData) =>
      apiClient.post<AuthResponse>('/auth/register', data).then((r) => r.data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: authKeys.all() });
    },
  });
}

export function useLogout() {
  const queryClient = useQueryClient();
  const router = useRouter();

  return useMutation({
    mutationFn: () => apiClient.post('/auth/logout').then((r) => r.data),
    onSuccess: () => {
      queryClient.clear();
      router.push('/login');
    },
  });
}
```

- [ ] **Step 2: Verify build**

Run: `cd apps/web && npx next build --no-lint 2>&1 | tail -5`
Expected: Build succeeds

- [ ] **Step 3: Commit**

```bash
git add apps/web/src/shared/api/mutations/auth.mutations.ts
git commit -m "feat(web): add auth mutations (login, register, logout)"
```

---

### Task 4: Create `proxy.ts` for route protection

**Files:**
- Create: `apps/web/src/proxy.ts`

- [ ] **Step 1: Create `proxy.ts`**

```ts
import { NextRequest, NextResponse } from 'next/server';

const API_URL =
  process.env.API_URL ||
  process.env.NEXT_PUBLIC_API_URL ||
  'http://localhost:5001/api/v1';

const PUBLIC_PATHS = ['/', '/login', '/register', '/catalog'];
const AUTH_PAGES = ['/login', '/register'];
const PROTECTED_PREFIXES = ['/me', '/seller', '/admin'];

const DEFAULT_ROUTES: Record<string, string> = {
  BUYER: '/me',
  SELLER: '/seller',
  SELLER_STAFF: '/seller',
  ADMIN: '/admin',
};

const ROLE_ACCESS: Record<string, string[]> = {
  BUYER: ['/me'],
  SELLER: ['/me', '/seller'],
  SELLER_STAFF: ['/me', '/seller'],
  ADMIN: ['/me', '/seller', '/admin'],
};

function decodeJwtPayload(token: string): { sub: string; type: string } | null {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) return null;
    const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
    return payload;
  } catch {
    return null;
  }
}

function isProtectedPath(pathname: string): boolean {
  return PROTECTED_PREFIXES.some(
    (prefix) => pathname === prefix || pathname.startsWith(prefix + '/'),
  );
}

function isAuthPage(pathname: string): boolean {
  return AUTH_PAGES.includes(pathname);
}

function hasRoleAccess(userType: string, pathname: string): boolean {
  const allowedPrefixes = ROLE_ACCESS[userType];
  if (!allowedPrefixes) return false;
  return allowedPrefixes.some(
    (prefix) => pathname === prefix || pathname.startsWith(prefix + '/'),
  );
}

async function tryRefreshToken(
  refreshToken: string,
): Promise<{ cookies: string[] } | null> {
  try {
    const res = await fetch(`${API_URL}/auth/refresh`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Cookie: `qadam_rt=${refreshToken}`,
      },
      body: '{}',
    });

    if (!res.ok) return null;

    const setCookies = res.headers.getSetCookie();
    if (!setCookies.length) return null;

    return { cookies: setCookies };
  } catch {
    return null;
  }
}

export async function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const accessToken = request.cookies.get('qadam_at')?.value;
  const refreshToken = request.cookies.get('qadam_rt')?.value;

  // Skip non-page requests (assets, API, etc.)
  if (
    pathname.startsWith('/api/') ||
    pathname.startsWith('/_next/') ||
    pathname.includes('.')
  ) {
    return NextResponse.next();
  }

  // Auth pages: redirect authenticated users to their dashboard
  if (isAuthPage(pathname) && accessToken) {
    const payload = decodeJwtPayload(accessToken);
    const defaultRoute = payload
      ? DEFAULT_ROUTES[payload.type] || '/me'
      : '/me';
    return NextResponse.redirect(new URL(defaultRoute, request.url));
  }

  // Public paths: no auth check needed
  if (!isProtectedPath(pathname)) {
    return NextResponse.next();
  }

  // Protected path: need authentication
  let currentAccessToken = accessToken;

  // No access token — try refresh
  if (!currentAccessToken && refreshToken) {
    const result = await tryRefreshToken(refreshToken);
    if (result) {
      // Extract new access token from Set-Cookie headers
      const response = NextResponse.next();
      for (const cookie of result.cookies) {
        response.headers.append('Set-Cookie', cookie);
        // Parse access token from cookie for role check
        const match = cookie.match(/qadam_at=([^;]+)/);
        if (match) {
          currentAccessToken = match[1];
        }
      }

      // Check role access with refreshed token
      if (currentAccessToken) {
        const payload = decodeJwtPayload(currentAccessToken);
        if (payload && !hasRoleAccess(payload.type, pathname)) {
          const redirectUrl = DEFAULT_ROUTES[payload.type] || '/me';
          const redirectResponse = NextResponse.redirect(
            new URL(redirectUrl, request.url),
          );
          for (const cookie of result.cookies) {
            redirectResponse.headers.append('Set-Cookie', cookie);
          }
          return redirectResponse;
        }
      }

      return response;
    }
  }

  // No tokens at all — redirect to login
  if (!currentAccessToken) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('callbackUrl', pathname);
    return NextResponse.redirect(loginUrl);
  }

  // Has access token — check role access
  const payload = decodeJwtPayload(currentAccessToken);
  if (payload && !hasRoleAccess(payload.type, pathname)) {
    const redirectUrl = DEFAULT_ROUTES[payload.type] || '/me';
    return NextResponse.redirect(new URL(redirectUrl, request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - _next/static (static files)
     * - _next/image (image optimization)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    '/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
};
```

- [ ] **Step 2: Verify build**

Run: `cd apps/web && npx next build --no-lint 2>&1 | tail -10`
Expected: Build succeeds. Proxy is detected by Next.js 16.

- [ ] **Step 3: Commit**

```bash
git add apps/web/src/proxy.ts
git commit -m "feat(web): add proxy.ts for route protection and server-side token refresh"
```

---

### Task 5: Wire LoginForm to `useLogin()` mutation

**Files:**
- Modify: `apps/web/src/features/auth/ui/LoginForm.tsx`
- Modify: `apps/web/src/app/(auth)/login/page.tsx` (if `<LoginForm>` is not wrapped in `<Suspense>` — required because `useLogin()` calls `useSearchParams()`)

**Note:** `useSearchParams()` requires the component to be inside a `<Suspense>` boundary. Check `apps/web/src/app/(auth)/login/page.tsx` — if `<LoginForm>` is not already wrapped in `<Suspense>`, add it.

- [ ] **Step 1: Add mutation, error state, and loading state**

Add imports at top of file (after existing imports):

```ts
import { useLogin } from '@/shared/api/mutations/auth.mutations';
```

Add after the existing `useState` declarations (after line 28):

```ts
const login = useLogin();
```

- [ ] **Step 2: Replace form onSubmit**

Replace line 79:
```ts
// Before:
<form onSubmit={(e) => e.preventDefault()} className="space-y-4">

// After:
<form
  onSubmit={(e) => {
    e.preventDefault();
    const loginValue = tab === 'phone' ? phone : email;
    if (!loginValue || !password) return;
    login.mutate({ login: loginValue, password });
  }}
  className="space-y-4"
>
```

- [ ] **Step 3: Add error display and loading state to submit button**

Replace the submit button (lines 133-138):
```tsx
// Before:
<button
  type="submit"
  className="w-full py-3.5 bg-[#1DB57A] text-white text-sm font-bold rounded-xl hover:bg-[#18a36e] transition-colors"
>
  Войти
</button>

// After:
{login.error && (
  <div className="rounded-xl bg-red-50 px-4 py-3 text-sm text-red-600">
    {'errorCode' in login.error && login.error.errorCode === 'INVALID_CREDENTIALS'
      ? 'Неверный логин или пароль'
      : 'Произошла ошибка. Попробуйте ещё раз'}
  </div>
)}

<button
  type="submit"
  disabled={login.isPending}
  className="w-full py-3.5 bg-[#1DB57A] text-white text-sm font-bold rounded-xl hover:bg-[#18a36e] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
  {login.isPending ? 'Вход...' : 'Войти'}
</button>
```

- [ ] **Step 4: Verify build**

Run: `cd apps/web && npx next build --no-lint 2>&1 | tail -5`
Expected: Build succeeds

- [ ] **Step 5: Commit**

```bash
git add apps/web/src/features/auth/ui/LoginForm.tsx
git commit -m "feat(web): wire LoginForm to useLogin mutation"
```

---

### Task 6: Wire RegisterForm to `useRegister()` mutation

**Files:**
- Modify: `apps/web/src/features/auth/ui/RegisterForm.tsx`

- [ ] **Step 1: Add mutation import and hook**

Add import after existing imports:
```ts
import { useRegister } from '@/shared/api/mutations/auth.mutations';
```

Add after existing `useState` declarations (inside `RegisterForm` function):
```ts
const register = useRegister();
```

- [ ] **Step 2: Replace step 2 form onSubmit (line 132)**

Replace line 132:
```tsx
// Before:
<form onSubmit={(e) => { e.preventDefault(); setStep(3); }} className="space-y-4">

// After:
<form
  onSubmit={(e) => {
    e.preventDefault();
    const accountType = role === 'buyer' ? 'BUYER' : 'SELLER';
    register.mutate(
      { email, phone, password, type: accountType },
      { onSuccess: () => setStep(3) },
    );
  }}
  className="space-y-4"
>
```

Note: `useRegister()` mutation only invalidates queries. The form controls navigation (step 3) via local `onSuccess` callback passed to `mutate()`.

- [ ] **Step 3: Add error display and loading state to step 2 submit button (lines 201-206)**

Replace:
```tsx
// Before:
<button
  type="submit"
  className="w-full py-3.5 bg-[#1DB57A] text-white text-sm font-bold rounded-xl hover:bg-[#18a36e] transition-colors"
>
  Создать аккаунт
</button>

// After:
{'errorCode' in (register.error ?? {}) && (register.error as any).errorCode === 'DUPLICATE'
  ? (
    <div className="rounded-xl bg-red-50 px-4 py-3 text-sm text-red-600">
      Аккаунт с таким email или телефоном уже существует
    </div>
  )
  : register.error && (
    <div className="rounded-xl bg-red-50 px-4 py-3 text-sm text-red-600">
      Произошла ошибка. Попробуйте ещё раз
    </div>
  )
}

<button
  type="submit"
  disabled={register.isPending}
  className="w-full py-3.5 bg-[#1DB57A] text-white text-sm font-bold rounded-xl hover:bg-[#18a36e] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
  {register.isPending ? 'Создание...' : 'Создать аккаунт'}
</button>
```

- [ ] **Step 4: Verify build**

Run: `cd apps/web && npx next build --no-lint 2>&1 | tail -5`
Expected: Build succeeds

- [ ] **Step 5: Commit**

```bash
git add apps/web/src/features/auth/ui/RegisterForm.tsx
git commit -m "feat(web): wire RegisterForm to useRegister mutation"
```

---

### Task 7: Wire Header auth state

**Files:**
- Modify: `apps/web/src/widgets/header/ui/Header.tsx`

- [ ] **Step 1: Add imports and hook**

Replace imports section and add hooks:

```ts
'use client';

import { useState, useEffect, useRef } from 'react';
import Link from 'next/link';
import { useTransition } from 'react';
import { LogIn, LogOut, User, ChevronDown, LayoutDashboard, Settings } from 'lucide-react';
import { useLocale } from 'next-intl';
import { useRouter } from 'next/navigation';
import { cn } from '@/shared/lib/utils';
import { setLocale } from '@/shared/i18n/actions';
import { useCurrentUser } from '@/shared/api/queries/auth.queries';
import { useLogout } from '@/shared/api/mutations/auth.mutations';
```

- [ ] **Step 2: Add role maps and hooks inside component**

Uncomment and update the role/cabinet maps. Add hooks inside `Header()`:

```ts
const ROLE_LABELS: Record<string, string> = {
  BUYER: 'Покупатель',
  SELLER: 'Продавец',
  SELLER_STAFF: 'Сотрудник',
  ADMIN: 'Админ',
};

const CABINET_ROUTES: Record<string, string> = {
  BUYER: '/me',
  SELLER: '/seller',
  SELLER_STAFF: '/seller',
  ADMIN: '/admin',
};
```

Inside the `Header` function, add:
```ts
const { user, isAuthenticated } = useCurrentUser();
const logout = useLogout();
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);

useEffect(() => {
  function handleClickOutside(e: MouseEvent) {
    if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
      setDropdownOpen(false);
    }
  }
  document.addEventListener('mousedown', handleClickOutside);
  return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);

const cabinetRoute = user ? CABINET_ROUTES[user.type] || '/me' : '/me';
```

- [ ] **Step 3: Replace auth buttons section**

Replace lines 82-162 (the static login/register buttons) with the conditional auth UI:

```tsx
<div className="flex items-center gap-2">
  {isAuthenticated && user ? (
    <div className="relative" ref={dropdownRef}>
      <button
        onClick={() => setDropdownOpen(!dropdownOpen)}
        className="flex items-center gap-2 px-3 py-2 bg-white rounded-xl cursor-pointer"
      >
        <div className="w-7 h-7 rounded-full bg-[#1DB57A] flex items-center justify-center">
          <User className="w-3.5 h-3.5 text-white" />
        </div>
        <span className="hidden md:block text-sm font-medium text-gray-700">
          {ROLE_LABELS[user.type] || user.type}
        </span>
        <ChevronDown className="w-3.5 h-3.5 text-gray-400" />
      </button>

      {dropdownOpen && (
        <div className="absolute right-0 top-full mt-1.5 w-48 bg-white rounded-xl border border-gray-100 shadow-lg py-1.5 z-50">
          <Link
            href={cabinetRoute}
            onClick={() => setDropdownOpen(false)}
            className="flex items-center gap-2.5 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
          >
            <LayoutDashboard className="w-4 h-4 text-gray-400" />
            Личный кабинет
          </Link>
          <Link
            href={`${cabinetRoute}/profile`}
            onClick={() => setDropdownOpen(false)}
            className="flex items-center gap-2.5 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
          >
            <Settings className="w-4 h-4 text-gray-400" />
            Настройки
          </Link>
          <div className="h-px bg-gray-100 mx-3 my-1" />
          <button
            onClick={() => {
              logout.mutate();
              setDropdownOpen(false);
            }}
            className="flex items-center gap-2.5 px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 transition-colors w-full text-left cursor-pointer"
          >
            <LogOut className="w-4 h-4" />
            Выйти
          </button>
        </div>
      )}
    </div>
  ) : (
    <>
      <Link
        href="/login"
        className="hidden md:flex items-center gap-1.5 px-3 py-2 bg-white rounded-xl text-sm font-medium text-black"
      >
        <LogIn className="w-4 h-4" />
        Войти
      </Link>
      <Link
        href="/register"
        className="flex items-center gap-1.5 px-3 py-2 bg-black text-white rounded-xl text-sm font-semibold hover:bg-black/90 transition-colors"
      >
        <User className="w-4 h-4" />
        <span className="hidden md:inline">Регистрация</span>
      </Link>
    </>
  )}
</div>
```

- [ ] **Step 4: Verify build**

Run: `cd apps/web && npx next build --no-lint 2>&1 | tail -5`
Expected: Build succeeds

- [ ] **Step 5: Commit**

```bash
git add apps/web/src/widgets/header/ui/Header.tsx
git commit -m "feat(web): wire Header auth state with useCurrentUser and useLogout"
```

---

### Task 8: Refactor protected layouts to Server Components with SSR prefetch

**Files:**
- Modify: `apps/web/src/app/me/layout.tsx`
- Modify: `apps/web/src/app/seller/layout.tsx`

- [ ] **Step 1: Refactor `/me/layout.tsx`**

Replace entire file. Extract sidebar navigation to a client component inline, make layout a Server Component:

```tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient } from '@/shared/lib/get-query-client';
import { serverFetch } from '@/shared/api/server';
import { authKeys } from '@/shared/api/keys/auth.keys';
import { Header } from '@/widgets/header';
import type { User } from '@/shared/api/api-types';
import { MeSidebar } from './MeSidebar';

export default async function MeLayout({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();

  await queryClient.prefetchQuery({
    queryKey: authKeys.me(),
    queryFn: () => serverFetch<User>('/auth/me'),
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <div className="min-h-screen bg-[#F9FAFB]">
        <Header />
        <div className="mx-auto max-w-7xl px-4 py-6 md:py-8">
          <div className="flex flex-col gap-6 md:flex-row">
            <MeSidebar />
            <div className="min-w-0 flex-1">{children}</div>
          </div>
        </div>
      </div>
    </HydrationBoundary>
  );
}
```

- [ ] **Step 2: Create `MeSidebar` client component**

Create `apps/web/src/app/me/MeSidebar.tsx`:

```tsx
'use client';

import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { LayoutDashboard, User, FileText, Star } from 'lucide-react';

const NAV_ITEMS = [
  { href: '/me', label: 'Обзор', icon: LayoutDashboard, exact: true },
  { href: '/me/profile', label: 'Профиль', icon: User },
  { href: '/me/leads', label: 'Мои заявки', icon: FileText },
  { href: '/me/reviews', label: 'Мои отзывы', icon: Star },
];

export function MeSidebar() {
  const pathname = usePathname();

  return (
    <aside className="w-full shrink-0 md:w-56">
      <nav className="rounded-2xl border border-gray-100 bg-white p-2 md:sticky md:top-20">
        {NAV_ITEMS.map((item) => {
          const isActive = item.exact
            ? pathname === item.href
            : pathname.startsWith(item.href);
          return (
            <Link
              key={item.href}
              href={item.href}
              className={`flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-medium transition-colors ${
                isActive
                  ? 'bg-[#E8F5E9] text-[#1DB57A]'
                  : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
              }`}
            >
              <item.icon className="h-4 w-4" />
              {item.label}
            </Link>
          );
        })}
      </nav>
    </aside>
  );
}
```

- [ ] **Step 3: Refactor `/seller/layout.tsx`**

Same pattern. Replace entire file:

```tsx
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { getQueryClient } from '@/shared/lib/get-query-client';
import { serverFetch } from '@/shared/api/server';
import { authKeys } from '@/shared/api/keys/auth.keys';
import { Header } from '@/widgets/header';
import type { User } from '@/shared/api/api-types';
import { SellerSidebar } from './SellerSidebar';

export default async function SellerLayout({ children }: { children: React.ReactNode }) {
  const queryClient = getQueryClient();

  await queryClient.prefetchQuery({
    queryKey: authKeys.me(),
    queryFn: () => serverFetch<User>('/auth/me'),
  });

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <div className="min-h-screen bg-[#F9FAFB]">
        <Header />
        <div className="mx-auto max-w-[1280px] px-4 py-6 md:py-8">
          <div className="flex flex-col gap-6 md:flex-row">
            <SellerSidebar />
            <div className="min-w-0 flex-1">{children}</div>
          </div>
        </div>
      </div>
    </HydrationBoundary>
  );
}
```

- [ ] **Step 4: Create `SellerSidebar` client component**

Create `apps/web/src/app/seller/SellerSidebar.tsx`:

```tsx
'use client';

import { usePathname } from 'next/navigation';
import Link from 'next/link';
import { LayoutDashboard, User, BookOpen, FileText, Users } from 'lucide-react';

const NAV_ITEMS = [
  { href: '/seller', label: 'Обзор', icon: LayoutDashboard, exact: true },
  { href: '/seller/profile', label: 'О школе', icon: User },
  { href: '/seller/items', label: 'Мои курсы', icon: BookOpen },
  { href: '/seller/leads', label: 'Заявки', icon: FileText },
  { href: '/seller/staff', label: 'Сотрудники', icon: Users },
];

export function SellerSidebar() {
  const pathname = usePathname();

  return (
    <aside className="w-full shrink-0 md:w-56">
      <nav className="rounded-2xl border border-gray-100 bg-white p-2 md:sticky md:top-20">
        {NAV_ITEMS.map((item) => {
          const isActive = item.exact
            ? pathname === item.href
            : pathname.startsWith(item.href);
          return (
            <Link
              key={item.href}
              href={item.href}
              className={`flex items-center gap-3 rounded-xl px-4 py-3 text-sm font-medium transition-colors ${
                isActive
                  ? 'bg-[#E8F5E9] text-[#1DB57A]'
                  : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
              }`}
            >
              <item.icon className="h-4 w-4" />
              {item.label}
            </Link>
          );
        })}
      </nav>
    </aside>
  );
}
```

- [ ] **Step 5: Verify build**

Run: `cd apps/web && npx next build --no-lint 2>&1 | tail -10`
Expected: Build succeeds

- [ ] **Step 6: Commit**

```bash
git add apps/web/src/app/me/layout.tsx apps/web/src/app/me/MeSidebar.tsx apps/web/src/app/seller/layout.tsx apps/web/src/app/seller/SellerSidebar.tsx
git commit -m "feat(web): refactor me/seller layouts to Server Components with SSR user prefetch"
```

---

### Task 9: Manual integration test

- [ ] **Step 1: Start API and web dev servers**

Run in separate terminals:
```bash
cd apps/api && pnpm dev
cd apps/web && pnpm dev
```

- [ ] **Step 2: Test unauthenticated flow**

1. Open `http://localhost:5002/me` → should redirect to `/login?callbackUrl=/me`
2. Open `http://localhost:5002/seller` → should redirect to `/login?callbackUrl=/seller`
3. Open `http://localhost:5002/` → should show page normally with "Войти" / "Регистрация" buttons

- [ ] **Step 3: Test login flow**

1. On `/login`, enter valid credentials, click "Войти"
2. Should redirect to callbackUrl or default dashboard by role
3. Header should show user dropdown with role label
4. Refresh page — no flash, SSR provides user data

- [ ] **Step 4: Test role-based access**

1. As BUYER, visit `/seller` → should redirect to `/me`
2. As BUYER, visit `/admin` → should redirect to `/me`

- [ ] **Step 5: Test logout**

1. Click "Выйти" in header dropdown
2. Should redirect to `/login`
3. Visit `/me` → should redirect to `/login`

- [ ] **Step 6: Test registration**

1. Go to `/register`, fill step 1 (role) and step 2 (account data)
2. Click "Далее" — should create account and move to step 3
3. Duplicate email/phone should show error message

- [ ] **Step 7: Test token refresh in proxy**

1. Log in, then wait 15+ minutes (or manually delete `qadam_at` cookie in devtools)
2. Refresh a protected page
3. Should NOT redirect to login — proxy refreshes token server-side
4. Page loads with user data, no flash
