Skip to content

feat: Implement comprehensive account and credential management system#10

Merged
FindMalek merged 81 commits intomainfrom
feat/account-page
Sep 30, 2025
Merged

feat: Implement comprehensive account and credential management system#10
FindMalek merged 81 commits intomainfrom
feat/account-page

Conversation

@FindMalek
Copy link
Copy Markdown
Owner

@FindMalek FindMalek commented Jun 22, 2025

🚀 Feature Overview

This PR introduces a complete account and credential management system with a redesigned dashboard interface, comprehensive CRUD operations, and enhanced user experience features.

✨ Key Features

📋 Account Management

  • Account listing page with search, filtering, and pagination
  • Individual account detail pages with full credential information
  • Breadcrumb navigation with dynamic resource selection

🔐 Credential Management System

  • Multiple view modes: Cards view and Grid view for flexible credential display
  • Advanced filtering and sorting with multi-criteria support (status, platform, tags)
  • Real-time search across credential properties
  • Bulk operations with item selection and actions dropdown
  • Inline editing with floating save toolbar
  • Tag management with visual tag display and selection

🎨 Enhanced UI/UX

  • Responsive design with adaptive layouts for all screen sizes
  • Loading states and skeletons for better perceived performance
  • Empty states with contextual messaging and call-to-action buttons
  • Status badges for visual credential status indication
  • Keyboard shortcuts with menu shortcut indicators
  • Accessibility improvements with proper ARIA labels and keyboard navigation

🏗️ Technical Improvements

  • 15+ new reusable components for credential management
  • Enhanced state management with custom hooks for filtering and sorting
  • Utility functions for colors, dates, and breadcrumb helpers
  • Database schema updates with required constraints for platform model
  • Package scoping and improved build scripts

🗂️ Major Changes

New Pages

  • accounts/page.tsx - Account listing with advanced filtering
  • accounts/[id]/page.tsx - Individual account detail view

New Component Architecture

  • Credential Management Suite: 10+ specialized components for credential handling
  • Advanced Filtering System: Multi-criteria filters with dynamic UI
  • View Controls: Flexible display options with sorting and configuration
  • Tag System: Visual tag management with selection capabilities
  • Action Components: Dropdown menus, toolbars, and bulk operations

Database & Schema Updates

  • Platform model enhancements with required logo and loginUrl fields
  • Updated tag model structure
  • Enhanced credential DTOs with comprehensive validation

📊 Impact

  • +3,924 lines added, -365 lines removed across 65 files
  • Improved user productivity with better credential organization
  • Enhanced scalability supporting future account management features
  • Better performance with optimized queries and loading states
  • Modern UX patterns following current design standards

🎯 Technical Highlights

  • Type-safe throughout with comprehensive TypeScript coverage
  • Server-side data fetching with proper error handling
  • Client-side state management for real-time interactions
  • Responsive design with mobile-first approach
  • Performance optimized with efficient data loading patterns

Preview: The Vercel deployment showcases the new account management interface with all features live and interactive.

Summary by CodeRabbit

  • New Features

    • Full Accounts: searchable/filterable credential views (cards/grid), detailed editable credential page, password editor, encrypted additional key‑value fields, container creation/selection, several dashboard placeholder pages.
  • Improvements

    • Deterministic tag colors, expanded icon set, multi‑filters & view controls, breadcrumb resource selector, floating save toolbar, refined date displays, improved copy‑to‑clipboard UX.
  • Security

    • Encrypted metadata/password flows, safer fetches for secrets, transactional operations for create/update.
  • UI/UX

    • Aggressive autofill/autosave prevention and consistent sizing utilities.
  • Documentation

    • Docker‑first README and new npm/docker scripts.

@FindMalek FindMalek self-assigned this Jun 22, 2025
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Jun 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
zero-locker Ready Ready Preview Comment Sep 30, 2025 9:53pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 22, 2025

Walkthrough

Adds comprehensive credential UI (list/detail/edit), encrypted credential key/value and password flows, deterministic tag colors, anti‑autofill protections and permission/default‑container system, many ORPC endpoints/hooks, Prisma schema migrations (key‑value pairs, plans, unique constraints), utilities, Docker config, and wide shared UI updates.

Changes

Cohort / File(s) Summary
Policies & Housekeeping
​.cursor/rules/general.mdc, TODO.md
Add project coding/accessibility rules; remove TODO.md.
Accounts Pages
app/(dashboard)/dashboard/accounts/page.tsx, app/(dashboard)/dashboard/accounts/[id]/page.tsx
Server components for accounts list and credential detail with metadata and suspense.
Credential List UI & Controls
components/app/dashboard-credential-client.tsx, components/app/dashboard-credential-{cards-view,grid-view}.tsx, components/app/dashboard-credentials-header.tsx, components/app/dashboard-credential-filters.tsx, components/shared/{dashboard-multi-filters,dashboard-view-controls}.tsx, hooks/{use-entity-filters,use-entity-sorting}.ts
New client credential listing with search, filters, multi-filter UI, view controls, sorting, cards/grid views and hooks.
Credential Detail & Editors
components/app/dashboard-credential-detail-view.tsx, components/app/dashboard-credential-detail-skeleton.tsx, components/app/dashboard-credential-{header,sidebar,footer,form,password-field,key-value-pairs,key-value-pairs-skeleton}.tsx, components/shared/{floating-save-toolbar,encrypted-key-value-display,key-value-pair-form}.tsx
New detail page, editable form, encrypted password field, secure key/value manager, sidebar/footer, skeletons and floating save toolbar.
Add-Credential Flow
components/app/dashboard-add-credential-dialog.tsx (removed components/app/dashboard-add-credential-form.tsx, components/app/dashboard-add-credential-metadata-form.tsx)
Replace old add dialog with two‑column encrypted flow; removed legacy form files.
Tag Colors & Helpers
config/consts.ts, lib/utils/color-helpers.ts, components/shared/tag-selector.tsx, lib/utils/tag-helpers.ts
Introduce TAG_COLOR_PALETTES and deterministic generateTagColor; migrate tag creation and tag helpers to use palettes and tx-aware DB client.
Date, Encryption & Utils
lib/date-utils.ts, lib/utils/index.ts, lib/utils/encryption-helpers.ts, lib/encryption.ts, lib/utils/default-containers.ts
Add date helpers, color exports, transaction-aware encrypted-data helpers, AES‑GCM defaults, generateSecureId, and default container creation utility.
Encryption Entities & Credential KV
entities/encryption/{entity,index}.ts, entities/credential/credential-key-value/{entity,query}.ts, entities/credential/*
Add EncryptedData select helper; new CredentialKeyValuePair entity/query; extend credential/metadata entity APIs and includes.
ORPC Routers & Hooks
orpc/routers/{credential,card,secret,user,container}.ts, orpc/hooks/{use-credentials,use-platforms,use-cards,use-secrets,use-containers,use-users}.ts, orpc/types.ts
Expand credential APIs (passwords, security settings, key‑value CRUD), transactional card/secret flows, user endpoints (getCurrentUser, initializeDefaultContainers), container default endpoints and enriched client hooks accepting options.
Permissions & Middleware
lib/permissions/{types,utils,index}.ts, lib/permissions/hooks/use-user-permissions.ts, middleware/permissions.ts
Add UserPlan enum usage, permission config/utilities, useUserPermissions hook, middleware for permission checks and default-container enforcement.
Containers UI
components/shared/container-selector.tsx, components/app/dashboard-create-container-dialog.tsx
Add ContainerSelector and create-container dialog; client flows and hooks tied to default containers.
Schemas & Types
schemas/credential/*, schemas/encryption/encryption.ts, schemas/utils/*, schemas/user/user.ts
Add credential include and form schemas, credential key‑value schemas, metadata keyValuePairs, platform/tag non-null updates, container types, and currentUser DTO.
Prisma Schema, Migrations & Seeds
prisma/schema/*, prisma/schema/migrations/*, prisma/seed/*
Add CredentialKeyValuePair model, unique constraints, UserPlan and container.isDefault, migrations and adjusted seed scripts.
Anti‑Autofill & Form Blocking
hooks/use-prevent-auto-save.ts, hooks/use-aggressive-form-blocker.ts, styles/globals.css, modified forms (various components)
Add hooks and CSS to prevent browser autofill/auto‑save, block Enter submissions, apply autocomplete/data attributes across vault forms.
Shared UI, Icons & Inputs
components/shared/icons.tsx, components/shared/{empty-state,item-actions-dropdown,status-badge,tag-display,menu-shortcut,upgrade-prompt}.tsx, components/ui/{input,badge,combobox-responsive,...} and many components
Large shared UI additions/updates: expanded Icons map, new components, input password variants (copy/generate), info badge variant, standardized size-* classes.
ComingSoon & Misc Pages
app/(dashboard)/dashboard/{logs,containers,iv,platforms,tags,cards,secrets}/page.tsx, app/not-found.tsx
Add placeholder ComingSoon pages, adjust stages, remove not-found image, unify icon sizing.
Docker & Project Config
docker-compose.yml, next.config.ts, package.json, README.md
Add docker-compose (Postgres + pgAdmin), update allowed image host, package rename and docker scripts, and Docker-focused README updates.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Browser
  participant RSC as Accounts Detail (RSC)
  participant Client as CredentialDetailView (Client)
  participant ORPC as oRPC Credential Router
  participant DB as Prisma/DB
  participant Crypto as Encryption Utils

  User->>Browser: open /dashboard/accounts/:id
  Browser->>RSC: request page
  RSC->>ORPC: server fetch credential + platforms (includes tags & metadata with encryption fields)
  ORPC->>DB: read credential + metadata (+ encrypted ids)
  DB-->>ORPC: credential data
  ORPC-->>RSC: credential + platforms
  RSC-->>Browser: render page + hydrate Client with initialData

  Browser->>Client: mounts, triggers client hooks
  Client->>ORPC: useCredential(id) (client-safe include)
  ORPC->>DB: read client-safe fields
  DB-->>ORPC: data
  ORPC-->>Client: hydrated data

  User->>Client: click "reveal password"
  Client->>ORPC: getCredentialPassword(id)
  ORPC->>DB: fetch encrypted value
  ORPC->>Crypto: decryptData (server-side)
  Crypto-->>ORPC: plaintext
  ORPC-->>Client: decrypted password

  User->>Client: save new password
  Client->>Crypto: generateEncryptionKey + encryptData (client)
  Crypto-->>Client: encrypted payload
  Client->>ORPC: updateCredentialPassword(payload)
  ORPC->>DB: transaction (create EncryptedData, credential history, update credential)
  DB-->>ORPC: ok
  ORPC-->>Client: success
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related PRs

  • Disable Browser Auto Save #14 — Prior work on autofill/autosave mitigation; overlaps with usePreventAutoSave, useAggressiveFormBlocker, CSS anti‑autofill rules and form isolation.
  • Feat/orpc #6 — Earlier oRPC migration and router/hook restructuring; related to expanded ORPC routers/hooks and client hooks changes.
  • Style/auth marketing #8 — UI and branding updates overlapping with Icons expansion and README/package changes.

Poem

In my burrow I code with care,
Keys encrypted, tags all fair.
Containers sprout where plans allow,
Filters, toolbars, Docker now.
A rabbit hops—deploy with flair! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly summarizes the main contribution of the changeset by focusing on the implementation of a comprehensive account and credential management system, aligns with the PR objectives, and omits unnecessary file lists or vague qualifiers.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/account-page

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
Copy Markdown

@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: 5

🧹 Nitpick comments (13)
app/(dashboard)/dashboard/accounts/page.tsx (1)

32-49: Log and trace fetch failures (don’t silently swallow).

Add a console/error logger (or Sentry) in catch to aid debugging/observability.

   } catch {
-    return {
+    console.error("AccountsPage:getInitialData failed to fetch initial data")
+    return {
       credentials: {
         credentials: [],
         total: 0,
         hasMore: false,
         page: 1,
         limit: 50,
       },
       platforms: {
         platforms: [],
         total: 0,
         hasMore: false,
         page: 1,
         limit: 100,
       },
     }
   }
schemas/utils/base-key-value-pair.ts (1)

3-11: Optional stricter variant for non-empty values.

Encryption paths require non-empty value; export a stricter schema to avoid runtime checks downstream.

 export const baseKeyValuePairSchema = z.object({
   id: z.string().optional(),
   key: z.string().min(1, "Key is required"),
   value: z.string(),
   createdAt: z.date().optional(),
   updatedAt: z.date().optional(),
   encrypted: z.boolean().optional(),
   isEncrypting: z.boolean().optional(),
 })
 
+// Use when value must be present (e.g., before encryption)
+export const requiredValueKeyValuePairSchema = baseKeyValuePairSchema.extend({
+  value: z.string().min(1, "Value is required"),
+})
components/shared/key-value-pair-form.tsx (2)

151-163: Add accessible label to the remove button.

Improves a11y and discoverability.

                   {localPairs.length > 1 && (
                     <Button
                       type="button"
                       variant="ghost"
                       size="icon"
                       onClick={() => handleRemovePair(index)}
                       disabled={disabled}
-                      className="text-muted-foreground hover:text-destructive flex-shrink-0"
+                      className="text-muted-foreground hover:text-destructive flex-shrink-0"
+                      aria-label="Remove key-value pair"
                     >
                       <Icons.trash className="size-4" />
                     </Button>
                   )}

170-183: Add accessible label to the add button.

Minor a11y polish.

         <div className="border-border border-t p-3 sm:p-4">
           <Button
             type="button"
             variant="outline"
             size="sm"
             onClick={handleAddPair}
             disabled={disabled}
             className="text-muted-foreground hover:text-foreground flex w-full items-center gap-2"
+            aria-label="Add another key-value pair"
           >
             <Icons.add className="size-4" />
             Add Another
           </Button>
         </div>
components/app/dashboard-add-credential-dialog.tsx (2)

538-538: Typo: fix password placeholder.

Small UI nit.

-                          placeholder="En.ter password"
+                          placeholder="Enter password"

441-467: Use computed selectedPlatform to simplify selectedItem.

Reduces repeated finds and re-renders.

-                      selectedItem={
-                        platforms.find(
-                          (p) => p.id === credentialForm.watch("platformId")
-                        )
-                          ? {
-                              value: credentialForm.watch("platformId"),
-                              label:
-                                platforms.find(
-                                  (p) =>
-                                    p.id === credentialForm.watch("platformId")
-                                )?.name || "",
-                              logo: getPlaceholderImage(
-                                platforms.find(
-                                  (p) =>
-                                    p.id === credentialForm.watch("platformId")
-                                )?.name || "",
-                                getLogoDevUrlWithToken(
-                                  platforms.find(
-                                    (p) =>
-                                      p.id ===
-                                      credentialForm.watch("platformId")
-                                  )?.logo || null
-                                )
-                              ),
-                            }
-                          : null
-                      }
+                      selectedItem={
+                        selectedPlatform
+                          ? {
+                              value: selectedPlatform.id,
+                              label: selectedPlatform.name,
+                              logo: getPlaceholderImage(
+                                selectedPlatform.name,
+                                getLogoDevUrlWithToken(selectedPlatform.logo)
+                              ),
+                            }
+                          : null
+                      }

Also applies to: 141-143

components/app/dashboard-credential-key-value-pairs.tsx (3)

232-234: Consider enabling duplicate‑key validation in UI.

Preventing duplicate keys at the source reduces backend conflicts and user confusion.

-      validateDuplicateKeys={false}
+      validateDuplicateKeys

46-67: Trim payload and de‑dupe keys before save.

Sanitize editingData: trim, drop empties, and collapse duplicates (last write wins or block). This avoids silent server‑side errors.

Example insertion before building existing/new arrays:

+      // Normalize and de-dupe by key (case-insensitive)
+      const norm = editingData
+        .map(p => ({ ...p, key: p.key?.trim() ?? "", value: p.value?.trim() ?? "" }))
+        .filter(p => p.key || p.value)
+      const seen = new Set<string>()
+      const normalized = []
+      for (let i = norm.length - 1; i >= 0; i--) {
+        const k = norm[i].key.toLowerCase()
+        if (!seen.has(k)) { seen.add(k); normalized.unshift(norm[i]) }
+      }

Then use normalized instead of editingData.


45-67: Minor: remove unused dep in useMemo.

credentialId isn’t used in displayData; drop from deps.

-  }, [keyValuePairs, editingData, credentialId])
+  }, [keyValuePairs, editingData])
components/shared/encrypted-key-value-display.tsx (2)

83-97: Duplicate-key guard silently ignores input.

Early-returning on duplicates with no feedback confuses users. Surface an inline error, tooltip, or expose an onDuplicateKeyAttempt callback so the parent can toast.

Minimal option:

-      if (validateDuplicateKeys && newKey.trim()) {
+      if (validateDuplicateKeys && newKey.trim()) {
         const isDuplicate = value.some(
           (pair, i) =>
             i !== index &&
             pair.key.trim().toLowerCase() === newKey.trim().toLowerCase()
         )
-        if (isDuplicate) {
-          return
-        }
+        if (isDuplicate) return // TODO: show inline error/tooltip or notify parent
       }

118-161: Avoid caching decrypted secrets; enforce single-use fetch.

gcTime: 0 is good, but decryptedValues persists in state indefinitely. Consider optional auto-clear (e.g., 30–60s timer) or clear on blur to reduce exposure.

entities/credential/credential-key-value/entity.ts (2)

116-135: Parallelize batch encryption.

Encrypt pairs concurrently to reduce latency for larger inputs.

-    const encryptedPairs: GenericEncryptedKeyValuePairDto[] = []
-
-    for (const pair of validPairs) {
-      const encryptedPair = await this.encryptKeyValuePair(pair)
-      encryptedPairs.push(encryptedPair)
-    }
-
-    return encryptedPairs
+    return Promise.all(validPairs.map((p) => this.encryptKeyValuePair(p)))

67-76: Prefer undefined over empty string for missing IDs.

Returning id: "" can leak into UI/logic; use undefined to signify absence.

-    return {
-      id: credential.id || "",
+    return {
+      id: credential.id,
       key: credential.key,
       value: "",
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a79e9e and 7754c17.

📒 Files selected for processing (14)
  • app/(dashboard)/dashboard/accounts/page.tsx (1 hunks)
  • components/app/auth-login-form.tsx (3 hunks)
  • components/app/auth-register-form.tsx (5 hunks)
  • components/app/dashboard-add-credential-dialog.tsx (8 hunks)
  • components/app/dashboard-add-secret-dialog.tsx (3 hunks)
  • components/app/dashboard-create-container-dialog.tsx (1 hunks)
  • components/app/dashboard-credential-key-value-pairs.tsx (1 hunks)
  • components/shared/encrypted-key-value-display.tsx (1 hunks)
  • components/shared/key-value-pair-form.tsx (1 hunks)
  • entities/credential/credential-key-value/entity.ts (1 hunks)
  • lib/utils/card-expiry-helpers.ts (0 hunks)
  • schemas/credential/credential-metadata.ts (1 hunks)
  • schemas/utils/base-key-value-pair.ts (1 hunks)
  • schemas/utils/index.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • lib/utils/card-expiry-helpers.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • components/app/dashboard-add-secret-dialog.tsx
  • schemas/credential/credential-metadata.ts
  • components/app/dashboard-create-container-dialog.tsx
  • components/app/auth-login-form.tsx
  • components/app/auth-register-form.tsx
🧰 Additional context used
🧬 Code graph analysis (6)
components/shared/key-value-pair-form.tsx (2)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
components/shared/icons.tsx (1)
  • Icons (134-252)
components/app/dashboard-credential-key-value-pairs.tsx (7)
types/index.d.ts (1)
  • KeyValuePair (56-59)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
hooks/use-toast.ts (1)
  • useToast (87-186)
orpc/hooks/use-credentials.ts (2)
  • useCredentialKeyValuePairs (68-78)
  • useUpdateCredentialKeyValuePairs (282-315)
lib/utils/index.ts (1)
  • handleErrors (139-212)
components/app/dashboard-credential-key-value-pairs-skeleton.tsx (1)
  • DashboardCredentialKeyValuePairsSkeleton (10-89)
components/shared/encrypted-key-value-display.tsx (1)
  • EncryptedKeyValueDisplay (32-264)
app/(dashboard)/dashboard/accounts/page.tsx (2)
orpc/context.ts (1)
  • createContext (7-24)
orpc/client/server.ts (1)
  • createServerClient (11-17)
entities/credential/credential-key-value/entity.ts (5)
entities/credential/credential-key-value/query.ts (2)
  • CredentialKeyValuePairSimpleDbData (9-12)
  • CredentialKeyValuePairDbData (4-7)
entities/encryption/entity.ts (1)
  • EncryptedDataEntity (6-30)
schemas/encryption/encryption.ts (1)
  • GenericEncryptedKeyValuePairDto (31-33)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
lib/encryption.ts (3)
  • generateEncryptionKey (14-28)
  • encryptData (37-108)
  • exportKey (193-200)
components/shared/encrypted-key-value-display.tsx (3)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
orpc/hooks/use-credentials.ts (1)
  • credentialKeys (21-28)
components/shared/icons.tsx (1)
  • Icons (134-252)
components/app/dashboard-add-credential-dialog.tsx (12)
hooks/use-prevent-auto-save.ts (1)
  • usePreventAutoSave (8-42)
hooks/use-aggressive-form-blocker.ts (1)
  • useAggressiveFormBlocker (8-75)
orpc/hooks/use-platforms.ts (1)
  • usePlatforms (19-29)
orpc/hooks/use-tags.ts (1)
  • useTags (16-22)
orpc/hooks/use-credentials.ts (1)
  • useCreateCredentialWithMetadata (154-177)
schemas/credential/credential.ts (3)
  • credentialDtoSchema (17-29)
  • accountStatusEnum (13-13)
  • AccountStatusInfer (15-15)
lib/utils/password-helpers.ts (1)
  • checkPasswordStrength (34-75)
entities/credential/credential-key-value/entity.ts (1)
  • CredentialKeyValuePairEntity (17-136)
lib/utils/index.ts (3)
  • getPlaceholderImage (227-236)
  • getLogoDevUrlWithToken (242-248)
  • cn (30-32)
components/shared/key-value-pair-form.tsx (1)
  • KeyValuePairForm (25-187)
config/converter.tsx (1)
  • statusConfig (38-57)
components/shared/container-selector.tsx (1)
  • ContainerSelector (199-339)
🔇 Additional comments (7)
schemas/utils/base-key-value-pair.ts (1)

13-13: LGTM.

Type export matches schema.

schemas/utils/index.ts (1)

1-1: LGTM.

Barrel re-export is correct.

components/app/dashboard-add-credential-dialog.tsx (4)

821-827: Remove unused event param and preventDefault in submit path.

This was flagged earlier; the button is type="button" and the form already blocks submit.

-              <Button
-                type="button"
-                disabled={createCredentialWithMetadataMutation.isPending}
-                onClick={(e) => {
-                  e.preventDefault()
-                  e.stopPropagation()
-                  onSubmit(e)
-                }}
-              >
+              <Button
+                type="button"
+                disabled={createCredentialWithMetadataMutation.isPending}
+                onClick={() => onSubmit()}
+              >
-  async function onSubmit(e?: React.FormEvent) {
-    if (e) {
-      e.preventDefault()
-      e.stopPropagation()
-    }
+  async function onSubmit() {

587-605: Server-side safeguard for duplicate tags (schema + atomic create).

Client dedupe helps UX, but enforce uniqueness and atomic creation in backend to fully resolve duplicates.

  • Add a composite unique index on Tag (name, userId, containerId).
  • Make tag creation idempotent/atomic (upserts or createMany with skipDuplicates) inside a transaction.

588-596: Deduplicate tags client‑side to avoid duplicate creation.

Normalize by name (case‑insensitive) before saving to the form.

-                    <TagSelector<TagDto>
+                    <TagSelector<TagDto>
                       selectedTags={credentialForm.watch("tags") || []}
                       availableTags={availableTags.map((tag) => ({
                         name: tag.name,
                         containerId: tag.containerId || undefined,
                         color: tag.color || undefined,
                       }))}
-                      onChange={(tags: TagDto[]) =>
-                        credentialForm.setValue("tags", tags)
-                      }
+                      onChange={(tags: TagDto[]) => {
+                        const seen = new Set<string>()
+                        const unique = []
+                        for (const t of tags) {
+                          const k = t.name.trim().toLowerCase()
+                          if (!seen.has(k)) {
+                            seen.add(k)
+                            unique.push(t)
+                          }
+                        }
+                        credentialForm.setValue("tags", unique)
+                      }}
                       getValue={(tag: TagDto) => tag.name}
                       getLabel={(tag: TagDto) => tag.name}
                       createTag={(inputValue: string): TagDto => ({
                         name: inputValue,
                         containerId: undefined,
                         color: undefined,
                       })}
                     />

204-224: Keep form onSubmit minimal; metadata encryption & validation confirmed.

  • Optional: if you have an inline onSubmit that calls e.stopPropagation() and returns false, replace with e.preventDefault() only to reduce event noise.
  • Verified: CredentialKeyValuePairEntity.encryptKeyValuePairs returns generic encrypted pairs (valueEncryption present); metadataForm.setValue(...) is called before metadataForm.trigger(); credentialKeyValuePairDtoSchema extends the generic encrypted schema and accepts the encrypted shape; server createWithMetadata consumes valueEncryption — flow is correct.
components/app/dashboard-credential-key-value-pairs.tsx (1)

163-198: Stop exposing KV data on window; gate to dev and remove plaintext.

displayData (may include plaintext) is attached to window; this is a serious leakage vector. Gate to development only and only expose safe actions/flags. This was flagged earlier and still persists.

Apply:

-  useEffect(() => {
-    if (typeof window !== "undefined") {
-      // Expose save/cancel handlers on window for toolbar integration
-      // TODO: Replace with proper React context when implementing save toolbar
-      const windowWithCredentials = window as Window & {
-        credentialKeyValuePairs?: {
-          save: () => Promise<{ success: boolean; error?: string }>
-          cancel: () => void
-          hasChanges: boolean
-          data: KeyValuePair[]
-        }
-      }
-
-      windowWithCredentials.credentialKeyValuePairs = {
-        save: handleSave,
-        cancel: handleCancel,
-        hasChanges,
-        data: displayData,
-      }
-    }
-
-    return () => {
-      if (typeof window !== "undefined") {
-        const windowWithCredentials = window as Window & {
-          credentialKeyValuePairs?: {
-            save: () => Promise<{ success: boolean; error?: string }>
-            cancel: () => void
-            hasChanges: boolean
-            data: KeyValuePair[]
-          }
-        }
-        delete windowWithCredentials.credentialKeyValuePairs
-      }
-    }
-  }, [handleSave, handleCancel, hasChanges, displayData])
+  useEffect(() => {
+    if (process.env.NODE_ENV === "development" && typeof window !== "undefined") {
+      const w = window as Window & {
+        credentialKeyValuePairs?: {
+          save: () => Promise<{ success: boolean; error?: string }>
+          cancel: () => void
+          hasChanges: boolean
+        }
+      }
+      w.credentialKeyValuePairs = { save: handleSave, cancel: handleCancel, hasChanges }
+      return () => { delete w.credentialKeyValuePairs }
+    }
+  }, [handleSave, handleCancel, hasChanges])

Comment thread components/app/dashboard-add-credential-dialog.tsx
Comment thread components/app/dashboard-add-credential-dialog.tsx
Comment thread components/app/dashboard-credential-key-value-pairs.tsx
Comment thread components/app/dashboard-credential-key-value-pairs.tsx
Comment on lines +101 to +113
const key = await generateEncryptionKey()
const encryptResult = await encryptData(pair.value, key)
const keyString = await exportKey(key as CryptoKey)

return {
id: pair.id,
key: pair.key,
valueEncryption: {
encryptedValue: encryptResult.encryptedData,
iv: encryptResult.iv,
encryptionKey: keyString,
},
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Server crash in Node: exportKey used with string key.

generateEncryptionKey returns a base64 string in Node; exportKey throws in Node. This path will throw during server-side encryption.

Fix:

-    const key = await generateEncryptionKey()
-    const encryptResult = await encryptData(pair.value, key)
-    const keyString = await exportKey(key as CryptoKey)
+    const key = await generateEncryptionKey()
+    const encryptResult = await encryptData(pair.value, key)
+    const keyString =
+      typeof key === "string" ? key : await exportKey(key as CryptoKey)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const key = await generateEncryptionKey()
const encryptResult = await encryptData(pair.value, key)
const keyString = await exportKey(key as CryptoKey)
return {
id: pair.id,
key: pair.key,
valueEncryption: {
encryptedValue: encryptResult.encryptedData,
iv: encryptResult.iv,
encryptionKey: keyString,
},
}
const key = await generateEncryptionKey()
const encryptResult = await encryptData(pair.value, key)
const keyString =
typeof key === "string" ? key : await exportKey(key as CryptoKey)
return {
id: pair.id,
key: pair.key,
valueEncryption: {
encryptedValue: encryptResult.encryptedData,
iv: encryptResult.iv,
encryptionKey: keyString,
},
}
🤖 Prompt for AI Agents
In entities/credential/credential-key-value/entity.ts around lines 101 to 113,
the code assumes generateEncryptionKey returns a CryptoKey and always calls
exportKey, but in Node generateEncryptionKey returns a base64 string so
exportKey throws; change the logic to handle both types by checking if key is a
string and, if so, use it directly as keyString, otherwise call exportKey(key as
CryptoKey); ensure encryptData is called with the appropriate key type and keep
the returned valueEncryption.encryptionKey set to the base64 string in either
case.

Copy link
Copy Markdown

@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: 3

🧹 Nitpick comments (5)
components/shared/container-selector.tsx (3)

249-263: Replace validateEntityForContainer import with ContainerEntity method; fold filters to one pass.

Removes a brittle import and reduces work.

Apply:

-  const compatibleContainers = useMemo(
-    () =>
-      containers
-        .filter((container) =>
-          validateEntityForContainer(container.type, entityType)
-        )
-        .filter(
-          (container) =>
-            container.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
-            container.description
-              ?.toLowerCase()
-              .includes(searchQuery.toLowerCase())
-        ),
-    [containers, entityType, searchQuery]
-  )
+  const compatibleContainers = useMemo(() => {
+    const q = searchQuery.trim().toLowerCase()
+    return containers.filter((container) => {
+      if (
+        !ContainerEntity.validateEntityForContainer(container.type, entityType)
+      )
+        return false
+      const name = container.name.toLowerCase()
+      const desc = container.description?.toLowerCase() ?? ""
+      return name.includes(q) || desc.includes(q)
+    })
+  }, [containers, entityType, searchQuery])

63-64: Surface upgrade context when selection is disabled.

Currently all options are disabled with no hint. Add a title to explain why.

Apply:

         <Button
           key={container.id}
@@
-          disabled={container.id === currentContainerId || !canSelect}
+          disabled={container.id === currentContainerId || !canSelect}
+          title={
+            !canSelect
+              ? `${getUpgradeMessage(Feature.CONTAINERS).title} ${getUpgradeMessage(Feature.CONTAINERS).description}`
+              : undefined
+          }
         >

68-69: Use design-token color that exists.

"text-success" may not exist in your theme; prior code used emerald.

Apply:

-            <Icons.check className="text-success ml-auto size-3" />
+            <Icons.check className="ml-auto size-3 text-emerald-600" />
lib/permissions/hooks/use-user-permissions.ts (1)

27-30: Gate “selection” on update, not create (policy check).

Selection feels closer to modify than create. If intentional, ignore.

Optional change:

-    canSelectContainers: config.features.containers.create, // Allow selection if can create
+    canSelectContainers: config.features.containers.update, // Selection aligns with modify capability
lib/permissions/utils.ts (1)

139-151: Hoist hierarchy map to module scope.

Avoids reallocating per call.

Apply:

-function isPermissionLevelSufficient(
-  userLevel: PermissionLevel,
-  requiredLevel: PermissionLevel
-): boolean {
-  const levelHierarchy = {
+const LEVEL_HIERARCHY: Record<PermissionLevel, number> = {
     [PermissionLevel.NONE]: 0,
     [PermissionLevel.READ]: 1,
     [PermissionLevel.WRITE]: 2,
     [PermissionLevel.ADMIN]: 3,
-  }
-
-  return levelHierarchy[userLevel] >= levelHierarchy[requiredLevel]
+}
+
+function isPermissionLevelSufficient(
+  userLevel: PermissionLevel,
+  requiredLevel: PermissionLevel
+): boolean {
+  return LEVEL_HIERARCHY[userLevel] >= LEVEL_HIERARCHY[requiredLevel]
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7754c17 and 3987be7.

📒 Files selected for processing (8)
  • components/shared/container-selector.tsx (1 hunks)
  • hooks/use-prevent-auto-save.ts (1 hunks)
  • lib/encryption.ts (6 hunks)
  • lib/permissions/hooks/use-user-permissions.ts (1 hunks)
  • lib/permissions/index.ts (1 hunks)
  • lib/permissions/types.ts (1 hunks)
  • lib/permissions/utils.ts (1 hunks)
  • middleware/permissions.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • middleware/permissions.ts
  • hooks/use-prevent-auto-save.ts
  • lib/permissions/index.ts
  • lib/encryption.ts
  • lib/permissions/types.ts
🧰 Additional context used
🧬 Code graph analysis (3)
lib/permissions/utils.ts (2)
lib/permissions/index.ts (8)
  • getPermissionConfig (4-4)
  • PermissionConfig (13-13)
  • hasPermission (5-5)
  • Feature (17-17)
  • PermissionLevel (17-17)
  • getUpgradeMessage (6-6)
  • canPerformAction (7-7)
  • Action (17-17)
lib/permissions/types.ts (1)
  • PermissionConfig (28-43)
components/shared/container-selector.tsx (6)
lib/permissions/utils.ts (1)
  • getUpgradeMessage (167-192)
lib/permissions/types.ts (1)
  • UserPermissionFlags (8-26)
lib/permissions/hooks/use-user-permissions.ts (1)
  • useUserPermissions (11-40)
orpc/hooks/use-containers.ts (1)
  • useContainers (37-45)
entities/utils/container/entity.ts (2)
  • ContainerEntity (7-108)
  • validateEntityForContainer (29-43)
components/app/dashboard-create-container-dialog.tsx (1)
  • DashboardCreateContainerDialog (64-231)
lib/permissions/hooks/use-user-permissions.ts (2)
orpc/hooks/use-users.ts (1)
  • useCurrentUser (16-23)
lib/permissions/utils.ts (1)
  • getPermissionConfig (14-45)
🔇 Additional comments (5)
components/shared/container-selector.tsx (1)

42-74: Nice DRY extraction.

ContainerList/CreateContainerButton/ContainerSelectorContent remove prior duplication.

Also applies to: 76-127, 129-189

lib/permissions/hooks/use-user-permissions.ts (2)

14-20: Good: flags derived from centralized config.

Keeps UI in sync with middleware.

Also applies to: 26-35


42-42: Export verified — no action required.
lib/permissions/types.ts exports UseUserPermissionsReturn (line 54); the export used in lib/permissions/hooks/use-user-permissions.ts is valid.

lib/permissions/utils.ts (2)

114-122: Fix included: Feature.CREDENTIALS mapping added.

Unblocks credential permission checks for Pro.


14-26: No serialization risk — keep Infinity. No JSON.stringify or DB-write usages of the permission config were found; getPermissionConfig is only used in in-app permission checks and the UI. Relevant locations: lib/permissions/utils.ts (maxCount: Infinity at line 25; comparison at line 231), lib/permissions/hooks/use-user-permissions.ts (maxContainers at line 34).

Comment thread components/shared/container-selector.tsx
Comment thread components/shared/container-selector.tsx
Comment thread components/shared/container-selector.tsx
@FindMalek
Copy link
Copy Markdown
Owner Author

42 comments should be resolved :) ...

Copy link
Copy Markdown

@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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/app/dashboard-add-credential-dialog.tsx (1)

292-338: Key-value metadata persists across new credentials
When we reset the form (both in the “create another” path and when closing the dialog) we never clear plainKeyValuePairs, so the additional-info fields from the previous credential show up—and will be resubmitted—in the next credential. We need to wipe that local state alongside the form resets.

               metadataForm.reset({
                 recoveryEmail: "",
                 phoneNumber: "",
                 keyValuePairs: [],
                 has2FA: false,
               })
-              setSensitiveData({ identifier: "", password: "" })
-              setPasswordStrength(null)
-              setShowAdvanced(false)
+              setSensitiveData({ identifier: "", password: "" })
+              setPlainKeyValuePairs([])
+              setPasswordStrength(null)
+              setShowAdvanced(false)
   if (!open) {
     credentialForm.reset()
     metadataForm.reset()
     setSensitiveData({ identifier: "", password: "" })
     setCreateMore(false)
+    setPlainKeyValuePairs([])
     setShowAdvanced(false)
     setPasswordStrength(null)
   }
🧹 Nitpick comments (1)
prisma/schema/utils.prisma (1)

42-56: Update the docstrings to match the new schema

The comments still describe Tag.color and Container.icon as optional, but both columns are now required. Please refresh the JSDoc so future readers (and generated docs) aren’t misled.

Also applies to: 73-93

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3987be7 and 775a421.

📒 Files selected for processing (26)
  • components/app/dashboard-add-card-dialog.tsx (3 hunks)
  • components/app/dashboard-add-card-form.tsx (9 hunks)
  • components/app/dashboard-add-credential-dialog.tsx (8 hunks)
  • components/app/dashboard-add-secret-dialog.tsx (3 hunks)
  • components/app/dashboard-credential-client.tsx (1 hunks)
  • components/app/dashboard-credential-detail-skeleton.tsx (1 hunks)
  • components/app/dashboard-credential-detail-view.tsx (1 hunks)
  • components/app/dashboard-credential-key-value-pairs-skeleton.tsx (1 hunks)
  • components/app/dashboard-credential-key-value-pairs.tsx (1 hunks)
  • components/app/dashboard-credential-password-field.tsx (1 hunks)
  • components/app/dashboard-credential-sidebar.tsx (1 hunks)
  • components/layout/breadcrumb-resource-select.tsx (1 hunks)
  • components/layout/dashboard-dynamic-breadcrumb.tsx (2 hunks)
  • components/shared/coming-soon.tsx (4 hunks)
  • components/shared/container-selector.tsx (1 hunks)
  • components/shared/dashboard-view-controls.tsx (1 hunks)
  • components/shared/encrypted-key-value-display.tsx (1 hunks)
  • components/shared/icons.tsx (6 hunks)
  • components/shared/key-value-pair-form.tsx (1 hunks)
  • config/consts.ts (1 hunks)
  • entities/credential/credential-metadata/query.ts (1 hunks)
  • entities/credential/credential/query.ts (1 hunks)
  • lib/utils/color-helpers.ts (1 hunks)
  • lib/utils/tag-helpers.ts (2 hunks)
  • orpc/routers/credential.ts (11 hunks)
  • prisma/schema/utils.prisma (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • components/shared/dashboard-view-controls.tsx
  • components/app/dashboard-credential-sidebar.tsx
  • components/app/dashboard-add-secret-dialog.tsx
  • components/app/dashboard-add-card-dialog.tsx
  • components/app/dashboard-credential-client.tsx
  • components/app/dashboard-credential-key-value-pairs-skeleton.tsx
  • lib/utils/color-helpers.ts
🧰 Additional context used
🧬 Code graph analysis (15)
components/app/dashboard-credential-password-field.tsx (6)
schemas/credential/dto.ts (1)
  • CredentialOutput (59-59)
hooks/use-toast.ts (1)
  • useToast (87-186)
orpc/hooks/use-credentials.ts (2)
  • useUpdateCredentialPassword (318-344)
  • useCredentialPassword (44-52)
lib/encryption.ts (3)
  • generateEncryptionKey (14-28)
  • encryptData (37-108)
  • exportKey (194-201)
lib/utils/index.ts (1)
  • handleErrors (139-212)
components/shared/icons.tsx (1)
  • Icons (139-262)
components/app/dashboard-credential-key-value-pairs.tsx (7)
types/index.d.ts (1)
  • KeyValuePair (54-57)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
hooks/use-toast.ts (1)
  • useToast (87-186)
orpc/hooks/use-credentials.ts (2)
  • useCredentialKeyValuePairs (68-78)
  • useUpdateCredentialKeyValuePairs (282-315)
lib/utils/index.ts (1)
  • handleErrors (139-212)
components/app/dashboard-credential-key-value-pairs-skeleton.tsx (1)
  • DashboardCredentialKeyValuePairsSkeleton (10-79)
components/shared/encrypted-key-value-display.tsx (1)
  • EncryptedKeyValueDisplay (32-256)
components/layout/breadcrumb-resource-select.tsx (12)
lib/utils/breadcrumb-helpers.ts (1)
  • ResourceType (31-31)
orpc/hooks/use-credentials.ts (1)
  • useCredentials (118-128)
orpc/hooks/use-cards.ts (1)
  • useCards (39-49)
orpc/hooks/use-secrets.ts (1)
  • useSecrets (39-49)
orpc/hooks/use-platforms.ts (1)
  • usePlatforms (19-29)
components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
components/shared/icons.tsx (1)
  • Icons (139-262)
components/ui/breadcrumb.tsx (1)
  • BreadcrumbPage (106-106)
components/ui/popover.tsx (3)
  • Popover (48-48)
  • PopoverTrigger (48-48)
  • PopoverContent (48-48)
components/ui/button.tsx (1)
  • Button (59-59)
lib/utils/index.ts (2)
  • getPlaceholderImage (227-236)
  • getLogoDevUrlWithToken (242-248)
components/ui/command.tsx (6)
  • Command (168-168)
  • CommandInput (170-170)
  • CommandList (171-171)
  • CommandEmpty (172-172)
  • CommandGroup (173-173)
  • CommandItem (174-174)
components/app/dashboard-add-card-form.tsx (1)
lib/utils/color-helpers.ts (1)
  • generateTagColor (11-29)
components/shared/container-selector.tsx (7)
components/shared/icons.tsx (1)
  • Icons (139-262)
lib/permissions/utils.ts (1)
  • getUpgradeMessage (167-192)
lib/permissions/types.ts (1)
  • UserPermissionFlags (8-26)
lib/permissions/hooks/use-user-permissions.ts (1)
  • useUserPermissions (11-40)
orpc/hooks/use-containers.ts (1)
  • useContainers (37-45)
entities/utils/container/entity.ts (2)
  • ContainerEntity (7-108)
  • validateEntityForContainer (29-43)
components/app/dashboard-create-container-dialog.tsx (1)
  • DashboardCreateContainerDialog (64-231)
components/shared/encrypted-key-value-display.tsx (2)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
orpc/hooks/use-credentials.ts (1)
  • credentialKeys (21-28)
components/app/dashboard-add-credential-dialog.tsx (11)
hooks/use-aggressive-form-blocker.ts (1)
  • useAggressiveFormBlocker (8-75)
hooks/use-prevent-auto-save.ts (1)
  • usePreventAutoSave (8-54)
orpc/hooks/use-platforms.ts (1)
  • usePlatforms (19-29)
orpc/hooks/use-credentials.ts (1)
  • useCreateCredentialWithMetadata (154-177)
orpc/hooks/use-tags.ts (1)
  • useTags (16-22)
schemas/credential/credential.ts (4)
  • CredentialDto (31-31)
  • credentialDtoSchema (17-29)
  • accountStatusEnum (13-13)
  • AccountStatusInfer (15-15)
entities/credential/credential-key-value/entity.ts (1)
  • CredentialKeyValuePairEntity (17-136)
lib/utils/index.ts (4)
  • handleErrors (139-212)
  • getPlaceholderImage (227-236)
  • getLogoDevUrlWithToken (242-248)
  • cn (30-32)
components/ui/combobox-responsive.tsx (1)
  • ComboboxResponsive (43-134)
components/shared/key-value-pair-form.tsx (1)
  • KeyValuePairForm (25-185)
config/converter.tsx (1)
  • statusConfig (38-57)
lib/utils/tag-helpers.ts (3)
schemas/utils/tag.ts (1)
  • TagDto (9-9)
prisma/client/database-client.ts (2)
  • PrismaTransactionClient (6-9)
  • getDatabaseClient (19-23)
lib/utils/color-helpers.ts (1)
  • generateTagColor (11-29)
components/shared/icons.tsx (1)
schemas/utils/utils.ts (2)
  • RecentItemType (16-16)
  • RecentItemTypeEnum (15-15)
components/shared/coming-soon.tsx (1)
components/shared/icons.tsx (1)
  • Icons (139-262)
components/layout/dashboard-dynamic-breadcrumb.tsx (3)
lib/utils/breadcrumb-helpers.ts (2)
  • isIndividualResourcePage (2-12)
  • getResourceType (15-28)
components/layout/breadcrumb-resource-select.tsx (1)
  • BreadcrumbResourceSelect (38-229)
lib/utils/index.ts (1)
  • capitalizeFirstLetter (54-56)
components/shared/key-value-pair-form.tsx (2)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
components/shared/icons.tsx (1)
  • Icons (139-262)
components/app/dashboard-credential-detail-view.tsx (7)
schemas/credential/dto.ts (2)
  • CredentialOutput (59-59)
  • UpdateCredentialInput (52-52)
schemas/utils/dto.ts (1)
  • ListPlatformsOutput (109-109)
hooks/use-toast.ts (1)
  • useToast (87-186)
orpc/hooks/use-credentials.ts (5)
  • useCredential (31-41)
  • useUpdateCredentialWithSecuritySettings (234-279)
  • useUpdateCredential (180-231)
  • useDeleteCredential (347-386)
  • useCredentialSecuritySettings (55-65)
schemas/credential/credential.ts (2)
  • CredentialFormDto (89-89)
  • credentialFormDtoSchema (77-87)
lib/utils/index.ts (1)
  • handleErrors (139-212)
components/shared/floating-save-toolbar.tsx (1)
  • FloatingSaveToolbar (14-45)
entities/credential/credential/query.ts (1)
entities/credential/credential-metadata/query.ts (1)
  • CredentialMetadataQuery (18-49)
orpc/routers/credential.ts (10)
schemas/credential/dto.ts (4)
  • getCredentialInputSchema (14-14)
  • updateCredentialPasswordInputSchema (40-47)
  • credentialOutputSchema (28-28)
  • CredentialOutput (59-59)
entities/credential/credential/query.ts (1)
  • CredentialQuery (17-44)
entities/credential/credential/entity.ts (1)
  • CredentialEntity (17-136)
schemas/credential/credential-key-value.ts (1)
  • credentialKeyValuePairWithValueRoSchema (46-52)
lib/encryption.ts (2)
  • decryptData (111-191)
  • encryptData (37-108)
middleware/permissions.ts (1)
  • requirePermission (46-104)
lib/utils/tag-helpers.ts (1)
  • createTagsAndGetConnections (21-96)
lib/utils/encryption-helpers.ts (1)
  • createEncryptedData (20-50)
schemas/credential/credential.ts (1)
  • credentialFormDtoSchema (77-87)
entities/credential/credential-metadata/query.ts (1)
  • CredentialMetadataQuery (18-49)
🔇 Additional comments (8)
components/app/dashboard-credential-password-field.tsx (1)

67-78: Stop sending client-side encryption keys to the server.

savePasswordChanges still exports the generated key and posts it in passwordEncryption.encryptionKey. That revives the critical issue we already discussed: the server ends up persisting the very key needed to decrypt the password, so the encryption adds no protection. Drop the client-generated key from the payload and align with the server-side fix (server-owned keys or KMS-wrapped secrets). Once the API contract is updated, prune the exportKey call and the encryptionKey field here.

components/shared/container-selector.tsx (1)

15-16: Reuse the canonical ContainerEntity validator instead of the utils alias.

We already import ContainerEntity, so wiring through ContainerEntity.validateEntityForContainer keeps this component on the official path and avoids the duplicate helper flagged earlier.

-import { validateEntityForContainer } from "@/lib/utils"
...
-          validateEntityForContainer(container.type, entityType)
+          ContainerEntity.validateEntityForContainer(
+            container.type,
+            entityType
+          )

Also applies to: 245-251

orpc/routers/credential.ts (4)

438-444: Enforce WRITE permission on every mutating credential route

Only createCredential applies requirePermission for WRITE access. updateCredential, updateCredentialPassword, updateCredentialWithSecuritySettings, updateCredentialKeyValuePairs, createCredentialWithMetadata, and deleteCredential currently bypass that guard, so a user with read-only plan can still mutate credentials. Please apply the same .use(requirePermission({ feature: Feature.CREDENTIALS, level: PermissionLevel.WRITE })) middleware to each of these procedures.

 export const updateCredential = authProcedure
+  .use(({ context, next }) =>
+    requirePermission({
+      feature: Feature.CREDENTIALS,
+      level: PermissionLevel.WRITE,
+    })({ context, next })
+  )
   .input(updateCredentialInputSchema)
@@
-export const deleteCredential = authProcedure
+export const deleteCredential = authProcedure
+  .use(({ context, next }) =>
+    requirePermission({
+      feature: Feature.CREDENTIALS,
+      level: PermissionLevel.WRITE,
+    })({ context, next })
+  )
   .input(deleteCredentialInputSchema)
@@
-export const updateCredentialPassword = authProcedure
+export const updateCredentialPassword = authProcedure
+  .use(({ context, next }) =>
+    requirePermission({
+      feature: Feature.CREDENTIALS,
+      level: PermissionLevel.WRITE,
+    })({ context, next })
+  )
@@
-export const updateCredentialWithSecuritySettings = authProcedure
+export const updateCredentialWithSecuritySettings = authProcedure
+  .use(({ context, next }) =>
+    requirePermission({
+      feature: Feature.CREDENTIALS,
+      level: PermissionLevel.WRITE,
+    })({ context, next })
+  )
@@
-export const updateCredentialKeyValuePairs = authProcedure
+export const updateCredentialKeyValuePairs = authProcedure
+  .use(({ context, next }) =>
+    requirePermission({
+      feature: Feature.CREDENTIALS,
+      level: PermissionLevel.WRITE,
+    })({ context, next })
+  )
@@
-export const createCredentialWithMetadata = authProcedure
+export const createCredentialWithMetadata = authProcedure
+  .use(({ context, next }) =>
+    requirePermission({
+      feature: Feature.CREDENTIALS,
+      level: PermissionLevel.WRITE,
+    })({ context, next })
+  )

Also applies to: 538-543, 600-605, 786-790, 930-936


563-577: Pass the transaction client to createEncryptedData

Inside the password-update transaction, createEncryptedData is executed on the global client. If any later step in the transaction fails, the new encrypted row is still committed, leaving orphaned password blobs. Pass tx through so the insert participates in the same transaction.

-      const newPasswordEncryptionResult = await createEncryptedData({
-        encryptedValue: passwordEncryption.encryptedValue,
-        encryptionKey: passwordEncryption.encryptionKey,
-        iv: passwordEncryption.iv,
-      })
+      const newPasswordEncryptionResult = await createEncryptedData(
+        {
+          encryptedValue: passwordEncryption.encryptedValue,
+          encryptionKey: passwordEncryption.encryptionKey,
+          iv: passwordEncryption.iv,
+        },
+        tx
+      )

722-729: Keep security-setting encryption inside the outer transaction

upsertSecuritySetting creates encrypted rows without the transaction client, so failures later in the tx leak orphaned encrypted records. Thread tx into createEncryptedData.

-  const encryptedDataResult = await createEncryptedData({
-    encryptedValue: encryptionResult.encryptedData,
-    encryptionKey: encryptionKey,
-    iv: encryptionResult.iv,
-  })
+  const encryptedDataResult = await createEncryptedData(
+    {
+      encryptedValue: encryptionResult.encryptedData,
+      encryptionKey: encryptionKey,
+      iv: encryptionResult.iv,
+    },
+    tx
+  )

1010-1064: Ensure key-value pair encryption is transactional

Within the $transaction, calls to createEncryptedData still execute on the global client, so they commit even if the transaction rolls back, leaving dangling encrypted blobs. Please pass tx into each call.

-            const encryptedDataResult = await createEncryptedData({
-              encryptedValue: encryptionResult.encryptedData,
-              encryptionKey: encryptionKey,
-              iv: encryptionResult.iv,
-            })
+            const encryptedDataResult = await createEncryptedData(
+              {
+                encryptedValue: encryptionResult.encryptedData,
+                encryptionKey: encryptionKey,
+                iv: encryptionResult.iv,
+              },
+              tx
+            )
@@
-          const encryptedDataResult = await createEncryptedData({
-            encryptedValue: encryptionResult.encryptedData,
-            encryptionKey: encryptionKey,
-            iv: encryptionResult.iv,
-          })
+          const encryptedDataResult = await createEncryptedData(
+            {
+              encryptedValue: encryptionResult.encryptedData,
+              encryptionKey: encryptionKey,
+              iv: encryptionResult.iv,
+            },
+            tx
+          )
entities/credential/credential-metadata/query.ts (1)

36-48: Nice separation of server vs. client includes.

Splitting getInclude/getClientSafeInclude keeps encryption metadata on the server path only—exactly what we need to avoid leaking keys.

components/app/dashboard-credential-key-value-pairs.tsx (1)

161-195: Do not leak key-value data via the global window.

window.credentialKeyValuePairs.data = displayData exposes plaintext edits (including secrets for newly added pairs) to every script on the page. This was previously flagged and is still present. Gate the bridge to NODE_ENV === "development" (or drop it entirely) and never attach the raw data array outside dev tooling; only expose inert callbacks/flags.

-  useEffect(() => {
-    if (typeof window !== "undefined") {
+  useEffect(() => {
+    if (
+      typeof window !== "undefined" &&
+      process.env.NODE_ENV === "development"
+    ) {
@@
-      windowWithCredentials.credentialKeyValuePairs = {
-        save: handleSave,
-        cancel: handleCancel,
-        hasChanges,
-        data: displayData,
-      }
+      windowWithCredentials.credentialKeyValuePairs = {
+        save: handleSave,
+        cancel: handleCancel,
+        hasChanges,
+      }
     }
@@
-      if (typeof window !== "undefined") {
+      if (
+        typeof window !== "undefined" &&
+        process.env.NODE_ENV === "development"
+      ) {
         const windowWithCredentials = window as Window & {
           credentialKeyValuePairs?: {
             save: () => Promise<{ success: boolean; error?: string }>
             cancel: () => void
             hasChanges: boolean
-            data: KeyValuePair[]
           }
         }
         delete windowWithCredentials.credentialKeyValuePairs
       }
     }
-  }, [handleSave, handleCancel, hasChanges, displayData])
+  }, [handleSave, handleCancel, hasChanges])

Comment thread components/app/dashboard-credential-key-value-pairs.tsx Outdated
Comment thread components/layout/breadcrumb-resource-select.tsx
Comment thread components/shared/container-selector.tsx
Comment thread components/shared/encrypted-key-value-display.tsx
Comment thread lib/utils/tag-helpers.ts Outdated
Copy link
Copy Markdown

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/utils/tag-helpers.ts (1)

84-96: Inconsistent case-sensitivity: findMany after createMany should use mode: "insensitive".

Lines 46-53 use mode: "insensitive" for the initial lookup, but lines 90-96 fetch newly created tags using a case-sensitive query. If the database has a case-insensitive unique constraint or if tags are stored with different casing, this query might miss records.

Apply this diff to make the retrieval consistent:

   newTags = await client.tag.findMany({
     where: {
-      name: { in: tagsToCreate.map((tag) => tag.name) },
+      name: { 
+        in: tagsToCreate.map((tag) => tag.name),
+        mode: "insensitive"
+      },
       userId,
       containerId: containerId || null,
     },
   })

Alternatively, if the concern about Prisma support for mode: "insensitive" with in is valid (see previous comment), consider querying by IDs returned from createMany or using a follow-up case-insensitive lookup strategy for the newly created tags.

🧹 Nitpick comments (2)
components/shared/encrypted-key-value-display.tsx (1)

138-140: Consider brief cache window for decrypted values.

Setting gcTime: 0 prevents any re-use if the user clicks the eye icon multiple times in quick succession, forcing redundant fetches. A brief cache (e.g., gcTime: 30000 for 30 seconds) would improve UX while maintaining security—React Query will still evict on unmount or after the window.

             queryFn: () =>
               orpc.credentials.getKeyValuePairValue.call({
                 credentialId,
                 keyValuePairId: pair.id!,
               }),
             staleTime: 0, // Always fetch fresh for security
-            gcTime: 0, // Don't cache values
+            gcTime: 30000, // Brief 30s cache for re-clicks
           })
components/app/dashboard-add-credential-dialog.tsx (1)

393-406: Strengthen honeypot inputs with ARIA and data attributes.

The hidden honeypot inputs lack aria-hidden="true" and could benefit from explicit data attributes to mark them for testing/debugging. This prevents screen readers from announcing them and makes intent clearer.

                   <div style={{ display: "none" }}>
                     <input
                       type="text"
                       name="username"
                       tabIndex={-1}
                       autoComplete="username"
+                      aria-hidden="true"
+                      data-honeypot="true"
                     />
                     <input
                       type="password"
                       name="password"
                       tabIndex={-1}
                       autoComplete="current-password"
+                      aria-hidden="true"
+                      data-honeypot="true"
                     />
                   </div>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 775a421 and 987c760.

📒 Files selected for processing (8)
  • components/app/dashboard-add-credential-dialog.tsx (8 hunks)
  • components/app/dashboard-credential-key-value-pairs.tsx (1 hunks)
  • components/layout/breadcrumb-resource-select.tsx (1 hunks)
  • components/shared/encrypted-key-value-display.tsx (1 hunks)
  • lib/utils/tag-helpers.ts (2 hunks)
  • orpc/hooks/use-credentials.ts (3 hunks)
  • orpc/hooks/use-platforms.ts (2 hunks)
  • orpc/hooks/use-users.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/app/dashboard-credential-key-value-pairs.tsx
  • components/layout/breadcrumb-resource-select.tsx
🧰 Additional context used
🧬 Code graph analysis (6)
orpc/hooks/use-platforms.ts (2)
schemas/utils/dto.ts (2)
  • ListPlatformsInput (106-106)
  • ListPlatformsOutput (109-109)
orpc/client/utils.ts (1)
  • orpc (5-7)
components/shared/encrypted-key-value-display.tsx (3)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
orpc/hooks/use-credentials.ts (1)
  • credentialKeys (21-28)
components/shared/icons.tsx (1)
  • Icons (139-262)
lib/utils/tag-helpers.ts (3)
schemas/utils/tag.ts (1)
  • TagDto (9-9)
prisma/client/database-client.ts (2)
  • PrismaTransactionClient (6-9)
  • getDatabaseClient (19-23)
lib/utils/color-helpers.ts (1)
  • generateTagColor (11-29)
components/app/dashboard-add-credential-dialog.tsx (13)
hooks/use-aggressive-form-blocker.ts (1)
  • useAggressiveFormBlocker (8-75)
hooks/use-prevent-auto-save.ts (1)
  • usePreventAutoSave (8-54)
orpc/hooks/use-platforms.ts (1)
  • usePlatforms (18-28)
orpc/hooks/use-credentials.ts (1)
  • useCreateCredentialWithMetadata (154-177)
orpc/hooks/use-tags.ts (1)
  • useTags (16-22)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
schemas/credential/credential.ts (4)
  • CredentialDto (31-31)
  • credentialDtoSchema (17-29)
  • accountStatusEnum (13-13)
  • AccountStatusInfer (15-15)
entities/credential/credential-key-value/entity.ts (1)
  • CredentialKeyValuePairEntity (17-136)
lib/utils/index.ts (3)
  • getPlaceholderImage (227-236)
  • getLogoDevUrlWithToken (242-248)
  • cn (30-32)
components/ui/combobox-responsive.tsx (1)
  • ComboboxResponsive (43-134)
schemas/utils/tag.ts (1)
  • TagDto (9-9)
components/shared/key-value-pair-form.tsx (1)
  • KeyValuePairForm (25-185)
config/converter.tsx (1)
  • statusConfig (38-57)
orpc/hooks/use-credentials.ts (2)
schemas/credential/dto.ts (4)
  • CredentialOutput (59-59)
  • ListCredentialsInput (57-57)
  • ListCredentialsOutput (63-63)
  • UpdateCredentialPasswordInput (53-55)
orpc/client/utils.ts (1)
  • orpc (5-7)
orpc/hooks/use-users.ts (2)
orpc/client/utils.ts (1)
  • orpc (5-7)
orpc/hooks/use-containers.ts (1)
  • containerKeys (15-25)
🔇 Additional comments (21)
orpc/hooks/use-platforms.ts (1)

18-27: LGTM! Clean implementation of optional query options.

The extension to accept UseQueryOptions follows best practices: the Omit type correctly prevents callers from overriding queryKey or queryFn, and the spread operator preserves existing behavior while allowing fine-grained control over caching, refetching, and other query settings.

orpc/hooks/use-users.ts (2)

17-25: LGTM! Sensible defaults for user data caching.

The 5-minute staleTime and retry: 1 configuration appropriately balance freshness with performance for current user data. The query key factory integration is clean.


75-91: Invalidation key is correct (past review comment no longer applies).

The invalidation at line 84 correctly uses containerKeys.all (without parentheses) because containerKeys.all is defined as a constant array ["containers"] as const, not a function. The past review comment suggesting containerKeys.all() is now incorrect.

The mutation implementation looks good: proper error logging, success-conditional invalidation, and correct cache key targeting.

orpc/hooks/use-credentials.ts (7)

31-41: LGTM! Consistent with the options pattern.

The extension to accept optional UseQueryOptions follows the same type-safe pattern as use-platforms.ts, preserving query key and function integrity.


44-52: Good security posture: no caching for passwords.

The staleTime: 0 and gcTime: 0 configuration ensures passwords are never cached, which is appropriate for this sensitive data. The enabled parameter allows controlled fetching.


55-65: Security-sensitive hooks correctly implement conditional caching.

The tiered approach is sound:

  • Security settings: 5-minute cache (less sensitive, can be cached)
  • Key-value pairs (keys only): 2-minute cache (metadata, less risky)
  • Key-value pairs with values: no cache (sensitive data)
  • Individual key-value pair values: no cache (sensitive data)

Each hook appropriately uses enabled to prevent unintended fetches of sensitive data.

Also applies to: 68-78, 81-92, 95-115


118-128: LGTM! Consistent with the options pattern.

The extension preserves the existing placeholderData behavior while allowing additional query configuration.


234-279: LGTM! Proper optimistic update with targeted invalidation.

The mutation correctly:

  • Implements optimistic updates with rollback on error
  • Invalidates both the credential list and the specific security settings query
  • Follows the established pattern from useUpdateCredential

282-315: LGTM! Comprehensive cache invalidation for key-value pairs.

The invalidation strategy correctly targets all related query keys:

  • key-value-pairs (keys only)
  • key-value-pairs-with-values (with sensitive values)
  • key-value-pair-value (individual values)
  • Main credential detail

This ensures all cached variants are refreshed after an update.


318-344: Password invalidation key is now correct (past review comment resolved).

Line 332 now correctly uses [...credentialKeys.detail(variables.id), "password"], which matches the fetch key at line 46. The past review comment flagging a mismatch with ["credential-password", id] has been addressed.

The mutation properly invalidates:

  • The credential detail
  • The password-specific query
  • The credential lists
components/shared/encrypted-key-value-display.tsx (1)

158-175: LGTM – Display logic now correct.

The updated logic correctly prioritizes explicit pair.value first, preventing the masked-display bug reported in the past review. Users can now edit values reliably.

components/app/dashboard-add-credential-dialog.tsx (3)

700-720: LGTM – Key-value pair encryption now correctly persisted.

The plainKeyValuePairs → encrypt → convertGenericToCredential flow ensures additionalInfos are correctly shaped and encrypted before submission, resolving the past reported issue.


81-82: LGTM – Aggressive anti-autofill strategy appropriate for vault forms.

The combination of useAggressiveFormBlocker, usePreventAutoSave, hidden honeypots, data-form-isolated attributes, and explicit autoComplete overrides provides defense-in-depth against browser/extension password managers. This is appropriate for a credential vault where autofill could compromise security.

Also applies to: 383-391


577-595: Duplicate tag creation issue: add DB uniqueness and atomic creation.

The reported duplicate-tag issue stems from a race condition in tag creation (lib/utils/tag-helpers.ts uses findMany → createMany without atomicity). The Tag model lacks a composite unique constraint on (name, userId, containerId), so concurrent requests can create duplicates.

Fix:

  1. Add @@unique([name, userId, containerId]) to the Tag model in prisma/schema/utils.prisma and run a migration.
  2. Update lib/utils/tag-helpers.ts to use createMany with skipDuplicates: true (if your DB supports it), or perform per-tag upserts inside a transaction.

Run the following script to confirm the Tag model lacks the uniqueness constraint:

#!/bin/bash
# Check Prisma schema for Tag uniqueness constraint
rg -nP 'model\s+Tag\s*\{' -A20 prisma/schema
lib/utils/tag-helpers.ts (7)

1-9: LGTM! Clean import reorganization.

The imports now properly include transaction support (getDatabaseClient, PrismaTransactionClient) and deterministic color generation (generateTagColor), aligning with the PR's goal of comprehensive credential management.


10-26: Function overloads correctly typed for transaction support.

The three overloads properly handle the optional tx parameter with correct TypeScript typing. This enables both standalone usage and transactional usage from callers like card.ts.


33-44: Normalization logic correctly deduplicates and preserves original case.

The implementation properly:

  • Trims and lowercases for the map key (deduplication)
  • Preserves the original trimmed case in the tag value
  • Filters out empty strings

55-73: Deduplication and filtering logic is sound.

The code correctly:

  • Deduplicates by lowercase key
  • Prefers container-specific tags over null-container tags
  • Filters out tags that already exist

79-79: Deterministic color generation aligns with PR goals.

Using generateTagColor instead of random colors ensures consistent tag appearance across sessions, improving UX.


99-99: LGTM! Correct aggregation of existing and new tags.

Using existingTagsByName.values() ensures deduplicated existing tags are combined with newly created tags.


46-53: Verify mode: "insensitive" + in (Postgres, @prisma/client ^6.9.0)

Repo uses @prisma/client ^6.9.0 with a PostgreSQL datasource; Prisma docs state mode: 'insensitive' is supported for Postgres string filters, but there are reported TypeScript/runtime issues when mode is used inside some nested/list filters. If name: { in: normalizedNames, mode: "insensitive" } fails to type-check or run, either run a quick local test or replace with an OR-of-equals fallback (map each name to { name: { equals: name, mode: "insensitive" } }) and adjust the containerId OR as needed. File: lib/utils/tag-helpers.ts (lines 46–53). (prismagraphql.com)

Comment thread components/app/dashboard-add-credential-dialog.tsx Outdated
Comment thread components/shared/encrypted-key-value-display.tsx
Copy link
Copy Markdown

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
orpc/routers/container.ts (1)

149-191: Make updateContainer tag updates transactional to avoid orphan Tag rows.

createTagsAndGetConnections here runs outside a transaction; if the subsequent update fails, newly created Tag rows remain. Wrap the whole update in a tx and pass tx to the tag helper.

-  .handler(async ({ input, context }): Promise<ContainerOutput> => {
+  .handler(async ({ input, context }): Promise<ContainerOutput> => {
     const { id, ...updateData } = input
-    // Verify container ownership
-    const existingContainer = await database.container.findFirst({
+    // Verify container ownership
+    const existingContainer = await database.container.findFirst({
       where: {
         id,
         userId: context.user.id,
         ...(context.permissions?.canOnlyAccessDefaultContainers && {
           isDefault: true,
         }),
       },
     })
     if (!existingContainer) {
       throw new ORPCError("NOT_FOUND")
     }
-    // Process the update data
-    const updatePayload: Prisma.ContainerUpdateInput = {}
+    // Process the update data
+    const updatePayload: Prisma.ContainerUpdateInput = {}
     if (updateData.name !== undefined) updatePayload.name = updateData.name
     if (updateData.icon !== undefined) updatePayload.icon = updateData.icon
     if (updateData.description !== undefined)
       updatePayload.description = updateData.description
     if (updateData.type !== undefined) updatePayload.type = updateData.type
-    // Handle tags if provided
-    if (updateData.tags !== undefined) {
-      const tagConnections = await createTagsAndGetConnections(
-        updateData.tags,
-        context.user.id,
-        undefined
-      )
-      updatePayload.tags = tagConnections
-    }
-
-    const updatedContainer = await database.container.update({
-      where: { id },
-      data: updatePayload,
-    })
+    const updatedContainer = await database.$transaction(async (tx) => {
+      // Handle tags if provided
+      if (updateData.tags !== undefined) {
+        const tagConnections = await createTagsAndGetConnections(
+          updateData.tags,
+          context.user.id,
+          undefined,
+          tx
+        )
+        updatePayload.tags = tagConnections
+      }
+      return tx.container.update({
+        where: { id },
+        data: updatePayload,
+      })
+    })
     return ContainerEntity.getSimpleRo(updatedContainer)
   })
orpc/routers/credential.ts (4)

317-344: List returns data from all containers; restrict to default containers for Normal users.

Add a conditional where clause to honor default‑only access. Also prefer a safe include for list responses.

   const where = {
     userId: context.user.id,
+    ...(context.permissions?.canOnlyAccessDefaultContainers && {
+      OR: [{ container: { isDefault: true } }, { containerId: null }],
+    }),
     ...(containerId && { containerId }),
     ...(platformId && { platformId }),
@@
-        include: { ...CredentialQuery.getInclude() },
+        include: { ...CredentialQuery.getClientSafeInclude() },

Also applies to: 349-351


47-57: Read endpoint should respect default‑only access.

Gate ownership lookups by container.isDefault for Normal users.

 const credential = await database.credential.findFirst({
   where: {
     id: input.id,
     userId: context.user.id,
+    ...(context.permissions?.canOnlyAccessDefaultContainers && {
+      container: { isDefault: true },
+    }),
   },
 })

Apply similar filtering to getPassword, getCredentialSecuritySettings, getCredentialKeyValuePairs, getCredentialKeyValuePairsWithValues, getCredentialKeyValuePairValue.

Also applies to: 63-70


437-456: Make updateCredential atomic; pass tx to encryption to avoid orphaned rows.

createEncryptedData runs outside a transaction; if the update throws (e.g., P2002), the encryptedData row leaks.

-  .handler(async ({ input, context }): Promise<CredentialOutput> => {
+  .handler(async ({ input, context }): Promise<CredentialOutput> => {
     const { id, ...updateData } = input
-    // Verify credential ownership
-    const existingCredential = await database.credential.findFirst({
+    // Verify credential ownership
+    const existingCredential = await database.credential.findFirst({
       where: {
         id,
         userId: context.user.id,
+        ...(context.permissions?.canOnlyAccessDefaultContainers && {
+          container: { isDefault: true },
+        }),
       },
     })
     if (!existingCredential) {
       throw new ORPCError("NOT_FOUND")
     }
-    // Process the update data
-    const updatePayload: Prisma.CredentialUpdateInput = {}
+    // Process the update data
+    const updatePayload: Prisma.CredentialUpdateInput = {}
@@
-    // Handle password encryption updates if provided
-    if (updateData.passwordEncryption) {
-      const passwordEncryptionResult = await createEncryptedData({
-        encryptedValue: updateData.passwordEncryption.encryptedValue,
-        encryptionKey: updateData.passwordEncryption.encryptionKey,
-        iv: updateData.passwordEncryption.iv,
-      })
-      if (!passwordEncryptionResult.success || !passwordEncryptionResult.encryptedData) {
-        throw new ORPCError("INTERNAL_SERVER_ERROR")
-      }
-      updatePayload.passwordEncryption = {
-        connect: { id: passwordEncryptionResult.encryptedData.id },
-      }
-    }
-    try {
-      const updatedCredential = await database.credential.update({
-        where: { id },
-        data: updatePayload,
-      })
+    try {
+      const updatedCredential = await database.$transaction(async (tx) => {
+        if (updateData.passwordEncryption) {
+          const passwordEncryptionResult = await createEncryptedData(
+            {
+              encryptedValue: updateData.passwordEncryption.encryptedValue,
+              encryptionKey: updateData.passwordEncryption.encryptionKey,
+              iv: updateData.passwordEncryption.iv,
+            },
+            tx
+          )
+          if (!passwordEncryptionResult.success || !passwordEncryptionResult.encryptedData) {
+            throw new ORPCError("INTERNAL_SERVER_ERROR")
+          }
+          updatePayload.passwordEncryption = {
+            connect: { id: passwordEncryptionResult.encryptedData.id },
+          }
+        }
+        return tx.credential.update({ where: { id }, data: updatePayload })
+      })
       return CredentialEntity.getSimpleRo(updatedCredential)
     } catch (error) {

Also applies to: 508-527


772-789: Delete should also respect default‑only access.

Restrict ownership check by container.isDefault for Normal users.

 const existingCredential = await database.credential.findFirst({
   where: {
     id: input.id,
     userId: context.user.id,
+    ...(context.permissions?.canOnlyAccessDefaultContainers && {
+      container: { isDefault: true },
+    }),
   },
 })
🧹 Nitpick comments (6)
prisma/seed/credentials.ts (1)

165-201: Consider removing redundant container check.

The if (accountsContainer) condition at line 166 is redundant because the code already guards against missing accountsContainer at line 56 with a continue. While defensive, the condition will always be true at this point.

Apply this diff to simplify:

-    // AWS credential - goes to default Accounts container
-    if (accountsContainer) {
+    // AWS credential - goes to default Accounts container
       // Prepare encrypted data for AWS password
       const awsPasswordEncrypted = await encryptDataSync(
         awsPassword,
         SEED_ENCRYPTION_CONFIG.MASTER_KEY,
         SEED_ENCRYPTION_CONFIG.CREDENTIAL_PASSWORD_IV
       )
       encryptedDataToCreate.push({
         id: awsPasswordEncId,
         encryptedValue: awsPasswordEncrypted,
         encryptionKey: SEED_ENCRYPTION_CONFIG.MASTER_KEY,
         iv: SEED_ENCRYPTION_CONFIG.CREDENTIAL_PASSWORD_IV,
       })
 
       // Prepare AWS credential
       credentialsToCreate.push({
         id: awsCredId,
         identifier: `${user.name.replace(/\s+/g, ".").toLowerCase()}@company.com`,
         passwordEncryptionId: awsPasswordEncId,
         status: AccountStatus.ACTIVE,
         description: "AWS account",
         updatedAt: new Date(),
         createdAt: new Date(),
         platformId: awsPlatform.id,
         userId: user.id,
         containerId: accountsContainer.id,
       })
 
       // Store tag connections for AWS credential
       if (workTag) {
         credentialTagConnections.push({
           credentialId: awsCredId,
           tagIds: [workTag.id],
         })
       }
-    }
components/app/dashboard-add-credential-dialog.tsx (2)

430-456: Consider extracting platform selection logic for clarity.

The nested platforms.find calls to construct selectedItem are repeated three times within the object literal, making the code harder to read and maintain.

You can extract this logic:

+                    const selectedPlatformItem = selectedPlatform ? {
+                      value: selectedPlatform.id,
+                      label: selectedPlatform.name,
+                      logo: getPlaceholderImage(
+                        selectedPlatform.name,
+                        getLogoDevUrlWithToken(selectedPlatform.logo || null)
+                      ),
+                    } : null
+
                     <ComboboxResponsive
                       items={platforms.map((platform) => ({
                         value: platform.id,
                         label: platform.name,
                         logo: getPlaceholderImage(
                           platform.name,
                           getLogoDevUrlWithToken(platform.logo || null)
                         ),
                       }))}
-                      selectedItem={
-                        platforms.find(
-                          (p) => p.id === credentialForm.watch("platformId")
-                        )
-                          ? {
-                              value: credentialForm.watch("platformId"),
-                              label:
-                                platforms.find(
-                                  (p) =>
-                                    p.id === credentialForm.watch("platformId")
-                                )?.name || "",
-                              logo: getPlaceholderImage(
-                                platforms.find(
-                                  (p) =>
-                                    p.id === credentialForm.watch("platformId")
-                                )?.name || "",
-                                getLogoDevUrlWithToken(
-                                  platforms.find(
-                                    (p) =>
-                                      p.id ===
-                                      credentialForm.watch("platformId")
-                                  )?.logo || null
-                                )
-                              ),
-                            }
-                          : null
-                      }
+                      selectedItem={selectedPlatformItem}
                       onSelect={(item) =>

696-716: KeyValuePairForm integration works but has subtle conversion flow.

The onChange handler (lines 698-707) converts pairs via convertFromKeyValueManager immediately, creating placeholder DTO shapes with empty encryption fields. The actual encryption happens later in onSubmit (lines 196-210), which overwrites these placeholders.

This works correctly but creates two conversion points that future maintainers might find confusing. The flow is:

  1. User edits → setPlainKeyValuePairs + set placeholder DTOs to form
  2. Submit → encrypt plainKeyValuePairs → convert to credential DTOs → overwrite form

Consider adding a comment explaining why both plainKeyValuePairs state and form state are maintained, or simplifying to only update plainKeyValuePairs state here and defer all DTO conversion to submit time.

prisma/schema/credential.prisma (1)

134-145: Enforce one-to-one between key/value rows and their encrypted payloads.

Each CredentialKeyValuePair should own exactly one encrypted blob. Without a uniqueness check on valueEncryptionId, nothing stops multiple rows from pointing at the same encrypted payload, which complicates deletes/rotations and risks stale data. Let’s tighten the schema by marking the column as unique.

-  valueEncryptionId String
+  valueEncryptionId String @unique
orpc/routers/credential.ts (2)

72-106: Reads: prefer client‑safe includes where decryption isn’t needed.

  • getCredentialSecuritySettings, getPassword must include encryption; OK.
  • For getCredentialKeyValuePairs (keys‑only), switch to CredentialMetadataQuery.getClientSafeInclude to avoid loading encryption keys unnecessarily.
-  include: CredentialQuery.getInclude(), // Note: Uses full include but only returns safe fields
+  include: CredentialQuery.getClientSafeInclude(),

This also applies to getCredentialKeyValuePairsWithValues only if you decrypt via a separate fetch for the selected kv; otherwise keep as-is.

Also applies to: 269-315, 107-127, 154-166, 215-235


954-967: Dedupe incoming keyValuePairs by key to reduce redundant writes.

If the payload contains duplicate keys, the loop repeats updates. Dedupe first, keeping the last value.

-  const { credentialId, keyValuePairs } = input
+  const { credentialId } = input
+  const kvByKey = new Map<string, { id?: string; key: string; value?: string }>()
+  for (const kv of input.keyValuePairs) {
+    const key = kv.key.trim()
+    if (!key) continue
+    kvByKey.set(key, { ...kv, key })
+  }
+  const keyValuePairs = Array.from(kvByKey.values())

Also applies to: 1020-1110

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 987c760 and a5501bb.

📒 Files selected for processing (13)
  • components/app/dashboard-add-credential-dialog.tsx (8 hunks)
  • components/shared/container-selector.tsx (1 hunks)
  • components/shared/encrypted-key-value-display.tsx (1 hunks)
  • lib/utils/default-containers.ts (1 hunks)
  • orpc/routers/container.ts (14 hunks)
  • orpc/routers/credential.ts (12 hunks)
  • orpc/routers/user.ts (3 hunks)
  • prisma/schema/credential.prisma (4 hunks)
  • prisma/schema/utils.prisma (7 hunks)
  • prisma/seed/containers.ts (1 hunks)
  • prisma/seed/credentials.ts (7 hunks)
  • prisma/seed/users.ts (5 hunks)
  • schemas/user/user.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • orpc/routers/user.ts
  • prisma/schema/utils.prisma
  • components/shared/encrypted-key-value-display.tsx
  • lib/utils/default-containers.ts
  • components/shared/container-selector.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
orpc/routers/container.ts (5)
middleware/permissions.ts (2)
  • requireDefaultContainerAccess (147-185)
  • requireContainerPermission (109-127)
lib/utils/tag-helpers.ts (1)
  • createTagsAndGetConnections (21-103)
entities/utils/container/entity.ts (1)
  • ContainerEntity (7-108)
lib/utils/encryption-helpers.ts (1)
  • createEncryptedData (20-50)
schemas/utils/utils.ts (1)
  • EntityTypeSchema (67-67)
components/app/dashboard-add-credential-dialog.tsx (9)
hooks/use-aggressive-form-blocker.ts (1)
  • useAggressiveFormBlocker (8-75)
hooks/use-prevent-auto-save.ts (1)
  • usePreventAutoSave (8-54)
schemas/utils/base-key-value-pair.ts (1)
  • BaseKeyValuePair (13-13)
schemas/credential/credential.ts (4)
  • CredentialDto (31-31)
  • credentialDtoSchema (17-29)
  • accountStatusEnum (13-13)
  • AccountStatusInfer (15-15)
entities/credential/credential-key-value/entity.ts (1)
  • CredentialKeyValuePairEntity (17-136)
lib/utils/index.ts (4)
  • handleErrors (139-212)
  • getPlaceholderImage (227-236)
  • getLogoDevUrlWithToken (242-248)
  • cn (30-32)
schemas/utils/tag.ts (1)
  • TagDto (9-9)
components/shared/key-value-pair-form.tsx (1)
  • KeyValuePairForm (25-185)
config/converter.tsx (1)
  • statusConfig (38-57)
orpc/routers/credential.ts (10)
schemas/credential/dto.ts (4)
  • getCredentialInputSchema (14-14)
  • updateCredentialPasswordInputSchema (40-47)
  • credentialOutputSchema (28-28)
  • CredentialOutput (59-59)
entities/credential/credential/query.ts (1)
  • CredentialQuery (17-44)
entities/credential/credential/entity.ts (1)
  • CredentialEntity (17-136)
schemas/credential/credential-key-value.ts (1)
  • credentialKeyValuePairWithValueRoSchema (46-52)
lib/encryption.ts (2)
  • decryptData (111-191)
  • encryptData (37-108)
middleware/permissions.ts (1)
  • requirePermission (46-104)
lib/utils/tag-helpers.ts (1)
  • createTagsAndGetConnections (21-103)
lib/utils/encryption-helpers.ts (1)
  • createEncryptedData (20-50)
schemas/credential/credential.ts (1)
  • credentialFormDtoSchema (77-87)
entities/credential/credential-metadata/query.ts (1)
  • CredentialMetadataQuery (18-49)
🔇 Additional comments (28)
schemas/user/user.ts (1)

1-1: Past review suggestions successfully implemented.

The schema now includes:

  • Email format validation with .email()
  • Flexible createdAt handling that accepts both Date objects (server-side) and ISO datetime strings (serialized/client-side)
  • Proper Prisma enum integration for the plan field

The implementation is robust and handles serialization boundaries correctly.

Also applies to: 32-41

prisma/seed/users.ts (3)

5-47: LGTM! Helper properly implements default containers for seeding.

The seeder-specific helper correctly creates the three default containers (Accounts, Cards, Environment Variables) with appropriate types and flags. The skipDuplicates: true makes this idempotent as requested in the past review.

Note: createMany returns { count: number }, not the actual container records, but since the caller doesn't use the return value this is fine.


52-86: LGTM! Plan assignments provide good test coverage.

The mix of NORMAL and PRO plans across seed users provides appropriate test data for both user tiers.


111-123: LGTM! Robust error handling for default container creation.

The try/catch with per-user logging ensures that seed failures are visible while allowing other users to be seeded successfully.

prisma/seed/credentials.ts (4)

50-61: LGTM! Guard properly prevents credential seeding without default container.

The guard correctly checks for the Accounts container and skips seeding with a clear warning if missing, preventing NOT NULL constraint violations as requested in the past review.


86-119: LGTM! Google credential correctly targets default Accounts container.


122-163: LGTM! GitHub credential properly handles multi-space names.

The identifier generation now uses /\s+/g to remove all whitespace globally, addressing the past review comment. Credential correctly targets the Accounts container.


204-239: LGTM! Bulk operations properly structured.

Transaction ensures atomicity for data creation, and tag connections are correctly handled separately since createMany doesn't support relation connections.

prisma/seed/containers.ts (3)

3-11: LGTM! Clear separation between default and additional containers.

The scope change to Pro users only is appropriate, as default containers are now created per user in users.ts. Logging and variable names accurately reflect that these are additional/custom containers.


13-41: LGTM! Pro user containers properly configured.

Work and Finance containers are correctly marked as non-default (isDefault: false) and use the flexible MIXED type appropriate for custom Pro user containers.


43-51: LGTM! Idempotent seeding with skipDuplicates.

The skipDuplicates: true option properly addresses the past review comment, making the seed script idempotent and safe to re-run.

components/app/dashboard-add-credential-dialog.tsx (4)

196-210: LGTM! additionalInfos persistence fix correctly implemented.

The fix for the reported bug ("additionalInfos are not being created") has been properly implemented. The code now:

  1. Encrypts the plainKeyValuePairs
  2. Converts them to CredentialKeyValuePairDto using convertGenericToCredential
  3. Sets the credential-shaped DTOs to the form

This ensures the keyValuePairs match the expected schema and will be persisted correctly.


803-812: LGTM! onSubmit signature correctly simplified.

The onSubmit function is now called without event parameters (line 806), and the function signature (line 166) no longer accepts unnecessary React.FormEvent parameters. This correctly reflects that it's invoked from a type="button" click handler, not a form submission.


81-82: Anti-autofill implementation is thorough.

The aggressive anti-autofill measures are comprehensive:

  • useAggressiveFormBlocker and usePreventAutoSave hooks
  • Hidden honeypot inputs (lines 389-402)
  • data-testid="vault-form" and data-form-isolated attributes
  • Password manager ignore attributes (data-lpignore, data-1p-ignore, data-bwignore)
  • autoComplete="off" and autoComplete="new-password"

This multi-layered approach effectively prevents browser and password manager interference with credential vault forms.

Also applies to: 383-402


573-591: Remove schema change – composite unique constraint already declared. Verify your backend tag-creation logic uses an atomic upsert or createMany…skipDuplicates (in your tag-helpers module or wherever you persist tags) to prevent duplicates at the database layer.

prisma/schema/credential.prisma (1)

51-51: Verify no existing Credential duplicates before applying the composite unique constraint.

Ensure DATABASE_URL is set, then run:

psql "$DATABASE_URL" <<'SQL'
SELECT identifier, platform_id, user_id, COUNT(*) AS duplicate_count
FROM credential
GROUP BY 1,2,3
HAVING COUNT(*) > 1;
SQL

If any rows are returned, dedupe them before applying @@unique([identifier, platformId, userId]).

orpc/routers/container.ts (8)

45-56: Default-only access enforced on getContainer — good.

Switch to authWithDefaultAccessProcedure and conditional isDefault filter look correct. This addresses prior gating concerns.


67-79: List respects default-only access — good.

Conditional isDefault constraint under canOnlyAccessDefaultContainers is correctly applied.


109-123: Create enforces plan restriction early — good.

Guarding Normal users and chaining permission checks via authWithDefaultAccessProcedure is correct.


125-142: Tags created inside a transaction — good.

Passing tx to createTagsAndGetConnections prevents partial writes.


201-216: Delete respects default-only access — good.

Ownership lookup includes isDefault gating; permission level ADMIN enforced.


231-258: Create-with-secrets is properly transactional and passes tx to encryption + tags — good.

Plan guard, tx usage, and helper calls are correct.

Also applies to: 271-303


337-359: Default-container lookup by entity type — good.

Schema usage and mapping via ContainerEntity.getDefaultContainerTypeForEntity look correct.


361-377: User default containers endpoint — good.

Filters and ordering are appropriate.

orpc/routers/credential.ts (4)

382-388: Duplicate tags on create — fixed.

createTagsAndGetConnections is now used with tx and normalized lookups; this should resolve duplicate tag creation.

Also applies to: 825-833


865-909: “additionalInfos not being created” — fixed.

Metadata and encrypted key‑value pairs are created within the same transaction and pass tx to createEncryptedData.
Please confirm the client uses createCredentialWithMetadata when additional infos are present; otherwise, those fields will be ignored by createCredential.


1-6: Consistency: enforce WRITE permission is now applied across mutations — good.

WRITE checks are consistently chained on create/update/delete and related mutations.

Also applies to: 31-36


826-841: All createEncryptedData calls inside tx — good.

Passing tx ensures no orphaned encryptedData on failures.

Also applies to: 880-907, 1039-1046

Comment thread components/app/dashboard-add-credential-dialog.tsx Outdated
Comment thread orpc/routers/credential.ts
@FindMalek FindMalek merged commit d7efefa into main Sep 30, 2025
2 of 3 checks passed
Copy link
Copy Markdown

@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

🧹 Nitpick comments (5)
lib/permissions/utils.ts (2)

180-184: Confusing upgrade message for CREDENTIALS feature.

The message states "Credential management is available" and "You can create and manage credentials with your current plan." This doesn't follow the upgrade-prompt pattern of other features (which encourage users to upgrade). Since both NORMAL and PRO users have WRITE access to CREDENTIALS, this message will be shown to both—but it reads more like a confirmation than an upgrade prompt.

Consider either:

  1. Removing this case entirely and relying on the default message (since both plans have access), or
  2. Clarifying when this message would actually be shown (e.g., if there's a future plan tier that doesn't have CREDENTIALS access)

Apply this diff if you prefer to remove the confusing case:

   const messages: Record<Feature, { title: string; description: string }> = {
     [Feature.CONTAINERS]: {
       title: "You can only use Containers on a Pro plan",
       description: "and above. Upgrade to Pro to continue.",
     },
     [Feature.EXPORTS]: {
       title: "Data exports require a Pro plan",
       description: "Upgrade to export your data in various formats.",
     },
-    [Feature.CREDENTIALS]: {
-      title: "Credential management is available",
-      description:
-        "You can create and manage credentials with your current plan.",
-    },
   }

14-45: Consider adding CREDENTIALS and EXPORTS to getPermissionConfig for consistency.

Currently, getPermissionConfig only returns configuration for the containers feature, but getUserPermissionLevel handles CONTAINERS, EXPORTS, and CREDENTIALS. If other parts of the codebase call getPermissionConfig expecting feature-specific limits or flags for CREDENTIALS or EXPORTS, they'll find only the containers field.

Extend PermissionConfig and getPermissionConfig to include CREDENTIALS and EXPORTS configurations (or document that getPermissionConfig is containers-only). For example:

 export function getPermissionConfig(plan: UserPlan): PermissionConfig {
   switch (plan) {
     case UserPlan.PRO:
       return {
         plan,
         features: {
           containers: {
             create: true,
             update: true,
             delete: true,
             unlimited: true,
             maxCount: Infinity,
           },
+          credentials: {
+            create: true,
+            update: true,
+            delete: true,
+          },
+          exports: {
+            enabled: true,
+          },
         },
       }
 
     case UserPlan.NORMAL:
     default:
       return {
         plan,
         features: {
           containers: {
             create: false,
             update: false,
             delete: false,
             unlimited: false,
             maxCount: 3,
           },
+          credentials: {
+            create: true,
+            update: true,
+            delete: true,
+          },
+          exports: {
+            enabled: false,
+          },
         },
       }
   }
 }

You'll also need to update lib/permissions/types.ts to reflect these new fields in PermissionConfig.

orpc/routers/credential.ts (2)

1066-1067: Consider validation or sanitization for key names.

Line 1066 skips empty keys (if (!kvPair.key.trim()) continue), which is good, but there's no validation for:

  • Maximum key length
  • Disallowed characters (e.g., control characters, special symbols that might break UI)
  • Reserved key names beyond passwordProtection and accessLogging

Add validation at the input schema level or within the handler:

       for (const kvPair of keyValuePairs) {
-        if (!kvPair.key.trim()) continue
+        const trimmedKey = kvPair.key.trim()
+        if (!trimmedKey) continue
+        if (trimmedKey.length > 100) {
+          throw new ORPCError("BAD_REQUEST", {
+            message: "Key name cannot exceed 100 characters",
+          })
+        }
+        if (/[^\w\s\-_.]/.test(trimmedKey)) {
+          throw new ORPCError("BAD_REQUEST", {
+            message: "Key name contains invalid characters",
+          })
+        }

Alternatively, define these constraints in the input schema at lines 1001-1007.


920-952: Consider extracting encrypted key-value creation into a helper.

The loop at lines 922-952 creates encrypted key-value pairs and is duplicated in updateCredentialKeyValuePairs (lines 1119-1152). Extracting this logic into a shared helper (similar to upsertSecuritySetting) would reduce duplication and improve maintainability.

Create a helper in lib/utils/encryption-helpers.ts:

export async function createEncryptedKeyValuePair(
  tx: PrismaTransactionClient,
  metadataId: string,
  key: string,
  value: string
): Promise<void> {
  const crypto = await import("crypto")
  const encryptionKey = crypto.randomBytes(32).toString("base64")
  const encryptionResult = await encryptData(value, encryptionKey)

  const encryptedDataResult = await createEncryptedData(
    {
      encryptedValue: encryptionResult.encryptedData,
      encryptionKey: encryptionKey,
      iv: encryptionResult.iv,
    },
    tx
  )

  if (!encryptedDataResult.success || !encryptedDataResult.encryptedData) {
    throw new ORPCError("INTERNAL_SERVER_ERROR", {
      message: "Failed to encrypt key-value pair",
    })
  }

  await tx.credentialKeyValuePair.create({
    data: {
      key,
      valueEncryptionId: encryptedDataResult.encryptedData.id,
      credentialMetadataId: metadataId,
    },
  })
}

Then replace the loops in both procedures with calls to this helper.

components/app/dashboard-add-credential-dialog.tsx (1)

696-708: KeyValuePairForm integration could be optimized, but functions correctly.

The onChange handler (lines 698-708) immediately converts pairs via convertFromKeyValueManager, which creates placeholder DTOs with empty encryption fields. These placeholders are then replaced during submission (lines 196-210) with properly encrypted data.

While functional, this creates unnecessary intermediate state updates. Consider simplifying by storing only the plain pairs in state and performing the conversion once during submission.

Simplify by removing the intermediate DTO conversion:

 <KeyValuePairForm
   value={plainKeyValuePairs}
-  onChange={(pairs: BaseKeyValuePair[]) => {
-    setPlainKeyValuePairs(pairs)
-    metadataForm.setValue(
-      "keyValuePairs",
-      pairs.map((pair) =>
-        CredentialKeyValuePairEntity.convertFromKeyValueManager(
-          pair
-        )
-      )
-    )
-  }}
+  onChange={(pairs: BaseKeyValuePair[]) => setPlainKeyValuePairs(pairs)}
   label="Additional Information"
   placeholder={{
     key: "Enter key (e.g., Security Question)",
     value: "Enter value (e.g., Mother's maiden name)",
   }}
   validateDuplicateKeys={true}
   showPasswordField={true}
 />

The metadataForm.setValue for keyValuePairs is unnecessary here since the properly encrypted pairs are set during submission at lines 196-210.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5501bb and f505e3d.

📒 Files selected for processing (4)
  • components/app/dashboard-add-credential-dialog.tsx (8 hunks)
  • lib/permissions/utils.ts (1 hunks)
  • orpc/routers/credential.ts (13 hunks)
  • prisma/seed/README.md (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • prisma/seed/README.md
🧰 Additional context used
🧬 Code graph analysis (3)
orpc/routers/credential.ts (10)
middleware/permissions.ts (2)
  • requireDefaultContainerAccess (147-185)
  • requirePermission (46-104)
schemas/credential/dto.ts (2)
  • getCredentialInputSchema (14-14)
  • updateCredentialPasswordInputSchema (40-47)
entities/credential/credential/query.ts (1)
  • CredentialQuery (17-44)
entities/credential/credential/entity.ts (1)
  • CredentialEntity (17-136)
schemas/credential/credential-key-value.ts (1)
  • credentialKeyValuePairWithValueRoSchema (46-52)
lib/encryption.ts (2)
  • decryptData (111-191)
  • encryptData (37-108)
lib/utils/tag-helpers.ts (1)
  • createTagsAndGetConnections (21-103)
lib/utils/encryption-helpers.ts (1)
  • createEncryptedData (20-50)
schemas/credential/credential.ts (1)
  • credentialFormDtoSchema (77-87)
entities/credential/credential-metadata/query.ts (1)
  • CredentialMetadataQuery (18-49)
lib/permissions/utils.ts (1)
lib/permissions/types.ts (1)
  • PermissionConfig (28-43)
components/app/dashboard-add-credential-dialog.tsx (12)
hooks/use-aggressive-form-blocker.ts (1)
  • useAggressiveFormBlocker (8-75)
hooks/use-prevent-auto-save.ts (1)
  • usePreventAutoSave (8-54)
orpc/hooks/use-platforms.ts (1)
  • usePlatforms (18-28)
orpc/hooks/use-credentials.ts (1)
  • useCreateCredentialWithMetadata (154-177)
orpc/hooks/use-tags.ts (1)
  • useTags (16-22)
entities/credential/credential-key-value/entity.ts (1)
  • CredentialKeyValuePairEntity (17-136)
lib/utils/index.ts (3)
  • getPlaceholderImage (227-236)
  • getLogoDevUrlWithToken (242-248)
  • cn (30-32)
components/ui/combobox-responsive.tsx (1)
  • ComboboxResponsive (43-134)
components/shared/tag-selector.tsx (1)
  • TagSelector (35-217)
components/shared/key-value-pair-form.tsx (1)
  • KeyValuePairForm (25-185)
config/converter.tsx (1)
  • statusConfig (38-57)
components/shared/container-selector.tsx (1)
  • ContainerSelector (196-350)
🔇 Additional comments (20)
lib/permissions/utils.ts (1)

114-122: LGTM: Feature.CREDENTIALS permission mapping correctly implemented.

Both PRO and NORMAL users are granted WRITE access to credentials, which aligns with the fix from past reviews and allows credential creation for all authenticated users.

orpc/routers/credential.ts (14)

49-51: LGTM: Default-access procedure correctly introduced.

The authWithDefaultAccessProcedure combines authentication with default-container access checks, properly addressing the past review concern about enforcing plan restrictions.


366-402: LGTM: createCredential properly enforces permissions and default-container restrictions.

The procedure correctly:

  • Uses authWithDefaultAccessProcedure to enforce default-container access for NORMAL users
  • Chains requirePermission middleware for Feature.CREDENTIALS / PermissionLevel.WRITE
  • Validates that NORMAL users can only use default containers (lines 385-402)

This addresses all past review concerns about permission checks and default-container access.


404-458: LGTM: Transaction handling and conflict detection properly implemented.

The create flow correctly:

  • Wraps all operations in database.$transaction
  • Passes tx to createTagsAndGetConnections and createEncryptedData
  • Handles PrismaClientKnownRequestError with code P2002 for unique constraint violations (lines 447-458)

This addresses past review concerns about atomicity and duplicate handling.


78-111: LGTM: Security settings retrieval properly decrypts on server.

getCredentialSecuritySettings correctly fetches the credential with full include, verifies ownership, and delegates decryption to CredentialEntity.getSecuritySettings. This keeps encryption keys server-side.


113-158: LGTM: Key-value pairs endpoint correctly filters sensitive keys.

getCredentialKeyValuePairs properly:

  • Verifies credential ownership
  • Filters out passwordProtection and accessLogging to prevent exposing internal security settings (lines 146-149)
  • Returns only safe metadata (id, key, timestamps) without values

160-219: LGTM: Decryption endpoint with proper error handling.

getCredentialKeyValuePairsWithValues correctly:

  • Filters sensitive keys (lines 183-186)
  • Decrypts each value server-side
  • Gracefully handles decryption failures by returning empty strings (lines 204-214) rather than exposing raw encrypted data

221-273: LGTM: Single key-value pair retrieval with ownership verification.

getCredentialKeyValuePairValue properly:

  • Verifies credential ownership
  • Validates the key-value pair exists
  • Decrypts server-side
  • Returns a clear error on decryption failure (lines 267-272)

275-321: LGTM: Password retrieval with proper security and audit tracking.

getCredentialPassword correctly:

  • Verifies credential ownership
  • Decrypts password server-side
  • Updates lastViewed timestamp for audit purposes (lines 309-312)
  • Handles decryption failures gracefully (lines 315-320)

462-552: LGTM: Update procedure with permission checks and conflict handling.

updateCredential correctly:

  • Enforces WRITE permission via middleware (lines 463-468)
  • Wraps update in try-catch (lines 533-551)
  • Handles unique constraint violations with CONFLICT error (lines 541-549)

This addresses past review concerns.


554-623: LGTM: Password update with transactional history tracking.

updateCredentialPassword correctly:

  • Enforces WRITE permission (lines 556-561)
  • Wraps operations in database.$transaction
  • Passes tx to createEncryptedData (line 591)
  • Creates history entry before updating credential (lines 604-611)

This addresses past review concerns about transaction handling.


625-794: LGTM: Security settings update fully transactional.

updateCredentialWithSecuritySettings and helper upsertSecuritySetting correctly:

  • Enforce WRITE permission (lines 627-632)
  • Execute all operations within a single transaction (lines 658-720)
  • Pass tx to createEncryptedData in helper (line 760)
  • Clean up old encrypted data on updates (lines 779-783)

This addresses past review concerns about transactional encryption operations.


827-987: LGTM: createCredentialWithMetadata properly secured and transactional.

The procedure correctly:

  • Uses authWithDefaultAccessProcedure and requirePermission middleware (lines 827-833)
  • Validates default-container restriction for NORMAL users (lines 850-866)
  • Wraps all operations in transaction (lines 869-956)
  • Passes tx to createTagsAndGetConnections and createEncryptedData (lines 874, 884, 930)
  • Handles unique constraint violations (lines 965-974)

This addresses all past review concerns about permissions, default-container access, and atomicity.


990-1158: LGTM: Key-value pairs update with comprehensive transaction handling.

updateCredentialKeyValuePairs correctly:

  • Enforces WRITE permission (lines 992-997)
  • Wraps all operations in transaction (lines 1028-1155)
  • Passes tx to createEncryptedData for all new/updated pairs (lines 1089, 1133)
  • Properly cleans up old encrypted data on updates (lines 1109-1112)
  • Filters out sensitive security settings (lines 1044-1046)

This addresses past review concerns about transactional encryption.


797-824: LGTM: Delete procedure with permission enforcement.

deleteCredential correctly enforces WRITE permission via middleware (lines 798-803) and verifies ownership before deletion.

components/app/dashboard-add-credential-dialog.tsx (5)

1-90: LGTM: Clean imports and setup.

The imports are well-organized, anti-autofill hooks are correctly integrated, and data fetching with usePlatforms and useTags is properly initialized.


166-324: Encryption and submission flow implemented correctly.

The credential submission logic properly:

  • Validates required fields before encryption
  • Encrypts password and key-value pairs with separate keys
  • Converts encrypted generic pairs to CredentialKeyValuePairDto (lines 202-204) ✓
  • Includes keyValuePairs in metadataDto with array validation (lines 253-258) ✓
  • Handles success/error cases with appropriate user feedback

The fixes for the reported "additionalInfos not persisted" bug are present and correct.


383-402: Anti-autofill measures correctly implemented.

The form properly prevents default submission (lines 383-387) and includes hidden honeypot inputs (lines 389-402) to discourage password manager interference. Combined with useAggressiveFormBlocker and usePreventAutoSave, this provides robust protection against unwanted autofill behavior.


338-819: Well-structured responsive dialog with comprehensive features.

The UI implementation is excellent:

  • Clear two-column layout with main form and sidebar
  • Proper accessibility with labels, tooltips, and ARIA attributes
  • Responsive design with mobile-first grid layouts
  • Platform selection with logo rendering
  • Password strength indicator
  • Advanced settings in collapsible section
  • Status management with visual badge and popover
  • Container selector integration
  • Footer with "create more" workflow support

The component provides a polished, professional credential creation experience.


573-591: Duplicate tag prevention confirmed

The Prisma schema includes @@unique([name, userId, containerId]), and lib/utils/tag-helpers.ts uses skipDuplicates: true, so the backend now atomically prevents duplicate tags. No changes needed in this component.

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