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
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
- Start the frontend: containers should already be running on
stg-app.membervu.com - Navigate to
https://stg-app.membervu.com/rcme/public/events - Expected: Redirects to
https://stg-app.membervu.com/rcme/eventsand shows event catalog - Navigate to
https://stg-app.membervu.com/rcme/eventsdirectly - Expected: Shows event catalog with events loaded (no 404)
- Check browser console — no 404 errors for API calls
- 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
mmp/auth-service/src/authMiddleware.ts— AddoptionalAuthMiddlewarefunctionmmp/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:
- Try to extract and verify the Bearer token
- If valid: populate
req.userwith the same fields asauthMiddlewaredoes (lines 130-138) - If missing or invalid: call
next()without error (req.user stays undefined)
/**
- 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
- Restart the backend container:
docker restart membervu-backend - Log in as guest user:
guest@demo.membervu.com/Guest123! - Navigate to the membership application page (click "Apply for Membership")
- Fill out the application form:
- Short Bio: at least 10 characters
- Why do you want to join: at least 20 characters
- Click "Submit Application"
- Expected: Application submits successfully (201 response), success message shown
- Not expected: 401 error, logout, or redirect to login page
- Check backend logs: should show "Application submitted" with memberId and tenantId populated
Execution Order
- Fix BUG-PHASE7-001 first (frontend only, no restart needed)
- Fix BUG-PHASE7-002 second (backend change, requires container restart)
- 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)