Skip to content

Conversation

@MohamedBassem
Copy link
Collaborator

Added the ability for admins to mark/unmark users as email verified:

  • Updated updateUser tRPC endpoint to accept emailVerified boolean
  • Modified endpoint to set emailVerified timestamp when true, null when false
  • Added emailVerified field to users.list output schema
  • Added Switch component to UpdateUserDialog for toggling email verification
  • Updated UserList to pass emailVerified status to edit modal

Added the ability for admins to mark/unmark users as email verified:
- Updated updateUser tRPC endpoint to accept emailVerified boolean
- Modified endpoint to set emailVerified timestamp when true, null when false
- Added emailVerified field to users.list output schema
- Added Switch component to UpdateUserDialog for toggling email verification
- Updated UserList to pass emailVerified status to edit modal
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 15, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds email verification state to admin user management: UI toggle in UpdateUserDialog, prop passing from UserList, API schema/type additions, router handling to persist emailVerified (timestamp or null), and OpenAPI request update.

Changes

Cohort / File(s) Summary
Admin UI
apps/web/components/admin/UpdateUserDialog.tsx, apps/web/components/admin/UserList.tsx
Adds `currentEmailVerified: Date
Shared types / Schema
packages/shared/types/admin.ts
Adds optional boolean emailVerified to the updateUserSchema input shape.
Admin router
packages/trpc/routers/admin.ts
When input.emailVerified is defined, sets updateData.emailVerified to current timestamp if true or null if false; otherwise leaves unchanged.
Users router
packages/trpc/routers/users.ts
Exposes emailVerified on user list responses as z.date().nullable() (date or null).
OpenAPI spec
packages/open-api/karakeep-openapi-spec.json
Adds boolean emailVerified field to the admin user update request body schema (PUT /admin/users/{userId}).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify consistent typing between frontend (boolean), shared schema (boolean), router input handling (expects boolean), and stored/exposed value (Date | null) in users router.
  • Check UI default/value mapping: Date | null → boolean default and that the Switch updates the submitted boolean.
  • Confirm database update logic sets timestamps in the expected timezone/format and that nulling behaves correctly.
  • Review OpenAPI change aligns with actual request handling and generated clients.

Pre-merge checks

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding admin capability to mark users as email verified.
Description check ✅ Passed The description is directly related to the changeset, providing specific details about the implementation across multiple files and components.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
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)
apps/web/components/admin/UpdateUserDialog.tsx (1)

93-97: Consider adding defaultValues to dependency array or recalculating inside effect.

The defaultValues object (defined at lines 59-65) is used inside this effect but not included in the dependency array. While this follows the existing pattern for other fields, it could lead to the form resetting with stale prop values if props change while the dialog is closed.

This would ensure the form always resets with current prop values:

 useEffect(() => {
   if (isOpen) {
-    form.reset(defaultValues);
+    form.reset({
+      userId,
+      role: currentRole,
+      bookmarkQuota: currentQuota,
+      storageQuota: currentStorageQuota,
+      emailVerified: currentEmailVerified !== null,
+    });
   }
-}, [isOpen, form]);
+}, [isOpen, form, userId, currentRole, currentQuota, currentStorageQuota, currentEmailVerified]);

Note: This is an existing pattern issue affecting all fields, not introduced by this PR.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6ef25 and f93d694.

📒 Files selected for processing (5)
  • apps/web/components/admin/UpdateUserDialog.tsx (4 hunks)
  • apps/web/components/admin/UserList.tsx (1 hunks)
  • packages/shared/types/admin.ts (1 hunks)
  • packages/trpc/routers/admin.ts (1 hunks)
  • packages/trpc/routers/users.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: typecheck
  • GitHub Check: tests
  • GitHub Check: format
🔇 Additional comments (7)
packages/shared/types/admin.ts (1)

17-17: LGTM!

The schema correctly defines emailVerified as an optional boolean field, consistent with the UI toggle control and backend logic.

packages/trpc/routers/users.ts (1)

74-74: LGTM!

The output schema correctly defines emailVerified as a nullable number (timestamp), matching the database storage format where Date.now() is stored for verified users and null for unverified users.

apps/web/components/admin/UserList.tsx (1)

148-148: LGTM!

The prop correctly passes the emailVerified value to the UpdateUserDialog, following the established naming convention of other current* props.

apps/web/components/admin/UpdateUserDialog.tsx (4)

29-29: LGTM!

Appropriate import of the Switch component for the email verification toggle.


46-46: LGTM!

The props interface and function signature are correctly extended to accept currentEmailVerified with the proper type (number | null), matching the API response shape.

Also applies to: 54-54


64-64: LGTM!

The conversion from nullable timestamp to boolean (currentEmailVerified !== null) correctly represents verification state for form initialization.


180-196: LGTM!

The FormField implementation for email verification is well-structured with proper form control binding and appropriate use of the Switch component for boolean toggle.

Comment on lines 372 to 374
if (input.emailVerified !== undefined) {
updateData.emailVerified = input.emailVerified ? Date.now() : null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Timestamp is overwritten unnecessarily, losing verification history.

When input.emailVerified is true, the code always sets a new timestamp via Date.now(), even if the user was already verified. This loses the original verification timestamp whenever an admin updates the user (even when not intending to change email verification status).

Consider one of these alternatives:

  1. Preserve existing timestamp when already verified:
 if (input.emailVerified !== undefined) {
-  updateData.emailVerified = input.emailVerified ? Date.now() : null;
+  if (input.emailVerified) {
+    // Only set new timestamp if not already verified
+    const user = await ctx.db.query.users.findFirst({
+      where: eq(users.id, input.userId),
+      columns: { emailVerified: true }
+    });
+    updateData.emailVerified = user?.emailVerified ?? Date.now();
+  } else {
+    updateData.emailVerified = null;
+  }
 }
  1. Or track form changes to only submit modified fields (requires UpdateUserDialog.tsx changes to detect which fields were actually changed)

This preserves audit trails and original verification dates, which may be important for compliance or business logic.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/trpc/routers/admin.ts around lines 372-374, the code unconditionally
overwrites the emailVerified timestamp with Date.now() whenever
input.emailVerified is true, losing the original verification timestamp; change
the logic so you only set Date.now() when transitioning from unverified to
verified (i.e., input.emailVerified === true AND the existing user.emailVerified
is falsy), preserve the existing timestamp if the user was already verified, and
when input.emailVerified is false set it to null; to implement this either load
the current user record before applying updateData or make the update call
conditional based on the previous value so you don't overwrite a pre-existing
timestamp.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 71 to 77
localUser: z.boolean(),
bookmarkQuota: z.number().nullable(),
storageQuota: z.number().nullable(),
emailVerified: z.number().nullable(),
}),
),
}),

Choose a reason for hiding this comment

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

P1 Badge Return correct type for emailVerified

The new users.list output schema declares emailVerified as z.number().nullable() (packages/trpc/routers/users.ts lines 62‑74), but the query returns users.map((u) => u.asPublicUser()) where asPublicUser exposes the raw users.emailVerified column. That column is defined as a timestamp_ms in packages/db/schema.ts, so Drizzle returns a Date | null, and the router is configured with superjson to serialize Dates. Because the procedure has an .output() parser, Zod will now try to coerce a Date into a number and throw whenever any user actually has a verified email, breaking the entire admin users list for normal deployments. The output schema either needs to change to z.date().nullable() or the resolver needs to convert the Date into a number before returning it.

Useful? React with 👍 / 👎.

@MohamedBassem MohamedBassem changed the title feat: Add admin ability to mark user as email vefified feat: Add admin ability to mark user as email verified Nov 15, 2025
Copy link
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: 0

♻️ Duplicate comments (1)
packages/trpc/routers/admin.ts (1)

372-374: Timestamp overwrite issue remains unaddressed.

The concern raised in the previous review is still present: setting new Date() unconditionally when input.emailVerified is true overwrites the original verification timestamp, losing audit history. Consider preserving the existing timestamp if the user is already verified.

🧹 Nitpick comments (1)
packages/open-api/karakeep-openapi-spec.json (1)

572-573: Consider adding a description for clarity.

The emailVerified field lacks a description. Adding one would improve API discoverability and help consumers understand that this boolean controls whether the user's email is marked as verified.

Example:

 "emailVerified": {
+  "description": "Set to true to mark the user's email as verified, or false to unmark it",
   "type": "boolean"
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f93d694 and 6b0820b.

📒 Files selected for processing (4)
  • apps/web/components/admin/UpdateUserDialog.tsx (4 hunks)
  • packages/open-api/karakeep-openapi-spec.json (1 hunks)
  • packages/trpc/routers/admin.ts (1 hunks)
  • packages/trpc/routers/users.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/trpc/routers/users.ts
  • apps/web/components/admin/UpdateUserDialog.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: tests
  • GitHub Check: typecheck

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.

3 participants