Phase 4 PR #15 — 컴포넌트 승인 워크플로우 (/approvals)#18
Merged
haksungjang merged 3 commits intoMay 8, 2026
Conversation
## Summary - 신규 component_approvals 테이블 + ENUM (approval_status) + Alembic 0008 - ComponentApproval 모델 (ETag 패턴: version 컬럼) - 5개 엔드포인트: GET /v1/approvals (list), GET /:id (detail+ETag), POST (create), PATCH /:id/transition (If-Match), DELETE - 상태 전이 매트릭스: pending → under_review/rejected → approved/rejected (terminal) - 권한 매트릭스: developer (자신 팀 보기/생성), team_admin (자신 팀 전이), super_admin (cross-team) - Existence-hide 패턴 (비-팀원 = 404) - Optimistic concurrency (SELECT FOR UPDATE + ETag echo) - Domain Problem extensions: approval_already_open / invalid_transition / etag_mismatch / terminal_state - Audit emit (생성/전이/삭제 모두) - 프론트: ApprovalsPage (필터/테이블/페이지네이션) + ApprovalsDrawer (조건부 액션 버튼) - EN/KO i18n approvals namespace 동시 추가 ## Test plan - [x] backend ruff clean - [x] frontend npx tsc --noEmit clean - [x] frontend vitest 419 tests pass (54 files), coverage 91% - [x] 백엔드 16+13 unit tests - [ ] CI 9/9 green (push 후 확인) 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…engine - CurrentUser annotation referenced before import (mypy name-defined error); hoisted import to module level. - Component model has no `version` or `kind` columns; updated fixture to use the actual schema (purl, name, package_type). - Module-scoped async db_session_factory caused asyncpg "another operation in progress" cross-loop errors under pytest-asyncio's function-scoped loop; changed to function-scope with a session-once alembic gate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- test_alembic_current_reports_head_revision: 0007 → 0008 after the component_approvals migration landed. - Frontend functions coverage dropped to 79.86% because Step 1 introduced Home.tsx (Navigate) and ScansPage.tsx (ComingSoon stub) without tests. Added minimal smoke tests for both → coverage back to 80.33%. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 8, 2026
haksungjang
added a commit
that referenced
this pull request
May 8, 2026
## Summary ### Notifications - 신규 notifications/ 모듈 - email.py: aiosmtplib 기반 SMTP 발송, MIME multipart (text + HTML alt) - slack.py: Slack Webhook POST (httpx.AsyncClient) - teams.py: MS Teams MessageCard - dispatcher.py: 통합 디스패처 (NotificationKind enum + per-channel dispatch) - tasks/notify.py: Celery task with autoretry_for + exponential backoff (max 5회) - 채널별 partial-success 보고 (어느 채널이 성공/실패했는지) ### Password Reset (Phase 6 미구현 부분) - POST /auth/forgot-password: uniform 204 (CWE-204) — 이메일 존재 여부 누설 X - POST /auth/reset-password: 토큰 + 새 비밀번호 (min_length 12) - services/password_reset_service.py: 1회용 토큰 (used_at), 1시간 만료 - 기존 unused 토큰 invalidate (새 요청 시) - email channel을 통해 reset link 발송 ### 환경변수 (core/config.py) - SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, SMTP_USE_TLS - SLACK_WEBHOOK_URL, TEAMS_WEBHOOK_URL - 모두 os.getenv() 런타임 호출 (CLAUDE.md §11) ## 보안 - token, password, secret 로그 X (PII 마스킹) - timing-attack 방지: forgot-password에서 dummy bcrypt - 토큰 평문 절대 저장 X (DB는 hash만) - 1회용 + 만료 검증 ## Test plan - [x] backend ruff clean - [x] backend mypy clean (226 source files) - [x] notifications unit tests (slack/teams/email/dispatcher/notify task) - [x] password reset integration tests - [ ] Frontend (다음 PR) - [ ] CI green (push 후 확인) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 8, 2026
* Phase 6 PR #18 (backend) — 알림 시스템 + 비밀번호 찾기 ## Summary ### Notifications - 신규 notifications/ 모듈 - email.py: aiosmtplib 기반 SMTP 발송, MIME multipart (text + HTML alt) - slack.py: Slack Webhook POST (httpx.AsyncClient) - teams.py: MS Teams MessageCard - dispatcher.py: 통합 디스패처 (NotificationKind enum + per-channel dispatch) - tasks/notify.py: Celery task with autoretry_for + exponential backoff (max 5회) - 채널별 partial-success 보고 (어느 채널이 성공/실패했는지) ### Password Reset (Phase 6 미구현 부분) - POST /auth/forgot-password: uniform 204 (CWE-204) — 이메일 존재 여부 누설 X - POST /auth/reset-password: 토큰 + 새 비밀번호 (min_length 12) - services/password_reset_service.py: 1회용 토큰 (used_at), 1시간 만료 - 기존 unused 토큰 invalidate (새 요청 시) - email channel을 통해 reset link 발송 ### 환경변수 (core/config.py) - SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM, SMTP_USE_TLS - SLACK_WEBHOOK_URL, TEAMS_WEBHOOK_URL - 모두 os.getenv() 런타임 호출 (CLAUDE.md §11) ## 보안 - token, password, secret 로그 X (PII 마스킹) - timing-attack 방지: forgot-password에서 dummy bcrypt - 토큰 평문 절대 저장 X (DB는 hash만) - 1회용 + 만료 검증 ## Test plan - [x] backend ruff clean - [x] backend mypy clean (226 source files) - [x] notifications unit tests (slack/teams/email/dispatcher/notify task) - [x] password reset integration tests - [ ] Frontend (다음 PR) - [ ] CI green (push 후 확인) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(test): resolve PR #22 CI failures (notify task, slack log, dispatcher, password-reset) - tests/unit/tasks/test_notify.py: Use task.__wrapped__ instead of task.run. bind=True auto-injects self via .run; passing _FakeTask() explicitly added a 6th positional arg. - tests/unit/notifications/test_slack.py: Silence httpx/httpcore loggers and filter caplog records to notifications.* only — httpx's INFO log echoes the full webhook URL, which is not our module's leak. - tests/unit/notifications/test_dispatcher.py: Override _send_email_channel with a real "no recipients raises NotificationDisabled" stub instead of trying to "restore" the already-noop'd version (autouse fixture had won). - tests/integration/test_auth_password_reset.py: Replace AsyncSession.expire_all() (which needs a greenlet-bound context) with await commit() to force fresh reads. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(test): split _run_notification helper for direct unit testing Celery's bind=True decorator overrides task.run with self-injection machinery and does not preserve __wrapped__, so neither approach (run nor __wrapped__) gives the test a way to call the body with an explicit fake self. Extract the body into a plain `_run_notification` function and have the Celery task delegate to it. Tests call `_run_notification` directly with the _FakeTask self. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
4 tasks
6 tasks
haksungjang
added a commit
that referenced
this pull request
May 26, 2026
`/admin/*` previously mounted under its own `AdminLayout` which rendered a
full 224px sidebar + 48px header in place of the AppShell. Entering Admin
therefore unmounted Dashboard / Projects / Scans / Approvals / Policies /
Integrations from the sidebar — users hit a dead-end where they couldn't
return to the rest of the portal without typing a URL.
Restructure the route table so `/admin/*` nests inside the AppShell route:
- `router.tsx`: move the entire admin Route group under the same `/`
AppShell parent. The AppShell sidebar already renders an "Administration"
section for super-admins (lines 188-199), so admin nav remains reachable
alongside the main nav at all times.
- `AdminLayout.tsx`: strip the now-redundant chrome (sidebar / header /
logout button / signed-in-as line). Reduced to the existence-hide guard
(returns `AdminNotFound` for non-super-admins) wrapping `<Outlet />`.
The `data-testid="admin-layout"` marker is preserved for harness compat.
- `AdminLayout.test.tsx`: drop assertions on the removed chrome
(`admin-sidebar`, `admin-nav-*`, `admin-logout`); retain the three guard
scenarios (super-admin / developer / no-user).
- `en/admin.json`, `ko/admin.json`: remove the unused `admin.layout.{title,
signed_in_as}` keys (no other call sites).
- `docs/plan-w4-ui-ia-overhaul.md`: new plan doc — single source of truth
for W4-A through W4-D.
- `docs/post-ga-execution-tracker.md`: mark W4-A "PR 대기".
Gates: typecheck clean · lint 0 errors · vitest 930 pass · i18n:check OK.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
haksungjang
added a commit
that referenced
this pull request
May 29, 2026
…lier compat) (#289) * feat(sbom): export licenses + real top-level version (SK Telecom supplier compat) SBOM export previously emitted NOASSERTION for every SPDX license and omitted CycloneDX `licenses` entirely, despite scans persisting real LicenseFinding rows — an oversight, not a deliberate choice. The top-level component version also used the scan UUID instead of a real software version. - Load licenses (`_load_scan_licenses`): join license_findings JOIN licenses, group by component_version + kind, deduped + sorted (byte-stable). - CycloneDX: per-component `licenses` from concluded -> declared -> detected; SPDX id -> license.id, else license.name. JSON + XML. - SPDX: licenseDeclared / licenseConcluded as SPDX expressions (concluded falls back to declared). SPDX-id-only; LicenseRef-* -> NOASSERTION (follow-up). copyrightText stays NOASSERTION (no source yet). - Top-level version: scan_metadata['release'] (Feature #18) when present, else scan id fallback -- no new input field. - Log generic_purls count to surface pkg:generic components (rejected by some supplier programs); root-cause fix out of scope (cdxgen/input limitation). - Tests: +13 (license id/name, concluded>declared, multi AND sorted, no-id NOASSERTION, no-license, byte-stability with licenses, version from release). - Docs (EN+KO) + ADR 0003. No schema change (data already persisted). Assessed against SK Telecom supplier SBOM requirements. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(sbom): document license export, top-level version, supplier compat Follow-up to the SBOM license/version export feature. Updates the user guide (EN + KO): - "What's included per component": licenses (concluded > declared > detected; SPDX expression vs NOASSERTION; copyrightText still NOASSERTION) and top-level version (scan_metadata.release with scan-id fallback). - "Supplier submission compatibility": SK Telecom supplier requirements coverage + caveats (pkg:generic rejection, LicenseRef NOASSERTION). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
component_approvals테이블 + Postgres ENUM (approval_status) + Alembic 0008/v1/approvals), ETag/If-Match optimistic concurrency, 상태 전이 매트릭스, RBAC, audit auto-emit/approvals페이지 + 드로어 (조건부 액션 버튼), TanStack Query, EN/KO i18napproval_already_open(409),approval_invalid_transition(409),approval_etag_mismatch(412),approval_terminal_state(409)State machine
Test plan
npx tsc --noEmitclean🤖 Generated with Claude Code