Files
k-photos/crates/domain/src/identity/entities.rs
Gabriel Kaszewski 957737ac9b feat: frontend MVP — auth, timeline, upload, albums, admin, image viewer
Backend:
- user roles (DB + JWT + first-user-is-admin)
- volume-aware file resolver (multi-volume asset serving)
- directory scanner uses volume URI directly
- date-summary endpoint (capture date from EXIF)
- timeline ordered by capture date
- list endpoints: volumes, plugins, pipelines, library paths
- delete endpoints: volumes, library paths
- configurable upload body limit (MAX_UPLOAD_BYTES)

Frontend:
- auth: login/register, token refresh, role-based admin gate
- timeline: date-grouped grid, infinite scroll, date scrubber
- image viewer: fullscreen zoom/pan/pinch, metadata sidebar
- upload: drag-drop, sequential upload, progress tracking
- albums: create, add/remove photos, asset picker dialog
- admin: storage (import library), jobs (pagination, error details),
  plugins (list + toggle), pipelines, sidecars, duplicates
- multi-select mode with add-to-album action
- TanStack Query for all data fetching
2026-06-01 01:35:43 +02:00

229 lines
5.7 KiB
Rust

use crate::common::errors::DomainError;
use crate::common::value_objects::{DateTimeStamp, Email, PasswordHash, SystemId};
use chrono::{DateTime, Utc};
use std::collections::HashSet;
// --- Permission ---
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum PermissionAction {
ReadAsset,
ReadMetadata,
ReadLocation,
ReadPerson,
WriteMetadata,
DeleteAsset,
ManageAccess,
ManageUsers,
ManageSystem,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum ResourceType {
Asset,
Album,
Collection,
Person,
Directory,
Global,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Permission {
pub action: PermissionAction,
pub resource_type: ResourceType,
}
impl Permission {
pub fn new(action: PermissionAction, resource_type: ResourceType) -> Self {
Self {
action,
resource_type,
}
}
}
pub fn viewer_permissions() -> HashSet<Permission> {
HashSet::from([
Permission::new(PermissionAction::ReadAsset, ResourceType::Global),
Permission::new(PermissionAction::ReadMetadata, ResourceType::Global),
])
}
pub fn contributor_permissions() -> HashSet<Permission> {
let mut perms = viewer_permissions();
perms.insert(Permission::new(
PermissionAction::WriteMetadata,
ResourceType::Global,
));
perms
}
pub fn admin_permissions() -> HashSet<Permission> {
let mut perms = contributor_permissions();
perms.insert(Permission::new(
PermissionAction::DeleteAsset,
ResourceType::Global,
));
perms.insert(Permission::new(
PermissionAction::ManageAccess,
ResourceType::Global,
));
perms.insert(Permission::new(
PermissionAction::ManageUsers,
ResourceType::Global,
));
perms.insert(Permission::new(
PermissionAction::ManageSystem,
ResourceType::Global,
));
perms.insert(Permission::new(
PermissionAction::ReadLocation,
ResourceType::Global,
));
perms.insert(Permission::new(
PermissionAction::ReadPerson,
ResourceType::Global,
));
perms
}
// --- Role ---
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Role {
pub role_id: SystemId,
pub name: String,
pub permissions: HashSet<Permission>,
pub is_system_default: bool,
}
impl Role {
pub fn new(
name: impl Into<String>,
permissions: HashSet<Permission>,
is_system_default: bool,
) -> Self {
Self {
role_id: SystemId::new(),
name: name.into(),
permissions,
is_system_default,
}
}
pub fn has_permission(&self, action: PermissionAction, resource_type: ResourceType) -> bool {
self.permissions
.contains(&Permission::new(action, resource_type))
}
}
// --- User ---
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
pub id: SystemId,
pub username: String,
pub email: Email,
pub password_hash: PasswordHash,
pub role: String,
pub created_at: DateTime<Utc>,
}
impl User {
pub fn new(username: impl Into<String>, email: Email, password_hash: PasswordHash) -> Self {
Self {
id: SystemId::new(),
username: username.into(),
email,
password_hash,
role: "user".to_string(),
created_at: Utc::now(),
}
}
pub fn is_admin(&self) -> bool {
self.role == "admin"
}
}
// --- RefreshToken ---
#[derive(Debug, Clone)]
pub struct RefreshToken {
pub token_id: SystemId,
pub user_id: SystemId,
pub token_hash: String,
pub expires_at: DateTimeStamp,
pub revoked: bool,
pub created_at: DateTimeStamp,
}
impl RefreshToken {
pub fn new(user_id: SystemId, token_hash: String, expires_at: DateTimeStamp) -> Self {
Self {
token_id: SystemId::new(),
user_id,
token_hash,
expires_at,
revoked: false,
created_at: DateTimeStamp::now(),
}
}
pub fn is_valid(&self) -> bool {
!self.revoked && *self.expires_at.as_datetime() > Utc::now()
}
}
// --- Group ---
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Group {
pub group_id: SystemId,
pub name: String,
pub owner_user_id: SystemId,
pub members: HashSet<SystemId>,
}
impl Group {
pub fn new(name: impl Into<String>, owner_user_id: SystemId) -> Self {
let mut members = HashSet::new();
members.insert(owner_user_id);
Self {
group_id: SystemId::new(),
name: name.into(),
owner_user_id,
members,
}
}
pub fn add_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
if self.members.contains(&user_id) {
return Err(DomainError::Conflict(format!(
"User {user_id} is already a member"
)));
}
self.members.insert(user_id);
Ok(())
}
pub fn remove_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
if user_id == self.owner_user_id {
return Err(DomainError::Validation(
"Cannot remove the group owner".to_string(),
));
}
if !self.members.remove(&user_id) {
return Err(DomainError::NotFound(format!(
"User {user_id} is not a member"
)));
}
Ok(())
}
pub fn is_member(&self, user_id: &SystemId) -> bool {
self.members.contains(user_id)
}
}