diff --git a/apps/dashboard/index.html b/apps/dashboard/index.html
index 8d83a20..ed97a39 100644
--- a/apps/dashboard/index.html
+++ b/apps/dashboard/index.html
@@ -5,6 +5,7 @@
Dashboard — Linkify
+
diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx
index 92a099f..5e8c11c 100644
--- a/apps/dashboard/src/App.tsx
+++ b/apps/dashboard/src/App.tsx
@@ -1,9 +1,11 @@
import { Routes, Route } from "react-router-dom"
+import AuthCallback from "@/pages/AuthCallback"
export default function App() {
return (
- Dashboard} />
+ } />
+ Dashboard} />
)
}
diff --git a/apps/dashboard/src/hooks/use-auth.tsx b/apps/dashboard/src/hooks/use-auth.tsx
index 1df3a1f..6996e47 100644
--- a/apps/dashboard/src/hooks/use-auth.tsx
+++ b/apps/dashboard/src/hooks/use-auth.tsx
@@ -6,7 +6,7 @@ import {
useCallback,
type ReactNode,
} from "react"
-import { useNavigate } from "react-router-dom"
+import { useNavigate, useLocation } from "react-router-dom"
import type { Session, User } from "@supabase/supabase-js"
import { supabase } from "@/lib/supabase"
import { fetchMe } from "@/lib/api"
@@ -26,8 +26,11 @@ const APP_URL = import.meta.env.VITE_APP_URL ?? "http://localhost:5173"
const AuthContext = createContext(undefined)
+const PUBLIC_PATHS = ["/auth/callback"]
+
export function AuthProvider({ children }: { children: ReactNode }) {
const navigate = useNavigate()
+ const location = useLocation()
const [state, setState] = useState({
user: null,
session: null,
@@ -45,9 +48,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}, [])
const redirectToLogin = useCallback(() => {
- const currentPath = window.location.pathname + window.location.search
- window.location.href = `${APP_URL}/login?redirectTo=${encodeURIComponent(currentPath)}`
- }, [])
+ const currentPath = location.pathname + location.search
+ window.location.replace(`${APP_URL}/login?redirectTo=${encodeURIComponent(currentPath)}`)
+ }, [location.pathname, location.search])
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
@@ -83,6 +86,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
}
}, [fetchProfile])
+ useEffect(() => {
+ if (state.isLoading) return
+ if (state.session) return
+ if (PUBLIC_PATHS.includes(location.pathname)) return
+
+ redirectToLogin()
+ }, [state.isLoading, state.session, location.pathname, redirectToLogin])
+
const signOut = useCallback(async () => {
await supabase.auth.signOut()
setState({
diff --git a/apps/dashboard/src/pages/AuthCallback.tsx b/apps/dashboard/src/pages/AuthCallback.tsx
new file mode 100644
index 0000000..9ba7927
--- /dev/null
+++ b/apps/dashboard/src/pages/AuthCallback.tsx
@@ -0,0 +1,69 @@
+import { useEffect, useState } from "react"
+import { useNavigate } from "react-router-dom"
+import { supabase } from "@/lib/supabase"
+
+export default function AuthCallback() {
+ const navigate = useNavigate()
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ async function handleCallback() {
+ const hash = window.location.hash.slice(1)
+ if (!hash) {
+ setError("No authentication data received")
+ return
+ }
+
+ const params = new URLSearchParams(hash)
+ const accessToken = params.get("access_token")
+ const refreshToken = params.get("refresh_token")
+ const redirectTo = params.get("redirectTo") || "/overview"
+
+ if (!accessToken || !refreshToken) {
+ setError("Invalid authentication data")
+ return
+ }
+
+ window.location.hash = ""
+
+ const { error: setSessionError } = await supabase.auth.setSession({
+ access_token: accessToken,
+ refresh_token: refreshToken,
+ })
+
+ if (setSessionError) {
+ setError(setSessionError.message)
+ return
+ }
+
+ navigate(redirectTo, { replace: true })
+ }
+
+ handleCallback()
+ }, [navigate])
+
+ if (error) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+ Signing you in...
+
+
+ )
+}
diff --git a/apps/web/src/hooks/use-auth.tsx b/apps/web/src/hooks/use-auth.tsx
index d6e9e13..d9efce1 100644
--- a/apps/web/src/hooks/use-auth.tsx
+++ b/apps/web/src/hooks/use-auth.tsx
@@ -10,6 +10,7 @@ import { useNavigate } from "react-router-dom"
import type { Session, User } from "@supabase/supabase-js"
import { supabase } from "@/lib/supabase"
import { fetchMe } from "@/lib/api"
+import { DASHBOARD_URL } from "@/lib/config"
interface AuthState {
user: User | null
@@ -32,6 +33,21 @@ interface AuthContextValue extends AuthState {
const AuthContext = createContext(undefined)
+function getRedirectTo(): string | null {
+ const params = new URLSearchParams(window.location.search)
+ return params.get("redirectTo")
+}
+
+function relaySessionToDashboard() {
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ if (!session) return
+ const { access_token, refresh_token } = session
+ const redirectTo = getRedirectTo() || "/overview"
+ const hash = `#access_token=${encodeURIComponent(access_token)}&refresh_token=${encodeURIComponent(refresh_token)}&redirectTo=${encodeURIComponent(redirectTo)}`
+ window.location.replace(`${DASHBOARD_URL}/auth/callback${hash}`)
+ })
+}
+
export function AuthProvider({ children }: { children: ReactNode }) {
const navigate = useNavigate()
const [state, setState] = useState({
@@ -97,7 +113,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
return { error: "Please verify your email before signing in. Check your inbox." }
}
- navigate("/")
+ const redirectTo = getRedirectTo()
+ if (redirectTo) {
+ relaySessionToDashboard()
+ } else {
+ navigate("/")
+ }
return { error: null }
},
[navigate]
@@ -105,11 +126,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const signUp = useCallback(
async (email: string, password: string) => {
+ const redirectTo = getRedirectTo()
const { error, data } = await supabase.auth.signUp({
email,
password,
options: {
- emailRedirectTo: `${window.location.origin}/auth/callback`,
+ emailRedirectTo: redirectTo
+ ? `${window.location.origin}/auth/callback?redirectTo=${encodeURIComponent(redirectTo)}`
+ : `${window.location.origin}/auth/callback`,
},
})
if (error) return { error: error.message, needsEmailVerification: false }
@@ -119,7 +143,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
data.session === null
if (!needsEmailVerification && data.session) {
- navigate("/")
+ if (redirectTo) {
+ relaySessionToDashboard()
+ } else {
+ navigate("/")
+ }
}
return { error: null, needsEmailVerification }
@@ -140,11 +168,14 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const signInWithProvider = useCallback(
async (provider: "google" | "github") => {
try {
+ const redirectTo = getRedirectTo()
+ const callbackUrl = redirectTo
+ ? `${window.location.origin}/auth/callback?redirectTo=${encodeURIComponent(redirectTo)}`
+ : `${window.location.origin}/auth/callback`
+
const { error } = await supabase.auth.signInWithOAuth({
provider,
- options: {
- redirectTo: `${window.location.origin}/auth/callback`,
- },
+ options: { redirectTo: callbackUrl },
})
if (error) return { error: error.message }
} catch {
diff --git a/apps/web/src/lib/config.ts b/apps/web/src/lib/config.ts
index a6729c9..4c3d377 100644
--- a/apps/web/src/lib/config.ts
+++ b/apps/web/src/lib/config.ts
@@ -1 +1,2 @@
export const APP_URL = import.meta.env.VITE_APP_URL ?? "http://localhost:5173"
+export const DASHBOARD_URL = import.meta.env.VITE_DASHBOARD_URL ?? "http://localhost:5174"
diff --git a/apps/web/src/pages/AuthCallback.tsx b/apps/web/src/pages/AuthCallback.tsx
index 4dd8f2e..d3b132e 100644
--- a/apps/web/src/pages/AuthCallback.tsx
+++ b/apps/web/src/pages/AuthCallback.tsx
@@ -1,6 +1,7 @@
import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { supabase } from "@/lib/supabase"
+import { DASHBOARD_URL } from "@/lib/config"
export default function AuthCallback() {
const navigate = useNavigate()
@@ -12,6 +13,7 @@ export default function AuthCallback() {
const code = params.get("code")
const errorParam = params.get("error")
const errorDescription = params.get("error_description")
+ const redirectTo = params.get("redirectTo")
if (errorParam) {
setError(errorDescription ?? "Authentication failed")
@@ -28,8 +30,15 @@ export default function AuthCallback() {
}
const { data: { session } } = await supabase.auth.getSession()
+
if (session) {
- navigate("/", { replace: true })
+ if (redirectTo) {
+ const { access_token, refresh_token } = session
+ const hash = `#access_token=${encodeURIComponent(access_token)}&refresh_token=${encodeURIComponent(refresh_token)}&redirectTo=${encodeURIComponent(redirectTo)}`
+ window.location.replace(`${DASHBOARD_URL}/auth/callback${hash}`)
+ } else {
+ navigate("/", { replace: true })
+ }
}
}