Skip to content

Commit 7596ad3

Browse files
committed
feat: phase 1, 2 ok.
0 parents  commit 7596ad3

58 files changed

Lines changed: 10468 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# ── 基本 ──
2+
SECRET_KEY=change-me-to-random-string
3+
ADMIN_USER=admin
4+
ADMIN_PASS=admin
5+
6+
# ── 路徑 ──
7+
DATA_DIR=./data
8+
DB_PATH=./data/just-wiki.db
9+
MEDIA_DIR=./data/media
10+
11+
# ── Frontend ──
12+
VITE_API_URL=http://localhost:8000
13+
14+
# ── AI (Phase 5, 可選) ──
15+
GEMINI_API_KEY=
16+
AI_ENABLED=false
17+
18+
# ── Webhook (Phase 7, 可選) ──
19+
WEBHOOK_URLS=

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
data/
2+
*.db
3+
__pycache__/
4+
*.pyc
5+
.venv/
6+
node_modules/
7+
dist/
8+
.env
9+
*.egg-info/
10+
.DS_Store

Makefile

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
.PHONY: dev dev-backend dev-frontend build backup clean
2+
3+
dev:
4+
@echo "Starting backend and frontend..."
5+
@make dev-backend &
6+
@make dev-frontend
7+
8+
dev-backend:
9+
cd backend && source .venv/bin/activate && PYTHON_GIL=1 uvicorn app.main:app --reload --port 8000
10+
11+
dev-frontend:
12+
cd frontend && npm run dev
13+
14+
build:
15+
cd frontend && npm run build
16+
17+
backup:
18+
@mkdir -p backup
19+
cp data/just-wiki.db backup/just-wiki-$$(date +%Y%m%d_%H%M%S).db
20+
@echo "Backup complete"
21+
22+
clean:
23+
rm -f data/just-wiki.db
24+
rm -rf data/media/*
25+
rm -rf frontend/dist
26+
@echo "Cleaned"
27+
28+
docker-up:
29+
docker-compose up -d
30+
31+
docker-down:
32+
docker-compose down
33+
34+
setup:
35+
@echo "Setting up backend..."
36+
cd backend && uv venv && source .venv/bin/activate && uv pip install -r requirements.txt
37+
@echo "Setting up frontend..."
38+
cd frontend && npm install
39+
@echo "Creating .env..."
40+
cp -n .env.example .env || true
41+
@echo "Done! Run 'make dev' to start."

backend/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
8+
COPY . .
9+
10+
EXPOSE 8000
11+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

backend/app/__init__.py

Whitespace-only changes.

backend/app/auth.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
from datetime import datetime, timedelta, timezone
2+
from fastapi import Depends, HTTPException, status, Request
3+
from jose import JWTError, jwt
4+
import bcrypt
5+
6+
from app.config import settings
7+
from app.database import get_db
8+
9+
ALGORITHM = "HS256"
10+
TOKEN_EXPIRE_HOURS = 24
11+
12+
13+
def hash_password(password: str) -> str:
14+
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
15+
16+
17+
def verify_password(password: str, hashed: str) -> bool:
18+
return bcrypt.checkpw(password.encode(), hashed.encode())
19+
20+
21+
def create_token(user_id: int, username: str, role: str) -> str:
22+
expire = datetime.now(timezone.utc) + timedelta(hours=TOKEN_EXPIRE_HOURS)
23+
payload = {
24+
"sub": str(user_id),
25+
"username": username,
26+
"role": role,
27+
"exp": expire,
28+
}
29+
return jwt.encode(payload, settings.SECRET_KEY, algorithm=ALGORITHM)
30+
31+
32+
async def get_current_user(request: Request):
33+
token = None
34+
35+
# Check Authorization header
36+
auth_header = request.headers.get("Authorization")
37+
if auth_header and auth_header.startswith("Bearer "):
38+
token = auth_header[7:]
39+
40+
# Check cookie
41+
if not token:
42+
token = request.cookies.get("token")
43+
44+
if not token:
45+
raise HTTPException(
46+
status_code=status.HTTP_401_UNAUTHORIZED,
47+
detail="Not authenticated",
48+
)
49+
50+
try:
51+
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
52+
user_id = int(payload["sub"])
53+
except (JWTError, KeyError, ValueError):
54+
raise HTTPException(
55+
status_code=status.HTTP_401_UNAUTHORIZED,
56+
detail="Invalid token",
57+
)
58+
59+
db = await get_db()
60+
row = await db.execute_fetchall(
61+
"SELECT id, username, role FROM users WHERE id = ?", (user_id,)
62+
)
63+
if not row:
64+
raise HTTPException(
65+
status_code=status.HTTP_401_UNAUTHORIZED,
66+
detail="User not found",
67+
)
68+
return dict(row[0])
69+
70+
71+
async def require_admin(user=Depends(get_current_user)):
72+
if user["role"] != "admin":
73+
raise HTTPException(status_code=403, detail="Admin required")
74+
return user
75+
76+
77+
async def ensure_admin_exists():
78+
db = await get_db()
79+
rows = await db.execute_fetchall("SELECT id FROM users WHERE role = 'admin'")
80+
if not rows:
81+
pw_hash = hash_password(settings.ADMIN_PASS)
82+
await db.execute(
83+
"INSERT INTO users (username, password_hash, role) VALUES (?, ?, 'admin')",
84+
(settings.ADMIN_USER, pw_hash),
85+
)
86+
await db.commit()

backend/app/config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from pydantic_settings import BaseSettings
2+
from pathlib import Path
3+
4+
5+
class Settings(BaseSettings):
6+
SECRET_KEY: str = "change-me-to-random-string"
7+
ADMIN_USER: str = "admin"
8+
ADMIN_PASS: str = "admin"
9+
10+
DATA_DIR: str = "./data"
11+
DB_PATH: str = "./data/just-wiki.db"
12+
MEDIA_DIR: str = "./data/media"
13+
14+
VITE_API_URL: str = "http://localhost:8000"
15+
16+
AI_ENABLED: bool = False
17+
GEMINI_API_KEY: str = ""
18+
19+
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
20+
21+
22+
settings = Settings()
23+
24+
# Ensure directories exist
25+
Path(settings.DATA_DIR).mkdir(parents=True, exist_ok=True)
26+
Path(settings.MEDIA_DIR).mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)