Monorepo Integration: Unified Backend, Frontend & Documentation

- Reorganize project into monorepo structure
  - backend/app/ - New FastAPI backend (modular with src/)
  - backend/legacy/ - Legacy database modules (relational & vector)
  - frontend/ - React text editor application

- Add launcher.py for easy full-stack startup
- Complete documentation in README.md
  - Quick start guide
  - API endpoints reference
  - Development setup
  - Troubleshooting

- Refactor main.py to 35 lines (app configuration only)
- Update .gitignore for full-stack project
- Add CHANGELOG.md with version history (v0.1.0-v0.1.1)

Structure is now clean and ready for team collaboration.
This commit is contained in:
Krzysztof Cieślik
2026-04-09 17:06:59 +02:00
parent fddaad962b
commit 6bbb24e633
55 changed files with 808 additions and 93 deletions

35
backend/app/main.py Normal file
View File

@@ -0,0 +1,35 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.config import ALLOWED_ORIGINS
from src.database import init_db
from src.routers import init, login, status
app = FastAPI(
title="Archivium Local Backend",
description="Local archive encryption and authentication system",
version="0.1.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["POST", "GET"],
allow_headers=["Content-Type"],
)
app.include_router(init.router)
app.include_router(login.router)
app.include_router(status.router)
@app.on_event("startup")
def startup():
"""Initialize database on startup."""
init_db()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)

View File

@@ -0,0 +1,19 @@
[project]
name = "archivium-backend"
version = "0.1.0"
description = "Local archive encryption and authentication system"
requires-python = ">=3.9,<3.13"
dependencies = [
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"pydantic>=2.5.0",
"sqlalchemy>=2.0.0",
"passlib[argon2]>=1.7.4",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"httpx>=0.25.0",
]

View File

@@ -0,0 +1 @@
"""Archivium Backend Application."""

10
backend/app/src/config.py Normal file
View File

@@ -0,0 +1,10 @@
import os
DB_PATH = os.getenv("DATABASE_PATH", "archivium.db")
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000,http://localhost:5173").split(",")
if os.getenv("ENVIRONMENT") == "development":
ALLOWED_ORIGINS = ["http://localhost:3000", "http://localhost:5173"]
elif os.getenv("ENVIRONMENT") == "production":
ALLOWED_ORIGINS = os.getenv("CORS_ORIGINS", "").split(",")

View File

@@ -0,0 +1,28 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from .config import DB_PATH
from .models import Base
DATABASE_URL = f"sqlite:///{DB_PATH}"
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def init_db():
"""Initialize database schema."""
Base.metadata.create_all(bind=engine)
def get_db():
"""Provide database session for dependency injection."""
db = SessionLocal()
try:
yield db
finally:
db.close()

13
backend/app/src/models.py Normal file
View File

@@ -0,0 +1,13 @@
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
Base = declarative_base()
class SecurityConfig(Base):
"""Storage for password and recovery key hashes."""
__tablename__ = "security_config"
id = Column(Integer, primary_key=True, index=True)
password_hash = Column(String, nullable=False)
recovery_key_hash = Column(String, nullable=False)

View File

@@ -0,0 +1 @@
"""Routers for Archivium Backend."""

View File

@@ -0,0 +1,39 @@
import os
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import SecurityConfig
from ..schemas import InitRequest
from ..security import hash_password, generate_recovery_key
from ..config import DB_PATH
router = APIRouter(prefix="/api", tags=["init"])
@router.post("/init")
def initialize_system(request: InitRequest, db: Session = Depends(get_db)):
"""Initialize system with master password and generate recovery key."""
if os.path.exists(DB_PATH):
raise HTTPException(
status_code=400,
detail="System already initialized",
)
recovery_key = generate_recovery_key()
hashed_password = hash_password(request.password)
hashed_recovery = hash_password(recovery_key)
db.add(
SecurityConfig(
password_hash=hashed_password,
recovery_key_hash=hashed_recovery,
)
)
db.commit()
return {
"status": "success",
"recovery_key": recovery_key,
"message": "System initialized. Save recovery key in safe place.",
}

View File

@@ -0,0 +1,50 @@
import os
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import SecurityConfig
from ..schemas import LoginRequest
from ..security import verify_password
from ..config import DB_PATH
router = APIRouter(prefix="/api", tags=["login"])
@router.post("/login")
def login(request: LoginRequest, db: Session = Depends(get_db)):
"""Authenticate with master password or recovery key."""
if not os.path.exists(DB_PATH):
raise HTTPException(
status_code=404,
detail="System not initialized",
)
config = db.query(SecurityConfig).first()
if not config:
raise HTTPException(
status_code=500,
detail="System configuration error",
)
if request.is_recovery:
if not verify_password(request.password, config.recovery_key_hash):
raise HTTPException(
status_code=401,
detail="Invalid recovery key",
)
return {
"status": "success",
"message": "Authenticated with recovery key. Please change password.",
}
if not verify_password(request.password, config.password_hash):
raise HTTPException(
status_code=401,
detail="Invalid password",
)
return {
"status": "success",
"message": "Successfully authenticated",
}

View File

@@ -0,0 +1,12 @@
import os
from fastapi import APIRouter
from ..config import DB_PATH
router = APIRouter(prefix="/api", tags=["status"])
@router.get("/status")
def get_status():
"""Check if system is initialized."""
return {"is_initialized": os.path.exists(DB_PATH)}

View File

@@ -0,0 +1,10 @@
from pydantic import BaseModel, Field
class InitRequest(BaseModel):
password: str = Field(..., min_length=8, max_length=128)
class LoginRequest(BaseModel):
password: str = Field(..., min_length=1, max_length=128)
is_recovery: bool = False

View File

@@ -0,0 +1,20 @@
import secrets
from passlib.hash import argon2
def hash_password(password: str) -> str:
"""Hash password using Argon2."""
return argon2.using(type="ID").hash(password)
def verify_password(password: str, hash_value: str) -> bool:
"""Verify password against hash."""
try:
return argon2.using(type="ID").verify(password, hash_value)
except Exception:
return False
def generate_recovery_key() -> str:
"""Generate random recovery key (32 hex characters)."""
return secrets.token_hex(16)