first commit

This commit is contained in:
Johan
2026-02-02 13:56:58 +01:00
commit 5156438be5
11 changed files with 388 additions and 0 deletions

9
backend/Dockerfile Executable file
View File

@@ -0,0 +1,9 @@
FROM python:3.12
WORKDIR /app
COPY . /app/
RUN pip install -r requirements.txt
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

24
backend/app/db.py Executable file
View File

@@ -0,0 +1,24 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
# SQLite local file. In CI, you can swap to Postgres later.
DATABASE_URL = "sqlite:///./taskmanager.db"
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False}, # needed for SQLite + threads
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

106
backend/app/main.py Executable file
View File

@@ -0,0 +1,106 @@
import os
import yaml
from fastapi import FastAPI, Body, Depends, Header, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from sqlalchemy import select, text
from datetime import datetime, timezone
from .db import Base, engine, get_db
from .models import Task
from .schemas import TaskCreate, TaskUpdate, TaskOut
API_KEY = "devsecops-demo-secret-<a_remplacer>"
app = FastAPI(title="Task Manager API", version="1.0.0")
# Allow local frontend (file:// or http://localhost) during training
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # tighten later for “good practices”
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Base.metadata.create_all(bind=engine)
@app.get("/debug")
def debug():
return {"env": dict(os.environ)}
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/admin/stats")
def admin_stats(x_api_key: str | None = Header(default=None)):
if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Unauthorized")
return {"tasks": ""}
@app.post("/import")
def import_yaml(payload: str = Body(embed=True)):
data = yaml.full_load(payload)
return {"imported": True, "keys": list(data.keys()) if isinstance(data, dict) else "n/a"}
@app.get("/tasks", response_model=list[TaskOut])
def list_tasks(db: Session = Depends(get_db)):
tasks = db.execute(select(Task).order_by(Task.id.desc())).scalars().all()
return tasks
@app.post("/tasks", response_model=TaskOut, status_code=201)
def create_task(payload: TaskCreate, db: Session = Depends(get_db)):
task = Task(title=payload.title.strip(), description=payload.description, status="TODO")
db.add(task)
db.commit()
db.refresh(task)
return task
@app.get("/tasks/search", response_model=list[TaskOut])
def search_tasks(q: str = Query(""), db: Session = Depends(get_db)):
sql = text(f"SELECT * FROM tasks WHERE title LIKE '%{q}%' OR description LIKE '%{q}%'")
rows = db.execute(sql).mappings().all()
return [Task(**r) for r in rows]
@app.get("/tasks/{task_id}", response_model=TaskOut)
def get_task(task_id: int, db: Session = Depends(get_db)):
task = db.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
@app.put("/tasks/{task_id}", response_model=TaskOut)
def update_task(task_id: int, payload: TaskUpdate, db: Session = Depends(get_db)):
task = db.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
if payload.title is not None:
task.title = payload.title.strip()
if payload.description is not None:
task.description = payload.description
if payload.status is not None:
task.status = payload.status
task.updated_at = datetime.now(timezone.utc)
db.add(task)
db.commit()
db.refresh(task)
return task
@app.delete("/tasks/{task_id}", status_code=204)
def delete_task(task_id: int, db: Session = Depends(get_db)):
task = db.get(Task, task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
db.delete(task)
db.commit()
return None

15
backend/app/models.py Executable file
View File

@@ -0,0 +1,15 @@
from sqlalchemy import String, Integer, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from datetime import datetime, timezone
from .db import Base
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(String(1000), nullable=True)
status: Mapped[str] = mapped_column(String(20), nullable=False, default="TODO") # TODO/DOING/DONE
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))

25
backend/app/schemas.py Executable file
View File

@@ -0,0 +1,25 @@
from pydantic import BaseModel, Field
from datetime import datetime
class TaskCreate(BaseModel):
title: str = Field(min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=1000)
class TaskUpdate(BaseModel):
title: str | None = Field(default=None, min_length=1, max_length=200)
description: str | None = Field(default=None, max_length=1000)
status: str | None = Field(default=None, pattern="^(TODO|DOING|DONE)$")
class TaskOut(BaseModel):
id: int
title: str
description: str | None
status: str
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True

6
backend/requirements.txt Executable file
View File

@@ -0,0 +1,6 @@
fastapi==0.115.6
uvicorn[standard]==0.34.0
sqlalchemy==2.0.36
pydantic==2.10.3
jinja2==2.10.1
PyYAML==5.3.1

BIN
backend/taskmanager.db Executable file

Binary file not shown.