Roles & Permissions
Role System
Dział zatytułowany „Role System”Vista implementuje Role-Based Access Control (RBAC) z czterema głównymi rolami.
Dostępne role
Dział zatytułowany „Dostępne role”| Rola | Opis | Typowe użycie |
|---|---|---|
| admin | Pełny dostęp do systemu | Właściciel kliniki, IT |
| vet | Lekarz weterynarii | Lekarze prowadzący wizyty |
| assistant | Asystent weterynaryjny | Technicy, asystenci |
| viewer | Tylko odczyt | Recepcja, praktykanci |
Permission Matrix
Dział zatytułowany „Permission Matrix”Zasoby i operacje
Dział zatytułowany „Zasoby i operacje”| Resource | Admin | Vet | Assistant | Viewer |
|---|---|---|---|---|
| Users Management | ✅ CRUD | ❌ | ❌ | ❌ |
| Patients - Create | ✅ | ✅ | ✅ | ❌ |
| Patients - Read | ✅ | ✅ | ✅ | ✅ |
| Patients - Update | ✅ | ✅ | ✅ | ❌ |
| Patients - Delete | ✅ | ✅ | ❌ | ❌ |
| Visits - Create | ✅ | ✅ | ❌ | ❌ |
| Visits - Read Own | ✅ | ✅ | ✅ | ✅ |
| Visits - Read All | ✅ | ❌ | ❌ | ❌ |
| Visits - Update | ✅ | ✅ (own) | ❌ | ❌ |
| Visits - Delete | ✅ | ✅ (own) | ❌ | ❌ |
| Appointments | ✅ All | ✅ Own | ✅ View | ✅ View |
| Settings - Clinic | ✅ | ❌ | ❌ | ❌ |
| Settings - Personal | ✅ | ✅ | ✅ | ✅ |
| Reports - All | ✅ | ❌ | ❌ | ❌ |
| Reports - Own | ✅ | ✅ | ✅ | ❌ |
| Audit Logs | ✅ Read | ❌ | ❌ | ❌ |
| AI Features | ✅ | ✅ | ✅ | ❌ |
Database Schema
Dział zatytułowany „Database Schema”-- Roles stored as JSON array in users tableCREATE TABLE users ( user_id TEXT PRIMARY KEY, -- ... roles TEXT NOT NULL, -- JSON: ["admin", "vet"] -- ...);
-- Example values:-- '["admin"]' - Administrator only-- '["vet"]' - Veterinarian only-- '["admin", "vet"]' - Admin who is also a vet-- '["assistant"]' - Assistant-- '["viewer"]' - Read-only accessRole Checking
Dział zatytułowany „Role Checking”Backend (Rust)
Dział zatytułowany „Backend (Rust)”pub fn has_role(user: &User, role: &str) -> bool { let roles: Vec<String> = serde_json::from_str(&user.roles) .unwrap_or_default(); roles.contains(&role.to_string())}
pub fn has_any_role(user: &User, required_roles: &[&str]) -> bool { let roles: Vec<String> = serde_json::from_str(&user.roles) .unwrap_or_default(); required_roles.iter().any(|r| roles.contains(&r.to_string()))}
pub fn require_role(user: &User, role: &str) -> Result<(), String> { if has_role(user, role) { Ok(()) } else { Err(format!("Access denied: requires {} role", role)) }}
// Usage in command#[tauri::command]pub async fn delete_user( db: State<'_, Database>, current_user: User, user_id_to_delete: String,) -> Result<(), String> { require_role(¤t_user, "admin")?;
// ... proceed with deletion}Frontend (TypeScript)
Dział zatytułowany „Frontend (TypeScript)”export const hasRole = (user: User | null, role: string): boolean => { if (!user) return false; const roles = JSON.parse(user.roles) as string[]; return roles.includes(role);};
export const hasAnyRole = (user: User | null, roles: string[]): boolean => { if (!user) return false; const userRoles = JSON.parse(user.roles) as string[]; return roles.some(role => userRoles.includes(role));};
export const canAccessResource = ( user: User | null, resource: string, action: 'create' | 'read' | 'update' | 'delete'): boolean => { if (!user) return false;
const permissions = getPermissionsForRoles(user.roles); return permissions[resource]?.[action] ?? false;};
// Usage in componentconst PatientDeleteButton = ({ patient }) => { const { user } = useAuth();
if (!canAccessResource(user, 'patients', 'delete')) { return null; }
return <Button onClick={() => deletePatient(patient.id)}>Delete</Button>;};Permission Guards
Dział zatytułowany „Permission Guards”Route Guard
Dział zatytułowany „Route Guard”interface PermissionGuardProps { roles?: string[]; resource?: string; action?: 'create' | 'read' | 'update' | 'delete'; fallback?: React.ReactNode; children: React.ReactNode;}
export const PermissionGuard: React.FC<PermissionGuardProps> = ({ roles, resource, action, fallback = null, children,}) => { const { user } = useAuth();
// Check role-based access if (roles && !hasAnyRole(user, roles)) { return <>{fallback}</>; }
// Check resource-based access if (resource && action && !canAccessResource(user, resource, action)) { return <>{fallback}</>; }
return <>{children}</>;};
// Usage<PermissionGuard roles={['admin', 'vet']}> <DeletePatientButton /></PermissionGuard>
<PermissionGuard resource="visits" action="create"> <NewVisitButton /></PermissionGuard>Visit Ownership
Dział zatytułowany „Visit Ownership”Own vs Shared Visits
Dział zatytułowany „Own vs Shared Visits”// Veterinarian can only edit their own visitsconst canEditVisit = (user: User, visit: Visit): boolean => { // Admin can edit all if (hasRole(user, 'admin')) return true;
// Vet can edit own visits if (hasRole(user, 'vet') && visit.user_id === user.user_id) return true;
// Check if visit is shared with user const isShared = await checkVisitShare(visit.visit_id, user.user_id); if (isShared && isShared.permissions.includes('edit')) return true;
return false;};Visit Sharing
Dział zatytułowany „Visit Sharing”CREATE TABLE visit_shares ( share_id TEXT PRIMARY KEY, visit_id TEXT NOT NULL REFERENCES visits(visit_id) ON DELETE CASCADE, shared_by TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, shared_with TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE, permissions TEXT NOT NULL, -- JSON: ["read", "edit", "comment"] created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, -- Optional expiration UNIQUE(visit_id, shared_with));Role Management
Dział zatytułowany „Role Management”Assigning Roles (Admin only)
Dział zatytułowany „Assigning Roles (Admin only)”#[tauri::command]pub async fn update_user_roles( db: State<'_, Database>, current_user: User, user_id: String, new_roles: Vec<String>,) -> Result<User, String> { // Only admin can change roles require_role(¤t_user, "admin")?;
// Validate roles let valid_roles = ["admin", "vet", "assistant", "viewer"]; for role in &new_roles { if !valid_roles.contains(&role.as_str()) { return Err(format!("Invalid role: {}", role)); } }
// Prevent removing last admin if user_id == current_user.user_id && !new_roles.contains(&"admin".to_string()) { let admin_count = count_admins(&db).await?; if admin_count <= 1 { return Err("Cannot remove the last admin".to_string()); } }
// Update roles let roles_json = serde_json::to_string(&new_roles)?; sqlx::query!( "UPDATE users SET roles = ? WHERE user_id = ?", roles_json, user_id ).execute(&db.pool).await?;
get_user(&db, &user_id).await}Audit Trail Integration
Dział zatytułowany „Audit Trail Integration”Wszystkie zmiany uprawnień są logowane:
INSERT INTO audit_trail ( audit_id, user_id, user_name, action, resource_type, resource_id, changes) VALUES ( ?, ?, -- admin who made the change ?, 'permission_change', 'user', ?, -- affected user ? -- JSON: {"old_roles": ["vet"], "new_roles": ["vet", "admin"]});