Files
jimi-gallery/backend/main.py
yakenator 098b55e3b0 feat: initial Jimi Gallery prototype
- Public site (Home/Artists/Exhibitions/News/About/Contact) with EN/KO/JA i18n
- Admin panel with login, CRUD, image upload, multilingual editing
- Exhibition slider/lightbox view
- FastAPI + MongoDB backend, JWT auth
- Docker Compose deployment, behind nginx at jimi.yakenator.io
2026-04-25 12:47:36 +09:00

229 lines
6.7 KiB
Python

"""FastAPI app — Jimi Gallery API + static file serving."""
import os
from pathlib import Path
from fastapi import Body, Depends, FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from auth import ADMIN_PASSWORD, issue_token, require_auth
from db import artists, exhibitions, news, settings_col, strip_id
from seed import SEED
app = FastAPI(title="Jimi Gallery API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
# -------------------- bootstrap --------------------
@app.on_event("startup")
async def bootstrap():
"""Seed an empty DB on first run so the app has something to show."""
if await settings_col.find_one({"_id": "singleton"}) is None:
await settings_col.insert_one({"_id": "singleton", **SEED["settings"]})
if await artists.estimated_document_count() == 0 and SEED["artists"]:
await artists.insert_many([dict(a) for a in SEED["artists"]])
if await exhibitions.estimated_document_count() == 0 and SEED["exhibitions"]:
await exhibitions.insert_many([dict(e) for e in SEED["exhibitions"]])
if await news.estimated_document_count() == 0 and SEED["news"]:
await news.insert_many([dict(n) for n in SEED["news"]])
# -------------------- auth --------------------
@app.post("/api/auth/login")
async def login(body: dict = Body(...)):
if body.get("password") != ADMIN_PASSWORD:
raise HTTPException(status_code=401, detail="Incorrect password")
return {"token": issue_token()}
@app.get("/api/auth/me")
async def me(_: bool = Depends(require_auth)):
return {"ok": True}
# -------------------- settings --------------------
@app.get("/api/settings")
async def get_settings():
doc = await settings_col.find_one({"_id": "singleton"})
if doc is None:
doc = {"_id": "singleton", **SEED["settings"]}
await settings_col.insert_one(dict(doc))
return strip_id(doc)
@app.put("/api/settings")
async def put_settings(body: dict = Body(...), _: bool = Depends(require_auth)):
body.pop("_id", None)
await settings_col.update_one(
{"_id": "singleton"}, {"$set": body}, upsert=True
)
return strip_id(await settings_col.find_one({"_id": "singleton"}))
# -------------------- helpers for collections --------------------
def _require_id(body):
if not body.get("id"):
raise HTTPException(status_code=400, detail="id is required")
async def _list(coll):
return [strip_id(d) async for d in coll.find({})]
async def _get(coll, id):
d = await coll.find_one({"id": id})
if d is None:
raise HTTPException(status_code=404, detail="not found")
return strip_id(d)
async def _upsert(coll, id, body):
body.pop("_id", None)
body["id"] = id
await coll.update_one({"id": id}, {"$set": body}, upsert=True)
return strip_id(await coll.find_one({"id": id}))
async def _delete(coll, id):
await coll.delete_one({"id": id})
return {"ok": True}
# -------------------- artists --------------------
@app.get("/api/artists")
async def list_artists():
return await _list(artists)
@app.get("/api/artists/{id}")
async def get_artist(id: str):
return await _get(artists, id)
@app.put("/api/artists/{id}")
async def put_artist(id: str, body: dict = Body(...), _: bool = Depends(require_auth)):
return await _upsert(artists, id, body)
@app.delete("/api/artists/{id}")
async def delete_artist(id: str, _: bool = Depends(require_auth)):
return await _delete(artists, id)
# -------------------- exhibitions --------------------
@app.get("/api/exhibitions")
async def list_exhibitions():
return await _list(exhibitions)
@app.get("/api/exhibitions/{id}")
async def get_exhibition(id: str):
return await _get(exhibitions, id)
@app.put("/api/exhibitions/{id}")
async def put_exhibition(id: str, body: dict = Body(...), _: bool = Depends(require_auth)):
return await _upsert(exhibitions, id, body)
@app.delete("/api/exhibitions/{id}")
async def delete_exhibition(id: str, _: bool = Depends(require_auth)):
return await _delete(exhibitions, id)
# -------------------- news --------------------
@app.get("/api/news")
async def list_news():
return await _list(news)
@app.get("/api/news/{id}")
async def get_news_item(id: str):
return await _get(news, id)
@app.put("/api/news/{id}")
async def put_news_item(id: str, body: dict = Body(...), _: bool = Depends(require_auth)):
return await _upsert(news, id, body)
@app.delete("/api/news/{id}")
async def delete_news_item(id: str, _: bool = Depends(require_auth)):
return await _delete(news, id)
# -------------------- admin: seed / export / import --------------------
async def _apply_full_set(settings_doc, artists_list, exhibitions_list, news_list):
await settings_col.delete_many({})
await artists.delete_many({})
await exhibitions.delete_many({})
await news.delete_many({})
s = dict(settings_doc)
s.pop("_id", None)
await settings_col.insert_one({"_id": "singleton", **s})
if artists_list:
await artists.insert_many([dict(a) for a in artists_list])
if exhibitions_list:
await exhibitions.insert_many([dict(e) for e in exhibitions_list])
if news_list:
await news.insert_many([dict(n) for n in news_list])
@app.post("/api/admin/seed")
async def apply_seed(_: bool = Depends(require_auth)):
await _apply_full_set(
SEED["settings"], SEED["artists"], SEED["exhibitions"], SEED["news"]
)
return {"ok": True}
@app.get("/api/admin/export")
async def export_all():
settings_doc = await settings_col.find_one({"_id": "singleton"}) or {}
return {
"settings": strip_id(settings_doc) or dict(SEED["settings"]),
"artists": [strip_id(d) async for d in artists.find({})],
"exhibitions": [strip_id(d) async for d in exhibitions.find({})],
"news": [strip_id(d) async for d in news.find({})],
}
@app.post("/api/admin/import")
async def import_all(body: dict = Body(...), _: bool = Depends(require_auth)):
missing = [
k for k in ("settings", "artists", "exhibitions", "news") if k not in body
]
if missing:
raise HTTPException(status_code=400, detail=f"missing keys: {missing}")
await _apply_full_set(
body["settings"], body["artists"], body["exhibitions"], body["news"]
)
return {"ok": True}
# -------------------- static file serving --------------------
STATIC_DIR = Path(
os.environ.get("STATIC_DIR", Path(__file__).resolve().parent.parent)
)
# Mounted last so /api/* routes defined above take precedence.
app.mount("/", StaticFiles(directory=str(STATIC_DIR), html=True), name="static")