Compare commits
2 Commits
fddaad962b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a7cf8518d | ||
|
|
6bbb24e633 |
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
ENVIRONMENT=development
|
||||
DATABASE_PATH=archivium.db
|
||||
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
|
||||
67
.gitignore
vendored
Normal 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
@@ -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
@@ -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>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "app",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
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
@@ -0,0 +1 @@
|
||||
"""Archivium Backend Application."""
|
||||
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
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
"""Routers for Archivium Backend."""
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
38
docker-compose.yml
Normal 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
@@ -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"]
|
||||
@@ -8,6 +8,7 @@
|
||||
"name": "text-editor",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@tiptap/extension-character-count": "^3.20.1",
|
||||
"@tiptap/extension-image": "^3.20.1",
|
||||
"@tiptap/extension-link": "^3.20.1",
|
||||
@@ -3378,6 +3379,21 @@
|
||||
"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": {
|
||||
"version": "3.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.1.tgz",
|
||||
@@ -3,6 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@tiptap/extension-character-count": "^3.20.1",
|
||||
"@tiptap/extension-image": "^3.20.1",
|
||||
"@tiptap/extension-link": "^3.20.1",
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
168
frontend/src/App.js
Normal 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;
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
85
justfile
Normal 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
@@ -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()
|
||||
105
main.py
@@ -1,56 +1,109 @@
|
||||
"""Archivium Launcher"""
|
||||
import subprocess
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
import shutil
|
||||
|
||||
|
||||
def install_missing_packages():
|
||||
packages = ["uvicorn", "fastapi", "sentence-transformers"]
|
||||
def check_dependencies():
|
||||
"""Check if required packages are installed"""
|
||||
packages = ["uvicorn", "fastapi", "sqlalchemy", "passlib"]
|
||||
missing = []
|
||||
|
||||
for package in packages:
|
||||
try:
|
||||
__import__(package.replace("-", "_"))
|
||||
__import__(package.split("[")[0].replace("-", "_"))
|
||||
except ImportError:
|
||||
print(f"[*] Instalowanie brakującej biblioteki: {package}...")
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
|
||||
missing.append(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():
|
||||
install_missing_packages()
|
||||
def check_node_js():
|
||||
"""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__))
|
||||
|
||||
backend_script = os.path.join(base_dir, "Database", "database.py")
|
||||
frontend_dir = os.path.join(base_dir, "TextEditor")
|
||||
|
||||
print("\n--- URUCHAMIANIE SYSTEMU ARCHIVIUM ---")
|
||||
|
||||
print(f"[*] Startuję bazę danych...")
|
||||
backend_dir = os.path.join(base_dir, "backend", "app")
|
||||
|
||||
print("Starting backend on port 8000...")
|
||||
|
||||
backend_process = subprocess.Popen(
|
||||
[sys.executable, backend_script],
|
||||
cwd=os.path.join(base_dir, "Database")
|
||||
["uv", "run", "python", "main.py"],
|
||||
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(
|
||||
"npm start",
|
||||
shell=True,
|
||||
cwd=frontend_dir,
|
||||
creationflags=subprocess.CREATE_NEW_CONSOLE
|
||||
)
|
||||
|
||||
print("[*] Oczekiwanie na gotowość...")
|
||||
time.sleep(5)
|
||||
|
||||
webbrowser.open("http://localhost:3000")
|
||||
|
||||
try:
|
||||
backend_process.wait()
|
||||
frontend_process.wait()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[*] Zamykanie systemu...")
|
||||
backend_process.terminate()
|
||||
frontend_process.terminate()
|
||||
backend_process.wait()
|
||||
frontend_process.wait()
|
||||
|
||||
|
||||
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
@@ -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",
|
||||
]
|
||||