Skip to content

feat: two-tier calendar password protection#40

Merged
panteLx merged 3 commits intomainfrom
fix/password-protection
Dec 7, 2025
Merged

feat: two-tier calendar password protection#40
panteLx merged 3 commits intomainfrom
fix/password-protection

Conversation

@panteLx
Copy link
Owner

@panteLx panteLx commented Dec 7, 2025

Implements a two-tier password protection model for calendars and integrates verification across server and client.

Adds server-side checks that require a password for mutations if a calendar has a passwordHash, and require a password for reads when the calendar is marked locked. Verifies passwords from query params or request bodies and returns 401 on invalid credentials. Ensures calendar existence checks before verification.

Updates client flows to use a password cache, automatically append cached passwords to requests, and gracefully handle locked calendars by returning empty datasets on unauthorized responses. Hides or disables UI actions when a calendar requires a password and none is cached, and provides an unlock UX that refetches data on success.

Also updates developer instructions and translations to describe the two-tier protection, and standardizes password handling utilities to avoid duplicate prompts and improve user experience.

Summary by CodeRabbit

  • New Features

    • Calendar password protection moved to server-validated unlocks with an integrated unlock form and mutation flow.
    • Client-side password caching and automatic refresh of shifts, presets, notes, syncs and stats after unlock.
    • UI gating: editing, sync and creation controls are hidden/disabled until a calendar is unlocked.
    • New localized messages for protected calendars and unlock actions.
  • Bug Fixes / UX

    • Graceful handling of unauthorized responses (locked calendars return empty data views).

✏️ Tip: You can customize this high-level summary in your review settings.

Implements a two-tier password protection model for calendars and integrates verification across server and client.

Adds server-side checks that require a password for mutations if a calendar has a passwordHash, and require a password for reads when the calendar is marked locked. Verifies passwords from query params or request bodies and returns 401 on invalid credentials. Ensures calendar existence checks before verification.

Updates client flows to use a password cache, automatically append cached passwords to requests, and gracefully handle locked calendars by returning empty datasets on unauthorized responses. Hides or disables UI actions when a calendar requires a password and none is cached, and provides an unlock UX that refetches data on success.

Also updates developer instructions and translations to describe the two-tier protection, and standardizes password handling utilities to avoid duplicate prompts and improve user experience.
Copilot AI review requested due to automatic review settings December 7, 2025 02:06
@coderabbitai
Copy link

coderabbitai bot commented Dec 7, 2025

Note

Other AI code review bot(s) detected

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

Walkthrough

Server-side password enforcement added: protected calendars (passwordHash + isLocked) require a provided password for GET (query) and mutations (body). Client caches/supplies passwords, gates UI, and refetches data after unlock; multiple API routes and hooks updated to verify passwords and return 401/404 consistently.

Changes

