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:
35
backend/app/main.py
Normal file
35
backend/app/main.py
Normal 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)
|
||||
19
backend/app/pyproject.toml
Normal file
19
backend/app/pyproject.toml
Normal 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",
|
||||
]
|
||||
1
backend/app/src/__init__.py
Normal file
1
backend/app/src/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Archivium Backend Application."""
|
||||
10
backend/app/src/config.py
Normal file
10
backend/app/src/config.py
Normal 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(",")
|
||||
28
backend/app/src/database.py
Normal file
28
backend/app/src/database.py
Normal 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
13
backend/app/src/models.py
Normal 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)
|
||||
1
backend/app/src/routers/__init__.py
Normal file
1
backend/app/src/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Routers for Archivium Backend."""
|
||||
39
backend/app/src/routers/init.py
Normal file
39
backend/app/src/routers/init.py
Normal 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.",
|
||||
}
|
||||
50
backend/app/src/routers/login.py
Normal file
50
backend/app/src/routers/login.py
Normal 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",
|
||||
}
|
||||
12
backend/app/src/routers/status.py
Normal file
12
backend/app/src/routers/status.py
Normal 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)}
|
||||
10
backend/app/src/schemas.py
Normal file
10
backend/app/src/schemas.py
Normal 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
|
||||
20
backend/app/src/security.py
Normal file
20
backend/app/src/security.py
Normal 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)
|
||||
Reference in New Issue
Block a user