From 3b8451fa1c271d4f73a3650bde2aa9ca66ef97d1 Mon Sep 17 00:00:00 2001 From: cyberbread-ru <186821264+cyberbread-ru@users.noreply.github.com> Date: Fri, 15 May 2026 16:06:28 +0400 Subject: [PATCH] initial vibecoded... thing --- .gitignore | 34 ++++++++++++++++++ app/__init__.py | 0 app/auth.py | 25 ++++++++++++++ app/models.py | 25 ++++++++++++++ app/routes.py | 45 ++++++++++++++++++++++++ app/services.py | 24 +++++++++++++ app/store.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 25 ++++++++++++++ requirements.txt | 3 ++ 9 files changed, 270 insertions(+) create mode 100644 .gitignore create mode 100644 app/__init__.py create mode 100644 app/auth.py create mode 100644 app/models.py create mode 100644 app/routes.py create mode 100644 app/services.py create mode 100644 app/store.py create mode 100644 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9952ba1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.pyd +.Python +*.egg-info/ +dist/ +build/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# Database +*.db +*.sqlite3 + +# Environment & secrets +.env +.env.* + +# Logs +*.log + +# OS +.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..08ee9fc --- /dev/null +++ b/app/auth.py @@ -0,0 +1,25 @@ +import os +import secrets +from fastapi import Security, HTTPException, status +from fastapi.security import APIKeyHeader + +API_KEY_NAME = "X-API-Key" +_header_scheme = APIKeyHeader(name=API_KEY_NAME, auto_error=False) + + +def _load_key() -> str: + key = os.getenv("API_KEY") + if not key: + raise RuntimeError("API_KEY environment variable is not set") + return key + + +def verify_api_key(api_key: str = Security(_header_scheme)) -> str: + expected = _load_key() + if not api_key or not secrets.compare_digest(api_key, expected): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or missing API key", + headers={"WWW-Authenticate": "X-API-Key"}, + ) + return api_key diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..1719d9c --- /dev/null +++ b/app/models.py @@ -0,0 +1,25 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from uuid import uuid4 + + +class NoteBase(BaseModel): + title: str + content: str + + +class NoteCreate(NoteBase): + pass + + +class NoteUpdate(NoteBase): + pass + + +class Note(NoteBase): + id: str + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..04651c4 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter, Depends, HTTPException + +from app.auth import verify_api_key +from app.models import Note, NoteCreate, NoteUpdate +from app.services import NoteService + +router = APIRouter( + prefix="/notes", + tags=["notes"], + dependencies=[Depends(verify_api_key)], +) +_service = NoteService() + + +@router.get("/", response_model=list[Note]) +def get_notes(): + return _service.list() + + +@router.get("/{note_id}", response_model=Note) +def get_note(note_id: str): + note = _service.get(note_id) + if not note: + raise HTTPException(status_code=404, detail="Note not found") + return note + + +@router.post("/", response_model=Note, status_code=201) +def create_note(payload: NoteCreate): + return _service.create(payload.title, payload.content) + + +@router.put("/{note_id}", response_model=Note) +def update_note(note_id: str, payload: NoteUpdate): + note = _service.update(note_id, payload.title, payload.content) + if not note: + raise HTTPException(status_code=404, detail="Note not found") + return note + + +@router.delete("/{note_id}", status_code=204) +def delete_note(note_id: str): + deleted = _service.remove(note_id) + if not deleted: + raise HTTPException(status_code=404, detail="Note not found") diff --git a/app/services.py b/app/services.py new file mode 100644 index 0000000..d37d4a7 --- /dev/null +++ b/app/services.py @@ -0,0 +1,24 @@ +from typing import Optional + +from app.models import Note +from app.store import NoteStore + + +class NoteService: + def __init__(self): + self._store = NoteStore() + + def list(self) -> list[Note]: + return self._store.find_all() + + def get(self, note_id: str) -> Optional[Note]: + return self._store.find_by_id(note_id) + + def create(self, title: str, content: str) -> Note: + return self._store.insert(title, content) + + def update(self, note_id: str, title: str, content: str) -> Optional[Note]: + return self._store.update(note_id, title, content) + + def remove(self, note_id: str) -> bool: + return self._store.delete(note_id) diff --git a/app/store.py b/app/store.py new file mode 100644 index 0000000..1bad07d --- /dev/null +++ b/app/store.py @@ -0,0 +1,89 @@ +import sqlite3 +from contextlib import contextmanager +from datetime import datetime +from typing import Optional +from uuid import uuid4 + +from app.models import Note + +DB_PATH = "notes.db" + + +def init_db(): + with get_conn() as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS notes ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + content TEXT NOT NULL, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """) + + +@contextmanager +def get_conn(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + try: + yield conn + conn.commit() + finally: + conn.close() + + +def _row_to_note(row: sqlite3.Row) -> Note: + return Note( + id=row["id"], + title=row["title"], + content=row["content"], + created_at=datetime.fromisoformat(row["created_at"]), + updated_at=datetime.fromisoformat(row["updated_at"]), + ) + + +class NoteStore: + def find_all(self) -> list[Note]: + with get_conn() as conn: + rows = conn.execute( + "SELECT * FROM notes ORDER BY updated_at DESC" + ).fetchall() + return [_row_to_note(r) for r in rows] + + def find_by_id(self, note_id: str) -> Optional[Note]: + with get_conn() as conn: + row = conn.execute( + "SELECT * FROM notes WHERE id = ?", (note_id,) + ).fetchone() + return _row_to_note(row) if row else None + + def insert(self, title: str, content: str) -> Note: + now = datetime.utcnow().isoformat() + note = Note( + id=str(uuid4()), + title=title, + content=content, + created_at=now, + updated_at=now, + ) + with get_conn() as conn: + conn.execute( + "INSERT INTO notes (id, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", + (note.id, note.title, note.content, note.created_at.isoformat(), note.updated_at.isoformat()), + ) + return note + + def update(self, note_id: str, title: str, content: str) -> Optional[Note]: + now = datetime.utcnow().isoformat() + with get_conn() as conn: + conn.execute( + "UPDATE notes SET title = ?, content = ?, updated_at = ? WHERE id = ?", + (title, content, now, note_id), + ) + return self.find_by_id(note_id) + + def delete(self, note_id: str) -> bool: + with get_conn() as conn: + cursor = conn.execute("DELETE FROM notes WHERE id = ?", (note_id,)) + return cursor.rowcount > 0 diff --git a/main.py b/main.py new file mode 100644 index 0000000..4dd32e7 --- /dev/null +++ b/main.py @@ -0,0 +1,25 @@ +import os +import secrets + +from fastapi import FastAPI +from app.routes import router +from app.store import init_db + +# If no API_KEY is set, generate one and print it (first-run helper) +if not os.getenv("API_KEY"): + generated = secrets.token_urlsafe(32) + os.environ["API_KEY"] = generated + print(f"\n⚠️ No API_KEY set — using generated key for this session:") + print(f" API_KEY={generated}") + print(" Set it permanently with: export API_KEY=\n") + +app = FastAPI(title="Notepad Backup Server", version="1.0.0") + +init_db() + +app.include_router(router) + + +@app.get("/health") +def health(): + return {"status": "ok"} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8157d14 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi==0.111.0 +uvicorn[standard]==0.29.0 +pydantic==2.7.1