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
5 changes: 4 additions & 1 deletion apps/web/app/(app)/ErrorMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export async function ErrorMessages() {
</ul>

{/* Avoids onClick. So it works in server components */}
<form action={clearUserErrorMessagesAction} className="mt-2">
<form
action={clearUserErrorMessagesAction as () => void}
className="mt-2"
>
<Button type="submit" variant="red" size="sm">
Clear
</Button>
Expand Down
22 changes: 22 additions & 0 deletions apps/web/app/(app)/PermissionsCheck.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { checkPermissionsAction } from "@/utils/actions/permissions";

let permissionsChecked = false;

export function PermissionsCheck() {
const router = useRouter();

useEffect(() => {
if (permissionsChecked) return;
permissionsChecked = true;

checkPermissionsAction().then((result) => {
if (!result?.hasAllPermissions) router.replace("/permissions/error");
});
}, [router]);

return null;
}
3 changes: 3 additions & 0 deletions apps/web/app/(app)/automation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@ import { BulkRunRules } from "@/app/(app)/automation/BulkRunRules";
import { Groups } from "@/app/(app)/automation/group/Groups";
import { RulesPrompt } from "@/app/(app)/automation/RulesPrompt";
import { OnboardingModal } from "@/components/OnboardingModal";
import { PermissionsCheck } from "@/app/(app)/PermissionsCheck";

export default async function AutomationPage() {
const session = await auth();
if (!session?.user) redirect("/login");

return (
<Suspense>
<PermissionsCheck />

<Tabs defaultValue="prompt">
<div className="content-container flex shrink-0 flex-col justify-between gap-x-4 space-y-2 border-b border-gray-200 bg-white py-2 shadow-sm md:flex-row md:gap-x-6 md:space-y-0">
<div className="w-full overflow-x-auto">
Expand Down
8 changes: 7 additions & 1 deletion apps/web/app/(app)/bulk-unsubscribe/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { PermissionsCheck } from "@/app/(app)/PermissionsCheck";
import { BulkUnsubscribe } from "./BulkUnsubscribe";
import { checkAndRedirectForUpgrade } from "@/utils/premium/check-and-redirect-for-upgrade";

export default async function BulkUnsubscribePage() {
await checkAndRedirectForUpgrade();
return <BulkUnsubscribe />;
return (
<>
<PermissionsCheck />
<BulkUnsubscribe />
</>
);
}
2 changes: 2 additions & 0 deletions apps/web/app/(app)/cold-email-blocker/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { Card } from "@/components/ui/card";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { PremiumAlertWithData } from "@/components/PremiumAlert";
import { ColdEmailRejected } from "@/app/(app)/cold-email-blocker/ColdEmailRejected";
import { PermissionsCheck } from "@/app/(app)/PermissionsCheck";

export default function ColdEmailBlockerPage() {
return (
<Suspense>
<PermissionsCheck />
<div className="content-container">
<PremiumAlertWithData className="mt-2" />
</div>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/(app)/mail/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { ThreadsResponse } from "@/app/api/google/threads/controller";
import { refetchEmailListAtom } from "@/store/email";
import { BetaBanner } from "@/app/(app)/mail/BetaBanner";
import { ClientOnly } from "@/components/ClientOnly";
import { PermissionsCheck } from "@/app/(app)/PermissionsCheck";

export default function Mail({
searchParams,
Expand Down Expand Up @@ -70,6 +71,7 @@ export default function Mail({

return (
<>
<PermissionsCheck />
<ClientOnly>
<BetaBanner />
</ClientOnly>
Expand Down
35 changes: 35 additions & 0 deletions apps/web/app/(app)/permissions/error/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";

import Image from "next/image";
import { Button } from "@/components/ui/button";
import { logOut } from "@/utils/user";
import { PageHeading, TypographyP } from "@/components/Typography";

export default function PermissionsErrorPage() {
return (
<div className="flex flex-col items-center justify-center sm:p-20 md:p-32">
<PageHeading className="text-center">
You are missing permissions 😔
</PageHeading>

<TypographyP className="mx-auto mt-4 max-w-prose text-center">
You must sign in and give access to all permissions for Inbox Zero to
work.
</TypographyP>

<Button className="mt-4" onClick={() => logOut("/login")}>
Sign in again
</Button>

<div className="mt-8">
<Image
src="/images/falling.svg"
alt=""
width={400}
height={400}
unoptimized
/>
Comment on lines +24 to +31
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve image accessibility and consider performance optimization.

While using the Next.js Image component is good for performance, there are a few areas that could be improved:

  1. The empty alt attribute might not be ideal for accessibility. If the image is decorative, consider using alt=" " (with a space) instead of an empty string.
  2. The unoptimized prop bypasses Next.js's image optimization. Unless there's a specific reason for this, consider removing it to leverage Next.js's built-in image optimization.

Apply the following changes to improve accessibility and potentially enhance performance:

 <Image
   src="/images/falling.svg"
-  alt=""
+  alt=" "
   width={400}
   height={400}
-  unoptimized
 />

Also, consider using responsive image sizing if appropriate for your design:

<Image
  src="/images/falling.svg"
  alt=" "
  width={400}
  height={400}
  sizes="(max-width: 768px) 100vw, 400px"
/>

This allows the image to adapt to different screen sizes while maintaining the aspect ratio.

</div>
</div>
);
}
8 changes: 7 additions & 1 deletion apps/web/app/(app)/stats/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { PermissionsCheck } from "@/app/(app)/PermissionsCheck";
import { Stats } from "./Stats";
import { checkAndRedirectForUpgrade } from "@/utils/premium/check-and-redirect-for-upgrade";

export default async function StatsPage() {
await checkAndRedirectForUpgrade();
return <Stats />;
return (
<>
<PermissionsCheck />
<Stats />
</>
);
}
2 changes: 1 addition & 1 deletion apps/web/components/ErrorDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const NotFound = (props: { children: React.ReactNode }) => {
);
};

export const NotLoggedIn = (props: {}) => {
export const NotLoggedIn = () => {
return (
<div className="flex flex-col items-center justify-center sm:p-20 md:p-32">
<div className="text-lg text-gray-700">You are not signed in 😞</div>
Expand Down
27 changes: 27 additions & 0 deletions apps/web/utils/actions/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use server";

import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { withActionInstrumentation } from "@/utils/actions/middleware";
import { getGmailAccessToken } from "@/utils/gmail/client";
import { checkGmailPermissions } from "@/utils/gmail/permissions";

export const checkPermissionsAction = withActionInstrumentation(
"checkPermissions",
async () => {
const session = await auth();
if (!session?.user.id) return { error: "Not logged in" };

try {
const token = await getGmailAccessToken(session);
if (!token.token) return { error: "No Gmail access token" };

const { hasAllPermissions, error } = await checkGmailPermissions(
token.token,
);
if (error) return { error };
return { hasAllPermissions };
} catch (error) {
return { error: "Failed to check permissions" };
}
},
);
2 changes: 1 addition & 1 deletion apps/web/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import prisma from "@/utils/prisma";
import { env } from "@/env";
import { captureException } from "@/utils/error";

const SCOPES = [
export const SCOPES = [
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",

Expand Down
50 changes: 50 additions & 0 deletions apps/web/utils/gmail/permissions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { SCOPES } from "@/utils/auth";

export async function checkGmailPermissions(accessToken: string): Promise<{
hasAllPermissions: boolean;
missingScopes: string[];
error?: string;
}> {
if (!accessToken) {
console.error("No access token available");
return {
hasAllPermissions: false,
missingScopes: SCOPES,
error: "No access token available",
};
}

try {
const response = await fetch(
`https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${accessToken}`,
);

const data = await response.json();

if (data.error) {
console.error("Error checking Gmail permissions:", data.error);
return {
hasAllPermissions: false,
missingScopes: SCOPES, // Assume all scopes are missing if we can't check
error: data.error,
};
}

const grantedScopes = data.scope?.split(" ") || [];
const missingScopes = SCOPES.filter(
(scope) => !grantedScopes.includes(scope),
);

return {
hasAllPermissions: missingScopes.length === 0,
missingScopes,
};
} catch (error) {
console.error("Error checking Gmail permissions:", error);
return {
hasAllPermissions: false,
missingScopes: SCOPES, // Assume all scopes are missing if we can't check
error: "Failed to check permissions",
};
}
}
Comment on lines +1 to +50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Codebase verification

Security Concern Confirmed: Sanitization of Error Logs Required

The review comment regarding the security implications of error logging has been validated. The checkGmailPermissions function in apps/web/utils/gmail/permissions.ts directly logs error objects, which can potentially expose sensitive information.

Affected Line:

  • apps/web/utils/gmail/permissions.ts: console.error("Error checking Gmail permissions:", error);

Recommendation:
Implement a sanitization function to ensure that only non-sensitive error messages are logged.

function sanitizeError(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}

// Usage:
console.error("Error checking Gmail permissions:", sanitizeError(error));
🔗 Analysis chain

Great implementation overall. Consider security implications of error logging.

The checkGmailPermissions function is well-implemented, with good error handling, proper use of TypeScript, and efficient logic. It effectively fulfills its purpose of checking Gmail permissions.

However, there's a potential security concern with logging error objects. These might contain sensitive information. Consider sanitizing the error logs:

If the script finds instances of logging error objects, consider implementing a sanitize function:

function sanitizeError(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}

// Usage:
console.error("Error checking Gmail permissions:", sanitizeError(error));

Action Required: Verify Sensitive Error Logging

The previous script encountered an error due to an unrecognized file type. Please use the corrected script below to check for potential sensitive data logging:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for potential sensitive data logging
rg --type typescript 'console\.(log|error|warn|info).*error' apps/web/

Length of output: 107


Script:

#!/bin/bash
# Check for potential sensitive data logging
rg --type ts 'console\.(log|error|warn|info).*error' apps/web/

Length of output: 3263

Loading