-
Notifications
You must be signed in to change notification settings - Fork 1.3k
security: fix missing Secure attribute on cookies #1217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5902c4e
ff331d9
5fa78db
547d5a3
3b4b8cf
3791f29
486e797
d93dc93
80ef765
22e00b0
caa93a7
03bb4cc
41f8011
b580311
07f0f7d
b2e6fc2
66d8556
61c0cab
b6eda9e
590854d
aef53b0
0c39c6f
823d56f
2db6dfb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { redirectToEmailAccountPath } from "@/utils/account"; | ||
|
|
||
| export default async function BulkArchivePage() { | ||
| await redirectToEmailAccountPath("/bulk-archive"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { redirectToEmailAccountPath } from "@/utils/account"; | ||
|
|
||
| export default async function QuickBulkArchivePage() { | ||
| await redirectToEmailAccountPath("/quick-bulk-archive"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| "use client"; | ||
|
|
||
| import { useState, useCallback } from "react"; | ||
| import { toast } from "sonner"; | ||
| import { | ||
| Card, | ||
| CardContent, | ||
| CardDescription, | ||
| CardHeader, | ||
| CardTitle, | ||
| } from "@/components/ui/card"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { bulkCategorizeSendersAction } from "@/utils/actions/categorize"; | ||
| import { useAccount } from "@/providers/EmailAccountProvider"; | ||
| import { useCategorizeProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress"; | ||
|
|
||
| export function AutoCategorizationSetup({ | ||
| hasCategorizedSenders, | ||
| }: { | ||
| hasCategorizedSenders: boolean; | ||
| }) { | ||
| const { emailAccountId } = useAccount(); | ||
| const { setIsBulkCategorizing } = useCategorizeProgress(); | ||
|
|
||
| const [isEnabling, setIsEnabling] = useState(false); | ||
|
|
||
| const enableFeature = useCallback(async () => { | ||
| setIsEnabling(true); | ||
| setIsBulkCategorizing(true); | ||
|
|
||
| try { | ||
| const result = await bulkCategorizeSendersAction(emailAccountId); | ||
|
|
||
| if (result?.serverError) { | ||
| throw new Error(result.serverError); | ||
| } | ||
|
|
||
| toast.success( | ||
| result?.data?.totalUncategorizedSenders | ||
| ? `Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.` | ||
| : "No uncategorized senders found.", | ||
| ); | ||
| } catch (error) { | ||
| toast.error( | ||
| `Failed to enable feature: ${error instanceof Error ? error.message : "Unknown error"}`, | ||
| ); | ||
| setIsBulkCategorizing(false); | ||
| } finally { | ||
| setIsEnabling(false); | ||
| } | ||
| }, [emailAccountId, setIsBulkCategorizing]); | ||
|
|
||
| // Don't show setup if user already has categorized senders | ||
| if (hasCategorizedSenders) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <Card className="m-4"> | ||
| <CardHeader> | ||
| <CardTitle>Bulk Archive</CardTitle> | ||
| <CardDescription> | ||
| Archive emails in bulk by sender category. We'll first categorize your | ||
| senders into groups like Newsletters, Receipts, Marketing, etc. Then | ||
| you can quickly archive entire categories at once. | ||
| </CardDescription> | ||
| </CardHeader> | ||
| <CardContent> | ||
| <Button onClick={enableFeature} loading={isEnabling}> | ||
| Enable feature | ||
| </Button> | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| "use client"; | ||
|
|
||
| import { useMemo, useCallback } from "react"; | ||
| import sortBy from "lodash/sortBy"; | ||
| import useSWR from "swr"; | ||
| import { AutoCategorizationSetup } from "@/app/(app)/[emailAccountId]/bulk-archive/AutoCategorizationSetup"; | ||
| import { BulkArchiveProgress } from "@/app/(app)/[emailAccountId]/bulk-archive/BulkArchiveProgress"; | ||
| import { BulkArchiveCards } from "@/components/BulkArchiveCards"; | ||
| import { useCategorizeProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress"; | ||
| import type { CategorizedSendersResponse } from "@/app/api/user/categorize/senders/categorized/route"; | ||
| import type { CategoryWithRules } from "@/utils/category.server"; | ||
|
|
||
| type Sender = { | ||
| id: string; | ||
| email: string; | ||
| category: { id: string; description: string | null; name: string } | null; | ||
| }; | ||
|
|
||
| export function BulkArchiveContent({ | ||
| initialSenders, | ||
| initialCategories, | ||
| }: { | ||
| initialSenders: Sender[]; | ||
| initialCategories: CategoryWithRules[]; | ||
| }) { | ||
| const { isBulkCategorizing } = useCategorizeProgress(); | ||
|
|
||
| // Poll for updates while categorization is in progress | ||
| const { data, mutate } = useSWR<CategorizedSendersResponse>( | ||
| "/api/user/categorize/senders/categorized", | ||
| { | ||
| refreshInterval: isBulkCategorizing ? 2000 : undefined, | ||
| fallbackData: { senders: initialSenders, categories: initialCategories }, | ||
| }, | ||
| ); | ||
|
|
||
| // Use SWR data if available, otherwise fall back to initial server data | ||
| const senders = data?.senders ?? initialSenders; | ||
| const categories = data?.categories ?? initialCategories; | ||
|
|
||
| const emailGroups = useMemo( | ||
| () => | ||
| sortBy(senders, (sender) => sender.category?.name).map((sender) => ({ | ||
| address: sender.email, | ||
| category: categories.find((c) => c.id === sender.category?.id) || null, | ||
| })), | ||
| [senders, categories], | ||
| ); | ||
|
|
||
| const handleProgressComplete = useCallback(() => { | ||
| // Refresh data when categorization completes | ||
| mutate(); | ||
| }, [mutate]); | ||
|
|
||
| return ( | ||
| <> | ||
| <BulkArchiveProgress onComplete={handleProgressComplete} /> | ||
| <AutoCategorizationSetup hasCategorizedSenders={senders.length > 0} /> | ||
| <BulkArchiveCards emailGroups={emailGroups} categories={categories} /> | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useState } from "react"; | ||
| import useSWR from "swr"; | ||
| import { ProgressPanel } from "@/components/ProgressPanel"; | ||
| import type { CategorizeProgress } from "@/app/api/user/categorize/senders/progress/route"; | ||
| import { useCategorizeProgress } from "@/app/(app)/[emailAccountId]/smart-categories/CategorizeProgress"; | ||
| import { useInterval } from "@/hooks/useInterval"; | ||
|
|
||
| export function BulkArchiveProgress({ | ||
| onComplete, | ||
| }: { | ||
| onComplete?: () => void; | ||
| }) { | ||
| const { isBulkCategorizing, setIsBulkCategorizing } = useCategorizeProgress(); | ||
| const [fakeProgress, setFakeProgress] = useState(0); | ||
|
|
||
| const { data } = useSWR<CategorizeProgress>( | ||
| "/api/user/categorize/senders/progress", | ||
| { | ||
| refreshInterval: isBulkCategorizing ? 1000 : undefined, | ||
| }, | ||
| ); | ||
|
|
||
| // Fake progress animation to make it feel responsive | ||
| useInterval( | ||
| () => { | ||
| if (!data?.totalItems) return; | ||
|
|
||
| setFakeProgress((prev) => { | ||
| const realCompleted = data.completedItems || 0; | ||
| if (realCompleted > prev) return realCompleted; | ||
|
|
||
| const maxProgress = Math.min( | ||
| Math.floor(data.totalItems * 0.9), | ||
| realCompleted + 30, | ||
| ); | ||
| return prev < maxProgress ? prev + 1 : prev; | ||
| }); | ||
| }, | ||
| isBulkCategorizing ? 1500 : null, | ||
| ); | ||
|
|
||
| // Handle completion | ||
| useEffect(() => { | ||
| let timeoutId: NodeJS.Timeout | undefined; | ||
| if ( | ||
| data?.completedItems && | ||
| data?.totalItems && | ||
| data.completedItems === data.totalItems | ||
| ) { | ||
| timeoutId = setTimeout(() => { | ||
| setIsBulkCategorizing(false); | ||
| setFakeProgress(0); | ||
| onComplete?.(); | ||
| }, 3000); | ||
| } | ||
| return () => { | ||
| if (timeoutId) clearTimeout(timeoutId); | ||
| }; | ||
| }, [ | ||
| data?.completedItems, | ||
| data?.totalItems, | ||
| setIsBulkCategorizing, | ||
| onComplete, | ||
| ]); | ||
|
|
||
| if (!isBulkCategorizing || !data?.totalItems) { | ||
| return null; | ||
| } | ||
|
|
||
| const totalItems = data.totalItems || 0; | ||
| const displayedProgress = Math.max(data.completedItems || 0, fakeProgress); | ||
|
|
||
| return ( | ||
| <ProgressPanel | ||
| totalItems={totalItems} | ||
| remainingItems={totalItems - displayedProgress} | ||
| inProgressText="Categorizing senders..." | ||
| completedText={`Categorization complete! ${displayedProgress} senders categorized!`} | ||
| itemLabel="senders" | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import prisma from "@/utils/prisma"; | ||
| import { ClientOnly } from "@/components/ClientOnly"; | ||
| import { TopBar } from "@/components/TopBar"; | ||
| import { getUserCategoriesWithRules } from "@/utils/category.server"; | ||
| import { PermissionsCheck } from "@/app/(app)/[emailAccountId]/PermissionsCheck"; | ||
| import { ArchiveProgress } from "@/app/(app)/[emailAccountId]/bulk-unsubscribe/ArchiveProgress"; | ||
| import { PremiumAlertWithData } from "@/components/PremiumAlert"; | ||
| import { checkUserOwnsEmailAccount } from "@/utils/email-account"; | ||
| import { BulkArchiveContent } from "@/app/(app)/[emailAccountId]/bulk-archive/BulkArchiveContent"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
| export const maxDuration = 300; | ||
|
|
||
| export default async function BulkArchivePage({ | ||
| params, | ||
| }: { | ||
| params: Promise<{ emailAccountId: string }>; | ||
| }) { | ||
| const { emailAccountId } = await params; | ||
| await checkUserOwnsEmailAccount({ emailAccountId }); | ||
|
|
||
| const [senders, categories] = await Promise.all([ | ||
| prisma.newsletter.findMany({ | ||
| where: { emailAccountId, categoryId: { not: null } }, | ||
| select: { | ||
| id: true, | ||
| email: true, | ||
| category: { select: { id: true, description: true, name: true } }, | ||
| }, | ||
| }), | ||
| getUserCategoriesWithRules({ emailAccountId }), | ||
| ]); | ||
|
|
||
| return ( | ||
| <> | ||
| <PermissionsCheck /> | ||
|
|
||
| <ClientOnly> | ||
| <ArchiveProgress /> | ||
| </ClientOnly> | ||
|
|
||
| <PremiumAlertWithData className="mx-2 mt-2 sm:mx-4" /> | ||
|
|
||
| <TopBar className="items-center"> | ||
| <h1 className="text-lg font-semibold">Bulk Archive</h1> | ||
| </TopBar> | ||
|
|
||
| <ClientOnly> | ||
| <BulkArchiveContent | ||
| initialSenders={senders} | ||
| initialCategories={categories} | ||
| /> | ||
| </ClientOnly> | ||
| </> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ export function ConnectCalendar({ | |
|
|
||
| const setOnboardingReturnCookie = () => { | ||
| if (onboardingReturnPath) { | ||
| document.cookie = `${CALENDAR_ONBOARDING_RETURN_COOKIE}=${encodeURIComponent(onboardingReturnPath)}; path=/; max-age=180`; | ||
| document.cookie = `${CALENDAR_ONBOARDING_RETURN_COOKIE}=${encodeURIComponent(onboardingReturnPath)}; path=/; max-age=180; SameSite=Lax; Secure`; | ||
| } | ||
| }; | ||
|
Comment on lines
23
to
27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Apply centralized cookie utility here as well. The security attributes addition is good, but this is another instance of direct 🔎 Refactor with cookie utility+import { setCookie } from '@/utils/cookie-manager';
import { CALENDAR_ONBOARDING_RETURN_COOKIE } from "@/utils/calendar/constants";
const setOnboardingReturnCookie = () => {
if (onboardingReturnPath) {
- document.cookie = `${CALENDAR_ONBOARDING_RETURN_COOKIE}=${encodeURIComponent(onboardingReturnPath)}; path=/; max-age=180; SameSite=Lax; Secure`;
+ setCookie(CALENDAR_ONBOARDING_RETURN_COOKIE, onboardingReturnPath, {
+ maxAge: 180,
+ });
}
};
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clear the categorizing flag when no senders are found.
When
totalUncategorizedSendersis 0, the success toast displays "No uncategorized senders found", butsetIsBulkCategorizing(true)from Line 29 is never cleared. This leaves the UI in a perpetual "categorizing" state even though no work is being performed.🔎 Proposed fix
try { const result = await bulkCategorizeSendersAction(emailAccountId); if (result?.serverError) { throw new Error(result.serverError); } - toast.success( - result?.data?.totalUncategorizedSenders - ? `Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.` - : "No uncategorized senders found.", - ); + if (result?.data?.totalUncategorizedSenders) { + toast.success( + `Categorizing ${result.data.totalUncategorizedSenders} senders... This may take a few minutes.` + ); + } else { + toast.success("No uncategorized senders found."); + setIsBulkCategorizing(false); + } } catch (error) {📝 Committable suggestion
🤖 Prompt for AI Agents