- Implemented SidebarStaff component tests to ensure staff users see only their dashboard and services. - Created StaffMyServicesPage to display assigned services for staff users. - Added migration to link clinic staff rows to user accounts for ROLE_STAFF access. - Defined StaffPermissions class for static permissions related to staff role. - Introduced StaffRouteGuardSubscriber to restrict API access for staff users. - Developed StaffAccountService for managing staff user accounts and linking them to clinic staff. - Added comprehensive tests for StaffAccountService to validate user creation, mobile number handling, and account attachment. - Implemented tests for staff dashboard access to ensure proper permissions and access control. - Created tests for staff login context to verify correct environment visibility based on user roles.
176 lines
5.0 KiB
TypeScript
176 lines
5.0 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
export interface ContextItem {
|
|
type: 'doctor' | 'clinic';
|
|
db_uuid: string;
|
|
name: string;
|
|
role: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'staff' | 'representation' | 'user';
|
|
scope?: string | null;
|
|
doctor_uuid?: string;
|
|
permissions?: Record<string, any>;
|
|
}
|
|
|
|
interface AuthState {
|
|
token: string | null;
|
|
refreshToken: string | null;
|
|
isAuthenticated: boolean;
|
|
userUuid: string | null;
|
|
userName: string | null;
|
|
primaryRole: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'staff' | 'representation' | 'user' | null;
|
|
dbUuid: string | null;
|
|
dbKey: string | null;
|
|
doctorUuid: string | null;
|
|
context: ContextItem | null;
|
|
availableContexts: ContextItem[];
|
|
|
|
login: (token: string, refreshToken?: string | null) => void;
|
|
logout: () => void;
|
|
refresh: () => Promise<string | null>;
|
|
fetchMe: () => Promise<void>;
|
|
switchContext: (dbUuid: string) => Promise<void>;
|
|
}
|
|
|
|
function getToken(): string | null {
|
|
try {
|
|
const raw = localStorage.getItem('clinicpro-auth');
|
|
if (!raw) return null;
|
|
return JSON.parse(raw)?.state?.token ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function apiFetch(path: string, options: RequestInit = {}) {
|
|
const token = getToken();
|
|
const res = await fetch(path, {
|
|
...options,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...options.headers,
|
|
},
|
|
});
|
|
return res.json();
|
|
}
|
|
|
|
export const useAuthStore = create<AuthState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
token: null,
|
|
refreshToken: null,
|
|
isAuthenticated: false,
|
|
userUuid: null,
|
|
userName: null,
|
|
primaryRole: null,
|
|
dbUuid: null,
|
|
dbKey: null,
|
|
doctorUuid: null,
|
|
context: null,
|
|
availableContexts: [],
|
|
|
|
login: (token, refreshToken = null) => {
|
|
set({ token, refreshToken, isAuthenticated: true });
|
|
get().fetchMe();
|
|
},
|
|
|
|
logout: () => {
|
|
const rt = get().refreshToken;
|
|
if (rt) {
|
|
apiFetch('/oauth/logout', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ refresh_token: rt }),
|
|
}).catch(() => {});
|
|
}
|
|
set({
|
|
token: null,
|
|
refreshToken: null,
|
|
isAuthenticated: false,
|
|
userUuid: null,
|
|
userName: null,
|
|
primaryRole: null,
|
|
dbUuid: null,
|
|
dbKey: null,
|
|
doctorUuid: null,
|
|
context: null,
|
|
availableContexts: [],
|
|
});
|
|
},
|
|
|
|
refresh: async () => {
|
|
const rt = get().refreshToken;
|
|
if (!rt) return null;
|
|
try {
|
|
const res = await fetch('/oauth/token/refresh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: rt }),
|
|
});
|
|
const json = await res.json();
|
|
if (!res.ok || !json?.access_token) return null;
|
|
set({
|
|
token: json.access_token,
|
|
refreshToken: json.refresh_token ?? rt,
|
|
isAuthenticated: true,
|
|
});
|
|
return json.access_token as string;
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
fetchMe: async () => {
|
|
try {
|
|
const res = await apiFetch('/oauth/userinfo');
|
|
if (!res.success) return;
|
|
const d = res.data;
|
|
set({
|
|
userUuid: d.uuid,
|
|
userName: d.realName,
|
|
primaryRole: d.primary_role ?? null,
|
|
dbUuid: d.db_uuid ?? null,
|
|
dbKey: d.db_key ?? null,
|
|
doctorUuid: d.doctor_uuid ?? null,
|
|
context: d.context ?? null,
|
|
availableContexts: d.available_contexts ?? [],
|
|
});
|
|
} catch {
|
|
// شبکه در دسترس نیست — state دستنخورده بماند
|
|
}
|
|
},
|
|
|
|
switchContext: async (dbUuid: string) => {
|
|
const res = await apiFetch('/api/v1/auth/switch-context', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ db_uuid: dbUuid }),
|
|
});
|
|
if (res.success) {
|
|
set({
|
|
dbUuid: res.data.db_uuid,
|
|
dbKey: res.data.db_key,
|
|
doctorUuid: res.data.context?.doctor_uuid ?? get().doctorUuid,
|
|
context: res.data.context,
|
|
primaryRole: res.data.context?.role ?? null,
|
|
});
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
name: 'clinicpro-auth',
|
|
partialize: (s) => ({
|
|
token: s.token,
|
|
refreshToken: s.refreshToken,
|
|
isAuthenticated: s.isAuthenticated,
|
|
userUuid: s.userUuid,
|
|
userName: s.userName,
|
|
primaryRole: s.primaryRole,
|
|
dbUuid: s.dbUuid,
|
|
dbKey: s.dbKey,
|
|
doctorUuid: s.doctorUuid,
|
|
context: s.context,
|
|
availableContexts: s.availableContexts,
|
|
}),
|
|
}
|
|
)
|
|
);
|