Skip to content

feat: add PENDING_VERIFICATION status to identity domain#1979

Merged
bjcoombs merged 6 commits intodevelopfrom
auth-email-flows--1--pending-verification-status
Mar 27, 2026
Merged

feat: add PENDING_VERIFICATION status to identity domain#1979
bjcoombs merged 6 commits intodevelopfrom
auth-email-flows--1--pending-verification-status

Conversation

@bjcoombs
Copy link
Copy Markdown
Collaborator

Summary

  • Adds PENDING_VERIFICATION identity status for self-registration flows where email verification is required before login
  • New NewSelfRegisteredIdentity(tenantID, email, verificationRequired) constructor returns PENDING_VERIFICATION or ACTIVE depending on the flag
  • New Verify() method transitions PENDING_VERIFICATION -> ACTIVE
  • connector.Login now returns ErrEmailNotVerified (not silent false) for PENDING_VERIFICATION accounts, allowing callers to distinguish unverified from wrong-password
  • Proto IdentityStatus enum extended with IDENTITY_STATUS_PENDING_VERIFICATION = 5
  • Migration adds PENDING_VERIFICATION to the chk_identity_status CHECK constraint

Changes Made

  • services/identity/domain/identity.go - new status constant, constructor, and Verify() method
  • services/identity/domain/errors.go - ErrNotPendingVerification, ErrEmailNotVerified
  • services/identity/connector/connector.go - reject PENDING_VERIFICATION with ErrEmailNotVerified
  • services/identity/service/mappers.go - proto status mapper case
  • api/proto/meridian/identity/v1/identity.proto + regenerated .pb.go
  • services/identity/migrations/20260327000001_add_pending_verification_status.sql - CHECK constraint update

Testing

  • Unit tests for NewSelfRegisteredIdentity (both paths, invalid inputs)
  • Unit tests for Verify() from correct and all incorrect states
  • Unit test for RecordLoginAttempt rejecting PENDING_VERIFICATION
  • Unit test for connector.Login returning ErrEmailNotVerified for unverified account
  • Unit test for proto mapper returning IDENTITY_STATUS_PENDING_VERIFICATION

- Add IdentityStatusPendingVerification constant to identity domain
- Add NewSelfRegisteredIdentity constructor (returns PENDING_VERIFICATION
  or ACTIVE depending on verificationRequired flag)
- Add Verify() method transitioning PENDING_VERIFICATION -> ACTIVE
- Add ErrNotPendingVerification and ErrEmailNotVerified domain errors
- Update connector.Login to return ErrEmailNotVerified for
  PENDING_VERIFICATION accounts (rather than silent false)
- Add migration to extend chk_identity_status CHECK constraint
- Add IDENTITY_STATUS_PENDING_VERIFICATION = 5 to identity proto enum
- Update domainStatusToProto mapper for new status
- Tests for all new paths (constructor, Verify, connector rejection)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 37d23623-ec0b-4183-8662-c1d91a97c9c8

📥 Commits

Reviewing files that changed from the base of the PR and between 4f7e964 and b60efb3.

⛔ Files ignored due to path filters (1)
  • services/identity/migrations/atlas.sum is excluded by !**/*.sum
📒 Files selected for processing (2)
  • services/identity/migrations/20260327000001_drop_identity_status_constraint.sql
  • services/identity/migrations/20260327000002_add_pending_verification_status.sql
✅ Files skipped from review due to trivial changes (1)
  • services/identity/migrations/20260327000001_drop_identity_status_constraint.sql

📝 Walkthrough

Walkthrough

Introduces a new PENDING_VERIFICATION identity status for self-registered accounts awaiting email verification. Adds NewSelfRegisteredIdentity() constructor, Verify() method for lifecycle transition, and rejects login attempts for unverified identities. Includes updated database constraints, service mappers, and comprehensive test coverage across domain and connector layers.

Changes

Cohort / File(s) Summary
Proto & Enum Definition
api/proto/meridian/identity/v1/identity.proto
Added IdentityStatus enum value IDENTITY_STATUS_PENDING_VERIFICATION = 5 to represent self-registered identities awaiting email verification.
Domain Model & Errors
services/identity/domain/errors.go, services/identity/domain/identity.go
Added ErrNotPendingVerification and ErrEmailNotVerified error sentinels. Added IdentityStatusPendingVerification constant, NewSelfRegisteredIdentity() constructor for self-registered accounts, and Verify() method to transition from pending verification to active status. Updated status transition rules in Activate, Suspend, Lock, and RecordLoginAttempt to handle the new pending verification state.
Domain Tests
services/identity/domain/identity_test.go
Added comprehensive test coverage for NewSelfRegisteredIdentity() constructor validation, pending verification status initialization, Verify() method transitions, and login attempt rejection on pending identities.
Connector Login Logic
services/identity/connector/connector.go, services/identity/connector/connector_test.go
Updated (*Connector).Login() to explicitly handle IdentityStatusPendingVerification by rejecting login and returning ErrEmailNotVerified. Added test case validating rejection behavior for pending verification accounts.
Service Mappers
services/identity/service/mappers.go, services/identity/service/mappers_test.go
Added mapping case in domainStatusToProto to translate domain.IdentityStatusPendingVerification to proto enum IDENTITY_STATUS_PENDING_VERIFICATION. Added test coverage for pending verification status proto conversion.
Database Migrations
services/identity/migrations/20260327000001_drop_identity_status_constraint.sql, services/identity/migrations/20260327000002_add_pending_verification_status.sql
Two-step migration to update the identity table's chk_identity_status constraint, dropping the existing constraint and adding a new one that includes PENDING_VERIFICATION as a valid status value.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding a new PENDING_VERIFICATION status to the identity domain, which is the primary focus of the entire changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering the new status, constructor, verification method, login behavior changes, proto updates, and migrations with specific implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch auth-email-flows--1--pending-verification-status

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude Bot commented Mar 27, 2026

Claude Code Review

Commit: b60efb35 | CI: running (all checks pending)

Summary

Clean, well-scoped addition of PENDING_VERIFICATION identity status for self-registration flows. The domain model correctly enforces the new state through explicit transition guards, the connector distinguishes unverified accounts from wrong-password via ErrEmailNotVerified, and the CockroachDB migration is correctly split into drop+recreate to avoid same-name constraint conflicts. All previous review findings (table name mismatch, explicit status cases in state machine) have been addressed.

Risk Assessment

Area Level Detail
Blast radius Low Additive status; existing identities unaffected
Rollback Safe New migrations can be reversed (drop new constraint, restore old). Proto enum is additive/backward-compatible
Scale Low No new queries, no loops, no hot paths affected
Cross-system Low Proto enum extension is non-breaking; callers that dont handle the new status fall through to UNSPECIFIED
Migration Safe Correctly split across two files for CockroachDB; atlas.sum updated

Findings

Severity Location Description Status
-- -- No actionable findings --

Previously Flagged

Severity Location Description Status
Critical migrations/20260327000001_* Table name mismatch (identities vs identity) Resolved -- migration now uses "identity" and is split into two files
Improvement domain/identity.go Add explicit PENDING_VERIFICATION cases to state machine methods Resolved -- Activate(), Suspend(), Lock() all have explicit cases

coderabbitai[bot]
coderabbitai Bot previously requested changes Mar 27, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
services/identity/domain/identity.go (1)

72-98: Consider extracting shared constructor bootstrap logic to prevent drift.

NewSelfRegisteredIdentity and NewIdentity currently duplicate validation and initialization fields. A small internal helper would reduce future divergence risk.

♻️ Proposed refactor
+func newIdentityWithStatus(tenantID tenant.TenantID, email string, status IdentityStatus) (*Identity, error) {
+	if tenantID.IsEmpty() {
+		return nil, ErrTenantIDRequired
+	}
+	if !emailRegex.MatchString(email) {
+		return nil, ErrInvalidEmail
+	}
+
+	now := time.Now()
+	return &Identity{
+		id:        uuid.New(),
+		tenantID:  tenantID,
+		email:     email,
+		status:    status,
+		createdAt: now,
+		updatedAt: now,
+		version:   1,
+	}, nil
+}
+
 // NewIdentity creates a new identity in PENDING_INVITE status.
 func NewIdentity(tenantID tenant.TenantID, email string) (*Identity, error) {
-	if tenantID.IsEmpty() {
-		return nil, ErrTenantIDRequired
-	}
-	if !emailRegex.MatchString(email) {
-		return nil, ErrInvalidEmail
-	}
-
-	now := time.Now()
-	return &Identity{
-		id:        uuid.New(),
-		tenantID:  tenantID,
-		email:     email,
-		status:    IdentityStatusPendingInvite,
-		createdAt: now,
-		updatedAt: now,
-		version:   1,
-	}, nil
+	return newIdentityWithStatus(tenantID, email, IdentityStatusPendingInvite)
 }
@@
 func NewSelfRegisteredIdentity(tenantID tenant.TenantID, email string, verificationRequired bool) (*Identity, error) {
-	if tenantID.IsEmpty() {
-		return nil, ErrTenantIDRequired
-	}
-	if !emailRegex.MatchString(email) {
-		return nil, ErrInvalidEmail
-	}
-
 	status := IdentityStatusActive
 	if verificationRequired {
 		status = IdentityStatusPendingVerification
 	}
-
-	now := time.Now()
-	return &Identity{
-		id:        uuid.New(),
-		tenantID:  tenantID,
-		email:     email,
-		status:    status,
-		createdAt: now,
-		updatedAt: now,
-		version:   1,
-	}, nil
+	return newIdentityWithStatus(tenantID, email, status)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/identity/domain/identity.go` around lines 72 - 98, Both
NewSelfRegisteredIdentity and NewIdentity duplicate validation and bootstrap
fields; extract a small unexported helper (e.g., newIdentityBase or
buildIdentity) that accepts tenantID, email and status (or a flag like
verificationRequired) and performs tenantID.IsEmpty check, emailRegex
validation, sets id (uuid.New()), tenantID, email, status, createdAt/updatedAt
(time.Now()) and version=1, then returns *Identity, error; update
NewSelfRegisteredIdentity and NewIdentity to call this helper (preserving the
current logic that sets IdentityStatusPendingVerification when
verificationRequired is true and IdentityStatusActive otherwise).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@services/identity/migrations/20260327000001_add_pending_verification_status.sql`:
- Around line 4-7: The migration is targeting the wrong table name; update both
ALTER TABLE statements to reference the existing quoted singular table name
"identity" (not identities) so the CHECK constraint chk_identity_status is
dropped and re-added on the correct table; ensure the ALTER TABLE lines use
"identity" and keep the constraint name chk_identity_status and the same CHECK
values.

---

Nitpick comments:
In `@services/identity/domain/identity.go`:
- Around line 72-98: Both NewSelfRegisteredIdentity and NewIdentity duplicate
validation and bootstrap fields; extract a small unexported helper (e.g.,
newIdentityBase or buildIdentity) that accepts tenantID, email and status (or a
flag like verificationRequired) and performs tenantID.IsEmpty check, emailRegex
validation, sets id (uuid.New()), tenantID, email, status, createdAt/updatedAt
(time.Now()) and version=1, then returns *Identity, error; update
NewSelfRegisteredIdentity and NewIdentity to call this helper (preserving the
current logic that sets IdentityStatusPendingVerification when
verificationRequired is true and IdentityStatusActive otherwise).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4875f50a-931c-4474-b2e8-e60f2333907e

📥 Commits

Reviewing files that changed from the base of the PR and between ef0800d and 449015c.

⛔ Files ignored due to path filters (2)
  • api/proto/meridian/identity/v1/identity.pb.go is excluded by !**/*.pb.go, !**/*.pb.go
  • services/identity/migrations/atlas.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • api/proto/meridian/identity/v1/identity.proto
  • services/identity/connector/connector.go
  • services/identity/connector/connector_test.go
  • services/identity/domain/errors.go
  • services/identity/domain/identity.go
  • services/identity/domain/identity_test.go
  • services/identity/migrations/20260327000001_add_pending_verification_status.sql
  • services/identity/service/mappers.go
  • services/identity/service/mappers_test.go

Comment thread services/identity/migrations/20260327000001_add_pending_verification_status.sql Outdated
claude[bot]
claude Bot previously requested changes Mar 27, 2026
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See summary comment for full review. 1 critical finding (migration table name), 1 suggestion.

Comment thread services/identity/migrations/20260327000001_add_pending_verification_status.sql Outdated
Comment thread services/identity/domain/identity.go Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 92.30769% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
services/identity/domain/identity.go 90.62% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
services/identity/domain/identity.go (1)

207-222: Consider updating the doc comment to mention PENDING_VERIFICATION.

The code correctly rejects PENDING_VERIFICATION (line 218), but the doc comment on lines 207-208 only mentions LOCKED as an invalid source state. For completeness, consider updating it to reflect this restriction.

📝 Suggested doc update
 // Activate transitions the identity to ACTIVE status.
-// Valid from PENDING_INVITE or SUSPENDED; invalid from LOCKED.
+// Valid from PENDING_INVITE or SUSPENDED; invalid from LOCKED or PENDING_VERIFICATION.
 func (i *Identity) Activate() error {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@services/identity/domain/identity.go` around lines 207 - 222, Update the
Activate method doc comment to accurately list invalid source states by
including PENDING_VERIFICATION in addition to LOCKED; locate the comment above
the Identity.Activate method and revise the sentence that currently reads "Valid
from PENDING_INVITE or SUSPENDED; invalid from LOCKED." so it states that
activation is invalid from LOCKED and PENDING_VERIFICATION (or equivalent
wording referencing IdentityStatusLocked and IdentityStatusPendingVerification),
keeping it concise and consistent with the behavior that returns
ErrInvalidStatusTransition for those states.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@services/identity/domain/identity.go`:
- Around line 207-222: Update the Activate method doc comment to accurately list
invalid source states by including PENDING_VERIFICATION in addition to LOCKED;
locate the comment above the Identity.Activate method and revise the sentence
that currently reads "Valid from PENDING_INVITE or SUSPENDED; invalid from
LOCKED." so it states that activation is invalid from LOCKED and
PENDING_VERIFICATION (or equivalent wording referencing IdentityStatusLocked and
IdentityStatusPendingVerification), keeping it concise and consistent with the
behavior that returns ErrInvalidStatusTransition for those states.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 14e1a363-6c3d-4224-93ef-94adfd382256

📥 Commits

Reviewing files that changed from the base of the PR and between 449015c and f31b571.

⛔ Files ignored due to path filters (1)
  • frontend/src/api/gen/meridian/identity/v1/identity_pb.ts is excluded by !**/gen/**
📒 Files selected for processing (1)
  • services/identity/domain/identity.go

claude[bot]
claude Bot previously requested changes Mar 27, 2026
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See summary comment for full review. Migration table name bug from prior review remains unfixed.

Comment thread services/identity/migrations/20260327000001_add_pending_verification_status.sql Outdated
@bjcoombs bjcoombs dismissed stale reviews from coderabbitai[bot], claude[bot], and claude[bot] March 27, 2026 08:50

Stale bot review

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve. Clean addition of PENDING_VERIFICATION status with correct state machine guards, CockroachDB-compatible migration, and thorough test coverage. See summary comment for full analysis.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All findings from this and previous reviews are resolved. No blocking issues, no unresolved bot threads. See summary comment for full analysis.

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All previous findings resolved. Domain model, state transitions, connector behavior, migrations, and tests are correct. LGTM.

@bjcoombs bjcoombs merged commit 23d76da into develop Mar 27, 2026
46 checks passed
@bjcoombs bjcoombs deleted the auth-email-flows--1--pending-verification-status branch March 27, 2026 10:12
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