feat(spec): update stack required and jwt auth template (#236)
* feat(spec): update STACK_REQUIRED for svelte and quality tools * fix(templates): align jwt auth with issue contracts * docs(spec): update template c stack and migration note * fix(ci): format tests/test_spec_status.py --------- Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
parent
35b6c6e542
commit
51a5300423
9 changed files with 36 additions and 22 deletions
|
|
@ -70,12 +70,22 @@ PHASE_FILES: dict[int, str] = {
|
||||||
VALID_TYPES = {"backend", "fullstack", "mcp-server", "cli", "bot", "worker"}
|
VALID_TYPES = {"backend", "fullstack", "mcp-server", "cli", "bot", "worker"}
|
||||||
|
|
||||||
STACK_REQUIRED: dict[str, list[str]] = {
|
STACK_REQUIRED: dict[str, list[str]] = {
|
||||||
"backend": ["fastapi", "tortoise", "uv", "pytest", "ruff", "mypy"],
|
"backend": ["fastapi", "tortoise", "uv", "pytest", "ruff", "mypy", "loguru", "pydantic"],
|
||||||
"fullstack": ["fastapi", "tortoise", "react", "vite", "biome", "uv"],
|
"fullstack": [
|
||||||
|
"fastapi",
|
||||||
|
"tortoise",
|
||||||
|
"svelte",
|
||||||
|
"sveltekit",
|
||||||
|
"biome",
|
||||||
|
"uv",
|
||||||
|
"ruff",
|
||||||
|
"mypy",
|
||||||
|
"pytest",
|
||||||
|
],
|
||||||
"mcp-server": ["fastapi", "mcp", "patchright", "uv"],
|
"mcp-server": ["fastapi", "mcp", "patchright", "uv"],
|
||||||
"cli": ["typer", "uv", "hatchling"],
|
"cli": ["typer", "uv", "hatchling", "ruff", "mypy", "pytest"],
|
||||||
"bot": ["aiogram", "fastapi", "uv"],
|
"bot": ["aiogram", "fastapi", "uv", "ruff", "mypy", "pytest"],
|
||||||
"worker": ["prefect", "uv"],
|
"worker": ["prefect", "uv", "ruff", "mypy", "pytest"],
|
||||||
}
|
}
|
||||||
|
|
||||||
PHASE_NAMES = [
|
PHASE_NAMES = [
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,17 @@ truth для порядка и действий — `spec-status` tool. На в
|
||||||
|
|
||||||
Общий для всех типов: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI.
|
Общий для всех типов: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI.
|
||||||
|
|
||||||
- **backend**: FastAPI + uvicorn, Tortoise ORM + Aerich, pydantic-settings, Loguru
|
- **backend**: FastAPI + uvicorn, Tortoise ORM (встроенные миграции `tortoise makemigrations`, НЕ Aerich — legacy), Pydantic v2 + pydantic-settings, Loguru, опц. JWT-auth (`passlib[bcrypt]` + `pyjwt`)
|
||||||
- **fullstack**: backend + frontend/ (React 19 + Vite + Biome + TS strict + Vitest + Knip + happy-dom)
|
- **fullstack**: backend + frontend/ (SvelteKit + Svelte 5 runes (JS) + Biome + Vitest + Knip)
|
||||||
- **mcp-server**: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key
|
- **mcp-server**: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key
|
||||||
- **cli**: Typer (default) / click / argparse, hatchling build
|
- **cli**: Typer (default) / click / argparse, hatchling build
|
||||||
- **bot**: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
|
- **bot**: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
|
||||||
- **worker**: Prefect flows + tasks, prefect.yaml, docker-compose worker profile
|
- **worker**: Prefect flows + tasks, prefect.yaml, docker-compose worker profile
|
||||||
|
|
||||||
|
### Миграционная заметка (stack.md)
|
||||||
|
|
||||||
|
Если существующий `docs/spec/stack.md` содержит `react` или `aerich` (устаревшие значения до issue #231), spec-status пометит Phase 2 как NOT_DONE (`STACK_REQUIRED` теперь требует `svelte`/`sveltekit` для fullstack и не требует `aerich`). Обнови stack.md: замени `react` → `svelte`/`sveltekit`, удали `aerich` (Tortoise 1.0+ имеет встроенные миграции `tortoise makemigrations`).
|
||||||
|
|
||||||
## 9 фаз
|
## 9 фаз
|
||||||
|
|
||||||
### Phase 0: DETECT (subagent, без вопроса юзеру)
|
### Phase 0: DETECT (subagent, без вопроса юзеру)
|
||||||
|
|
@ -61,7 +65,7 @@ Prompt template A (см. ниже).
|
||||||
```
|
```
|
||||||
Выбери тип проекта:
|
Выбери тип проекта:
|
||||||
[1] backend — FastAPI + Tortoise, REST API, без frontend
|
[1] backend — FastAPI + Tortoise, REST API, без frontend
|
||||||
[2] fullstack — backend + React 19/Vite dashboard (monorepo)
|
[2] fullstack — backend + SvelteKit/Svelte 5 dashboard (monorepo)
|
||||||
[3] mcp-server — MCP + REST сервер (Patchright/Playwright over CDP)
|
[3] mcp-server — MCP + REST сервер (Patchright/Playwright over CDP)
|
||||||
[4] cli — Python CLI tool (Typer)
|
[4] cli — Python CLI tool (Typer)
|
||||||
[5] bot — Telegram bot (aiogram 3)
|
[5] bot — Telegram bot (aiogram 3)
|
||||||
|
|
@ -84,7 +88,7 @@ backend:
|
||||||
- Auth: [1] none v1 / [2] JWT / [3] X-API-Key
|
- Auth: [1] none v1 / [2] JWT / [3] X-API-Key
|
||||||
|
|
||||||
fullstack:
|
fullstack:
|
||||||
- frontend: [1] React 19 (default) / [2] SvelteKit / [3] add later
|
- frontend: [1] SvelteKit + Svelte 5 (default) / [2] add later
|
||||||
- DB: (same as backend)
|
- DB: (same as backend)
|
||||||
- Auth: (same as backend)
|
- Auth: (same as backend)
|
||||||
|
|
||||||
|
|
@ -231,8 +235,8 @@ Spec complete. Issues: #N1, #N2, ...
|
||||||
Тип проекта: <type> (из frontmatter meta.md).
|
Тип проекта: <type> (из frontmatter meta.md).
|
||||||
Default stack для типа (хардкод, добавить всегда):
|
Default stack для типа (хардкод, добавить всегда):
|
||||||
- Общий: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI
|
- Общий: Python 3.12+, uv, hatchling, ruff, mypy strict, pytest 90% cov, xenon, pre-commit, .editorconfig, .gitignore, LICENSE MIT, dependabot, CI
|
||||||
- backend: FastAPI + uvicorn, Tortoise ORM + Aerich, pydantic-settings, Loguru
|
- backend: FastAPI + uvicorn, Tortoise ORM (встроенные миграции `tortoise makemigrations`, НЕ Aerich), Pydantic v2 + pydantic-settings, Loguru, опц. JWT-auth (`passlib[bcrypt]` + `pyjwt`)
|
||||||
- fullstack: + frontend/ (React 19 + Vite + Biome + TS strict + Vitest + Knip + happy-dom)
|
- fullstack: + frontend/ (SvelteKit + Svelte 5 runes (JS) + Biome + Vitest + Knip)
|
||||||
- mcp-server: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key
|
- mcp-server: FastAPI + MCP SDK, Patchright/Playwright over CDP, X-API-Key
|
||||||
- cli: Typer (default) / click / argparse, hatchling build
|
- cli: Typer (default) / click / argparse, hatchling build
|
||||||
- bot: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
|
- bot: aiogram 3.x, FastAPI webhook/polling, Tortoise (опц.), Pydantic AI (опц.)
|
||||||
|
|
|
||||||
|
|
@ -45,5 +45,5 @@ async def get_current_user(
|
||||||
Returns the username/subject of the token. Only present when
|
Returns the username/subject of the token. Only present when
|
||||||
``use_auth == "yes"``.
|
``use_auth == "yes"``.
|
||||||
"""
|
"""
|
||||||
return await auth.get_current_user(credentials.credentials)
|
return await auth.decode_access_token(credentials.credentials)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -15,7 +15,7 @@ class User(Model):
|
||||||
username = fields.CharField(max_length=64, unique=True)
|
username = fields.CharField(max_length=64, unique=True)
|
||||||
email = fields.CharField(max_length=128, unique=True)
|
email = fields.CharField(max_length=128, unique=True)
|
||||||
{% if cookiecutter.use_auth == "yes" %}
|
{% if cookiecutter.use_auth == "yes" %}
|
||||||
hashed_password = fields.CharField(max_length=128)
|
hashed_password = fields.CharField(max_length=255)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class AuthService:
|
||||||
return _pwd.verify(plain, hashed)
|
return _pwd.verify(plain, hashed)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_token(username: str) -> str:
|
def create_access_token(username: str) -> str:
|
||||||
expire = datetime.now(timezone.utc) + timedelta(
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
minutes=settings.jwt.expire_minutes,
|
minutes=settings.jwt.expire_minutes,
|
||||||
)
|
)
|
||||||
|
|
@ -52,10 +52,10 @@ class AuthService:
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid credentials",
|
detail="Invalid credentials",
|
||||||
)
|
)
|
||||||
return AuthService.create_token(username)
|
return AuthService.create_access_token(username)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_current_user(token: str) -> str:
|
async def decode_access_token(token: str) -> str:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token, settings.jwt.secret, algorithms=[settings.jwt.algorithm],
|
token, settings.jwt.secret, algorithms=[settings.jwt.algorithm],
|
||||||
|
|
|
||||||
|
|
@ -45,5 +45,5 @@ async def get_current_user(
|
||||||
Returns the username/subject of the token. Only present when
|
Returns the username/subject of the token. Only present when
|
||||||
``use_auth == "yes"``.
|
``use_auth == "yes"``.
|
||||||
"""
|
"""
|
||||||
return await auth.get_current_user(credentials.credentials)
|
return await auth.decode_access_token(credentials.credentials)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -15,7 +15,7 @@ class User(Model):
|
||||||
username = fields.CharField(max_length=64, unique=True)
|
username = fields.CharField(max_length=64, unique=True)
|
||||||
email = fields.CharField(max_length=128, unique=True)
|
email = fields.CharField(max_length=128, unique=True)
|
||||||
{% if cookiecutter.use_auth == "yes" %}
|
{% if cookiecutter.use_auth == "yes" %}
|
||||||
hashed_password = fields.CharField(max_length=128)
|
hashed_password = fields.CharField(max_length=255)
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class AuthService:
|
||||||
return _pwd.verify(plain, hashed)
|
return _pwd.verify(plain, hashed)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_token(username: str) -> str:
|
def create_access_token(username: str) -> str:
|
||||||
expire = datetime.now(timezone.utc) + timedelta(
|
expire = datetime.now(timezone.utc) + timedelta(
|
||||||
minutes=settings.jwt.expire_minutes,
|
minutes=settings.jwt.expire_minutes,
|
||||||
)
|
)
|
||||||
|
|
@ -52,10 +52,10 @@ class AuthService:
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
detail="Invalid credentials",
|
detail="Invalid credentials",
|
||||||
)
|
)
|
||||||
return AuthService.create_token(username)
|
return AuthService.create_access_token(username)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_current_user(token: str) -> str:
|
async def decode_access_token(token: str) -> str:
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(
|
payload = jwt.decode(
|
||||||
token, settings.jwt.secret, algorithms=[settings.jwt.algorithm],
|
token, settings.jwt.secret, algorithms=[settings.jwt.algorithm],
|
||||||
|
|
|
||||||
|
|
@ -515,7 +515,7 @@ def test_check_stack_case_insensitive(monkeypatch, tmp_path):
|
||||||
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
spec_dir = _set_spec_dir(monkeypatch, tmp_path)
|
||||||
_write_meta(spec_dir, project="foo", type="backend")
|
_write_meta(spec_dir, project="foo", type="backend")
|
||||||
(spec_dir / "stack.md").write_text(
|
(spec_dir / "stack.md").write_text(
|
||||||
"- FastAPI\n- Tortoise ORM\n- UV\n- Pytest\n- Ruff\n- Mypy\n"
|
"- FastAPI\n- Tortoise ORM\n- UV\n- Pytest\n- Ruff\n- Mypy\n- Loguru\n- Pydantic\n"
|
||||||
)
|
)
|
||||||
result = ss.check_stack()
|
result = ss.check_stack()
|
||||||
assert result.status == ss.PhaseStatus.DONE
|
assert result.status == ss.PhaseStatus.DONE
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue