Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
6dcf854
fix(files): single close button in preview; floating bulk action bar
AperturePlus May 12, 2026
571b74d
fix(shell): wrap /agent in MainLayout; silent refresh removes table f…
AperturePlus May 12, 2026
6f3a997
feat(p4): migrate Shared / Trash / ShareAccess to industrial dashboard
AperturePlus May 12, 2026
d1cda74
chore(p5): align trash fixtures with new RecycleBinItem fields
AperturePlus May 12, 2026
15b20b4
feat(organisms/auth): add AuthForm (login/register/forgot modes)
AperturePlus May 12, 2026
9df5955
feat(dev/library): add Organisms · Auth section
AperturePlus May 12, 2026
562fe16
refactor(pages/login): rewrite Login against AuthForm (100 lines)
AperturePlus May 12, 2026
7016940
refactor(pages/register): rewrite Register against AuthForm (56 lines)
AperturePlus May 12, 2026
71149f7
refactor(pages/forgot-password): rewrite against AuthForm (47 lines)
AperturePlus May 12, 2026
64c07fd
refactor(pages/verify-email): rewrite against atoms+molecules (94 lines)
AperturePlus May 12, 2026
d75caa5
feat(i18n): localize file move and delete flows
AperturePlus May 12, 2026
6451832
feat(share): localize share access and settings surfaces
AperturePlus May 12, 2026
c034845
feat(sharing): localize shared items center
AperturePlus May 12, 2026
e1e27f8
feat(trash): localize recycle bin surfaces
AperturePlus May 12, 2026
bed9aaa
refactor(layout): simplify footer links
AperturePlus May 12, 2026
4344078
refactor(app): migrate backend package layout to src/fileflash
AperturePlus May 12, 2026
108fb75
fix(app): guard startup with schema check and add avatar migration
AperturePlus May 12, 2026
a099093
test(app): migrate pytest imports to fileflash package paths
AperturePlus May 12, 2026
d531174
feat(app): make worker launch configurable
AperturePlus May 12, 2026
18aba5a
fix(app): backfill missing job IDs in queued jobs
AperturePlus May 12, 2026
eaaf277
feat(app): wrap worker task errors for pickle transport
AperturePlus May 12, 2026
a86a106
feat(web): route archive files to extract flow
AperturePlus May 12, 2026
f095787
feat(web): modernize archive extraction folder picker
AperturePlus May 12, 2026
2bdff97
test(web): mock archive picker folder contents
AperturePlus May 12, 2026
edbe4dd
docs(superpowers): add P5 public auth flow plan
AperturePlus May 12, 2026
b61b3f5
feat(upload): add background merge jobs and transcode pipeline
AperturePlus May 13, 2026
f954eab
feat(media): prefer optimized objects for shares and listings
AperturePlus May 13, 2026
e165ab9
feat(web): surface media optimization and merge job status
AperturePlus May 13, 2026
70346ca
feat(i18n): add settings locale strings
AperturePlus May 13, 2026
6be06cb
refactor(settings): localize settings page copy
AperturePlus May 13, 2026
844a8de
feat(files): improve new-folder rename flow
AperturePlus May 13, 2026
43a15d5
test(files): cover new-folder rename flow
AperturePlus May 13, 2026
b373d61
fix(sharing): limit received permission labels
AperturePlus May 13, 2026
a4660fa
fix: json-encode upload merge and worker results
AperturePlus May 13, 2026
37547e9
agents.md
AperturePlus May 13, 2026
066183f
feat(star): add starred items limit with config, service enforcement,…
AperturePlus May 15, 2026
b3289b9
feat(star): order starred list by most recently favorited and add sta…
AperturePlus May 15, 2026
ffb4ee3
feat(mock): enforce starred items limit and sort by starredAt
AperturePlus May 15, 2026
9f6f196
feat(web): show starred items in left sidebar with path labels
AperturePlus May 15, 2026
d9a7ff4
feat(web): show star error toast and refresh sidebar on star toggle
AperturePlus May 15, 2026
250ba95
chore: remove unused library-tokens PNG assets
AperturePlus May 15, 2026
d95b558
test(web): add LeftSidebar component spec
AperturePlus May 15, 2026
2ca40d1
test(star): add starred_items_limit setting tests
AperturePlus May 15, 2026
c472967
feat(web): overwrite web folder with ky branch changes
Yukino4141 May 20, 2026
477c963
Merge pull request #15 from Yukino4141/develop
Yukino4141 May 20, 2026
2a7479d
feat(config): enhance security with HMAC token hashing and runtime va…
AperturePlus May 20, 2026
4ffaa95
feat(email): add registration email domain rules and SMTP delivery se…
AperturePlus May 20, 2026
cd91bf0
feat(auth): integrate email delivery and domain rules into auth service
AperturePlus May 20, 2026
20a4f73
feat(web): add registration email domain rule types and API layer
AperturePlus May 20, 2026
709ae93
feat(web): add mock handlers for registration email domain rules
AperturePlus May 20, 2026
c4d7dd2
feat(web): add domain rule UI to dashboard and improve email verifica…
AperturePlus May 20, 2026
9f5c3bc
Merge branch 'develop' of https://github.com/AperturePlus/fileflash i…
AperturePlus May 20, 2026
5ca96a2
docs(spec): P7 agent track redesign — Workspace + Skills
AperturePlus May 20, 2026
58a3a88
feat(web): add back-to-home topbar and agent-specific clauses to lega…
AperturePlus May 20, 2026
4a6043c
docs(plan): P7 agent track implementation plan
AperturePlus May 20, 2026
836032b
feat(web): add Modal molecule
AperturePlus May 20, 2026
e575d9a
feat(web): add Pagination molecule
AperturePlus May 20, 2026
5be3825
feat(web): add FileDrop molecule
AperturePlus May 20, 2026
d42ccb1
feat(web): add Select molecule
AperturePlus May 20, 2026
ecead33
feat(web): add useAgentSession composable with localStorage persistence
AperturePlus May 20, 2026
d871182
feat(web): add useAgentSkills composable
AperturePlus May 20, 2026
2c0aa28
feat(web): add SessionList + SessionItem agent organisms
AperturePlus May 20, 2026
ac88d2e
feat(web): add PlanActionRow + TurnEntry agent organisms
AperturePlus May 20, 2026
e81b4e5
feat(web): add TaskTimeline + TaskInputDock agent organisms
AperturePlus May 20, 2026
219e164
feat(web): add PlanInspector agent organism
AperturePlus May 20, 2026
bd05e42
feat(web): add Skill organism trio (Card + EditorPanel + ImportPanel)
AperturePlus May 20, 2026
4b24dfc
feat(web): rewrite AgentLayout with SegmentedControl tabs
AperturePlus May 20, 2026
1a5e647
feat(web): rewrite AgentWorkspace as three-column dashboard
AperturePlus May 20, 2026
0792ef7
feat(web): rewrite AgentSkills with new molecules + organisms
AperturePlus May 20, 2026
27e7389
chore(web): register p7 molecules and agent organisms in dev library
AperturePlus May 20, 2026
729a8bf
fix: session creation
AperturePlus May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
- 前端类型定义: `web/src/types`
- 前端 mock: `web/src/mock/handlers` + `web/src/mock/state.ts`
- 前端鉴权状态: `web/src/store/user.ts`
- 后端入口: `app/src/main.py`
- 后端路由: `app/src/routers`
- 后端服务层: `app/src/services`
- 后端依赖/鉴权: `app/src/core/deps.py`
- 后端 schema: `app/src/schemas`
- 后端模型: `app/src/models/tables_*.py`
- 数据库会话: `app/src/db`
- 后端入口: `app/src/fileflash/main.py`
- 后端路由: `app/src/fileflash/routers`
- 后端服务层: `app/src/fileflash/services`
- 后端依赖/鉴权: `app/src/fileflash/core/deps.py`
- 后端 schema: `app/src/fileflash/schemas`
- 后端模型: `app/src/fileflash/models/tables_*.py`
- 数据库会话: `app/src/fileflash/db`

## 3. 全局接口契约

Expand Down Expand Up @@ -75,7 +75,7 @@
2. `web/src/api`
3. `web/src/mock/handlers` + `web/src/mock/state.ts`
4. 对应页面/store
5. 后端 `app/src/schemas` + `app/src/routers/services`
5. 后端 `app/src/fileflash/schemas` + `app/src/fileflash/routers/services`
- 鉴权状态规则:
- 仅持久化 `accessToken`(当前策略)
- 刷新流程依赖 Cookie(`axios.withCredentials = true`)
Expand Down Expand Up @@ -103,7 +103,7 @@
- 前端类型检查: `bun run check`(`web` 目录)
- 前端构建: `bun run build`(`web` 目录)
- 后端测试: `uv run pytest`(`app` 目录)
- 后端启动冒烟: `uv run python -c "from src.main import app; print(app.title)"`(`app` 目录)
- 后端启动冒烟: `uv run python -c "from fileflash.main import app; print(app.title)"`(`app` 目录)

## 10. 安全与配置要求

Expand Down
24 changes: 18 additions & 6 deletions app/src/.env.example → app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ FF_DB_URI=postgresql://root:password@localhost:5432/fileflash
# DATABASE_URL=postgresql://root:password@localhost:5432/fileflash
APP_ENV=development

JWT_SECRET_KEY=please-change-me
JWT_SECRET_KEY=please-set-at-least-32-bytes-secret-key
# Optional dedicated HMAC key for token hash persistence.
# Falls back to JWT_SECRET_KEY when omitted.
# TOKEN_HASH_SECRET=please-set-at-least-32-bytes-and-different-from-jwt-secret
ACCESS_TOKEN_EXPIRE_MINUTES=4320
REFRESH_TOKEN_EXPIRE_DAYS=7

Expand All @@ -23,12 +26,14 @@ UPLOAD_CHUNK_SIZE_DEFAULT=5242880
UPLOAD_CHUNK_SIZE_MIN=1048576
UPLOAD_CHUNK_SIZE_MAX=16777216
UPLOAD_SINGLE_FILE_SIZE_MAX=5368709120
STARRED_ITEMS_LIMIT=20
UPLOAD_SESSION_TTL_HOURS=24
UPLOAD_TEMP_PREFIX=tmp
UPLOAD_OBJECT_PREFIX=objects

WORKER_POLL_INTERVAL_SECONDS=2
WORKER_CONCURRENCY=2
WORKER_PROCESS_COUNT=1
WORKER_TASK_TIMEOUT_SECONDS=900
WORKER_DEFAULT_MAX_ATTEMPTS=5
WORKER_RETRY_BACKOFF_SECONDS=30,120,600,1800,7200
Expand Down Expand Up @@ -60,8 +65,15 @@ AGENT_MCP_ENDPOINTS=[]
# FFPROBE_BINARY=ffprobe

# Optional SMTP settings for real email delivery.
# MAIL_FROM=no-reply@example.com
# MAIL_USERNAME=
# MAIL_PASSWORD=
# MAIL_SERVER=
# MAIL_PORT=587
# In development, EMAIL_VERIFY_BASE_URL defaults to http://localhost:8080 when empty.
# EMAIL_VERIFY_BASE_URL=http://localhost:5173
# For providers like 163, MAIL_FROM should match MAIL_USERNAME.
# MAIL_FROM=your-account@example.com
# MAIL_SERVER=smtp.example.com
# MAIL_PORT=465
# MAIL_USERNAME=your-account@example.com
# MAIL_PASSWORD=replace-with-app-password
# MAIL_STARTTLS=false
# MAIL_SSL_TLS=true
# MAIL_USE_CREDENTIALS=true
# MAIL_VALIDATE_CERTS=true
36 changes: 36 additions & 0 deletions app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## Run Backend (API + Workers)

Use one command to start backend API and file workers together:

```bash
uv run python -m fileflash.scripts.run_with_workers
```

Common options:

```bash
# custom host/port
uv run python -m fileflash.scripts.run_with_workers --host 127.0.0.1 --port 8080

# start multiple worker processes
uv run python -m fileflash.scripts.run_with_workers --worker-count 2

# API only (without workers)
uv run python -m fileflash.scripts.run_with_workers --no-worker
```

Notes:
- This runner starts `uvicorn fileflash.main:app` and `python -m fileflash.workers.consumer`.
- If any subprocess exits, the runner stops all other subprocesses.
- If your environment resolves project scripts correctly, `uv run fileflash-dev` is equivalent.

## Database Migration Requirement

Before starting API processes, ensure Flyway migrations are fully applied (including `V10__identity_avatar.sql` and later).

Recommended startup order:
1. Start PostgreSQL
2. Run Flyway migrate
3. Start API (`uv run fileflash`) or runner (`uv run fileflash-dev`)

If the schema is outdated, API startup will fail fast with an explicit compatibility error.
10 changes: 9 additions & 1 deletion app/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ dependencies = [
"python-multipart>=0.0.24",
]
[project.scripts]
fileflash = "main:main"
fileflash = "fileflash.main:main"
fileflash-dev = "fileflash.scripts.run_with_workers:main"

