Skip to content

Conversation

@mkrokosz
Copy link

@mkrokosz mkrokosz commented Feb 2, 2026

Summary

Users who deleted their ClawHub account cannot sign in again with the same GitHub account. This PR fixes that by restoring self-deleted accounts on re-authentication.

The Problem

When a user deletes their account:

  1. deletedAt timestamp is set (soft-delete)
  2. User record remains in database with GitHub ID

When they try to sign in again:

  1. Convex Auth finds existing user by GitHub ID
  2. Session is created
  3. But me query filters deleted users: if (!user || user.deletedAt) return null
  4. Frontend sees "not logged in", shows sign-in button
  5. User is stuck - can never log in, no error message

The Fix

Adds a createOrUpdateUser callback in convex/auth.ts that:

  1. Detects soft-deleted users during OAuth
  2. Checks audit logs to determine if user was banned vs self-deleted
  3. If banned → throws error "This account has been suspended"
  4. If self-deleted → clears deletedAt to restore account

Security Consideration

Both deleteAccount and banUser set the same deletedAt field. This fix ensures banned users cannot restore their accounts by re-authenticating - only self-deleted users can restore.

Performance

The callback runs on every OAuth sign-in, but:

User State Extra Queries Notes
New user 0 Returns null immediately
Active user 0 Single if check on already-loaded field
Soft-deleted user 1 Indexed query on auditLogs.by_target

For 99% of logins (active users), there is zero performance impact. The audit log query only runs for soft-deleted users attempting to sign in, and uses the existing by_target index.

Testing

  1. Create account → Delete account → Try to sign in again → Should restore and log in
  2. Create account → Get banned by moderator → Try to sign in → Should see "suspended" error
  3. Normal active user signs in → No change in behavior

🤖 Generated with Claude Code

Greptile Overview

Greptile Summary

This PR adds a callbacks.createOrUpdateUser hook in convex/auth.ts to restore soft-deleted user records when the same GitHub account re-authenticates. The callback:

  • Lets Convex Auth create brand-new users (returns null when there is no existingUserId).
  • For existing, active users, returns early without extra queries.
  • For soft-deleted users, checks the auditLogs.by_target index for a user.ban record and blocks login with a suspension error if present; otherwise it clears deletedAt and updates updatedAt to restore the account.

This aligns the auth flow with the existing me/requireUser behavior that filters out deletedAt users, while preserving bans by treating ban audit logs as the source of truth.

Confidence Score: 4/5

  • This PR looks safe to merge and addresses the soft-delete re-login bug with minimal behavioral surface area.
  • The change is localized to the Convex Auth callback, follows existing ban semantics (via user.ban audit logs), and avoids extra queries for normal logins. The main concern is a minor schema/type mismatch (auditLogs.targetId is string while code compares against an Id<'users'>), which could make the ban check brittle depending on how IDs are serialized in practice.
  • convex/auth.ts

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Context used:

  • Context from dashboard - AGENTS.md (source)

Users who deleted their account were unable to sign in again with the
same GitHub account. The OAuth flow would complete but the user would
remain logged out due to the `deletedAt` field being set.

This fix adds a `createOrUpdateUser` callback that:
1. Detects soft-deleted users during OAuth
2. Checks audit logs to determine if user was BANNED vs SELF-DELETED
3. If banned → throws error "This account has been suspended"
4. If self-deleted → clears `deletedAt` to restore account

Security: Both `deleteAccount` and `banUser` set the same `deletedAt`
field. This fix ensures banned users cannot restore their accounts.

Performance: The callback runs on every sign-in, but the audit log
query ONLY executes for soft-deleted users (rare edge case). Normal
active users just hit a single `if` check - no extra queries. When
the audit log query does run, it uses the `by_target` index for
efficient lookup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link
Contributor

vercel bot commented Feb 2, 2026

@mkrokosz is attempting to deploy a commit to the Amantus Machina Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 47 to 50
.withIndex('by_target', (q) =>
q.eq('targetType', 'user').eq('targetId', userId)
)
.filter((q) => q.eq(q.field('action'), 'user.ban'))
Copy link

Choose a reason for hiding this comment

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

[P1] auditLogs.targetId is a v.string() in the schema, but this query compares it to userId (an Id<'users'>). Convex IDs are often string-like, but relying on implicit coercion makes this brittle (e.g., if the ID type changes or strict typing is enforced on the query builder).

Consider comparing against userId.toString() (or storing targetId as v.id('users') for user targets) so the index lookup is guaranteed to match.

Prompt To Fix With AI
This is a comment left during a code review.
Path: convex/auth.ts
Line: 47:50

Comment:
[P1] `auditLogs.targetId` is a `v.string()` in the schema, but this query compares it to `userId` (an `Id<'users'>`). Convex IDs are often string-like, but relying on implicit coercion makes this brittle (e.g., if the ID type changes or strict typing is enforced on the query builder).

Consider comparing against `userId.toString()` (or storing `targetId` as `v.id('users')` for user targets) so the index lookup is guaranteed to match.

How can I resolve this? If you propose a fix, please make it concise.

The auditLogs.targetId field is v.string() in the schema, so explicitly
convert the Id<'users'> to string to ensure type-safe comparison.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@mkrokosz mkrokosz force-pushed the fix/restore-soft-deleted-users-on-reauth branch from ec6a17d to 9fc36ba Compare February 2, 2026 22:17
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