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
229 lines
5.7 KiB
Rust
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)
|
|
}
|
|
}
|