Przejdź do głównej zawartości

Roles & Permissions

Vista implementuje Role-Based Access Control (RBAC) z czterema głównymi rolami.

RolaOpisTypowe użycie
adminPełny dostęp do systemuWłaściciel kliniki, IT
vetLekarz weterynariiLekarze prowadzący wizyty
assistantAsystent weterynaryjnyTechnicy, asystenci
viewerTylko odczytRecepcja, praktykanci

ResourceAdminVetAssistantViewer
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

-- Roles stored as JSON array in users table
CREATE 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 access

src-tauri/src/utils/auth.rs
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(&current_user, "admin")?;
// ... proceed with deletion
}
src/utils/permissions.ts
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 component
const PatientDeleteButton = ({ patient }) => {
const { user } = useAuth();
if (!canAccessResource(user, 'patients', 'delete')) {
return null;
}
return <Button onClick={() => deletePatient(patient.id)}>Delete</Button>;
};

src/components/auth/PermissionGuard.tsx
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>

// Veterinarian can only edit their own visits
const 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;
};
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)
);

#[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(&current_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
}

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"]}
);