Phase 1: P0 Blockers — Guest Portal Critical Path

> Execution Document for Claude CLI Agent

> Goal: Fix 2 P0 bugs that completely block the guest portal experience.

> Scope: Frontend routing + Backend auth middleware

> Dependencies: None — can be executed independently


Bug 1: BUG-PHASE7-001 — Public Events Page 404

Root Cause

The frontend router defines /:tenantSlug/events at mmp/frontend/pwa-app/src/router.tsx:784. When a user navigates to /public/events, React Router captures tenantSlug = "public". The PublicEventCatalogPage component passes this to listPublicEvents("public", ...) in publicClient.ts, which constructs the URL ${API_BASE_URL}/api/public/public/events — a duplicate "public" segment. The backend has no tenant with slug "public", so it returns 404.

The correct URL pattern is /${tenantSlug}/events where tenantSlug is a real tenant like "rcme". The /public/events URL is a UX convenience path that should redirect to the default tenant.

Files to Modify

  1. mmp/frontend/pwa-app/src/router.tsx (line ~783)

Exact Changes

In router.tsx, add redirect routes for /public/events and /public/events/:eventSlug BEFORE the existing /:tenantSlug/events routes (line 783). These redirects should map /public/events to /${DEFAULT_TENANT_SLUG}/events.

Step 1: Find the import section at the top of router.tsx. Locate or add an import for the default tenant ID:

const DEFAULT_TENANT_SLUG = import.meta.env.VITE_DEFAULT_TENANT_ID || "rcme";

Step 2: Add redirect routes BEFORE the /:tenantSlug/events route (before line 783):

{/* Redirect /public/events to default tenant events */}

<Route

path="/public/events"

element={<Navigate to={/${DEFAULT_TENANT_SLUG}/events} replace />}

/>

<Route

path="/public/events/:eventSlug"

element={<PublicEventRedirect />}

/>

Step 3: Create a small redirect component (can be inline above the router or in the same file):

const PublicEventRedirect: React.FC = () => {

const { eventSlug } = useParams();

return <Navigate to={/${DEFAULT_TENANT_SLUG}/events/${eventSlug}} replace />;

};

Important: The Navigate component and useParams should already be imported from react-router-dom in this file. Verify before adding duplicate imports.

Verification

  1. Start the frontend: containers should already be running on stg-app.membervu.com
  2. Navigate to https://stg-app.membervu.com/rcme/public/events
  3. Expected: Redirects to https://stg-app.membervu.com/rcme/events and shows event catalog
  4. Navigate to https://stg-app.membervu.com/rcme/events directly
  5. Expected: Shows event catalog with events loaded (no 404)
  6. Check browser console — no 404 errors for API calls
  7. Check network tab — API call should go to /api/public/rcme/events (single "public")

Bug 2: BUG-PHASE7-002 — Guest Membership Application 401

Root Cause

The route app.post("/api/membership/apply", submitApplicationHandler) is at server.ts:449, positioned BEFORE the authMiddleware. This means req.user is ALWAYS undefined — even for authenticated guests who send a valid Bearer token.

The handler at membershipApplicationHandlers.ts:40-42 reads:

const tenantId = req.user?.tenantId;   // undefined

const memberId = req.user?.memberId; // undefined

At line 103:

if (!targetMemberId || !targetTenantId) {

return errors.unauthorized(res, "Authentication required or provide invitation token");

}

Since both are undefined (no invitation token provided by authenticated guests), it returns 401.

Files to Modify

  1. mmp/auth-service/src/authMiddleware.ts — Add optionalAuthMiddleware function
  2. mmp/auth-service/src/server.ts (line ~449) — Apply optional auth to the membership apply route

Exact Changes

Step 1: In mmp/auth-service/src/authMiddleware.ts, add a new exported function optionalAuthMiddleware AFTER the existing authMiddleware function (after line ~139). This function should:

/**
  • Optional auth middleware - populates req.user if valid token present,
  • but allows request to proceed without auth (for routes that support both
  • authenticated and unauthenticated access).

*/

export async function optionalAuthMiddleware(req: Request, res: Response, next: NextFunction) {

const header = req.headers.authorization;

if (!header || !header.startsWith("Bearer ")) {

return next(); // No token - proceed without auth

}

const token = header.slice("Bearer ".length).trim();

if (!token) return next(); // Empty token - proceed without auth

try {

const payload = verifyToken(token);

const user = await prisma.user.findUnique({

where: { id: payload.userId },

include: {

roleAssignments: {

select: { role: true }

}

},

});

if (!user || user.tenantId !== payload.tenantId) {

return next(); // Invalid user - proceed without auth

}

let roles: string[] = [];

if (payload.roles && Array.isArray(payload.roles) && payload.roles.length > 0) {

roles = payload.roles.map((r) => r.toUpperCase());

} else {

const legacyRoles = ((user.roles || []) as string[]).map((r) => r.toUpperCase());

const assignmentRoles = Array.isArray(user.roleAssignments)

? user.roleAssignments.map((ra: { role?: string }) => (ra.role || "").toUpperCase())

: [];

roles = Array.from(

new Set([...legacyRoles, ...assignmentRoles].filter((r) => !!r))

) as string[];

}

const userType = payload.userType || "GUEST";

(req as AuthenticatedRequest).user = {

userId: user.id,

tenantId: user.tenantId,

roles,

platformRoles: Array.isArray(payload.platformRoles) ? payload.platformRoles : [],

email: user.email,

memberId: user.memberId ?? null,

userType: userType as "MEMBER" | "GUEST",

};

} catch (err) {

// Token verification failed - proceed without auth

logger.debug("optionalAuthMiddleware: token verification failed, proceeding without auth");

}

next();

}

Step 2: In mmp/auth-service/src/server.ts, at line 449, update the route to use optional auth middleware:

Find:

app.post("/api/membership/apply", submitApplicationHandler);

app.post("/membership/apply", submitApplicationHandler);

Replace with:

import { optionalAuthMiddleware } from "./authMiddleware";

// ... (add to existing import if authMiddleware is already imported)

app.post("/api/membership/apply", optionalAuthMiddleware, submitApplicationHandler);

app.post("/membership/apply", optionalAuthMiddleware, submitApplicationHandler);

Note: The import for authMiddleware already exists at server.ts:18. Add optionalAuthMiddleware to that import:

import { authMiddleware, optionalAuthMiddleware } from "./authMiddleware";

Verification

  1. Restart the backend container: docker restart membervu-backend
  2. Log in as guest user: guest@demo.membervu.com / Guest123!
  3. Navigate to the membership application page (click "Apply for Membership")
  4. Fill out the application form:
  1. Click "Submit Application"
  2. Expected: Application submits successfully (201 response), success message shown
  3. Not expected: 401 error, logout, or redirect to login page
  4. Check backend logs: should show "Application submitted" with memberId and tenantId populated

Execution Order

  1. Fix BUG-PHASE7-001 first (frontend only, no restart needed)
  2. Fix BUG-PHASE7-002 second (backend change, requires container restart)
  3. Verify both fixes together: browse public events as guest, then apply for membership

Docker Commands

# Rebuild and restart backend after changes:

docker restart membervu-backend

Check backend logs:

docker logs membervu-backend --tail 50

Frontend hot-reloads automatically (Vite dev server)