Compare commits

2 Commits

Author SHA1 Message Date
Krzysztof Cieślik
9a7cf8518d force rewrited code 2026-04-15 17:38:49 +02:00
Krzysztof Cieślik
6bbb24e633 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.
2026-04-09 17:06:59 +02:00
64 changed files with 3854 additions and 81 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
ENVIRONMENT=development
DATABASE_PATH=archivium.db
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173

67
.gitignore vendored Normal file
View File

@@ -0,0 +1,67 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.venv/
venv/
env/
# Database
*.db
*.sqlite
*.sqlite3
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# Environment
.env
.env.local
.env.*.local
# Testing
.pytest_cache/
.coverage
htmlcov/
# OS
.DS_Store
Thumbs.db
# Node/Frontend
node_modules/
npm-debug.log
yarn-error.log
dist/
build/
# Lock files (optional - uncomment if you want to enforce exact versions)
# package-lock.json
# pnpm-lock.yaml

8
.idea/.gitignore generated vendored
View File

@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/Editor.iml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Editor.iml" filepath="$PROJECT_DIR$/.idea/Editor.iml" />
</modules>
</component>
</project>

141
INSTALL_NODEJS.md Normal file
View File

@@ -0,0 +1,141 @@
# 🔧 Instalacja Node.js na Fedora Atomic
## Problem
Na Fedora Atomic nie ma prostej opcji `dnf install nodejs` ze względu na niezmienność systemu. Trzeba użyć **toolbox** lub alternatywnych sposobów.
## Rozwiązanie 1: Toolbox (Rekomendowane)
Toolbox tworzy kontener z pełnym dostępem do pakietów.
```bash
# Krok 1: Stwórz nowy toolbox kontener
toolbox create
# Krok 2: Wejdź do kontenera
toolbox enter
# Krok 3: Zainstaluj Node.js i npm
sudo dnf install nodejs npm -y
# Krok 4: Wyjdź z kontenera
exit
# Krok 5: Sprawdź instalację (powinno działać z poziomu hosta)
node --version
npm --version
```
## Rozwiązanie 2: Homebrew
Homebrew działa natywnie na Fedora Atomic.
```bash
# Krok 1: Zainstaluj Homebrew (jeśli nie masz)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Krok 2: Dodaj do PATH (jeśli nie dodano automatycznie)
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.bashrc
source ~/.bashrc
# Krok 3: Zainstaluj Node.js
brew install node
# Krok 4: Sprawdź
node --version
npm --version
```
## Rozwiązanie 3: NVM (Node Version Manager)
NVM pozwala na installowanie wielu wersji Node.js.
```bash
# Krok 1: Zainstaluj NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# Krok 2: Załaduj NVM
source ~/.bashrc
# Krok 3: Zainstaluj Node.js
nvm install node
# Krok 4: Sprawdź
node --version
npm --version
```
## Rozwiązanie 4: Pobrać prebuilt (Szybko)
```bash
# Pobierz Node.js
cd ~/Downloads
wget https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-x64.tar.xz
# Rozpakuj
tar -xf node-v20.10.0-linux-x64.tar.xz
# Dodaj do PATH
export PATH="$HOME/Downloads/node-v20.10.0-linux-x64/bin:$PATH"
# Dodaj na stałe do .bashrc aby zachowało się między restartami
echo 'export PATH="$HOME/Downloads/node-v20.10.0-linux-x64/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# Sprawdź
node --version
npm --version
```
---
## Po instalacji Node.js
### Uruchom frontend:
```bash
cd frontend
npm install # Zainstaluj zależności (tylko raz)
npm start # Uruchom aplikację
```
### Lub uruchom całą aplikację:
```bash
python main.py
# Wybierz opcję 1 (uruchomić tylko backend) lub q (zainstalować Node.js)
```
---
## Diagnostyka jeśli coś nie działa
```bash
# Sprawdź gdzie jest node
which node
which npm
# Sprawdź wersje
node --version
npm --version
# Uwaga: Jeśli Node.js zainstalowany w toolbox, mogą być dostępne tylko w toolbox
# Użyj wtedy:
toolbox run node --version
toolbox run npm --version
```
## Najszybsze rozwiązanie dla Fedora Atomic:
```bash
# Terminal 1 - Backend
python main.py # wybiez opcja 1
# Terminal 2 - Frontend (jeśli Node zainstalowany)
cd frontend && npm install && npm start
# Lub jeśli Node w toolbox:
# Terminal 2
toolbox run bash -c 'cd /var/home/krzysztof/Documents/inz/git_fix/frontend && npm install && npm start'
```
---
**Osobiście polecam Rozwiązanie 1 (Toolbox)** - najprościej i najczystsze dla Fedora Atomic.

334
README.md Normal file
View File

@@ -0,0 +1,334 @@
# 📚 Archivium - System Zarządzania Archiwum
**Archivium** to integrowany system do szyfrowania, przechowywania i wyszukiwania archiwów z bezpiecznym uwierzytelnianiem i interfejsem edytora tekstu.
## 🏗️ Architektura projektu
```
archivium/
├── backend/
│ ├── app/ # Nowoczesny backend FastAPI
│ │ ├── src/
│ │ │ ├── config.py # Konfiguracja
│ │ │ ├── models.py # Modele bazy
│ │ │ ├── schemas.py # Walidacja
│ │ │ ├── database.py # Zarządzanie BD
│ │ │ ├── security.py # Hashing hasła
│ │ │ └── routers/ # Endpointy API
│ │ │ ├── init.py
│ │ │ ├── login.py
│ │ │ └── status.py
│ │ ├── main.py # Punkt wejścia (35 linii)
│ │ └── pyproject.toml
│ │
│ └── legacy/ # Stare moduły bazy danych
│ ├── relational_database.py
│ └── vector_database.py
├── frontend/ # React aplikacja (edytor tekstu)
│ ├── public/
│ ├── src/
│ └── package.json
├── launcher.py # Uruchamiacz całej aplikacji
├── requirements.txt # Zależności Python
├── README.md # Ta dokumentacja
├── CHANGELOG.md # Historia zmian
└── .git/ # Repozytorium Git
```
## 🚀 Szybki start
### Uruchomienie całej aplikacji (jedno polecenie)
```bash
python launcher.py
```
To uruchomi:
- ✅ Backend FastAPI (port 8000)
- ✅ Frontend React (port 3000)
- ✅ Otworzy przeglądarkę na http://localhost:3000
### Ręczne uruchomienie poszczególnych części
**Backend:**
```bash
cd backend/app
python main.py
```
**Frontend:**
```bash
cd frontend
npm install
npm start
```
## 📡 API Endpoints
### 1. Inicjalizacja systemu
```http
POST /api/init
Content-Type: application/json
{
"password": "twoje_bezpieczne_haslo_8znaków_min"
}
```
**Response:**
```json
{
"status": "success",
"recovery_key": "a1b2c3d4e5f6...",
"message": "System initialized. Save recovery key in safe place."
}
```
### 2. Logowanie - hasłem głównym
```http
POST /api/login
Content-Type: application/json
{
"password": "twoje_haslo",
"is_recovery": false
}
```
**Response:**
```json
{
"status": "success",
"message": "Successfully authenticated"
}
```
### 3. Logowanie - kluczem awaryjnym
```http
POST /api/login
Content-Type: application/json
{
"password": "a1b2c3d4e5f6...",
"is_recovery": true
}
```
**Response:**
```json
{
"status": "success",
"message": "Authenticated with recovery key. Please change password."
}
```
### 4. Sprawdzenie statusu
```http
GET /api/status
```
**Response:**
```json
{
"is_initialized": true
}
```
## 🔐 Bezpieczeństwo
### Zaimplementowane zabezpieczenia:
-**Hashing haseł** - Argon2 (type="ID")
-**Losowe klucze awaryjne** - 32 znaki hex
-**CORS** - ograniczony do zaufanych domen
-**Walidacja danych** - Pydantic
-**Dwie ścieżki dostępu** - hasło główne + klucz awaryjny
### Zmienne środowiskowe (.env)
```bash
ENVIRONMENT=development # development | production
DATABASE_PATH=archivium.db # lokalizacja bazy
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
```
### Rekomendacje produkcyjne:
1. Użyj **PostgreSQL** zamiast SQLite
2. Włącz **HTTPS/SSL**
3. Dodaj **rate limiting**
4. Włącz **logging i monitoring**
5. Regularne **backupy** bazy danych
6. Przechowuj sekrety w zmiennych środowiskowych
## 📚 Szczegółowa dokumentacja struktury
### Backend - Endpointy (src/routers/)
**init.py** - `POST /api/init`
- Inicjalizuje system
- Generuje recovery key (32 znaki)
- Hashuje hasło główne z Argon2
**login.py** - `POST /api/login`
- Logowanie hasłem głównym lub kluczem awaryjnym
- Weryfikuje hash haseł
- Zwraca status autoryzacji
**status.py** - `GET /api/status`
- Sprawdza czy system zainicjalizowany
- Zwraca boolean `is_initialized`
### Backend - Core modules (src/)
**config.py** - Konfiguracja
- CORS configuration
- DATABASE_PATH z zmiennych środowiskowych
- ALLOWED_ORIGINS
**models.py** - SQLAlchemy ORM
- SecurityConfig model
- Tabela `security_config`
- Przechowuje hashe haseł i kluczy
**schemas.py** - Pydantic validation
- InitRequest - walidacja do /api/init
- LoginRequest - walidacja do /api/login
**database.py** - Zarządzanie bazą
- SQLite setup
- Session management
- `init_db()` - inicjalizuje schemat
- `get_db()` - dependency injection dla sessionów
**security.py** - Funkcje bezpieczeństwa
- `hash_password(password: str) -> str`
- `verify_password(password: str, hash_value: str) -> bool`
- `generate_recovery_key() -> str`
**main.py** - FastAPI aplikacja (35 linii!)
- Konfiguracja CORS middleware
- Include routery
- Startup event dla init_db()
## 🛠️ Development
### Instalacja dependencies
```bash
# Backend
pip install -r requirements.txt
# Lub z uv:
cd backend/app && uv sync
# Frontend
cd frontend && npm install
```
### Testowanie API
```bash
# Curl - Inicjalizacja
curl -X POST http://localhost:8000/api/init \
-H "Content-Type: application/json" \
-d '{"password": "TestPassword123"}'
# Curl - Logowanie
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-d '{"password": "TestPassword123", "is_recovery": false}'
# Status
curl http://localhost:8000/api/status
```
### Dokumentacja interaktywna
Gdy serwer działa, otwórz: **http://localhost:8000/docs**
- Swagger UI do testowania API
- Automatyczna dokumentacja OpenAPI
- Try it out - testuj bezpośrednio z przeglądarki
## 📝 Historia zmian
Pełna historia zmian w [CHANGELOG.md](CHANGELOG.md)
### v0.1.1 - Monorepo Integration
- Reorganizacja całego projektu do struktury monorepo
- Backend w `backend/app/` (35 linii main.py!)
- Launcher do uruchamiania całej aplikacji
- Integracja z starym kodem z `Database/` i `TextEditor/`
- Kompletna dokumentacja
### v0.1.0 - Refactorization
- Refaktoryzacja kodu, usunięcie komentarzy
- Modularyzacja na src/
- Bezpieczeństwo (CORS, Argon2, walidacja)
## 🐛 Troubleshooting
### Problem: Port 8000 już w użyciu
```bash
# Zmień w backend/app/main.py lub użyj:
cd backend/app && uv run uvicorn main:app --port 8001
```
### Problem: Frontend nie łączy się z backendem
Sprawdź w `backend/app/src/config.py`:
```python
ALLOWED_ORIGINS = ["http://localhost:3000"]
```
### Problem: Baza danych uszkodzona
```bash
rm archivium.db # Usuń stary plik
python launcher.py # Przeinicjalizuj
```
### Problem: npm start nie działa
```bash
cd frontend
rm -rf node_modules package-lock.json
npm install
npm start
```
## 👥 Współpraca (Git)
```bash
# Clone repozytorium
git clone http://gitea.archvium.eu:30230/SzymonS/Kod.git
# Stwórz feature branch
git checkout -b feature/my-feature
# Commit
git add .
git commit -am "Add new feature"
# Push
git push origin feature/my-feature
```
## 📦 Zależności
### Backend (Python)
- **FastAPI** (0.104.0+) - Framework
- **SQLAlchemy** (2.0.0+) - ORM
- **Pydantic** (2.5.0+) - Walidacja
- **Passlib[argon2]** (1.7.4+) - Hashing
- **Uvicorn** (0.24.0+) - ASGI server
### Frontend (Node.js)
- **React** - UI framework
- Inne (patrz: `frontend/package.json`)
Pełna lista: [requirements.txt](requirements.txt)
## 📞 Support
Kontakt: SzymonS @ gitea.archvium.eu
---
**Ostatnia aktualizacja:** April 9, 2026