Cohort / File(s) Summary
API: Calendar & Shift protection
app/api/calendars/[id]/route.ts, app/api/shifts/route.ts, app/api/shifts/[id]/route.ts, app/api/shifts/stats/route.ts
Added password extraction (query/body), calendar lookup, and verifyPassword checks before returning or mutating shift/stats data; return 401 for invalid password and 404 when calendar missing. POST now accepts password in payload where applicable.
API: Presets & Notes
app/api/presets/route.ts, app/api/presets/[id]/route.ts, app/api/notes/route.ts, app/api/notes/[id]/route.ts
GET handlers fetch related calendar and enforce password verification for protected/locked calendars (password via query); added preset-by-id GET with the same checks; 401/404 handling added.
API: External Syncs & Syncs Manual Trigger & Logs
app/api/external-syncs/route.ts, app/api/external-syncs/[id]/route.ts, app/api/external-syncs/[id]/sync/route.ts, app/api/sync-logs/route.ts
External-syncs and sync-log endpoints now fetch the associated calendar and require verifyPassword (query for GET, body for mutations) when calendar is protected & locked; early validation and 401/404 early-returns added.
Client: Hooks & Cache propagation
hooks/useShifts.ts, hooks/useNotes.ts, hooks/usePresets.ts
Hooks read cached password (getCachedPassword) and append as query param for GETs; include password in mutation bodies; 401/non-ok GET responses degrade to clearing lists and returning early.
Client: Page + Unlock flow
app/page.tsx
Computes requiresPassword vs cached password to gate UI interactions, hides/disables actions when password missing, integrates unlock form, and triggers refetches (shifts, presets, notes, external syncs, sync status, stats) on successful unlock.
Client: Dialogs & Sync management
components/external-sync-manage-dialog.tsx, components/sync-notification-dialog.tsx
Read cached password and include it in all API calls (query params or request bodies) for managing external syncs and sync-related log actions.
Client: UI components & props
components/preset-list.tsx, components/preset-selector.tsx, components/calendar-selector.tsx, components/calendar-grid.tsx, components/shift-card.tsx, components/shifts-list.tsx, components/shift-stats.tsx, components/preset-list.tsx
Made several handler props optional (onCreateNew, onManageClick, onUnlock, onDayRightClick, onNoteIconClick, onLongPress, onDelete, onDeleteShift) and added guards; preset list now supports unlock hint and onUnlock callback; shift-stats and other components now append cached password to fetches.
Client: Password cache util
lib/password-cache.ts
getCachedPassword signature broadened to accept `string
Localization & Docs
messages/en.json, messages/de.json, .github/copilot-instructions.md
Added password UI strings (unlockRequired, unlockRequiredDescription, unlockCalendar) and updated guidance documenting the two-tier password flow and client caching behavior.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User as Browser (UI)
participant UI as Hooks/Components
participant API as Server (Route handlers)
participant DB as Database (calendars table)
Note over User,UI: Page load or protected action
User->>UI: attempt load/action
UI->>UI: read cached password (getCachedPassword(calendarId))
UI->>API: GET/POST /api/... ?calendarId=...&password=... or body { ..., password }
API->>DB: fetch calendar by id
DB-->>API: calendar { passwordHash, isLocked, ... }
alt calendar protected & isLocked
API->>API: verifyPassword(provided, passwordHash)
alt password valid
API-->>UI: 200 + data
UI-->>User: render data / enable interactions
else invalid
API-->>UI: 401 Unauthorized
UI-->>User: show unlock form / hide gated UI
else not protected or unlocked
API-->>UI: 200 + data
UI-->>User: render data

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Review consistency of verifyPassword usage and uniform 401/404 semantics across all modified endpoints.
  • Check UI gating logic in app/page.tsx to ensure all sensitive interactions are disabled/hidden when required and that refetch triggers cover shifts, presets, notes, external syncs, sync status, and stats.
  • Validate hooks’ error paths: ensure 401/non-ok responses clear state as intended without swallowing actionable errors.
  • Confirm optionalized props are safely guarded where components are consumed.

Possibly related PRs

Poem

🐇 I guard the calendar gate tonight,
A tiny hop, a hashed delight.
Cache a secret, whisper true,
Unlock the days — the view’s for you.
Fresh fetches dance in morning light. 🎩🔐

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.48% 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 'feat: two-tier calendar password protection' accurately summarizes the main objective: implementing a two-tier password protection model for calendars with server-side and client-side components.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/password-protection

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.

@panteLx panteLx linked an issue Dec 7, 2025 that may be closed by this pull request
@panteLx panteLx changed the title fix: two-tier calendar password protection feat: two-tier calendar password protection Dec 7, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request implements a comprehensive two-tier password protection system for calendars, distinguishing between write-only protection (passwordHash set, isLocked=false) and full protection (passwordHash set, isLocked=true). The implementation ensures that calendars with passwords require authentication for mutations (POST/PUT/PATCH/DELETE), while fully locked calendars additionally require passwords for read operations (GET).

Key changes:

  • Server-side password verification added to all API routes with consistent two-tier checking (passwordHash for mutations, passwordHash + isLocked for reads)
  • Client-side hooks automatically append cached passwords to requests and gracefully handle 401 responses with empty arrays
  • UI components conditionally hide or disable actions when calendars require passwords but none is cached, with an unlock UX for write-only protected calendars

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
messages/en.json Added translation keys for unlock prompts (unlockRequired, unlockRequiredDescription, unlockCalendar)
messages/de.json Added German translations for unlock prompts
hooks/useShifts.ts Auto-appends cached password to shift requests; returns empty array on 401
hooks/usePresets.ts Auto-appends cached password to preset requests; returns empty array on 401
hooks/useNotes.ts Auto-appends cached password to note requests; returns empty array on 401
components/sync-notification-dialog.tsx Adds password to sync log GET/DELETE/PATCH requests
components/shifts-list.tsx Makes onDeleteShift optional to support disabled state
components/shift-card.tsx Makes onDelete optional and conditionally renders delete button
components/shift-stats.tsx Adds password parameter to stats requests with data validation
components/preset-selector.tsx Passes onUnlock handler to preset list
components/preset-list.tsx Shows unlock hint for write-only protected calendars; makes action handlers optional
components/calendar-selector.tsx Hides external sync buttons when password required but not cached
components/calendar-grid.tsx Makes day interaction handlers optional for conditional disabling
components/external-sync-manage-dialog.tsx Adds password to all external sync API calls
app/page.tsx Implements shouldHideUIElements to conditionally disable UI; refetches all data after password success
app/api/sync-logs/route.ts Adds two-tier password verification to GET/PATCH/DELETE routes
app/api/shifts/stats/route.ts Adds read-level password check (passwordHash + isLocked)
app/api/shifts/route.ts Adds read-level check to GET, mutation-level check to POST
app/api/shifts/[id]/route.ts Adds read-level check to GET, mutation-level check to PUT/DELETE
app/api/presets/route.ts Adds read-level check to GET, mutation-level check to POST
app/api/presets/[id]/route.ts Adds GET endpoint with read-level check; PATCH/DELETE have mutation-level check
app/api/notes/route.ts Adds read-level check to GET, mutation-level check to POST
app/api/notes/[id]/route.ts Adds read-level check to GET, mutation-level check to PUT/DELETE
app/api/external-syncs/route.ts Adds read-level check to GET, mutation-level check to POST
app/api/external-syncs/[id]/sync/route.ts Adds mutation-level password check to POST sync trigger
app/api/external-syncs/[id]/route.ts Adds read-level check to GET, mutation-level check to PATCH/DELETE
app/api/calendars/[id]/route.ts Adds read-level password check to GET
.github/copilot-instructions.md Documents two-tier protection system with implementation examples and client flow

Copy link

@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 (3)
components/sync-notification-dialog.tsx (2)

143-160: Implement password dialog flow for 401 responses.

DELETE operations should handle 401 responses by showing the PasswordDialog component, allowing users to enter the password and retry the operation. The current generic error toast doesn't guide users to unlock the calendar when password verification fails.

Consider implementing:

const response = await fetch(`/api/sync-logs?calendarId=${calendarId}`, {
  method: "DELETE",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ password }),
});

if (response.status === 401) {
  // Show PasswordDialog, set pendingAction, retry after successful auth
} else if (response.ok) {
  setLogs([]);
  toast.success(t("syncNotifications.deleteSuccess"));
} else {
  toast.error(t("syncNotifications.deleteError"));
}

Based on coding guidelines: "Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication."


170-192: Implement password dialog flow for 401 responses.

PATCH operations should handle 401 responses by showing the PasswordDialog component, allowing users to enter the password and retry the operation. The current generic error toast doesn't guide users to unlock the calendar when password verification fails.

Consider implementing:

const response = await fetch(
  `/api/sync-logs?calendarId=${calendarId}&action=markErrorsAsRead`,
  {
    method: "PATCH",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ password }),
  }
);

if (response.status === 401) {
  // Show PasswordDialog, set pendingAction, retry after successful auth
} else if (response.ok) {
  await fetchLogs();
  toast.success(t("syncNotifications.markedAsRead"));
  onErrorsMarkedRead?.();
} else {
  toast.error(t("syncNotifications.markAsReadError"));
}

Based on coding guidelines: "Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication."

components/external-sync-manage-dialog.tsx (1)

507-566: calendarId dependency missing from useCallback.

The saveExternalSyncChanges callback uses calendarId (line 512) but it's not included in the dependency array. This could cause stale closure issues where the wrong password is sent if calendarId changes while editing.

   saveExternalSyncChanges,
+  calendarId,
   t,
 ]
🧹 Nitpick comments (7)
components/sync-notification-dialog.tsx (3)

65-75: Consider handling 401 responses with password dialog flow.

While the current silent failure approach aligns with gracefully handling locked calendars, users cannot unlock the calendar from this dialog when password verification fails. Consider checking for 401 status and showing the PasswordDialog component to allow password entry and retry, as per coding guidelines.

Example pattern:

const response = await fetch(`/api/sync-logs?${params}`);
if (response.status === 401) {
  // Show password dialog and retry on success
} else if (response.ok) {
  const data = await response.json();
  setLogs(data);
}

Based on coding guidelines: "Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication."


141-141: Remove redundant null check.

The ternary operator checking calendarId is unnecessary since line 136 already returns early if !calendarId.

Apply this diff:

-    const password = calendarId ? getCachedPassword(calendarId) : null;
+    const password = getCachedPassword(calendarId);

168-168: Remove redundant null check.

The ternary operator checking calendarId is unnecessary since line 164 already returns early if !calendarId.

Apply this diff:

-    const password = calendarId ? getCachedPassword(calendarId) : null;
+    const password = getCachedPassword(calendarId);
components/shift-stats.tsx (1)

7-7: Password‑aware stats fetch looks good; consider handling non‑OK responses and zero totals

Using getCachedPassword and URLSearchParams to append password is consistent with the shared password‑cache utilities and the new server contract. Two small robustness tweaks worth considering:

  1. Differentiate 401/other HTTP errors and log them explicitly

Right now any non‑OK response is treated as “no stats” (because data.stats will be missing), which hides server errors and stale data. You can special‑case 401 (locked/invalid password) vs other errors and log the latter:

       try {
         const password = getCachedPassword(calendarId);
         const params = new URLSearchParams({
           calendarId,
           period,
           date: currentDate.toISOString(),
         });
         if (password) {
           params.append("password", password);
         }

-        const response = await fetch(`/api/shifts/stats?${params}`);
-        const data = await response.json();
-
-        // Only set stats if response has the expected structure
-        if (data.stats && typeof data.stats === "object") {
-          setStats(data);
-        }
+        const response = await fetch(`/api/shifts/stats?${params}`);
+
+        if (!response.ok) {
+          if (response.status === 401) {
+            // Locked calendar or invalid password → treat as “no stats”
+            setStats(null);
+          } else {
+            console.error(
+              "Failed to fetch shift statistics:",
+              response.status,
+              response.statusText
+            );
+          }
+          return;
+        }
+
+        const data = await response.json();
+
+        // Only set stats if response has the expected structure
+        if (data.stats && typeof data.stats === "object") {
+          setStats(data);
+        } else {
+          setStats(null);
+        }
       } catch (error) {
         console.error("Failed to fetch shift statistics:", error);
       } finally {
  1. Avoid division by zero when all counts are 0

If stats.stats exists but all values are 0, totalShifts is 0 and (count / totalShifts) * 100 yields Infinity%/NaN%. A simple denominator guard keeps widths valid:

-  const totalShifts =
-    stats && stats.stats
-      ? Object.values(stats.stats).reduce((sum, count) => sum + count, 0)
-      : 0;
+  const totalShifts =
+    stats && stats.stats
+      ? Object.values(stats.stats).reduce((sum, count) => sum + count, 0)
+      : 0;
+  const totalShiftsDenominator = totalShifts || 1;
@@
-                          style={{
-                            width: `${(count / totalShifts) * 100}%`,
-                          }}
+                          style={{
+                            width: `${(count / totalShiftsDenominator) * 100}%`,
+                          }}

Also applies to: 48-56, 58-64, 76-79

messages/en.json (1)

63-66: English password/unlock strings are consistent with new protection model

The added password.unlock* keys clearly describe the locked calendar state and unlock action, and they align with the two‑tier password flow. Just ensure they are referenced in the unlock UI so they don’t become unused per the translation guidelines.

components/calendar-grid.tsx (1)

19-21: Optional interaction callbacks are good; consider stopping propagation on note icon clicks

Making onDayRightClick, onNoteIconClick, and onLongPress optional and guarding their use (including only setting long‑press timers when provided) makes the grid safer and more reusable.

One small UX tweak: clicking the note icon currently still triggers the parent day’s onClick (since the click bubbles). If the intended behavior is “open notes without toggling shifts,” you can stop propagation:

-              {dayNote && onNoteIconClick && (
+              {dayNote && onNoteIconClick && (
                 <motion.div
                   className="group/note relative"
-                  onClick={(e) => onNoteIconClick(e, day)}
+                  onClick={(e) => {
+                    e.stopPropagation();
+                    onNoteIconClick(e, day);
+                  }}
                   title={dayNote.note}
                   whileHover={{ scale: 1.1 }}
                   whileTap={{ scale: 0.9 }}
                 >

Also applies to: 98-104, 119-121, 158-174

app/page.tsx (1)

906-914: Use setCachedPassword() instead of direct localStorage.setItem().

Per coding guidelines, password-protected calendars should use utilities from lib/password-cache.ts. Line 908-910 directly accesses localStorage instead of using setCachedPassword().

+import {
+  getCachedPassword,
+  setCachedPassword,
+  verifyAndCachePassword,
+} from "@/lib/password-cache";

Then update the form handler:

                          if (data.valid) {
-                           localStorage.setItem(
-                             `calendar_password_${selectedCalendar}`,
-                             password
-                           );
+                           setCachedPassword(selectedCalendar, password);
                            setIsCalendarUnlocked(true);

Based on learnings, use utilities from lib/password-cache.ts instead of direct localStorage access.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c87420 and 9fd1028.

📒 Files selected for processing (28)
  • .github/copilot-instructions.md (1 hunks)
  • app/api/calendars/[id]/route.ts (2 hunks)
  • app/api/external-syncs/[id]/route.ts (5 hunks)
  • app/api/external-syncs/[id]/sync/route.ts (2 hunks)
  • app/api/external-syncs/route.ts (3 hunks)
  • app/api/notes/[id]/route.ts (2 hunks)
  • app/api/notes/route.ts (1 hunks)
  • app/api/presets/[id]/route.ts (1 hunks)
  • app/api/presets/route.ts (1 hunks)
  • app/api/shifts/[id]/route.ts (2 hunks)
  • app/api/shifts/route.ts (4 hunks)
  • app/api/shifts/stats/route.ts (2 hunks)
  • app/api/sync-logs/route.ts (4 hunks)
  • app/page.tsx (8 hunks)
  • components/calendar-grid.tsx (4 hunks)
  • components/calendar-selector.tsx (1 hunks)
  • components/external-sync-manage-dialog.tsx (11 hunks)
  • components/preset-list.tsx (6 hunks)
  • components/preset-selector.tsx (1 hunks)
  • components/shift-card.tsx (2 hunks)
  • components/shift-stats.tsx (3 hunks)
  • components/shifts-list.tsx (1 hunks)
  • components/sync-notification-dialog.tsx (4 hunks)
  • hooks/useNotes.ts (1 hunks)
  • hooks/usePresets.ts (2 hunks)
  • hooks/useShifts.ts (2 hunks)
  • messages/de.json (1 hunks)
  • messages/en.json (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
app/api/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/api/**/*.ts: In Next.js 16 API routes, use async params destructuring: const { id } = await params; for dynamic routes instead of accessing params directly.
API routes must validate required query parameters and return NextResponse.json with appropriate status codes (e.g., 400 for missing parameters).
All API errors must be logged to console using console.error() for debugging purposes.

Files:

  • app/api/calendars/[id]/route.ts
  • app/api/presets/route.ts
  • app/api/presets/[id]/route.ts
  • app/api/notes/[id]/route.ts
  • app/api/notes/route.ts
  • app/api/external-syncs/[id]/route.ts
  • app/api/shifts/stats/route.ts
  • app/api/external-syncs/route.ts
  • app/api/sync-logs/route.ts
  • app/api/shifts/route.ts
  • app/api/shifts/[id]/route.ts
  • app/api/external-syncs/[id]/sync/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
All TypeScript/React components must use useTranslations() for all user-facing text strings and reference translation keys from messages/{de,en}.json.
Date formatting must use formatDateToLocal() function to output YYYY-MM-DD format.
Color values must be stored in hex format (e.g., #3b82f6) and use 20% opacity for backgrounds by appending '20' to the hex value (e.g., ${color}20).
Add comments only for complex logic or non-obvious behavior. Avoid over-commenting simple or self-documenting code.
Use next-intl library with messages/{de,en}.json for translations. Format dates using locale-specific functions: de or enUS from date-fns.

Files:

  • app/api/calendars/[id]/route.ts
  • app/api/presets/route.ts
  • components/shift-stats.tsx
  • app/api/presets/[id]/route.ts
  • app/api/notes/[id]/route.ts
  • hooks/usePresets.ts
  • components/calendar-selector.tsx
  • components/sync-notification-dialog.tsx
  • components/shift-card.tsx
  • app/api/notes/route.ts
  • hooks/useNotes.ts
  • app/api/external-syncs/[id]/route.ts
  • components/preset-selector.tsx
  • components/shifts-list.tsx
  • hooks/useShifts.ts
  • app/api/shifts/stats/route.ts
  • app/api/external-syncs/route.ts
  • app/api/sync-logs/route.ts
  • components/external-sync-manage-dialog.tsx
  • app/api/shifts/route.ts
  • components/calendar-grid.tsx
  • app/api/shifts/[id]/route.ts
  • components/preset-list.tsx
  • app/api/external-syncs/[id]/sync/route.ts
  • app/page.tsx
**/*.{ts,tsx,json}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All code, comments, variable names, and messages must be written in English.

Files:

  • app/api/calendars/[id]/route.ts
  • app/api/presets/route.ts
  • components/shift-stats.tsx
  • app/api/presets/[id]/route.ts
  • app/api/notes/[id]/route.ts
  • hooks/usePresets.ts
  • components/calendar-selector.tsx
  • components/sync-notification-dialog.tsx
  • messages/de.json
  • messages/en.json
  • components/shift-card.tsx
  • app/api/notes/route.ts
  • hooks/useNotes.ts
  • app/api/external-syncs/[id]/route.ts
  • components/preset-selector.tsx
  • components/shifts-list.tsx
  • hooks/useShifts.ts
  • app/api/shifts/stats/route.ts
  • app/api/external-syncs/route.ts
  • app/api/sync-logs/route.ts
  • components/external-sync-manage-dialog.tsx
  • app/api/shifts/route.ts
  • components/calendar-grid.tsx
  • app/api/shifts/[id]/route.ts
  • components/preset-list.tsx
  • app/api/external-syncs/[id]/sync/route.ts
  • app/page.tsx
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

components/**/*.{ts,tsx}: All dialog components must follow the unified design pattern with gradient backgrounds, consistent padding (p-6, px-6, pb-6), border styling (border-border/50), backdrop blur effects, and gradient text for titles.
Components must support real-time updates via Server-Sent Events (SSE). Listen to relevant SSE events and implement silent refresh patterns using fetchData(false) to update without loading states.
Calendar interactions follow these patterns: Left-click toggles shift with selected preset, right-click opens note dialog (prevent default context menu), toggle logic deletes if exists or creates if not.

Files:

  • components/shift-stats.tsx
  • components/calendar-selector.tsx
  • components/sync-notification-dialog.tsx
  • components/shift-card.tsx
  • components/preset-selector.tsx
  • components/shifts-list.tsx
  • components/external-sync-manage-dialog.tsx
  • components/calendar-grid.tsx
  • components/preset-list.tsx
components/**/*[Dd]ialog*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Shift dialog has saveAsPreset enabled by default to allow quick preset creation during shift creation.

Files:

  • components/sync-notification-dialog.tsx
  • components/external-sync-manage-dialog.tsx
messages/{de,en}.json

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Unused translation keys in messages/{de,en}.json must be removed to keep translation files clean. All translation keys should be actively used in components.

Files:

  • messages/de.json
  • messages/en.json
app/page.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/page.tsx: Use counter-based props (e.g., syncLogRefreshTrigger) as refresh triggers to update component data smoothly without UI disruption or flashing.
Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.
Use separate mobile calendar dialog (showMobileCalendarDialog) for responsive design on mobile devices.

Files:

  • app/page.tsx
🧠 Learnings (15)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().

Applied to files:

  • app/api/calendars/[id]/route.ts
  • app/api/presets/route.ts
  • components/shift-stats.tsx
  • app/api/presets/[id]/route.ts
  • app/api/notes/[id]/route.ts
  • hooks/usePresets.ts
  • components/calendar-selector.tsx
  • components/sync-notification-dialog.tsx
  • messages/en.json
  • app/api/notes/route.ts
  • hooks/useNotes.ts
  • app/api/external-syncs/[id]/route.ts
  • components/preset-selector.tsx
  • hooks/useShifts.ts
  • app/api/shifts/stats/route.ts
  • app/api/external-syncs/route.ts
  • app/api/sync-logs/route.ts
  • components/external-sync-manage-dialog.tsx
  • .github/copilot-instructions.md
  • app/api/shifts/route.ts
  • components/calendar-grid.tsx
  • app/api/shifts/[id]/route.ts
  • components/preset-list.tsx
  • app/api/external-syncs/[id]/sync/route.ts
  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Database schema: Use crypto.randomUUID() for ID generation, store timestamps as integers with { mode: 'timestamp' } which auto-converts to Date objects, hash passwords using SHA-256 via lib/password-utils.ts.

Applied to files:

  • components/shift-stats.tsx
  • components/sync-notification-dialog.tsx
  • app/api/external-syncs/[id]/route.ts
  • hooks/useShifts.ts
  • app/api/shifts/stats/route.ts
  • app/api/external-syncs/route.ts
  • app/api/sync-logs/route.ts
  • components/external-sync-manage-dialog.tsx
  • .github/copilot-instructions.md
  • app/api/shifts/route.ts
  • app/api/external-syncs/[id]/sync/route.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : Use next-intl library with messages/{de,en}.json for translations. Format dates using locale-specific functions: de or enUS from date-fns.

Applied to files:

  • components/shift-stats.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : All TypeScript/React components must use useTranslations() for all user-facing text strings and reference translation keys from messages/{de,en}.json.

Applied to files:

  • components/shift-stats.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.

Applied to files:

  • components/shift-stats.tsx
  • hooks/usePresets.ts
  • hooks/useNotes.ts
  • app/api/external-syncs/[id]/route.ts
  • hooks/useShifts.ts
  • app/api/shifts/stats/route.ts
  • app/api/external-syncs/route.ts
  • app/api/sync-logs/route.ts
  • components/external-sync-manage-dialog.tsx
  • .github/copilot-instructions.md
  • app/api/shifts/route.ts
  • components/calendar-grid.tsx
  • components/preset-list.tsx
  • app/api/external-syncs/[id]/sync/route.ts
  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.

Applied to files:

  • components/shift-stats.tsx
  • components/sync-notification-dialog.tsx
  • components/preset-selector.tsx
  • components/external-sync-manage-dialog.tsx
  • .github/copilot-instructions.md
  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to components/**/*.{ts,tsx} : Calendar interactions follow these patterns: Left-click toggles shift with selected preset, right-click opens note dialog (prevent default context menu), toggle logic deletes if exists or creates if not.

Applied to files:

  • components/calendar-selector.tsx
  • components/shift-card.tsx
  • components/shifts-list.tsx
  • .github/copilot-instructions.md
  • components/calendar-grid.tsx
  • components/preset-list.tsx
  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use separate mobile calendar dialog (showMobileCalendarDialog) for responsive design on mobile devices.

Applied to files:

  • components/calendar-selector.tsx
  • components/calendar-grid.tsx
  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: When creating new components with dialogs: (1) Create in components/ using shadcn/ui Dialog, (2) Accept props: open, onOpenChange, onSubmit, optional onDelete, (3) Use useTranslations(), (4) Reset local state when open changes to false, (5) Integrate in app/page.tsx.

Applied to files:

  • components/sync-notification-dialog.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to messages/{de,en}.json : Unused translation keys in messages/{de,en}.json must be removed to keep translation files clean. All translation keys should be actively used in components.

Applied to files:

  • messages/de.json
  • messages/en.json
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Implement cascade delete relationships in database schema: calendars → shifts, shiftPresets, calendarNotes (cascade delete); shiftPresets → shifts (set null on delete).

Applied to files:

  • components/shift-card.tsx
  • components/shifts-list.tsx
  • app/api/shifts/stats/route.ts
  • app/api/shifts/route.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to components/**/*[Dd]ialog*.{ts,tsx} : Shift dialog has saveAsPreset enabled by default to allow quick preset creation during shift creation.

Applied to files:

  • components/preset-selector.tsx
  • components/preset-list.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/**/*.ts : Drizzle ORM must be used for all database operations. drizzle.config.ts must be included in Docker runner stage for migrations to work.

Applied to files:

  • app/api/shifts/stats/route.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Database migrations: After schema changes in lib/db/schema.ts, run 'npm run db:generate' followed by 'npm run db:migrate'. Never use db:push - prefer explicit migrations. Migrations must be version-controlled.

Applied to files:

  • app/api/shifts/stats/route.ts
🧬 Code graph analysis (17)
app/api/calendars/[id]/route.ts (1)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
app/api/presets/route.ts (3)
lib/db/index.ts (1)
  • db (19-19)
lib/db/schema.ts (1)
  • calendars (4-18)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
components/shift-stats.tsx (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
app/api/presets/[id]/route.ts (2)
app/api/calendars/[id]/route.ts (1)
  • GET (8-53)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
app/api/notes/[id]/route.ts (3)
lib/db/index.ts (1)
  • db (19-19)
lib/db/schema.ts (1)
  • calendars (4-18)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
hooks/usePresets.ts (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
components/calendar-selector.tsx (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
components/sync-notification-dialog.tsx (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
app/api/notes/route.ts (3)
lib/db/index.ts (1)
  • db (19-19)
lib/db/schema.ts (1)
  • calendars (4-18)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
hooks/useNotes.ts (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
hooks/useShifts.ts (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
app/api/shifts/stats/route.ts (3)
lib/db/index.ts (1)
  • db (19-19)
lib/db/schema.ts (1)
  • calendars (4-18)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
app/api/sync-logs/route.ts (4)
app/api/calendars/[id]/route.ts (1)
  • GET (8-53)
app/api/presets/route.ts (1)
  • GET (9-58)
lib/db/schema.ts (1)
  • calendars (4-18)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
components/external-sync-manage-dialog.tsx (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
components/preset-list.tsx (2)
lib/db/schema.ts (1)
  • calendars (4-18)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
app/api/external-syncs/[id]/sync/route.ts (2)
lib/db/schema.ts (2)
  • externalSyncs (20-47)
  • calendars (4-18)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
app/page.tsx (2)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
lib/db/schema.ts (1)
  • calendars (4-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: build-dev
🔇 Additional comments (37)
components/calendar-selector.tsx (1)

44-47: Password gating logic for sync buttons is correct.

The condition properly gates sync buttons based on the passwordHash field from CalendarWithCount. The implementation correctly uses getCachedPassword() from lib/password-cache.ts as required by the coding guidelines, and the logic—hiding sync buttons when a password is required but not cached—aligns with the two-tier password protection model (passwordHash for mutations, isLocked for reads).

components/sync-notification-dialog.tsx (1)

5-5: LGTM!

Correct usage of the password cache utility as per coding guidelines.

app/api/notes/route.ts (1)

22-45: Notes GET password gating aligns with two‑tier protection

Fetching the calendar first, returning 404 when missing, and only enforcing verifyPassword when passwordHash && isLocked correctly implements “read requires password only when locked” and “existence check before auth” for notes.

components/shift-card.tsx (1)

12-12: Making onDelete optional and guarding the button is a safe improvement

Allowing onDelete to be optional and only rendering the delete button when both !shift.externalSyncId and onDelete are truthy avoids unsafe calls and correctly disables deletion for externally synced shifts.

Also applies to: 52-60

messages/de.json (1)

63-66: German unlock strings match the English semantics

The added password.unlock* German translations accurately mirror the English messages for locked calendars and the unlock action, fitting the new two‑tier password UX.

app/api/shifts/stats/route.ts (1)

3-8: Stats GET password verification correctly follows the two‑tier model

Loading the calendar first, returning 404 when not found, and only enforcing verifyPassword when passwordHash && isLocked cleanly gates statistics behind a password for locked calendars while keeping unlocked ones readable. This is consistent with the wider password protection rules.

Also applies to: 10-10, 35-58

.github/copilot-instructions.md (1)

67-133: LGTM! Comprehensive password protection documentation.

The two-tier protection model is clearly explained with accurate code examples that match the actual implementation across the API routes. The implementation notes provide valuable guidance for avoiding common pitfalls like double password prompts and ensuring graceful 401 handling.

components/shifts-list.tsx (1)

13-13: LGTM! Optional callback supports password-gated deletion.

Making onDeleteShift optional aligns with the password protection model where delete actions may be unavailable until authentication succeeds. This allows the component to render shifts in locked calendars without requiring a delete handler.

app/api/notes/[id]/route.ts (1)

15-51: LGTM! Correct implementation of two-tier protection for notes.

The password verification correctly checks both passwordHash and isLocked for read access. The implementation properly handles the case where the note exists but the calendar doesn't (returns 404 for calendar).

Note: The same security consideration about passwords in query parameters applies here (see comment on app/api/presets/route.ts).

hooks/useShifts.ts (2)

16-32: LGTM! Graceful handling of locked calendars.

The implementation correctly uses getCachedPassword() from the password-cache utilities as per coding guidelines. The graceful degradation on non-ok responses (returning empty array) prevents UI crashes when calendars are locked without valid cached passwords.

As per coding guidelines: Using utilities from lib/password-cache.ts instead of direct localStorage access.


57-65: LGTM! Password included in shift creation.

Correctly retrieves and includes the cached password in the POST request body for write-protected calendars.

hooks/usePresets.ts (1)

18-30: LGTM! Consistent password handling pattern.

The implementation mirrors the pattern in useShifts.ts, correctly using getCachedPassword() and gracefully degrading to empty arrays on 401 responses. The comment on line 26 clearly explains the behavior for locked calendars.

As per coding guidelines: Using utilities from lib/password-cache.ts for password-protected calendars.

components/preset-selector.tsx (1)

179-179: Clarify the purpose of the empty async function.

The onUnlock prop is passed an empty async function async () => {} which doesn't perform any action after successful password verification. This seems incomplete.

Is this intentional? Typically after successful unlock, you would want to:

  • Refetch presets: () => onPasswordRequired(async () => { await fetchPresets(); })
  • Or trigger the parent's preset refresh: () => onPasswordRequired(onPresetsChange)

If PresetList handles its own data refresh after unlock, this might be acceptable. Please verify the intended behavior and ensure data is refreshed after successful password entry.

app/api/calendars/[id]/route.ts (1)

14-37: LGTM! Correct two-tier protection with efficient order of operations.

The implementation correctly checks both passwordHash and isLocked for read access. The order of operations is optimal: fetch calendar → verify password → fetch related data. This prevents unnecessary shift queries when password verification fails.

Note: The same security consideration about passwords in query parameters applies here (see comment on app/api/presets/route.ts).

app/api/presets/route.ts (1)

21-44: Passwords are transmitted via GET query parameters, which are logged by servers, proxies, and browsers.

The implementation correctly validates the calendar exists and verifies the password, but this transmission method carries inherent security risks. This pattern is used consistently across multiple endpoints (shifts, notes, calendars), indicating a deliberate architectural choice. If tighter security is required, consider:

  • Requiring POST requests for protected data retrieval
  • Using request headers instead of query parameters
  • Implementing session-based authentication after initial password verification via POST
app/api/presets/[id]/route.ts (1)

8-58: LGTM! GET handler follows established patterns.

The new GET handler correctly implements the two-tier password protection model:

  • Fetches preset first to verify existence (404 handling)
  • Fetches related calendar to check protection status
  • Requires password only when passwordHash && isLocked (read-tier protection)
  • Uses query parameter for password (consistent with other GET endpoints)
  • Proper error logging and status codes
app/api/external-syncs/route.ts (2)

26-49: LGTM! GET handler password protection is correct.

The implementation correctly applies the two-tier model for reads: password is required only when both passwordHash and isLocked are true. Calendar existence check precedes password verification.


92-114: LGTM! POST handler password protection is correct.

Mutations correctly require password whenever passwordHash exists (regardless of isLocked status), following the PR's two-tier protection model where writes are always protected.

components/external-sync-manage-dialog.tsx (2)

99-105: LGTM! Password is correctly appended to fetch requests.

The implementation correctly uses getCachedPassword() from the password-cache utilities (as per coding guidelines) and appends it to query parameters for GET requests.


334-340: LGTM! Sync request includes password in body.

The sync request correctly sends the password in the JSON body for the POST mutation, consistent with the server-side expectation.

app/api/external-syncs/[id]/sync/route.ts (1)

347-395: LGTM! Password protection for sync operation is correct.

The implementation:

  • Reads password from JSON body (appropriate for POST mutation)
  • Validates external sync exists before calendar lookup
  • Uses mutation-tier protection (passwordHash only, not checking isLocked)
  • Gracefully handles body parsing failures
app/api/shifts/[id]/route.ts (1)

46-67: LGTM! Password protection for shift GET is correct.

The implementation correctly applies the read-tier protection model (passwordHash && isLocked). While the calendar data could potentially be extracted from the existing join query (lines 32-36), the separate fetch provides cleaner separation of concerns for password verification logic.

app/api/sync-logs/route.ts (3)

21-42: LGTM! GET handler implements read-tier protection correctly.

Password verification uses the correct two-tier model for reads (passwordHash && isLocked).


74-108: LGTM! PATCH handler implements mutation-tier protection correctly.

Password verification correctly requires password whenever passwordHash exists, regardless of lock status.


144-178: LGTM! DELETE handler implements mutation-tier protection correctly.

Consistent with PATCH handler, correctly requiring password for any protected calendar.

app/api/shifts/route.ts (2)

22-45: LGTM! GET handler implements read-tier protection correctly.

The implementation correctly:

  • Extracts password from query parameters
  • Validates calendar existence with 404 response
  • Applies two-tier model (passwordHash && isLocked) for reads

122-153: LGTM! POST handler implements mutation-tier protection correctly.

Password is extracted from request body and verification requires password whenever passwordHash exists.

app/page.tsx (5)

113-127: LGTM! Password is correctly appended to external syncs fetch.

Uses getCachedPassword() utility as per coding guidelines and appends to query parameters for the GET request.


218-228: LGTM! UI gating logic is correct.

shouldHideUIElements correctly computes whether the calendar requires a password but none is cached. This drives the conditional rendering of interactive elements.


370-380: LGTM! Comprehensive data refresh after password success.

The handlePasswordSuccess function now correctly refetches all relevant data (shifts, presets, notes, external syncs, sync error status) and triggers a stats refresh after successful authentication.


998-1006: LGTM! UI interactions correctly gated.

Right-click, note icon click, and long press handlers are conditionally passed as undefined when shouldHideUIElements is true, effectively disabling these interactions for locked calendars without a cached password.


1054-1056: LGTM! Delete action correctly hidden.

The delete shift handler is passed as undefined when UI elements should be hidden, preventing unauthorized deletions.

components/preset-list.tsx (2)

15-17: LGTM: Props correctly updated for password protection flow.

Making onCreateNew and onManageClick optional allows the component to gracefully handle locked states by disabling actions when callbacks aren't provided. The new onUnlock prop enables the password entry flow.

Also applies to: 29-29


126-136: LGTM: Buttons correctly disabled when callbacks unavailable.

The conditional disabled={!onCreateNew} and disabled={!onManageClick} properly prevent user interaction when the parent component doesn't provide handlers, which aligns with the password protection requirements.

Also applies to: 157-162, 169-177

app/api/external-syncs/[id]/route.ts (3)

19-56: LGTM: Two-tier read protection correctly implemented.

The GET handler properly enforces password verification only when calendar.passwordHash && calendar.isLocked (line 49), allowing reads without password when the calendar is not locked. Calendar existence is validated before password checks, and appropriate status codes (404, 401) are returned.


100-122: LGTM: Mutation password protection correctly enforced.

The PATCH handler properly requires password verification when calendar.passwordHash exists (line 114), regardless of isLocked status. This correctly implements tier-one protection (mutations always need password), with calendar existence validated before password checks.


232-279: LGTM: DELETE handler properly enforces password protection.

The DELETE handler correctly implements tier-one protection by requiring password verification when calendar.passwordHash exists (line 272). The optional body parsing with error handling (lines 236-242) gracefully handles requests without JSON bodies, and proper sequencing ensures calendar existence before password validation.

Copy link

@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

Caution

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

⚠️ Outside diff range comments (2)
app/page.tsx (2)

909-916: Use password cache utilities instead of direct localStorage access.

Per coding guidelines, password-protected calendars should use utilities from lib/password-cache.ts instead of direct localStorage access. The verifyAndCachePassword() function already handles both verification and caching.

                         .then((data) => {
                           if (data.valid) {
-                            localStorage.setItem(
-                              `calendar_password_${selectedCalendar}`,
-                              password
-                            );
+                            // Use password cache utility
+                            import("@/lib/password-cache").then(({ setCachedPassword }) => {
+                              setCachedPassword(selectedCalendar, password);
+                            });
                             setIsCalendarUnlocked(true);
                             // Trigger data refetch after successful password entry
                             handlePasswordSuccess(password);

Alternatively, refactor the entire form submission to use verifyAndCachePassword() which already exists in scope:

                   onSubmit={(e) => {
                     e.preventDefault();
                     const formData = new FormData(e.currentTarget);
                     const password = formData.get("password") as string;
                     if (password && selectedCalendar) {
-                      fetch(
-                        `/api/calendars/${selectedCalendar}/verify-password`,
-                        {
-                          method: "POST",
-                          headers: { "Content-Type": "application/json" },
-                          body: JSON.stringify({ password }),
-                        }
-                      )
-                        .then((response) => response.json())
-                        .then((data) => {
-                          if (data.valid) {
-                            localStorage.setItem(
-                              `calendar_password_${selectedCalendar}`,
-                              password
-                            );
-                            setIsCalendarUnlocked(true);
-                            // Trigger data refetch after successful password entry
-                            handlePasswordSuccess(password);
-                          } else {
-                            toast.error(t("password.errorIncorrect"));
-                          }
-                        })
-                        .catch(() => {
-                          toast.error(t("password.errorIncorrect"));
-                        });
+                      verifyAndCachePassword(selectedCalendar, password)
+                        .then((result) => {
+                          if (result.valid) {
+                            setIsCalendarUnlocked(true);
+                            handlePasswordSuccess(password);
+                          } else {
+                            toast.error(t("password.errorIncorrect"));
+                          }
+                        })
+                        .catch(() => {
+                          toast.error(t("password.errorIncorrect"));
+                        });
                     }
                   }}

Based on learnings, password cache utilities should be used consistently.


290-317: Wrap handlePasswordSuccess with useCallback and add to useEffect dependencies.

The function is called at line 292 but missing from the dependency array at line 317. Since handlePasswordSuccess depends on refetchShifts, refetchPresets, refetchNotes, fetchExternalSyncs, fetchSyncErrorStatus, and pendingAction, the effect should re-run when these change. Wrap it with useCallback to memoize the function and add it to the dependencies array, following the pattern used for fetchExternalSyncs and fetchSyncErrorStatus elsewhere in this file.

🧹 Nitpick comments (2)
app/page.tsx (2)

119-126: Handle 401 unauthorized responses explicitly.

Per the PR objectives, the client should "gracefully handle locked calendars by returning empty datasets on unauthorized responses." Currently, non-200 responses silently fail without distinguishing authorization failures from other errors.

       const response = await fetch(`/api/external-syncs?${params}`);
       if (response.ok) {
         const data = await response.json();
         setExternalSyncs(data);
+      } else if (response.status === 401) {
+        // Unauthorized - calendar is locked, clear data gracefully
+        setExternalSyncs([]);
       }
     } catch (error) {
       console.error("Failed to fetch external syncs:", error);
     }

146-157: Handle 401 unauthorized responses for sync logs as well.

Same issue as external syncs - unauthorized responses should explicitly clear error status rather than silently failing.

       const response = await fetch(`/api/sync-logs?${params}`);
       if (response.ok) {
         const logs = await response.json();
-        // Only show errors that are not read
         const hasErrors = logs.some(
           (log: any) => log.status === "error" && !log.isRead
         );
         setHasSyncErrors(hasErrors);
+      } else if (response.status === 401) {
+        // Unauthorized - calendar is locked, hide error indicators
+        setHasSyncErrors(false);
       }
     } catch (error) {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9fd1028 and 167d48e.

📒 Files selected for processing (1)
  • app/page.tsx (8 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
All TypeScript/React components must use useTranslations() for all user-facing text strings and reference translation keys from messages/{de,en}.json.
Date formatting must use formatDateToLocal() function to output YYYY-MM-DD format.
Color values must be stored in hex format (e.g., #3b82f6) and use 20% opacity for backgrounds by appending '20' to the hex value (e.g., ${color}20).
Add comments only for complex logic or non-obvious behavior. Avoid over-commenting simple or self-documenting code.
Use next-intl library with messages/{de,en}.json for translations. Format dates using locale-specific functions: de or enUS from date-fns.

Files:

  • app/page.tsx
app/page.tsx

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/page.tsx: Use counter-based props (e.g., syncLogRefreshTrigger) as refresh triggers to update component data smoothly without UI disruption or flashing.
Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.
Use separate mobile calendar dialog (showMobileCalendarDialog) for responsive design on mobile devices.

Files:

  • app/page.tsx
**/*.{ts,tsx,json}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All code, comments, variable names, and messages must be written in English.

Files:

  • app/page.tsx
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to components/**/*.{ts,tsx} : Calendar interactions follow these patterns: Left-click toggles shift with selected preset, right-click opens note dialog (prevent default context menu), toggle logic deletes if exists or creates if not.

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Database schema: Use crypto.randomUUID() for ID generation, store timestamps as integers with { mode: 'timestamp' } which auto-converts to Date objects, hash passwords using SHA-256 via lib/password-utils.ts.

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use counter-based props (e.g., syncLogRefreshTrigger) as refresh triggers to update component data smoothly without UI disruption or flashing.

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use separate mobile calendar dialog (showMobileCalendarDialog) for responsive design on mobile devices.

Applied to files:

  • app/page.tsx
🧬 Code graph analysis (1)
app/page.tsx (2)
lib/password-cache.ts (1)
  • getCachedPassword (9-12)
lib/db/schema.ts (1)
  • calendars (4-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev
🔇 Additional comments (5)
app/page.tsx (5)

219-230: LGTM!

The shouldHideUIElements memo correctly gates UI based on password protection status. Using passwordCacheTrigger as a dependency ensures the memo re-evaluates when the password cache changes, working around localStorage not triggering React updates.


371-383: LGTM!

The refactored handlePasswordSuccess properly refetches all password-protected data after successful authentication and updates the passwordCacheTrigger to ensure UI gating re-evaluates. Using Promise.all for parallel fetches is efficient.


1000-1008: LGTM!

Properly gates note-related interactions (right-click, note icon click, long press) when the calendar requires a password but none is cached. Setting handlers to undefined is a clean approach.


1014-1043: LGTM!

Hiding the note hint block when UI elements are gated is good UX - users won't see instructions for features they can't currently use.


1056-1066: LGTM!

Consistent UI gating for shift deletion and the floating action button. This aligns with the PR objective to "hide or disable UI actions when a calendar requires a password and none is cached."

Copy link

@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

Caution

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

⚠️ Outside diff range comments (1)
components/external-sync-manage-dialog.tsx (1)

507-566: Missing calendarId in useCallback dependency array.

The saveExternalSyncChanges callback uses calendarId via getCachedPassword(calendarId) on line 512, but calendarId is not included in the dependency array (lines 555-565). This could cause stale closure issues if calendarId changes while editing.

   [
     editingSync,
     formName,
     formUrl,
     formColor,
     formDisplayMode,
     formAutoSyncInterval,
     fetchSyncs,
     onSyncComplete,
     t,
+    calendarId,
   ]
🧹 Nitpick comments (2)
components/sync-notification-dialog.tsx (1)

141-147: Password sent correctly in DELETE body, but 401 response isn't handled specially.

The password is correctly included in the request body. However, unlike useShifts and useNotes which have onPasswordRequired callbacks for 401 responses, this dialog shows a generic error toast on failure.

Consider whether sync log deletion should trigger a password prompt on 401, or if the current behavior (generic error) is acceptable since sync logs are secondary data.

hooks/useShifts.ts (1)

56-67: Consider adding onPasswordRequired callback for consistency with update/delete.

The createShift function includes the password in the request body but doesn't have an onPasswordRequired callback parameter like updateShift and deleteShift do. If the server returns 401 on a protected calendar, the user sees a generic error instead of being prompted to authenticate.

If the UI guarantees the calendar is unlocked before allowing shift creation, this may be acceptable. Otherwise, consider adding the callback for consistency:

 const createShift = async (
   formData: ShiftFormData,
+  onPasswordRequired?: () => void
 ) => {

And handle 401 responses similarly to updateShift/deleteShift.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 167d48e and 53b7a04.

📒 Files selected for processing (6)
  • components/external-sync-manage-dialog.tsx (11 hunks)
  • components/shift-stats.tsx (3 hunks)
  • components/sync-notification-dialog.tsx (4 hunks)
  • hooks/useNotes.ts (3 hunks)
  • hooks/useShifts.ts (4 hunks)
  • lib/password-cache.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/shift-stats.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.
All TypeScript/React components must use useTranslations() for all user-facing text strings and reference translation keys from messages/{de,en}.json.
Date formatting must use formatDateToLocal() function to output YYYY-MM-DD format.
Color values must be stored in hex format (e.g., #3b82f6) and use 20% opacity for backgrounds by appending '20' to the hex value (e.g., ${color}20).
Add comments only for complex logic or non-obvious behavior. Avoid over-commenting simple or self-documenting code.
Use next-intl library with messages/{de,en}.json for translations. Format dates using locale-specific functions: de or enUS from date-fns.

Files:

  • components/sync-notification-dialog.tsx
  • lib/password-cache.ts
  • components/external-sync-manage-dialog.tsx
  • hooks/useShifts.ts
  • hooks/useNotes.ts
components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

components/**/*.{ts,tsx}: All dialog components must follow the unified design pattern with gradient backgrounds, consistent padding (p-6, px-6, pb-6), border styling (border-border/50), backdrop blur effects, and gradient text for titles.
Components must support real-time updates via Server-Sent Events (SSE). Listen to relevant SSE events and implement silent refresh patterns using fetchData(false) to update without loading states.
Calendar interactions follow these patterns: Left-click toggles shift with selected preset, right-click opens note dialog (prevent default context menu), toggle logic deletes if exists or creates if not.

Files:

  • components/sync-notification-dialog.tsx
  • components/external-sync-manage-dialog.tsx
**/*.{ts,tsx,json}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All code, comments, variable names, and messages must be written in English.

Files:

  • components/sync-notification-dialog.tsx
  • lib/password-cache.ts
  • components/external-sync-manage-dialog.tsx
  • hooks/useShifts.ts
  • hooks/useNotes.ts
components/**/*[Dd]ialog*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Shift dialog has saveAsPreset enabled by default to allow quick preset creation during shift creation.

Files:

  • components/sync-notification-dialog.tsx
  • components/external-sync-manage-dialog.tsx
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : For password-protected calendars, use utilities from lib/password-cache.ts instead of direct localStorage access: getCachedPassword(), setCachedPassword(), removeCachedPassword(), verifyAndCachePassword(), hasValidCachedPassword().

Applied to files:

  • components/sync-notification-dialog.tsx
  • lib/password-cache.ts
  • components/external-sync-manage-dialog.tsx
  • hooks/useShifts.ts
  • hooks/useNotes.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to **/*.{ts,tsx} : Show PasswordDialog component when password verification fails (result.protected && !result.valid), set pendingAction state, and retry the operation after successful authentication.

Applied to files:

  • components/sync-notification-dialog.tsx
  • components/external-sync-manage-dialog.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to lib/db/schema.ts : Database schema: Use crypto.randomUUID() for ID generation, store timestamps as integers with { mode: 'timestamp' } which auto-converts to Date objects, hash passwords using SHA-256 via lib/password-utils.ts.

Applied to files:

  • components/sync-notification-dialog.tsx
  • lib/password-cache.ts
  • components/external-sync-manage-dialog.tsx
  • hooks/useShifts.ts
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: When creating new components with dialogs: (1) Create in components/ using shadcn/ui Dialog, (2) Accept props: open, onOpenChange, onSubmit, optional onDelete, (3) Use useTranslations(), (4) Reset local state when open changes to false, (5) Integrate in app/page.tsx.

Applied to files:

  • components/sync-notification-dialog.tsx
📚 Learning: 2025-12-07T00:25:27.363Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-07T00:25:27.363Z
Learning: Applies to app/page.tsx : Use useEffect for data fetching in response to calendar/date changes, useState for managing shifts/presets/notes/calendars state, and useRouter().replace() for URL state synchronization.

Applied to files:

  • components/external-sync-manage-dialog.tsx
  • hooks/useShifts.ts
  • hooks/useNotes.ts
🧬 Code graph analysis (4)
components/sync-notification-dialog.tsx (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-14)
components/external-sync-manage-dialog.tsx (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-14)
hooks/useShifts.ts (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-14)
hooks/useNotes.ts (1)
lib/password-cache.ts (1)
  • getCachedPassword (9-14)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev
🔇 Additional comments (15)
lib/password-cache.ts (1)

9-14: LGTM! Signature broadening handles nullable calendar IDs gracefully.

The updated signature accepting string | null | undefined with early return for falsy values is a clean approach that:

  • Prevents runtime errors when calendarId is not yet available
  • Guards against SSR environments where window is undefined
  • Returns null consistently for invalid inputs, aligning with the return type

This correctly supports the various call sites across hooks and components that may invoke getCachedPassword before a calendar is selected.

components/sync-notification-dialog.tsx (2)

65-71: LGTM! Password correctly appended to GET request via query parameters.

The implementation properly:

  • Retrieves the cached password after the calendarId guard
  • Uses URLSearchParams for clean query string construction
  • Conditionally appends password only when present

This aligns with the two-tier protection model where read operations on locked calendars require password verification.


168-177: LGTM! Password correctly sent in PATCH body.

Consistent pattern with the DELETE handler. The password is properly included in the JSON body for the mark-as-read mutation.

hooks/useShifts.ts (3)

16-33: LGTM! Fetch handles password-protected calendars correctly.

The implementation:

  • Builds query params with password when available
  • Clears shifts on any non-OK response (appropriate for locked calendars returning 401)
  • Clears shifts on fetch errors to prevent stale data display

This aligns with the PR objective to "gracefully handle locked calendars by returning empty datasets on unauthorized responses."


94-106: LGTM! Proper 401 handling with password required callback.

The implementation correctly:

  • Retrieves cached password (returns null if calendarId is undefined)
  • Includes password in the PATCH body
  • Handles 401 responses by invoking the onPasswordRequired callback

This follows the coding guidelines pattern for showing PasswordDialog when password verification fails.


134-144: LGTM! Consistent 401 handling pattern.

Delete follows the same correct pattern as update: password in body with onPasswordRequired callback for handling authentication failures.

hooks/useNotes.ts (3)

16-40: LGTM! Improved error handling addresses previous review feedback.

The implementation now correctly:

  • Clears notes only on 401/403 (authentication failures for locked calendars)
  • Preserves existing notes on other server errors (5xx, etc.)
  • Logs errors without clearing data on network failures

This is a significant improvement over the previous version that cleared notes on any non-OK response.


96-102: LGTM! Consistent password handling for update mutation.

Follows the established pattern with password in body and proper 401 callback handling.


135-143: LGTM! Consistent password handling for delete mutation.

Same correct pattern as update.

components/external-sync-manage-dialog.tsx (6)

99-116: LGTM! Password correctly appended to both sync and log fetches.

The implementation properly:

  • Retrieves password once after the calendarId guard
  • Reuses the same password for both /api/external-syncs and /api/sync-logs requests
  • Uses URLSearchParams for clean query construction

241-258: LGTM! Password included in create request.

Password is correctly retrieved and included in the POST body for creating new external syncs.


294-307: LGTM! Password correctly included in update request.


334-340: LGTM! Password sent in sync trigger request.


375-381: LGTM! Password included in delete request.


451-460: LGTM! Password included in visibility toggle request.

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.

add api route password protection if calendar password is set

1 participant