Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,29 @@ export async function PUT(

Calendars can be password-protected. Implementation flow:

1. Check localStorage: `calendar_password_${calendarId}`
2. Verify via `/api/calendars/[id]/verify-password` POST
3. On 401: Show `PasswordDialog`, store in localStorage
1. Check localStorage via `getCachedPassword(calendarId)` from `lib/password-cache.ts`
2. Verify via `verifyAndCachePassword(calendarId, password)` - automatically caches valid passwords
3. On invalid password: Show `PasswordDialog`, which automatically caches on success
4. Use `pendingAction` state to retry operation after authentication

**Important**: Always use the utilities from `lib/password-cache.ts` instead of direct localStorage access:

- `getCachedPassword(calendarId)` - Get cached password
- `setCachedPassword(calendarId, password)` - Cache password after verification
- `removeCachedPassword(calendarId)` - Remove cached password
- `verifyAndCachePassword(calendarId, password)` - Verify and auto-cache if valid
- `hasValidCachedPassword(calendarId)` - Check if cached password is still valid

```typescript
setPendingAction({ type: "edit", shiftId: id, formData });
setShowPasswordDialog(true);
// Example: Password check before action
const password = getCachedPassword(calendarId);
const result = await verifyAndCachePassword(calendarId, password);

if (result.protected && !result.valid) {
setPendingAction({ type: "edit", shiftId: id, formData });
setShowPasswordDialog(true);
return;
}
```

### State Management
Expand Down
133 changes: 41 additions & 92 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useState, useEffect, useCallback, useMemo, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useTranslations, useLocale } from "next-intl";
import { toast } from "sonner";
import { CalendarDialog } from "@/components/calendar-dialog";
import { ShiftDialog, ShiftFormData } from "@/components/shift-dialog";
import { PasswordDialog } from "@/components/password-dialog";
Expand Down Expand Up @@ -43,7 +44,10 @@ import { de, enUS } from "date-fns/locale";
import { CalendarNote, ShiftPreset, ICloudSync } from "@/lib/db/schema";
import { ShiftWithCalendar } from "@/lib/types";
import { formatDateToLocal } from "@/lib/date-utils";
import { toast } from "sonner";
import {
getCachedPassword,
verifyAndCachePassword,
} from "@/lib/password-cache";
import { motion } from "motion/react";
import { useCalendars } from "@/hooks/useCalendars";
import { useShifts } from "@/hooks/useShifts";
Expand Down Expand Up @@ -212,34 +216,17 @@ function HomeContent() {

// Check if calendar is locked
if (selectedCalendarIsLocked) {
// Check if we have a valid password in localStorage
const storedPassword = localStorage.getItem(
`calendar_password_${selectedCalendar}`
);
const cachedPassword = getCachedPassword(selectedCalendar);

if (storedPassword) {
if (cachedPassword) {
// Set loading state and verify password before unlocking
setIsVerifyingCalendarPassword(true);
setIsCalendarUnlocked(false);

// Verify the stored password
fetch(`/api/calendars/${selectedCalendar}/verify-password`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: storedPassword }),
})
.then((response) => response.json())
.then((data) => {
if (data.valid) {
// Password valid, unlock calendar
setIsCalendarUnlocked(true);
} else {
// Password invalid, remove from storage and keep locked
localStorage.removeItem(
`calendar_password_${selectedCalendar}`
);
setIsCalendarUnlocked(false);
}
// Verify the cached password
verifyAndCachePassword(selectedCalendar, cachedPassword)
.then((result) => {
setIsCalendarUnlocked(result.valid);
})
.catch(() => {
// On error, keep calendar locked
Expand All @@ -250,7 +237,7 @@ function HomeContent() {
setIsVerifyingCalendarPassword(false);
});
} else {
// No stored password, set unlocked to false - form will handle it
// No cached password, set unlocked to false
setIsCalendarUnlocked(false);
setIsVerifyingCalendarPassword(false);
}
Expand Down Expand Up @@ -278,35 +265,22 @@ function HomeContent() {

// Check if calendar is password protected
if (calendar.passwordHash) {
const storedPassword = localStorage.getItem(
`calendar_password_${selectedCalendar}`
);
const cachedPassword = getCachedPassword(selectedCalendar);

if (storedPassword) {
// Verify stored password
try {
const response = await fetch(
`/api/calendars/${selectedCalendar}/verify-password`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: storedPassword }),
}
);
if (cachedPassword) {
// Verify cached password
const result = await verifyAndCachePassword(
selectedCalendar,
cachedPassword
);

if (response.ok) {
setShowICloudSyncDialog(true);
return;
} else {
// Password invalid, remove from storage
localStorage.removeItem(`calendar_password_${selectedCalendar}`);
}
} catch (error) {
console.error("Password verification failed:", error);
if (result.valid) {
setShowICloudSyncDialog(true);
return;
}
}

// Show password dialog
// Show password dialog if no valid cached password
setPendingAction({
type: "edit",
presetAction: async () => {
Expand Down Expand Up @@ -519,38 +493,23 @@ function HomeContent() {
const handleManualShiftCreation = async () => {
if (!selectedCalendar) return;

try {
const password = localStorage.getItem(
`calendar_password_${selectedCalendar}`
);

const response = await fetch(
`/api/calendars/${selectedCalendar}/verify-password`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
}
);

const data = await response.json();

if (data.protected && !data.valid) {
localStorage.removeItem(`calendar_password_${selectedCalendar}`);
setPendingAction({
type: "edit",
presetAction: handleManualShiftCreation,
});
setShowPasswordDialog(true);
return;
}
const cachedPassword = getCachedPassword(selectedCalendar);
const result = await verifyAndCachePassword(
selectedCalendar,
cachedPassword
);

setSelectedDate(new Date());
setShowShiftDialog(true);
} catch (error) {
console.error("Failed to verify password:", error);
toast.error(t("password.errorVerification"));
if (result.protected && !result.valid) {
setPendingAction({
type: "edit",
presetAction: handleManualShiftCreation,
});
setShowPasswordDialog(true);
return;
}

setSelectedDate(new Date());
setShowShiftDialog(true);
};

const handleShowAllShifts = (date: Date, dayShifts: ShiftWithCalendar[]) => {
Expand Down Expand Up @@ -593,23 +552,13 @@ function HomeContent() {

try {
const password = selectedCalendar
? localStorage.getItem(`calendar_password_${selectedCalendar}`)
? getCachedPassword(selectedCalendar)
: null;

if (selectedCalendar) {
const response = await fetch(
`/api/calendars/${selectedCalendar}/verify-password`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
}
);

const data = await response.json();
const result = await verifyAndCachePassword(selectedCalendar, password);

if (data.protected && !data.valid) {
localStorage.removeItem(`calendar_password_${selectedCalendar}`);
if (result.protected && !result.valid) {
setPendingAction({
type: "edit",
presetAction: () => handleAddShift(targetDate),
Expand Down
17 changes: 11 additions & 6 deletions components/manage-password-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { removeCachedPassword, setCachedPassword } from "@/lib/password-cache";

interface ManagePasswordDialogProps {
open: boolean;
Expand Down Expand Up @@ -112,12 +113,16 @@ export function ManagePasswordDialog({
return;
}

// Clear cached password from localStorage
localStorage.removeItem(`calendar_password_${calendarId}`);

// If new password was set, cache it
if (!removePassword && newPassword) {
localStorage.setItem(`calendar_password_${calendarId}`, newPassword);
// Handle localStorage based on what changed
if (removePassword) {
// Only remove from localStorage if password was actually removed
removeCachedPassword(calendarId);
} else if (newPassword) {
// New password was set, update localStorage
setCachedPassword(calendarId, newPassword);
} else if (hasPassword && currentPassword) {
// Only isLocked changed, keep the current password cached
setCachedPassword(calendarId, currentPassword);
}

onSuccess();
Expand Down
16 changes: 3 additions & 13 deletions components/password-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { verifyAndCachePassword } from "@/lib/password-cache";

interface PasswordDialogProps {
open: boolean;
Expand Down Expand Up @@ -46,20 +47,9 @@ export function PasswordDialog({
setLoading(true);

try {
const response = await fetch(
`/api/calendars/${calendarId}/verify-password`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
}
);
const result = await verifyAndCachePassword(calendarId, password);

const data = await response.json();

if (data.valid) {
// Store password in localStorage
localStorage.setItem(`calendar_password_${calendarId}`, password);
if (result.valid) {
onSuccess(password);
onOpenChange(false);
} else {
Expand Down
9 changes: 8 additions & 1 deletion hooks/useCalendars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import { CalendarWithCount } from "@/lib/types";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
import { removeCachedPassword, setCachedPassword } from "@/lib/password-cache";

export function useCalendars(initialCalendarId?: string | null) {
const t = useTranslations();
Expand Down Expand Up @@ -63,6 +64,12 @@ export function useCalendars(initialCalendarId?: string | null) {
const newCalendar = await response.json();
setCalendars((prev) => [...prev, newCalendar]);
setSelectedCalendar(newCalendar.id);

// Cache the password if one was provided
if (password) {
setCachedPassword(newCalendar.id, password);
}

toast.success(t("calendar.created"));
} catch (error) {
console.error("Failed to create calendar:", error);
Expand Down Expand Up @@ -99,7 +106,7 @@ export function useCalendars(initialCalendarId?: string | null) {

return remainingCalendars;
});
localStorage.removeItem(`calendar_password_${calendarId}`);
removeCachedPassword(calendarId);

toast.success(t("calendar.deleted"));
return true;
Expand Down
11 changes: 4 additions & 7 deletions hooks/useNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CalendarNote } from "@/lib/db/schema";
import { formatDateToLocal } from "@/lib/date-utils";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
import { getCachedPassword } from "@/lib/password-cache";

export function useNotes(calendarId: string | undefined) {
const t = useTranslations();
Expand Down Expand Up @@ -34,7 +35,7 @@ export function useNotes(calendarId: string | undefined) {
if (!calendarId) return false;

try {
const password = localStorage.getItem(`calendar_password_${calendarId}`);
const password = getCachedPassword(calendarId);

const response = await fetch("/api/notes", {
method: "POST",
Expand Down Expand Up @@ -79,9 +80,7 @@ export function useNotes(calendarId: string | undefined) {
onPasswordRequired?: () => void
) => {
try {
const password = calendarId
? localStorage.getItem(`calendar_password_${calendarId}`)
: null;
const password = calendarId ? getCachedPassword(calendarId) : null;

const response = await fetch(`/api/notes/${noteId}`, {
method: "PUT",
Expand Down Expand Up @@ -120,9 +119,7 @@ export function useNotes(calendarId: string | undefined) {
onPasswordRequired?: () => void
) => {
try {
const password = calendarId
? localStorage.getItem(`calendar_password_${calendarId}`)
: null;
const password = calendarId ? getCachedPassword(calendarId) : null;

const response = await fetch(`/api/notes/${noteId}`, {
method: "DELETE",
Expand Down
Loading