[dependency-groups]
dev = [
Expand All @@ -43,3 +44,10 @@ target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/fileflash"]
19 changes: 0 additions & 19 deletions app/src/db/engine.py

This file was deleted.

File renamed without changes.
File renamed without changes.
File renamed without changes.
25 changes: 22 additions & 3 deletions app/src/core/deps.py → app/src/fileflash/core/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
from ..services.agent import ExecuteService, McpService, MemoryService, PlanService, SessionService, SettingsService, SkillService
from ..services.auth import AuthService
from ..services.background_jobs import BackgroundJobService
from ..services.email_delivery import VerificationEmailDeliveryService
from ..services.file import FileService
from ..services.folder import FolderService
from ..services.job_queue import RedisStreamJobQueue
from ..services.messaging import InProcessAuthEventPublisher
from ..services.rate_limiter import RedisRateLimiter
from ..services.registration_email_domain_rule import RegistrationEmailDomainRuleService
from ..services.share import ShareService
from ..services.upload import UploadService
from ..s3 import MinioObjectStorageClient
Expand Down Expand Up @@ -108,15 +110,23 @@ def get_auth_service(
settings=settings,
rate_limiter=rate_limiter,
event_publisher=event_publisher,
verification_email_delivery=VerificationEmailDeliveryService(settings=settings),
)


def get_registration_email_domain_rule_service(
db: AsyncSession = Depends(get_db),
) -> RegistrationEmailDomainRuleService:
return RegistrationEmailDomainRuleService(db=db)


def get_upload_service(
db: AsyncSession = Depends(get_db),
settings: Settings = Depends(get_settings_dep),
storage: MinioObjectStorageClient = Depends(get_object_storage),
jobs: BackgroundJobService = Depends(get_background_job_service),
) -> UploadService:
return UploadService(db=db, settings=settings, storage=storage)
return UploadService(db=db, settings=settings, storage=storage, jobs=jobs)


def get_share_service(
Expand All @@ -138,14 +148,23 @@ def get_archive_service(
def get_file_service(
db: AsyncSession = Depends(get_db),
storage: MinioObjectStorageClient = Depends(get_object_storage),
settings: Settings = Depends(get_settings_dep),
) -> FileService:
return FileService(db=db, storage=storage)
return FileService(
db=db,
storage=storage,
starred_items_limit=settings.starred_items_limit,
)


def get_folder_service(
db: AsyncSession = Depends(get_db),
settings: Settings = Depends(get_settings_dep),
) -> FolderService:
return FolderService(db=db)
return FolderService(
db=db,
starred_items_limit=settings.starred_items_limit,
)



Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import hashlib
import hmac
import secrets
import uuid
from datetime import UTC, datetime, timedelta
Expand All @@ -26,8 +27,9 @@ def create_refresh_token() -> str:
return secrets.token_urlsafe(48)


def hash_token(token: str) -> str:
return hashlib.sha256(token.encode("utf-8")).hexdigest()
def hash_token(token: str, settings: Settings) -> str:
secret = settings.effective_token_hash_secret.encode("utf-8")
return hmac.new(secret, token.encode("utf-8"), hashlib.sha256).hexdigest()


def create_access_token(user_id: int, settings: Settings) -> str:
Expand Down
81 changes: 80 additions & 1 deletion app/src/core/settings.py → app/src/fileflash/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from functools import lru_cache
from os import cpu_count
from pathlib import Path
from typing import ClassVar
from urllib.parse import urlsplit

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
Expand All @@ -14,8 +16,10 @@ def _default_worker_concurrency() -> int:


class Settings(BaseSettings):
MIN_SECRET_LENGTH: ClassVar[int] = 32

model_config = SettingsConfigDict(
env_file=str(Path(__file__).resolve().parents[1] / ".env"),
env_file=str(Path(__file__).resolve().parents[3] / ".env"),
env_file_encoding="utf-8",
extra="ignore",
)
Expand All @@ -31,6 +35,7 @@ class Settings(BaseSettings):
default="change-this-in-production-please-use-32-plus-bytes",
alias="JWT_SECRET_KEY",
)
token_hash_secret: str | None = Field(default=None, alias="TOKEN_HASH_SECRET")
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 60 * 24 * 3
refresh_token_expire_days: int = 7
Expand All @@ -45,6 +50,17 @@ class Settings(BaseSettings):
redis_url: str | None = Field(default=None, alias="REDIS_URL")
rabbitmq_url: str | None = Field(default=None, alias="RABBITMQ_URL")

email_verify_base_url: str = Field(default="", alias="EMAIL_VERIFY_BASE_URL")
mail_from: str | None = Field(default=None, alias="MAIL_FROM")
mail_server: str | None = Field(default=None, alias="MAIL_SERVER")
mail_port: int = Field(default=587, alias="MAIL_PORT")
mail_username: str | None = Field(default=None, alias="MAIL_USERNAME")
mail_password: str | None = Field(default=None, alias="MAIL_PASSWORD")
mail_starttls: bool = Field(default=True, alias="MAIL_STARTTLS")
mail_ssl_tls: bool = Field(default=False, alias="MAIL_SSL_TLS")
mail_use_credentials: bool = Field(default=True, alias="MAIL_USE_CREDENTIALS")
mail_validate_certs: bool = Field(default=True, alias="MAIL_VALIDATE_CERTS")

object_storage_endpoint: str = Field(default="localhost:9000", alias="OBJECT_STORAGE_ENDPOINT")
object_storage_access_key: str = Field(default="admin", alias="OBJECT_STORAGE_ACCESS_KEY")
object_storage_secret_key: str = Field(default="minio-admin", alias="OBJECT_STORAGE_SECRET_KEY")
Expand All @@ -56,6 +72,7 @@ class Settings(BaseSettings):
upload_chunk_size_min: int = Field(default=1 * 1024 * 1024, alias="UPLOAD_CHUNK_SIZE_MIN")
upload_chunk_size_max: int = Field(default=16 * 1024 * 1024, alias="UPLOAD_CHUNK_SIZE_MAX")
upload_single_file_size_max: int = Field(default=5 * 1024 * 1024 * 1024, alias="UPLOAD_SINGLE_FILE_SIZE_MAX")
starred_items_limit: int = Field(default=20, alias="STARRED_ITEMS_LIMIT")
upload_session_ttl_hours: int = Field(default=24, alias="UPLOAD_SESSION_TTL_HOURS")
upload_temp_prefix: str = Field(default="tmp", alias="UPLOAD_TEMP_PREFIX")
upload_object_prefix: str = Field(default="objects", alias="UPLOAD_OBJECT_PREFIX")
Expand All @@ -82,6 +99,7 @@ class Settings(BaseSettings):
default_factory=_default_worker_concurrency,
alias="WORKER_CONCURRENCY",
)
worker_process_count: int = Field(default=1, alias="WORKER_PROCESS_COUNT")
worker_task_timeout_seconds: int = Field(default=900, alias="WORKER_TASK_TIMEOUT_SECONDS")
worker_default_max_attempts: int = Field(default=5, alias="WORKER_DEFAULT_MAX_ATTEMPTS")
worker_retry_backoff_seconds: str = Field(
Expand Down Expand Up @@ -151,6 +169,28 @@ def access_token_ttl_seconds(self) -> int:
def refresh_token_ttl_seconds(self) -> int:
return self.refresh_token_expire_days * 24 * 60 * 60

@property
def effective_token_hash_secret(self) -> str:
secret = (self.token_hash_secret or "").strip()
if secret:
return secret
return self.jwt_secret_key

@property
def security_configuration_issues(self) -> tuple[str, ...]:
issues: list[str] = []
if len(self.jwt_secret_key.encode("utf-8")) < self.MIN_SECRET_LENGTH:
issues.append(f"JWT_SECRET_KEY must be at least {self.MIN_SECRET_LENGTH} bytes")
token_hash_secret = (self.token_hash_secret or "").strip()
if token_hash_secret and len(token_hash_secret.encode("utf-8")) < self.MIN_SECRET_LENGTH:
issues.append(f"TOKEN_HASH_SECRET must be at least {self.MIN_SECRET_LENGTH} bytes")
return tuple(issues)

def assert_runtime_security(self) -> None:
issues = self.security_configuration_issues
if issues:
raise ValueError("; ".join(issues))

@property
def worker_retry_backoff_schedule(self) -> tuple[int, ...]:
values: list[int] = []
Expand Down Expand Up @@ -184,6 +224,45 @@ def is_development_env(self) -> bool:
def is_production_env(self) -> bool:
return self.normalized_app_env in {"prod", "production"}

@property
def normalized_email_verify_base_url(self) -> str:
base_url = self.email_verify_base_url.strip()
if not base_url and self.is_development_env:
base_url = "http://localhost:8080"
if base_url and "://" not in base_url:
base_url = f"http://{base_url}"
return base_url.rstrip("/")

@property
def mail_configuration_issues(self) -> tuple[str, ...]:
issues: list[str] = []
if self.mail_port <= 0:
issues.append("MAIL_PORT must be a positive integer")
base_url = self.normalized_email_verify_base_url
if not base_url:
issues.append("EMAIL_VERIFY_BASE_URL is required")
parsed_base_url = urlsplit(base_url) if base_url else None
if parsed_base_url and parsed_base_url.scheme not in {"http", "https"}:
issues.append("EMAIL_VERIFY_BASE_URL must start with http:// or https://")
if parsed_base_url and not parsed_base_url.netloc:
issues.append("EMAIL_VERIFY_BASE_URL must include host")
if not (self.mail_from or "").strip():
issues.append("MAIL_FROM is required")
if not (self.mail_server or "").strip():
issues.append("MAIL_SERVER is required")
if self.mail_ssl_tls and self.mail_starttls:
issues.append("MAIL_SSL_TLS and MAIL_STARTTLS cannot both be true")
if self.mail_use_credentials:
if not (self.mail_username or "").strip():
issues.append("MAIL_USERNAME is required when MAIL_USE_CREDENTIALS=true")
if not (self.mail_password or "").strip():
issues.append("MAIL_PASSWORD is required when MAIL_USE_CREDENTIALS=true")
return tuple(issues)

@property
def is_mail_configured(self) -> bool:
return len(self.mail_configuration_issues) == 0

@property
def agent_mcp_endpoints(self) -> tuple[str, ...]:
raw = self.agent_mcp_endpoints_raw.strip()
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading