DEV Community

linou518
linou518

Posted on

Fixing a Login Loop in a Unified Admin for ERP + EC Platform: The Dual-Token Auth Approach

Fixing a Login Loop in a Unified Admin for ERP + EC Platform: The Dual-Token Auth Approach

What Happened

While building the Admin panel for techsfree-platform (a unified dashboard controlling both an ERP system and an EC shop), I hit an annoying bug.

Symptom: after logging in, EC-related pages (products, orders, members) loaded fine — but navigating to any ERP-related page (sales, purchasing, inventory) redirected to /login. Clicking around just created an infinite login loop.

Architecture Background

This project has two separate backends controlled by one Admin UI:

  • Platform backend (port 3210) — EC features (products/orders/members), with its own JWT (tf_token)
  • ERP backend (port 8520) — Sales/purchasing/inventory, also with its own JWT (erp_token)

The ERP part was originally a separate project, so the auth tokens are completely independent.

Root Cause

One look at erpApi.ts (the Axios client for ERP calls) revealed the culprit:

instance.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      window.location.href = '/login'; // ← This is the problem
    }
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

The ERP backend only accepts ERP-specific JWTs. Sending the Platform's tf_token inevitably gets a 401. And every 401 triggers a forced redirect to /login.

The cycle: navigate to ERP page → API call with wrong token → 401 → redirect to login → repeat forever.

Solution: Dual-Login

The fix: when a user logs in, simultaneously log into the ERP backend too, and store both tokens.

Backend Change (Platform Side)

Modified the /auth/login endpoint: when the user is admin or staff, automatically log into the ERP backend in the background, get the erp_token, and include it in the response:

// fetchErpToken(): logs into ERP backend and returns its token
async function fetchErpToken(email: string, password: string): Promise<string | null> {
  try {
    const response = await axios.post(
      `${process.env.ERP_URL}/auth/login`,
      { email, password },
      { timeout: 5000 }
    );
    return response.data?.token || null;
  } catch {
    return null; // ERP failure shouldn't block Platform login
  }
}

// In the login endpoint:
const erp_token = await fetchErpToken(email, password);
res.json({ token, erp_token, user });
Enter fullscreen mode Exit fullscreen mode

Key design: returning null on failure ensures ERP issues don't block the Platform login. If ERP is down, EC features should still work.

Frontend Changes

AuthContext.tsx — save erp_token to localStorage on login:

localStorage.setItem('tf_token', data.token);
if (data.erp_token) {
  localStorage.setItem('tf_erp_token', data.erp_token);
}
Enter fullscreen mode Exit fullscreen mode

erpApi.ts interceptors — prefer tf_erp_token, and stop redirecting on 401:

// Request: prefer erp_token
instance.interceptors.request.use(config => {
  const token = localStorage.getItem('tf_erp_token')
              || localStorage.getItem('tf_token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Response: don't redirect on 401
instance.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 401) {
      localStorage.removeItem('tf_erp_token'); // Only clear ERP token
      // window.location.href = '/login'; ← Removed
    }
    return Promise.reject(error);
  }
);
Enter fullscreen mode Exit fullscreen mode

One more gotcha: the ERP backend had no admin user created, so I had to manually promote the role via direct DB operation:

UPDATE users SET role='admin' WHERE email='admin@example.com';
Enter fullscreen mode Exit fullscreen mode

Lessons Learned

In a multi-backend architecture with one frontend, authentication is the most complex part.

Putting window.location.href in an Axios interceptor is convenient but fragile. When multiple API clients exist, global side effects like this can trigger from any direction. Better to propagate errors upward and manage auth state centrally via Context.

Key takeaways:

  1. Keep auth independent per API client — one backend's 401 shouldn't affect another backend's session
  2. Degrade gracefully — ERP being down shouldn't break EC functionality
  3. Avoid global side effects in interceptors — redirect logic belongs in Context, not inside erpApi.ts

Given the constraint of integrating a pre-existing ERP, the dual-token approach proved to be a pragmatic solution.

Top comments (0)