fd38f5659f
Let super admins act as a user from the user detail dashboard for
support/debugging ("full with guardrails", audit-only).
Backend: AuthService.impersonate issues a short-lived token for the
target carrying an `act` claim (acting admin); refresh preserves it and
JwtStrategy surfaces `impersonatedBy`. Guardrails: cannot impersonate an
admin or a deactivated account; new ImpersonationGuard blocks sensitive
self-service (change own password/phone) while impersonating. New
AuditLog entity records impersonation start/stop (admin, target, ip,
time); admin endpoints POST users/:id/impersonate + .../impersonation/
stop and GET users/:id/audit.
Frontend: lib/impersonation swaps admin/impersonation tokens in
localStorage; persistent banner with exit; "Login as user" button and an
"Admin access log" tab on the detail page; logout clears impersonation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import { create } from 'zustand';
|
|
import api from '@/lib/api';
|
|
import { clearImpersonation } from '@/lib/impersonation';
|
|
import type { User, AuthResponse } from '@/types';
|
|
|
|
export interface RegisterData {
|
|
phone: string;
|
|
password: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
email?: string;
|
|
}
|
|
|
|
/** Either the user is fully authenticated, or a phone OTP is required next. */
|
|
export type AuthResult =
|
|
| { status: 'authenticated' }
|
|
| { status: 'verify'; phone: string };
|
|
|
|
interface VerificationRequired {
|
|
requiresVerification: true;
|
|
phone: string;
|
|
}
|
|
|
|
interface AuthState {
|
|
user: User | null;
|
|
isLoading: boolean;
|
|
isAuthenticated: boolean;
|
|
/** Password login by mobile. May require OTP verification. */
|
|
login: (phone: string, password: string) => Promise<AuthResult>;
|
|
/** Register by mobile — always returns a verify step. */
|
|
register: (data: RegisterData) => Promise<{ phone: string }>;
|
|
/** Send a one-time login code to a mobile number. */
|
|
requestOtp: (phone: string) => Promise<void>;
|
|
/** Verify a one-time code (completes registration or OTP login). */
|
|
verifyOtp: (phone: string, code: string) => Promise<void>;
|
|
logout: () => void;
|
|
loadUser: () => Promise<void>;
|
|
setUser: (user: User) => void;
|
|
}
|
|
|
|
function persistAuth(data: AuthResponse) {
|
|
localStorage.setItem('accessToken', data.accessToken);
|
|
localStorage.setItem('refreshToken', data.refreshToken);
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>((set) => ({
|
|
user: null,
|
|
isLoading: true,
|
|
isAuthenticated: false,
|
|
|
|
login: async (phone, password) => {
|
|
const { data } = await api.post<AuthResponse | VerificationRequired>(
|
|
'/auth/login',
|
|
{ phone, password },
|
|
);
|
|
if ('requiresVerification' in data) {
|
|
return { status: 'verify', phone: data.phone };
|
|
}
|
|
persistAuth(data);
|
|
set({ user: data.user, isAuthenticated: true });
|
|
return { status: 'authenticated' };
|
|
},
|
|
|
|
register: async (registerData) => {
|
|
const { data } = await api.post<VerificationRequired>('/auth/register', registerData);
|
|
return { phone: data.phone };
|
|
},
|
|
|
|
requestOtp: async (phone) => {
|
|
await api.post('/auth/otp/request', { phone });
|
|
},
|
|
|
|
verifyOtp: async (phone, code) => {
|
|
const { data } = await api.post<AuthResponse>('/auth/otp/verify', { phone, code });
|
|
persistAuth(data);
|
|
set({ user: data.user, isAuthenticated: true });
|
|
},
|
|
|
|
logout: () => {
|
|
localStorage.removeItem('accessToken');
|
|
localStorage.removeItem('refreshToken');
|
|
clearImpersonation();
|
|
set({ user: null, isAuthenticated: false });
|
|
},
|
|
|
|
loadUser: async () => {
|
|
try {
|
|
const token = localStorage.getItem('accessToken');
|
|
if (!token) {
|
|
set({ isLoading: false });
|
|
return;
|
|
}
|
|
const { data } = await api.get<User>('/users/me');
|
|
set({ user: data, isAuthenticated: true, isLoading: false });
|
|
} catch {
|
|
set({ user: null, isAuthenticated: false, isLoading: false });
|
|
}
|
|
},
|
|
|
|
setUser: (user) => set({ user }),
|
|
}));
|