✒️ Quill

Bu saytni Claude Code bilan qanday qurganman

by ada · 2026-05-01 · edited 2026-05-01

Bu saytni Claude Code bilan qanday qurganman

O'qib turgan ushbu blog — loyihaning o'zi. Bu kichik portfolio uchun mo'ljallangan namuna ish bo'lib, men uni Anthropic'ning Claude Code (Claude uchun CLI) yordamida bir o'tirishda yig'ib chiqdim. Yozishimning maqsadi — ichkaridagi narsalarni ko'rsatish, shunda bu yozuvni o'qigan har kim uni qismlarga bo'lib o'rgana oladi.

Tizim shakli

Bitta port orqasidagi olti konteyner:

:8080 ──▶ gateway (nginx) ──┬─▶ /auth/*       auth (Flask)  ─┐
                            ├─▶ /api/*        blog (Flask)  ─┼─▶ postgres
                            ├─▶ /media/*      blog (static) ─┘
                            └─▶ qolgani       web (Flask + Jinja2 + HTMX)

docker-compose.yml ularni bir-biriga ulaydi. Nginx — bu yo'lning oldidagi yengil teskari proxy bo'lib, prefiks bo'yicha marshrutlaydi. Auth servis foydalanuvchilarni boshqaradi. Blog servis postlar va yuklangan rasmlar uchun. Web servis esa yagona HTML render qiladigan qism — u auth va blog bilan ichki Docker tarmog'i orqali muloqot qiladi. Postgres'da ikkita mantiqiy ma'lumotlar bazasi bor — har bir servis o'ziga tegishli sxemasiga egalik qiladi.

Aynan shu — chinakam mikroservis hikoyasi: har bir Flask ilovasi mustaqil deploy qilinadi, o'z migration tarixiga ega, va boshqalar bilan faqat HTTP orqali gaplashadi.

Servislar orasida auth qanday oqadi

Foydalanuvchi tizimga kiradi. Web servis /auth/login'ga POST qiladi. Auth servis bcrypt hashni tekshiradi va HS256 bilan, env'dan olingan umumiy maxfiy kalit yordamida imzolangan JWT qaytaradi. Web servis bu JWT'ni HttpOnly, SameSite=Lax sessiya cookie'sida saqlaydi.

Foydalanuvchi post yaratganda, web servis JWT'ni Authorization: Bearer <token> sarlavhasida blog servisga uzatadi. Blog servis JWT'ni lokal tarzda — xuddi shu umumiy maxfiy kalit bilan tasdiqlaydi, ya'ni har bir so'rovda auth'ga qaytib bormaydi. Bu standart JWT yondashuvi: bekor qilish moslashuvchanligini latency uchun almashtirish.

JWT'ning o'zi foydalanuvchi id'sini (sub da'vosi) va username'ni (biz issuance vaqtida qo'shadigan maxsus claim) olib yuradi. Blog servis ikkalasiga ham ishonadi, chunki imzo ularning auth'dan kelganligini isbotlaydi.

Pagination qanday qilingan

Pagination services/blog/app/routes.py ichida joylashgan:

@bp.get("/api/posts")
def list_posts():
    page = max(1, int(request.args.get("page", 1)))
    page_size = current_app.config["PAGE_SIZE"]
    stmt = select(Post).where(Post.published.is_(True)).order_by(desc(Post.created_at))
    total = db.session.execute(select(func.count()).select_from(stmt.subquery())).scalar_one()
    posts = db.session.execute(stmt.offset((page - 1) * page_size).limit(page_size)).scalars().all()
    return jsonify(items=[p.to_dict() for p in posts], page=page, page_size=page_size, total=total)

Ikkita query: bittasi sahifa bo'lagi uchun (offset + limit), ikkinchisi umumiy soni uchun (front-end "Sahifa 2 / 5" ni chiza olishi uchun). Web servis bu sonni iste'mol qiladi va Jinja shabloniga uzatadi, shablon esa orqaga/oldinga tugmalarini faqat ma'noli bo'lganda chizadi:

{% set total_pages = (total + page_size - 1) // page_size %}
{% if total_pages > 1 %}
  ...
{% endif %}

(total + page_size - 1) // page_size — bu standart "yuqoriga yaxlitlash" idiomasi: faqat butun sonlar bilan ceil(total / page_size) ni hisoblashning kalkulyatorsiz usuli.

Rasm yuklash qanday ishlaydi

O'xshab ko'ringan, lekin aslida boshqa-boshqa ikkita yo'l bor:

  1. To'g'ridan-to'g'ri API yuklash — Bearer token va multipart file maydoni bilan POST /api/images. Avtomatlashtirilgan chaqiriqchilar uchun.
  2. Brauzer muharriri yuklashi — EasyMDE Markdown muharriridagi rasm tugmasi web servisning /upload/image'iga POST qiladi, u esa blog servisga proxy qiladi. Brauzer faqat CSRF tokenni ko'radi; JWT esa HttpOnly cookie'da yashaydi va server tomonidan qo'shiladi.

Bu ikkinchi yo'l muhim: agar brauzer JWT'ni to'g'ridan-to'g'ri /api/images'ga yuborishi kerak bo'lganida, biz JWT'ni JavaScript'ga ochishimizga to'g'ri kelar edi, ya'ni har qanday uchinchi tomon kutubxonadagi XSS xatosi uni o'g'irlashi mumkin edi. Web servis orqali proxy qilish bilan JWT cookie'dan hech qachon chiqmaydi.

Blog servis yuklamani Pillow yordamida tasdiqlaydi:

img = PILImage.open(io.BytesIO(raw))
img.verify()                    # tuzilmaviy aql tekshiruvi
img = PILImage.open(io.BytesIO(raw))   # qayta ochish — verify() oqimni tugatadi
if img.format not in ALLOWED_FORMATS: raise ImageError(...)
if img.width > max_width:
    img = img.resize((max_width, int(img.height * max_width / img.width)), PILImage.LANCZOS)

Keyin yangi uuid4().hex fayl nomini yasaydi, /var/media'ga ulangan Docker named volume'ga yozadi va {"url": "/media/<filename>"} qaytaradi. Asl fayl nomi diskka tegmaydi — yo'l traversal va "men ../etc/passwd deb nomlangan faylni yukladim" kabi sho'xliklar shu tarzda bartaraf etiladi.

Xavfsiz Markdown render qilish qanday ishlaydi

Foydalanuvchi yozgan Markdown'ni xom holda render qilib bo'lmaydi — <script> hammaning brauzerida ishga tushadi. Ikki kutubxona bu ishni bajaradi:

raw_html = markdown.markdown(body, extensions=["fenced_code", "tables", "nl2br", "sane_lists"])
cleaned = bleach.clean(
    raw_html,
    tags=["a", "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li",
          "strong", "em", "code", "pre", "blockquote", "img",
          "table", "thead", "tbody", "tr", "th", "td", "span", "div", "br", "hr"],
    attributes={"a": ["href", "title", "rel"], "img": ["src", "alt", "title", "width", "height"], ...},
    protocols=["http", "https", "data", "mailto"],
    strip=True,
)

Allowlist sukut bo'yicha ehtiyotkor. <script> ro'yxatda yo'q, demak olib tashlanadi. onclick hech qaysi tegning atribut ro'yxatida yo'q, demak olib tashlanadi. URL sxemalari faqat ma'lum bo'lgan xavfsizlariga cheklangan, shuning uchun javascript:alert(1) o'lik havola bo'lib qoladi. Omon qolganlari — bu foydalanuvchi haqiqatan ham yozgan narsadir.

Test to'plami uning ishlashini qanday isbotlaydi

Ikki to'plam, ikkalasi ham tirik docker compose up stack'ga qarshi ishlaydi:

tests/e2e/test_api.py — qora quti HTTP. Register, login, /me bo'yicha tekshirish; anonim uchun yozish 401 beradi; muallif o'z postini to'liq CRUD qila oladi, lekin boshqa foydalanuvchi tahrir/o'chirishda 403 oladi; rasm yuklash baytlar rasm sifatida dekod qilinadigan /media/... URL qaytaradi; pagination kutilgan sonlarni beradi; slug to'qnashuvlari avtomatik suffiks oladi.

tests/e2e/test_browser.py — Playwright orqali haqiqiy Chromium. Register oqimi → bosh sahifa; noto'g'ri parol → ko'rinadigan flash; muharrir orqali post yaratish (CodeMirror API'siga to'g'ridan-to'g'ri yozib); tahrirlash; o'chirish; boshqa birovning postini tahrir qila olmaslik (403 sahifasi); anonim o'qiy oladi.

Ikkala to'plam ham bitta Make maqsadi orqali ulanadi:

make test-e2e

Bu stack'ni ko'taradi, healthcheck'larni kutadi, migrationlarni ishga tushiradi, ikkala to'plamni ishlatadi, keyin to'xtatadi. Biror narsa muvaffaqiyatsiz bo'lsa, nolga teng bo'lmagan kod bilan chiqadi.

Yordamchi nima qildi va men nima qildim

Arxitektura, fayllar tuzilishi, xavfsizlik tanlovlari (bleach allowlist, yuklamalar uchun JWT proxy, formaning yashirin EasyMDE textarea'si yuborilishni bloklamasligi uchun novalidate), test to'plami, editorial Tailwind ko'rinishi — bularning hammasi bitta Claude Code seansidan chiqdi. Men yo'naltirdim: men jilolangan portfolio asari so'radim, u savol bergan joylarda variantlar tanladim va atrofiga bosib ko'rganimdan keyin tuzatish kerak bo'lgan ikkita narsani aytdim (cover rasm URL joylashtirilishi emas, yuklanadigan bo'lishi kerak edi; email_validator bitta servisning requirements'idan tushib qolgani uchun ro'yxatdan o'tish formasi to'g'ri kiritishni rad etayotgan edi).

Qiziq narsa LLM kod yozgani emas. Qiziq narsa — ish jarayoni: tizimni tasvirlab berdim, aniqlovchi savollarga javob berdim, plan yozilishini tomosha qildim, plan bajarilishini tomosha qildim, jonli stack ishlatdim, atrofidan bosib ko'rdim, ikkita bug yozdim, ularning tuzatilishini tomosha qildim. Vaqti-vaqti bilan code review kerak bo'ladigan tez harakatlanuvchi hamkasb bilan ishlashga o'xshaydi.

Sinab ko'rmoqchi bo'lsangiz: https://claude.com/claude-code.