View File

@@ -1,18 +0,0 @@
import React from 'react';
import TextEditor from './TextEditor';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header" style={{ minHeight: 'auto', padding: '20px', backgroundColor: '#282c34', color: 'white' }}>
<h1>Edytor tekstowy</h1>
</header>
<main style={{ padding: '20px' }}>
<TextEditor />
</main>
</div>
);
}
export default App;

22
backend/app/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM python:3.11-slim
WORKDIR /app
# Install uv package manager
RUN pip install uv
# Copy project configuration
COPY pyproject.toml .
# Copy uv.lock if it exists, otherwise just the pyproject (using wildcard)
COPY uv.lock* ./
# Install dependencies using uv
RUN uv sync
# Copy application code
COPY . .
EXPOSE 8000
# Run the FastAPI server
CMD ["uv", "run", "python", "main.py"]

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

@@ -0,0 +1,39 @@
from contextlib import asynccontextmanager
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
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize database on startup."""
init_db()
yield
app = FastAPI(
title="Archivium Local Backend",
description="Local archive encryption and authentication system",
version="0.1.0",
lifespan=lifespan,
)
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)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)

6
backend/app/package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "app",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

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)

1191
backend/app/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

38
docker-compose.yml Normal file
View File

@@ -0,0 +1,38 @@
version: '3.8'
services:
backend:
build:
context: ./backend/app
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./backend/app:/app
# Cache for uv environments to speed up things inside container
- uv-cache:/root/.cache/uv
# Avoid overwriting container's installed venv with local dev venv
- /app/.venv
environment:
- DATABASE_PATH=archivium.db
- ALLOWED_ORIGINS=http://localhost:3000
restart: unless-stopped
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
volumes:
- ./frontend:/app
# Hide container's node_modules from local disk
- /app/node_modules
environment:
- REACT_APP_API_URL=http://localhost:8000
depends_on:
- backend
restart: unless-stopped
volumes:
uv-cache:

18
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM node:18-alpine
WORKDIR /app
# Copy package management files
# Try to catch package-lock.json if it exists
COPY package.json package-lock.json* ./
# Install dependencies (ignoring optional ones if they fail in alpine)
RUN npm install
# Copy application source code
COPY . .
EXPOSE 3000
# Start development server
CMD ["npm", "start"]

View File

@@ -8,6 +8,7 @@
"name": "text-editor", "name": "text-editor",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.6.0",
"@tiptap/extension-character-count": "^3.20.1", "@tiptap/extension-character-count": "^3.20.1",
"@tiptap/extension-image": "^3.20.1", "@tiptap/extension-image": "^3.20.1",
"@tiptap/extension-link": "^3.20.1", "@tiptap/extension-link": "^3.20.1",
@@ -3378,6 +3379,21 @@
"url": "https://github.com/sponsors/gregberge" "url": "https://github.com/sponsors/gregberge"
} }
}, },
"node_modules/@tauri-apps/api": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz",
"integrity": "sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==",
"license": "Apache-2.0 OR MIT",
"engines": {
"node": ">= 14.6.0",
"npm": ">= 6.6.0",
"yarn": ">= 1.19.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/tauri"
}
},
"node_modules/@tiptap/core": { "node_modules/@tiptap/core": {
"version": "3.20.1", "version": "3.20.1",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.1.tgz", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.1.tgz",

View File

@@ -3,6 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@tauri-apps/api": "^1.6.0",
"@tiptap/extension-character-count": "^3.20.1", "@tiptap/extension-character-count": "^3.20.1",
"@tiptap/extension-image": "^3.20.1", "@tiptap/extension-image": "^3.20.1",
"@tiptap/extension-link": "^3.20.1", "@tiptap/extension-link": "^3.20.1",

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

168
frontend/src/App.js Normal file
View File

@@ -0,0 +1,168 @@
import React, { useState, useEffect } from 'react';
import TextEditor from './TextEditor';
import './App.css';
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
function App() {
const [appState, setAppState] = useState({
loading: true,
initialized: false,
authenticated: false,
error: null,
});
const [password, setPassword] = useState('');
const [isRecovery, setIsRecovery] = useState(false);
const [recoveryKey, setRecoveryKey] = useState(null);
useEffect(() => {
checkStatus();
}, []);
const checkStatus = async () => {
try {
const res = await fetch(`${API_URL}/api/status`);
const data = await res.json();
setAppState({
...appState,
loading: false,
initialized: data.is_initialized,
error: null,
});
} catch (err) {
setAppState({
...appState,
loading: false,
error: 'Nie można połączyć z backendem.',
initialized: false,
authenticated: false
});
}
};
const handleInit = async (e) => {
e.preventDefault();
try {
const res = await fetch(`${API_URL}/api/init`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
const data = await res.json();
if (res.ok) {
setRecoveryKey(data.recovery_key);
setAppState({ ...appState, initialized: true, error: null });
setPassword('');
} else {
setAppState({ ...appState, error: data.detail || 'Błąd inicjalizacji' });
}
} catch (err) {
setAppState({ ...appState, error: 'Błąd połączenia' });
}
};
const handleLogin = async (e) => {
e.preventDefault();
try {
const res = await fetch(`${API_URL}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password, is_recovery: isRecovery }),
});
const data = await res.json();
if (res.ok && data.status === 'success') {
setAppState({ ...appState, authenticated: true, error: null });
} else {
// If not initialized (e.g. wiped database), reload state
if (res.status === 404) {
checkStatus();
return;
}
setAppState({ ...appState, error: data.detail || 'Błędne hasło/klucz' });
}
} catch (err) {
setAppState({ ...appState, error: 'Błąd połączenia' });
}
};
if (appState.loading) return <div style={{ padding: '20px', textAlign: 'center' }}>Ładowanie zabezpieczeń...</div>;
// Ekran tworzenia konta
if (!appState.initialized) {
return (
<div style={{ maxWidth: '400px', margin: '100px auto', fontFamily: 'sans-serif', textAlign: 'center', padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h2>Witamy w Archivium</h2>
<p>Utwórz pierwsze hasło do swojego sejfu.</p>
<form onSubmit={handleInit} style={{ display: 'flex', flexDirection: 'column', gap: '15px', marginTop: '20px' }}>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Główne hasło (min. 8 znaków)"
required
minLength={8}
style={{ padding: '10px', fontSize: '16px' }}
/>
<button type="submit" style={{ padding: '10px', background: '#282c34', color: '#fff', border: 'none', cursor: 'pointer', fontSize: '16px' }}>Zainicjuj Baze</button>
</form>
{appState.error && <p style={{ color: 'red', marginTop: '15px' }}>{appState.error}</p>}
</div>
);
}
// Ekran po inicjalizacji pokazujący wygenerowany klucz odzyskiwania
if (recoveryKey) {
return (
<div style={{ maxWidth: '500px', margin: '100px auto', fontFamily: 'sans-serif', textAlign: 'center', border: '2px solid red', padding: '30px', borderRadius: '8px' }}>
<h2 style={{ color: 'red', marginTop: 0 }}> ZAPISZ TEN KLUCZ</h2>
<p>To jedyny moment, w którym go widzisz. Użyjesz go do odzyskania dostępu, jeśli zapomnisz hasła.</p>
<code style={{ display: 'block', padding: '15px', background: '#eee', fontSize: '18px', margin: '20px 0', wordBreak: 'break-all' }}>
{recoveryKey}
</code>
<button onClick={() => setRecoveryKey(null)} style={{ padding: '10px 20px', background: '#282c34', color: '#fff', cursor: 'pointer', border: 'none', fontSize: '16px' }}>Zapisany, przejdź do logowania</button>
</div>
);
}
// Ekran logowania
if (!appState.authenticated) {
return (
<div style={{ maxWidth: '400px', margin: '100px auto', fontFamily: 'sans-serif', textAlign: 'center', padding: '20px', border: '1px solid #ccc', borderRadius: '8px' }}>
<h2>Logowanie</h2>
<p style={{ color: '#666', marginBottom: '20px' }}>Wprowadź hasło aby odblokować sejf.</p>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input
type={isRecovery ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={isRecovery ? "Wklej klucz odzyskiwania" : "Twoje hasło"}
required
style={{ padding: '10px', fontSize: '16px' }}
/>
<label style={{ display: 'flex', alignItems: 'center', gap: '10px', justifyContent: 'center', cursor: 'pointer' }}>
<input type="checkbox" checked={isRecovery} onChange={(e) => setIsRecovery(e.target.checked)} />
Zaloguj kluczem odzyskiwania
</label>
<button type="submit" style={{ padding: '10px', background: '#282c34', color: '#fff', border: 'none', cursor: 'pointer', fontSize: '16px' }}>Odblokuj</button>
</form>
{appState.error && <p style={{ color: 'red', marginTop: '15px' }}>{appState.error}</p>}
</div>
);
}
// Główny widok (Text Editor) - po zalogowaniu
return (
<div className="App">
<header className="App-header" style={{ minHeight: 'auto', padding: '15px 30px', backgroundColor: '#282c34', color: 'white', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1 style={{ margin: 0, fontSize: '22px' }}>Archivium</h1>
<button onClick={() => { setAppState({...appState, authenticated: false}); setPassword(''); }} style={{ padding: '8px 15px', background: '#dc3545', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>Wyloguj</button>
</header>
<main style={{ padding: '20px' }}>
<TextEditor />
</main>
</div>
);
}
export default App;

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

85
justfile Normal file
View File

@@ -0,0 +1,85 @@
# Configuration
set shell := ["bash", "-c"]
export VIRTUAL_ENV := ""
backend_dir := "backend/app"
frontend_dir := "frontend"
venv_bin := backend_dir + "/.venv/bin"
# ============================================================================
# Default: Show help
# ============================================================================
[doc("Show available recipes")]
default:
@just --list
# ============================================================================
# Setup: Initialize dependencies
# ============================================================================
[doc("Install all dependencies")]
install: install-backend install-frontend
[doc("Install backend dependencies with uv")]
install-backend:
cd {{backend_dir}} && uv sync
[doc("Install portable Node.js and frontend dependencies")]
install-frontend: install-backend
@echo "Ensuring Node.js is available..."
@if ! command -v npm >/dev/null 2>&1 && [ ! -f "{{venv_bin}}/npm" ]; then \
echo "Installing portable Node.js inside Python venv..."; \
cd {{backend_dir}} && uv pip install nodeenv && uv run nodeenv -p; \
fi
@echo "Installing frontend dependencies..."
export PATH="$PWD/{{venv_bin}}:$PATH" && cd {{frontend_dir}} && npm install
# ============================================================================
# Development: Run services
# ============================================================================
[doc("Run backend server")]
run-backend: install-backend
cd {{backend_dir}} && uv run python3 main.py
[doc("Run frontend development server")]
run-frontend: install-frontend
export PATH="$PWD/{{venv_bin}}:$PATH" && cd {{frontend_dir}} && npm start
[doc("Run both services (Backend + GUI)")]
dev: install
@echo "Starting backend and frontend..."
export PATH="$PWD/{{venv_bin}}:$PATH"; \
(cd {{backend_dir}} && uv run python3 main.py) & \
BACKEND_PID=$$!; \
sleep 2; \
(cd {{frontend_dir}} && npm start) & \
FRONTEND_PID=$$!; \
trap "kill $$BACKEND_PID $$FRONTEND_PID 2>/dev/null" EXIT INT TERM; \
wait
# ============================================================================
# Build & Clean
# ============================================================================
[doc("Build frontend")]
build-frontend: install-frontend
export PATH="$PWD/{{venv_bin}}:$PATH" && cd {{frontend_dir}} && npm run build
[doc("Clean build artifacts and all dependencies")]
clean-all:
rm -rf {{backend_dir}}/.pytest_cache
rm -rf {{backend_dir}}/__pycache__
rm -rf {{backend_dir}}/src/__pycache__
rm -rf {{frontend_dir}}/build
rm -rf {{backend_dir}}/.venv
rm -rf {{frontend_dir}}/node_modules
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
[doc("Check if tools are available")]
check:
@echo "Checking prerequisites..."
@command -v uv >/dev/null 2>&1 && echo "[OK] uv" || echo "[MISSING] uv"
@if command -v node >/dev/null 2>&1 || [ -f "{{venv_bin}}/node" ]; then echo "[OK] Node.js"; else echo "[MISSING] Node.js"; fi
@if command -v npm >/dev/null 2>&1 || [ -f "{{venv_bin}}/npm" ]; then echo "[OK] npm"; else echo "[MISSING] npm"; fi
@command -v python3 >/dev/null 2>&1 && echo "[OK] python3" || echo "[MISSING] python3"

149
launcher.py Normal file
View File

@@ -0,0 +1,149 @@
"""Archivium Launcher - Uruchamia całą aplikację"""
import subprocess
import os
import sys
import time
import webbrowser
import shutil
def install_missing_packages():
"""Instaluj brakujące pakiety"""
packages = ["uvicorn", "fastapi", "sqlalchemy", "passlib[argon2]"]
for package in packages:
try:
__import__(package.split("[")[0].replace("-", "_"))
except ImportError:
print(f"[*] Instalowanie: {package}...")
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
def check_node_js():
"""Sprawdź czy Node.js jest zainstalowany"""
return shutil.which("node") is not None and shutil.which("npm") is not None
def run_backend_only():
"""Uruchom tylko backend"""
base_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.join(base_dir, "backend", "app")
print("\n" + "="*60)
print(" ARCHIVIUM - Backend Only Mode")
print("="*60 + "\n")
print("[*] Startuje backend FastAPI na porcie 8000...")
print("[*] Czekaj 2-3 sekundy na zainicjalizowanie...\n")
backend_process = subprocess.Popen(
[sys.executable, "main.py"],
cwd=backend_dir
)
time.sleep(2)
print("\n" + "="*60)
print("✓ Backend uruchomiony!")
print("="*60)
print("\n📡 API dostępne:")
print(" http://localhost:8000/api/status")
print(" http://localhost:8000/docs (Swagger UI)")
print("\n🔧 Testy API:")
print(" curl http://localhost:8000/api/status")
print(" curl -X POST http://localhost:8000/api/init \\")
print(" -H 'Content-Type: application/json' \\")
print(" -d '{\"password\": \"TestPassword123\"}'")
print("\n❌ Frontend nie dostępny (Node.js nie zainstalowany)")
print("\n📝 Aby zainstalować Node.js na Fedora Atomic:")
print(" 1. Za pomocą toolbox:")
print(" toolbox create")
print(" toolbox run sudo dnf install nodejs npm")
print(" 2. Za pomocą brew:")
print(" brew install node")
print(" 3. Lub pobrać z https://nodejs.org/")
print("\n Po instalacji Node.js, uruchom:")
print(" cd frontend && npm install && npm start")
print("\nAby zatrzymać backend: Ctrl+C\n")
try:
backend_process.wait()
except KeyboardInterrupt:
print("\n[*] Zatrzymywanie backendu...")
backend_process.terminate()
backend_process.wait()
print("[✓] Backend zatrzymany")
def run_archivium():
"""Uruchom całą aplikację Archivium"""
install_missing_packages()
base_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.join(base_dir, "backend", "app")
frontend_dir = os.path.join(base_dir, "frontend")
# Sprawdź czy Node.js jest dostępny
has_node = check_node_js()
if not has_node:
print("\n⚠️ Node.js/npm nie znaleziony!")
print("\nOpcje:")
print("1. Uruchomić tylko backend (Enter)")
print("2. Wyjść i zainstalować Node.js (q)\n")
choice = input("Wybór [1/q]: ").strip().lower()
if choice == 'q':
print("[*] Wyjście. Zainstaluj Node.js i uruchom ponownie.")
sys.exit(1)
else:
run_backend_only()
return
print("\n" + "="*50)
print(" ARCHIVIUM - System Zarządzania Archiwum")
print("="*50 + "\n")
print("[1/3] Startuje backend (Port 8000)...")
backend_process = subprocess.Popen(
[sys.executable, "main.py"],
cwd=backend_dir
)
print("[2/3] Startuje frontend (Port 3000)...")
frontend_process = subprocess.Popen(
"npm start",
shell=True,
cwd=frontend_dir,
)
print("[3/3] Otwieranie przeglądarki...")
time.sleep(3)
try:
webbrowser.open("http://localhost:3000")
except Exception as e:
print(f"[!] Nie udało się otworzyć przeglądarki: {e}")
print("[*] Otwórz ręcznie: http://localhost:3000")
print("\n" + "="*50)
print("✓ Aplikacja uruchomiona!")
print(" Backend: http://localhost:8000")
print(" Frontend: http://localhost:3000")
print(" Docs: http://localhost:8000/docs")
print("="*50)
print("\nAby zatrzymać: Ctrl+C\n")
try:
backend_process.wait()
frontend_process.wait()
except KeyboardInterrupt:
print("\n[*] Zatrzymywanie systemu...")
backend_process.terminate()
frontend_process.terminate()
backend_process.wait()
frontend_process.wait()
print("[✓] System zatrzymany")
if __name__ == "__main__":
run_archivium()

101
main.py
View File

@@ -1,56 +1,109 @@
"""Archivium Launcher"""
import subprocess import subprocess
import os import os
import sys import sys
import time import time
import webbrowser import shutil
def install_missing_packages(): def check_dependencies():
packages = ["uvicorn", "fastapi", "sentence-transformers"] """Check if required packages are installed"""
packages = ["uvicorn", "fastapi", "sqlalchemy", "passlib"]
missing = []
for package in packages: for package in packages:
try: try:
__import__(package.replace("-", "_")) __import__(package.split("[")[0].replace("-", "_"))
except ImportError: except ImportError:
print(f"[*] Instalowanie brakującej biblioteki: {package}...") missing.append(package)
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
if missing:
print(f"Missing dependencies: {', '.join(missing)}")
print("\nInstall with:")
print(" cd backend/app")
print(" uv sync")
sys.exit(1)
def run_archivium(): def check_node_js():
install_missing_packages() """Check if Node.js is available"""
return shutil.which("node") is not None and shutil.which("npm") is not None
def run_backend():
"""Run backend only"""
base_dir = os.path.dirname(os.path.abspath(__file__)) base_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.join(base_dir, "backend", "app")
backend_script = os.path.join(base_dir, "Database", "database.py") print("Starting backend on port 8000...")
frontend_dir = os.path.join(base_dir, "TextEditor")
print("\n--- URUCHAMIANIE SYSTEMU ARCHIVIUM ---")
print(f"[*] Startuję bazę danych...")
backend_process = subprocess.Popen( backend_process = subprocess.Popen(
[sys.executable, backend_script], ["uv", "run", "python", "main.py"],
cwd=os.path.join(base_dir, "Database") cwd=backend_dir
) )
print(f"[*] Startuję edytor...") try:
backend_process.wait()
except KeyboardInterrupt:
backend_process.terminate()
backend_process.wait()
def run_full_stack():
"""Run backend and frontend"""
check_dependencies()
base_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.join(base_dir, "backend", "app")
frontend_dir = os.path.join(base_dir, "frontend")
if not check_node_js():
if not sys.stdin.isatty():
run_backend()
return
response = input("Node.js not found. Run backend only? (y/n): ").strip().lower()
if response == 'y':
run_backend()
return
print("Starting backend and frontend...")
backend_process = subprocess.Popen(
["uv", "run", "python", "main.py"],
cwd=backend_dir
)
time.sleep(2)
frontend_process = subprocess.Popen( frontend_process = subprocess.Popen(
"npm start", "npm start",
shell=True, shell=True,
cwd=frontend_dir, cwd=frontend_dir,
creationflags=subprocess.CREATE_NEW_CONSOLE
) )
print("[*] Oczekiwanie na gotowość...")
time.sleep(5)
webbrowser.open("http://localhost:3000")
try: try:
backend_process.wait() backend_process.wait()
frontend_process.wait() frontend_process.wait()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\n[*] Zamykanie systemu...")
backend_process.terminate() backend_process.terminate()
frontend_process.terminate() frontend_process.terminate()
backend_process.wait()
frontend_process.wait()
if __name__ == "__main__": if __name__ == "__main__":
run_archivium() if len(sys.argv) > 1:
if sys.argv[1] == "--backend-only":
check_dependencies()
run_backend()
elif sys.argv[1] == "--help":
print("Usage: python main.py [option]")
print("\nOptions:")
print(" --backend-only Run backend only")
print(" --help Show this message")
else:
print(f"Unknown option: {sys.argv[1]}")
else:
run_full_stack()

13
pyproject.toml Normal file
View File

@@ -0,0 +1,13 @@
[project]
name = "archivium"
version = "0.1.0"
description = "Archivium - Archive Management System"
requires-python = ">=3.9"
dependencies = [
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"pydantic>=2.5.0",
"sqlalchemy>=2.0.0",
"passlib[argon2]>=1.7.4",
"python-dotenv>=1.2.0",
]

Binary file not shown.

1281
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff