Skip to content

feat(auth): 이메일 인증 후 회원 생성 + 회장 회원 추가 - #448

Merged
choijunhuk merged 13 commits into
mainfrom
feat/preverified-signup
Sep 15, 2026
Merged

choijunhuk merged 13 commits into
mainfrom
feat/preverified-signup

Conversation

@choijunhuk

Copy link
Copy Markdown
Collaborator

변경 사항

  • 일반 회원가입은 pending_signups에만 임시 저장하고 이메일 인증 성공 시 members 생성
  • 인증 전 로그인·회원 목록·권한·작성자 검색에서 완전히 제외
  • 졸업생 명부의 G연도-ID도 인증 성공 트랜잭션에서만 확정
  • 인증코드 재전송은 pending 커밋 후 메일 발송, 실패 시 정확한 코드 버전만 정리
  • 만료 pending 24시간 정리 및 만료 이메일 즉시 재사용
  • 동시 같은 이메일 가입은 한 건 성공, 다른 한 건 409
  • 회장 전용 POST /api/admin/members와 회원 관리 회원 추가 폼
  • 관리자 추가는 인증 완료 상태, 임시 비밀번호 허용, 명부와 회원을 한 트랜잭션으로 생성
  • 관리자 직접 생성 역할은 준회원·회원·임원·부회장만 허용

호환성

기존 /signup → /email-verification/confirm-signup 순서와 응답 필드를 유지해 캐시된 가입 화면도 동작합니다.

검증

  • 백엔드 fresh 전체: 497 tests, 0 failures, PostgreSQL 전용 1 local skip
  • 웹 npm test, production build 통과
  • Playwright 전체 smoke: 61 passed
  • 실백엔드 auth E2E: 2 passed
  • Task별 구현/리뷰 및 전체 브랜치 보안·동시성 리뷰 완료
  • PR CI에서 PostgreSQL 16 마이그레이션 테스트가 실제 실행되고 skip=0인지 확인 예정

배포 후 운영 작업

현재 이메일 미인증 회원을 다시 조회해 승인된 대상만 기존 purge 경로로 삭제하고, V92·서비스 상태·감사로그를 검증합니다.

Separate pending signup data from real members and add a president-only direct member creation path.

Constraint: Preserve cached signup client request flow

Rejected: Create unverified members then expire them | still occupies the student ID and leaks pending accounts into member surfaces

Directive: Never store signup passwords or verification codes in plaintext

Confidence: high

Scope-risk: broad

Not-tested: Implementation follows after design approval
…oning

Plan the pending signup lifecycle, compatibility contract, admin creation, verification and production cleanup as independently testable tasks.

Constraint: General signup must not create members before verification

Directive: Execute each task test-first and keep production cleanup after V92 deployment

Confidence: high

Scope-risk: broad

Tested: Plan self-review for spec coverage, placeholders and interface consistency
Pending signup state now has its own persistence model, and roster signup validation can produce an immutable prepared result without claiming a graduate roster row until confirmation. This keeps unverified user-supplied data out of member identity tables while preserving the current AuthService lifecycle for later tasks.

Constraint: Task 1 only; no frontend, AuthController/AuthService lifecycle, admin creation, OpenAPI, production, or main changes.

Rejected: Reusing validateAndClaimGraduateSignup for preparation | it mutates the roster before email ownership is proven.

Confidence: high

Scope-risk: narrow

Directive: Do not wire pending signup into AuthService until the signup lifecycle task owns email send, resend, confirm, and rollback semantics.

Tested: ./gradlew test --tests '*PendingSignupPersistenceTest' --tests '*EligibleMemberServiceTest' --tests '*LatestMigrationsSmokeTest' --tests '*MigrationVersionTest'

Not-tested: Real PostgreSQL PostgresMigrationExecutionTest; COMS_PG_TEST_URL was not set locally.
The prepared-signup claim path now declares its own write transaction, and lock-taking repository methods are explicit ForUpdate queries instead of locking ordinary finder methods. This keeps non-locking reads safe while preserving pessimistic locking where confirmation mutates roster identity.

Constraint: Fix round 1 only; no Task 2, AuthService lifecycle, frontend, admin, OpenAPI, production, or main changes.

Rejected: Leaving lock annotations on ordinary finders | non-locking reads should not unexpectedly request pessimistic locks.

Confidence: high

Scope-risk: narrow

Tested: ./gradlew test --tests '*EligibleMemberSignupLockingTest' --tests '*PendingSignupPersistenceTest'

Tested: ./gradlew test --tests '*EligibleMemberServiceTest' --tests '*EligibleMemberSignupLockingTest' --tests '*PendingSignupPersistenceTest' --tests '*LatestMigrationsSmokeTest' --tests '*MigrationVersionTest'

Not-tested: Real PostgreSQL PostgresMigrationExecutionTest; COMS_PG_TEST_URL was not set locally.
Resend now commits the pending-code replacement before SMTP and uses a REQUIRES_NEW exact-hash cleanup that only expires the failed version, leaving newer pending rows intact.

Constraint: Task 2 fix scope only; preserve existing API sequence and AuthResponse fields
Rejected: Keep SMTP inside @transactional | rollback and cleanup semantics could touch uncommitted or stale pending state
Confidence: high
Scope-risk: narrow
Tested: ./gradlew test --tests '*PendingSignupServiceTest' --tests '*AuthControllerCookieTest' --tests '*AuthServiceTest' --tests '*LoginFailureRetentionJobTest' --tests '*AuthServiceLoginThrottleTest'
Tested: ./gradlew test
Direct admin member creation must not mint graduate-year numeric accounts or accidentally admit future enum roles. The service now delegates the current-student admission-year decision to EligibleMemberService, which already owns clock-based graduate classification, and AdminService uses an explicit role allowlist for creatable non-president roles.

Constraint: Task 3 fix round limited changes to Task 3 backend files and report

Rejected: Duplicate admission-year cutoff logic in AdminService | would drift from EligibleMemberService clock behavior

Confidence: high

Scope-risk: narrow

Tested: ./gradlew test --tests '*AdminServiceTest' --tests '*OperationsSecurityIntegrationTest'

Tested: ./gradlew test
Regenerated the OpenAPI document from the dev/H2 backend on the Task 5 isolated port so downstream clients see the settled pending-signup and admin-member contract surface.

Constraint: Task 5 local scope permits only backend/openapi.json changes and forbids push, PR, merge, deploy, production writes, or member deletion

Constraint: OpenAPI was captured from dev/H2 on server.port=18083 and the listener PID was stopped after capture

Confidence: high

Scope-risk: narrow

Tested: ./gradlew test

Tested: ./gradlew test --rerun-tasks

Tested: npm test

Tested: npm run build

Tested: npm run smoke

Tested: npm run test:e2e:backend

Not-tested: PR, merge, deployment, production health, production anonymous 403, and unverified-member cleanup are explicitly out of local-only scope
Expired pending rows with a candidate email are removed inside the signup start transaction before duplicate validation, while active conflicts and the lower(email) unique index remain intact. The admin create form now also has handler-level duplicate-submit protection and alert semantics for create errors.

Constraint: Keep the lower(email) pending signup unique index and do not alter spec, plan, push, PR, or production state.

Rejected: Waiting for nightly cleanup to free expired pending email conflicts | leaves a valid signup blocked by stale data.

Rejected: Rely only on disabled button state for admin create submit | React state can race two rapid submit events.

Confidence: high

Scope-risk: narrow

Tested: cd backend && ./gradlew test --tests '*PendingSignupServiceTest'

Tested: cd backend && ./gradlew test --tests '*PendingSignupServiceTest' --tests '*AuthControllerCookieTest' --tests '*AuthServiceTest' --tests '*LoginFailureRetentionJobTest' --tests '*AuthServiceLoginThrottleTest'

Tested: node --import tsx tests/preverifiedSignup.test.mjs && node --import tsx tests/adminAccessibility.test.mjs && npm run smoke -- --grep 'signup|president adds'

Tested: cd backend && ./gradlew test

Tested: npm test

Tested: npm run build && npm run smoke

Not-tested: PostgreSQL migration execution locally because COMS_PG_TEST_URL is not set; no push, PR, deployment, or production cleanup performed.
Hold bounded email serialization through transaction completion and convert only the named pending-email unique collision into the public conflict contract.

Constraint: The email monitor must span TransactionTemplate commit and remain bounded under attacker-controlled input.
Constraint: Keep uq_pending_signups_email_ci as the cross-instance authority.
Rejected: Per-email ConcurrentHashMap locks | unbounded growth from arbitrary candidate emails.
Rejected: Translate every DataIntegrityViolationException | would mask unrelated persistence defects.
Confidence: high
Scope-risk: narrow
Directive: Keep email locking outside the complete transactionTemplate.execute call and scope conflict translation to uq_pending_signups_email_ci.
Tested: PendingSignupServiceTest 14 tests, 0 failures; full backend 497 tests, 0 failures, 0 errors, 1 PostgreSQL-gated skip.
Not-tested: PostgreSQL migration execution because COMS_PG_TEST_URL is not set; no frontend, push, PR, or production actions.
Remove the accidentally tracked final-fix report from repository history while preserving the local evidence file under the worktree-local exclusion policy.

Constraint: Keep the local report file while removing only its tracked index entry.
Constraint: .superpowers/ exclusion remains in local Git metadata and must not be committed.
Confidence: high
Scope-risk: narrow
Tested: Verified one staged deletion, local artifact existence, and git check-ignore resolution.
Not-tested: Application tests were not run because this cleanup changes no production or test code.
@choijunhuk
choijunhuk merged commit f76e290 into main Sep 15, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant