-
Couldn't load subscription status.
- Fork 1.1k
Chrome extension #526
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
Open
miha-yy
wants to merge
2
commits into
elie222:staging
Choose a base branch
from
miha-yy:feat/chrome-extension
base: staging
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Chrome extension #526
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { auth } from "@/app/api/auth/[...nextauth]/auth"; | ||
| import { generateSecureToken } from "@/utils/api-key"; | ||
| import prisma from "@/utils/prisma"; | ||
| import { withError } from "@/utils/middleware"; | ||
|
|
||
| export const POST = withError(async (request) => { | ||
| const session = await auth(); | ||
|
|
||
| if (!session?.user?.email) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| // Clean up any existing tokens for this user | ||
| await prisma.extensionSession.deleteMany({ | ||
| where: { | ||
| userId: session.user.id, | ||
| }, | ||
| }); | ||
|
|
||
| // Generate a secure session token | ||
| const sessionToken = generateSecureToken(); | ||
|
|
||
| // Store the session token with user info and expiration | ||
| const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours | ||
|
|
||
| await prisma.extensionSession.create({ | ||
| data: { | ||
| token: sessionToken, | ||
| userId: session.user.id, | ||
| expiresAt, | ||
| }, | ||
| }); | ||
|
|
||
| return NextResponse.json({ sessionToken }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import prisma from "@/utils/prisma"; | ||
| import { withError } from "@/utils/middleware"; | ||
|
|
||
| export const POST = withError(async (request) => { | ||
| const authHeader = request.headers.get("Authorization"); | ||
|
|
||
| if (!authHeader || !authHeader.startsWith("Bearer ")) { | ||
| return NextResponse.json( | ||
| { error: "Missing or invalid authorization header" }, | ||
| { status: 401 }, | ||
| ); | ||
| } | ||
|
|
||
| const token = authHeader.substring(7); // Remove "Bearer " prefix | ||
|
|
||
| // Find the session token and check if it's valid | ||
| const session = await prisma.extensionSession.findUnique({ | ||
| where: { token }, | ||
| include: { | ||
| user: { | ||
| select: { | ||
| id: true, | ||
| email: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| if (!session) { | ||
| return NextResponse.json( | ||
| { error: "Invalid session token" }, | ||
| { status: 401 }, | ||
| ); | ||
| } | ||
|
|
||
| // Check if the session has expired | ||
| if (session.expiresAt < new Date()) { | ||
| // Clean up expired session | ||
| await prisma.extensionSession.delete({ | ||
| where: { id: session.id }, | ||
| }); | ||
|
|
||
| return NextResponse.json({ error: "Session expired" }, { status: 401 }); | ||
| } | ||
|
|
||
| return NextResponse.json({ | ||
| valid: true, | ||
| userId: session.user.id, | ||
| email: session.user.email, | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| "use client"; | ||
|
|
||
| import { useEffect, useState, useRef } from "react"; | ||
| import { signIn, useSession } from "next-auth/react"; | ||
| import { useSearchParams, useRouter } from "next/navigation"; | ||
| import { createScopedLogger } from "@/utils/logger"; | ||
|
|
||
| const logger = createScopedLogger("extension-auth"); | ||
|
|
||
| export default function ExtensionAuthPage() { | ||
| const { data: session, status } = useSession(); | ||
| const [isGeneratingToken, setIsGeneratingToken] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const searchParams = useSearchParams(); | ||
| const router = useRouter(); | ||
| const hasGeneratedToken = useRef(false); | ||
|
|
||
| useEffect(() => { | ||
| // If not authenticated, redirect to sign in | ||
| if (status === "unauthenticated") { | ||
| signIn("google", { callbackUrl: "/extension-auth" }); | ||
| return; | ||
| } | ||
|
|
||
| // If authenticated and we haven't generated a token yet, generate session token | ||
| if (status === "authenticated" && session && !hasGeneratedToken.current) { | ||
| hasGeneratedToken.current = true; | ||
| generateSessionToken(); | ||
| } | ||
| }, [status, session]); | ||
|
|
||
| const generateSessionToken = async () => { | ||
| try { | ||
| setIsGeneratingToken(true); | ||
| setError(null); | ||
|
|
||
| const response = await fetch("/api/extension/generate-session", { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error("Failed to generate session token"); | ||
| } | ||
|
|
||
| const { sessionToken } = await response.json(); | ||
|
|
||
| // Redirect back to extension with session token | ||
| const redirectUrl = `chrome-extension://${searchParams.get("extensionId") || ""}?sessionToken=${sessionToken}`; | ||
|
|
||
| // For development, we'll use a different approach since we can't redirect to chrome-extension:// | ||
| // We'll use a custom protocol or just show the token | ||
| if (process.env.NODE_ENV === "development") { | ||
| // In development, we'll redirect to a page that shows the token | ||
| router.push(`/extension-auth/success?sessionToken=${sessionToken}`); | ||
| } else { | ||
| // In production, redirect to the extension | ||
| window.location.href = redirectUrl; | ||
| } | ||
| } catch (error) { | ||
| logger.error("Error generating session token:", { error }); | ||
| setError("Failed to generate session token"); | ||
| // Reset the flag so user can try again | ||
| hasGeneratedToken.current = false; | ||
| } finally { | ||
| setIsGeneratingToken(false); | ||
| } | ||
| }; | ||
|
|
||
| if (status === "loading" || isGeneratingToken) { | ||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center bg-gray-50"> | ||
| <div className="text-center"> | ||
| <div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-b-2 border-blue-600"></div> | ||
| <p className="text-gray-600"> | ||
| {status === "loading" | ||
| ? "Loading..." | ||
| : "Generating session token..."} | ||
| </p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center bg-gray-50"> | ||
| <div className="text-center"> | ||
| <div className="mb-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"> | ||
| {error} | ||
| </div> | ||
| <button | ||
| onClick={() => window.close()} | ||
| className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700" | ||
| > | ||
| Close | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center bg-gray-50"> | ||
| <div className="text-center"> | ||
| <div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-b-2 border-blue-600"></div> | ||
| <p className="text-gray-600">Setting up your session...</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| "use client"; | ||
|
|
||
| import { useSearchParams } from "next/navigation"; | ||
| import { useEffect, useState } from "react"; | ||
| import { createScopedLogger } from "@/utils/logger"; | ||
|
|
||
| const logger = createScopedLogger("extension"); | ||
|
|
||
| export default function ExtensionPage() { | ||
| const searchParams = useSearchParams(); | ||
| const token = searchParams.get("token"); | ||
| const [isValidating, setIsValidating] = useState(true); | ||
| const [isValid, setIsValid] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [userData, setUserData] = useState<any>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (token) { | ||
| validateSessionToken(); | ||
| } else { | ||
| setError("No session token provided"); | ||
| setIsValidating(false); | ||
| } | ||
| }, [token]); | ||
|
|
||
| const validateSessionToken = async () => { | ||
| if (!token) return; | ||
|
|
||
| try { | ||
| const response = await fetch("/api/extension/verify-session", { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bearer ${token}`, | ||
| }, | ||
| }); | ||
|
|
||
| if (response.ok) { | ||
| const data = await response.json(); | ||
| setIsValid(true); | ||
| setUserData(data); | ||
| // Store the token for the main app to use | ||
| if (typeof window !== "undefined") { | ||
| sessionStorage.setItem("extensionToken", token); | ||
| sessionStorage.setItem("extensionUser", JSON.stringify(data)); | ||
| } | ||
| } else { | ||
| const errorData = await response.json(); | ||
| setError(errorData.error || "Invalid session token"); | ||
| } | ||
| } catch (error) { | ||
| logger.error("Error validating session token:", { error }); | ||
| setError("Failed to validate session token"); | ||
| } finally { | ||
| setIsValidating(false); | ||
| } | ||
| }; | ||
|
|
||
| if (isValidating) { | ||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center bg-gray-50"> | ||
| <div className="text-center"> | ||
| <div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-b-2 border-blue-600"></div> | ||
| <p className="text-gray-600">Validating session...</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (!isValid || error) { | ||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center bg-gray-50"> | ||
| <div className="text-center"> | ||
| <div className="mb-4 rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700"> | ||
| {error || "Invalid session"} | ||
| </div> | ||
| <p className="text-gray-600"> | ||
| Please re-authenticate in the extension. | ||
| </p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| // If we have a valid session token, load the main app content | ||
| if (isValid && token && userData) { | ||
| return ( | ||
| <div className="h-screen w-full"> | ||
| <iframe | ||
| src="/" | ||
| className="h-full w-full border-0" | ||
| title="Inbox Zero" | ||
| allow="clipboard-read; clipboard-write" | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="flex min-h-screen items-center justify-center bg-gray-50"> | ||
| <div className="text-center"> | ||
| <div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-b-2 border-blue-600"></div> | ||
| <p className="text-gray-600">Loading...</p> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
apps/web/prisma/migrations/20250627020614_extension_session/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| -- CreateTable | ||
| CREATE TABLE "ExtensionSession" ( | ||
| "id" TEXT NOT NULL, | ||
| "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "updatedAt" TIMESTAMP(3) NOT NULL, | ||
| "token" TEXT NOT NULL, | ||
| "userId" TEXT NOT NULL, | ||
| "expiresAt" TIMESTAMP(3) NOT NULL, | ||
|
|
||
| CONSTRAINT "ExtensionSession_pkey" PRIMARY KEY ("id") | ||
| ); | ||
|
|
||
| -- CreateIndex | ||
| CREATE UNIQUE INDEX "ExtensionSession_token_key" ON "ExtensionSession"("token"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE INDEX "ExtensionSession_userId_idx" ON "ExtensionSession"("userId"); | ||
|
|
||
| -- CreateIndex | ||
| CREATE INDEX "ExtensionSession_expiresAt_idx" ON "ExtensionSession"("expiresAt"); | ||
|
|
||
| -- AddForeignKey | ||
| ALTER TABLE "ExtensionSession" ADD CONSTRAINT "ExtensionSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
You can do:
withAuth()and then drop the next 2 lines