Add admin enhancements: impersonation, plan management, org status enforcement

Enhancement 1 - Block suspended/archived org access:
- Add org status check in switchOrganization() (auth.service.ts)
- Filter suspended/archived orgs from login response (generateTokenResponse)
- Add org status guard with 60s cache in TenantMiddleware
- Frontend: filter orgs in SelectOrgPage, add 403 handler in api.ts

Enhancement 2 - Change tenant plan level:
- Add updatePlanLevel() to organizations.service.ts
- Add PUT /admin/organizations/:id/plan endpoint
- Frontend: clickable plan dropdown in Organizations table + confirmation modal
- Plan level Select in tenant detail drawer

Enhancement 3 - User impersonation:
- Add impersonateUser() to auth.service.ts with impersonatedBy JWT claim
- Add POST /admin/impersonate/:userId endpoint
- Frontend: Impersonate button in Users tab (disabled for admins)
- Impersonation state management in authStore (start/stop/persist)
- Orange impersonation banner in AppLayout header with stop button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 13:21:59 -05:00
parent e156cf7c87
commit d9bb9363dd
10 changed files with 345 additions and 18 deletions

View File

@@ -6,6 +6,7 @@ interface Organization {
name: string;
role: string;
schemaName?: string;
status?: string;
}
interface User {
@@ -17,23 +18,34 @@ interface User {
isPlatformOwner?: boolean;
}
interface ImpersonationOriginal {
token: string;
user: User;
organizations: Organization[];
currentOrg: Organization | null;
}
interface AuthState {
token: string | null;
user: User | null;
organizations: Organization[];
currentOrg: Organization | null;
impersonationOriginal: ImpersonationOriginal | null;
setAuth: (token: string, user: User, organizations: Organization[]) => void;
setCurrentOrg: (org: Organization, token?: string) => void;
startImpersonation: (token: string, user: User, organizations: Organization[]) => void;
stopImpersonation: () => void;
logout: () => void;
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
(set, get) => ({
token: null,
user: null,
organizations: [],
currentOrg: null,
impersonationOriginal: null,
setAuth: (token, user, organizations) =>
set({
token,
@@ -47,22 +59,51 @@ export const useAuthStore = create<AuthState>()(
currentOrg: org,
token: token || state.token,
})),
startImpersonation: (token, user, organizations) => {
const state = get();
set({
impersonationOriginal: {
token: state.token!,
user: state.user!,
organizations: state.organizations,
currentOrg: state.currentOrg,
},
token,
user,
organizations,
currentOrg: null,
});
},
stopImpersonation: () => {
const { impersonationOriginal } = get();
if (impersonationOriginal) {
set({
token: impersonationOriginal.token,
user: impersonationOriginal.user,
organizations: impersonationOriginal.organizations,
currentOrg: impersonationOriginal.currentOrg,
impersonationOriginal: null,
});
}
},
logout: () =>
set({
token: null,
user: null,
organizations: [],
currentOrg: null,
impersonationOriginal: null,
}),
}),
{
name: 'ledgeriq-auth',
version: 3,
version: 4,
migrate: () => ({
token: null,
user: null,
organizations: [],
currentOrg: null,
impersonationOriginal: null,
}),
},
),