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:
@@ -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,
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user