Przejdź do głównej zawartości

Authentication Flow

graph TB
subgraph "Authentication Layers"
BIOMETRIC["🔐 Biometric Auth<br/>Touch ID / Face ID"]
PASSWORD["🔑 Password Auth<br/>bcrypt + salt"]
SESSION["🎫 Session Tokens<br/>JWT with refresh"]
API_KEY["🗝️ API Key Storage<br/>System Keychain"]
end
subgraph "Authorization Layers"
ROLES["👥 Role-Based Access<br/>admin/vet/assistant/viewer"]
PERMISSIONS["📋 Resource Permissions<br/>read/write/delete/share"]
AUDIT["📝 Audit Trail<br/>All actions logged"]
end
BIOMETRIC --> SESSION
PASSWORD --> SESSION
SESSION --> ROLES
ROLES --> PERMISSIONS
PERMISSIONS --> AUDIT
API_KEY --> SESSION

const authMethods = {
password: 'standard_login', // Email + password
biometric: 'biometric_login', // Touch/Face ID
invitation: 'invitation_login', // Team invitation token
demo: 'demo_login' // Demo mode access
};
interface LoginRequest {
email: string;
password: string;
useBiometric?: boolean;
}
interface LoginResponse {
user: User;
session_id: string;
expires_at: string; // ISO timestamp
api_key?: string; // For AI services
refresh_token?: string; // For session renewal
}

sequenceDiagram
participant UI as Frontend
participant Auth as AuthContext
participant BE as Tauri Backend
participant DB as SQLite
participant KC as Keychain
UI->>Auth: login(email, password)
Auth->>BE: invoke('login', {email, password})
BE->>DB: SELECT user WHERE email = ?
DB-->>BE: User record
BE->>BE: bcrypt.verify(password, hash)
alt Password Valid
BE->>DB: INSERT session
BE->>KC: Store API key (if needed)
BE-->>Auth: LoginResponse
Auth->>Auth: setUser(user)
Auth-->>UI: Success
else Password Invalid
BE-->>Auth: Error("Invalid credentials")
Auth-->>UI: Show error
end

Vista używa bcrypt z salt do hashowania haseł.

src-tauri/src/commands/auth.rs
use bcrypt::{hash, verify, DEFAULT_COST};
pub fn hash_password(password: &str) -> Result<String, Error> {
hash(password, DEFAULT_COST)
.map_err(|e| Error::Auth(format!("Hashing failed: {}", e)))
}
pub fn verify_password(password: &str, hash: &str) -> Result<bool, Error> {
verify(password, hash)
.map_err(|e| Error::Auth(format!("Verification failed: {}", e)))
}

pub struct Session {
pub session_id: String, // UUID v4
pub user_id: String,
pub token: String, // Random 32-byte hex
pub created_at: DateTime,
pub expires_at: DateTime, // +24h default
pub last_activity: DateTime,
}
stateDiagram-v2
[*] --> Created: login()
Created --> Active: Token validated
Active --> Active: refresh_session()
Active --> Expired: expires_at passed
Active --> Terminated: logout()
Expired --> [*]
Terminated --> [*]
#[tauri::command]
pub async fn get_active_session(
db: State<'_, Database>,
) -> Result<Option<Session>, String>
#[tauri::command]
pub async fn refresh_session(
db: State<'_, Database>,
session_id: String,
) -> Result<Session, String>
#[tauri::command]
pub async fn logout(
db: State<'_, Database>,
session_id: String,
) -> Result<(), String>

PlatformStorageAPI
macOSKeychainsecurity-framework
WindowsCredential Storekeyring-rs
LinuxSecret Servicekeyring-rs
use security_framework::os::macos::keychain::SecKeychain;
pub fn store_api_key(service: &str, key: &str) -> Result<(), Error> {
let keychain = SecKeychain::default()?;
keychain.set_generic_password(service, "api_key", key.as_bytes())?;
Ok(())
}
pub fn get_api_key(service: &str) -> Result<String, Error> {
let keychain = SecKeychain::default()?;
let (password, _) = keychain.find_generic_password(service, "api_key")?;
Ok(String::from_utf8(password.to_vec())?)
}

sequenceDiagram
participant U as User
participant FE as Frontend
participant BE as Backend
participant DB as Database
U->>FE: Request password reset
FE->>BE: request_password_reset(email)
BE->>DB: INSERT password_reset_token
BE-->>FE: Token created (expires 1h)
Note over U: User receives token (display in app)
U->>FE: Enter new password + token
FE->>BE: reset_password(token, new_password)
BE->>DB: Verify token not expired
BE->>BE: bcrypt.hash(new_password)
BE->>DB: UPDATE user SET password_hash
BE->>DB: DELETE password_reset_token
BE-->>FE: Success
CREATE TABLE password_reset_tokens (
token_id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(user_id),
token TEXT NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL, -- +1 hour
used_at TIMESTAMP -- NULL until used
);

#[tauri::command]
pub async fn change_password(
db: State<'_, Database>,
user_id: String,
current_password: String,
new_password: String,
) -> Result<(), String> {
// 1. Get current hash
let user = get_user(&db, &user_id).await?;
// 2. Verify current password
if !verify_password(&current_password, &user.password_hash)? {
return Err("Current password is incorrect".to_string());
}
// 3. Hash new password
let new_hash = hash_password(&new_password)?;
// 4. Update in database
sqlx::query!(
"UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?",
new_hash,
user_id
).execute(&db.pool).await?;
// 5. Invalidate all sessions (force re-login)
sqlx::query!(
"DELETE FROM user_sessions WHERE user_id = ?",
user_id
).execute(&db.pool).await?;
Ok(())
}

FieldRules
EmailValid format, unique
PasswordMin 8 chars, complexity rules (TODO)
Session token32 random bytes, hex encoded
Reset tokenUUID v4, 1h expiry