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
4 changes: 3 additions & 1 deletion src/components/navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ChevronDown, ExternalLink, LogOut } from "lucide-react"
import { useNavigate } from "@tanstack/react-router"
import { BrandLockup } from "@/components/brand-lockup"
import {
DropdownMenu,
Expand All @@ -13,6 +14,7 @@ import { useAuth } from "@/lib/auth"

export function Navbar() {
const auth = useAuth()
const navigate = useNavigate()

return (
<nav className="border-border/50 bg-background/80 fixed top-0 z-50 w-full border-b backdrop-blur-sm">
Expand Down Expand Up @@ -49,7 +51,7 @@ export function Navbar() {
<DropdownMenuItem
onClick={async () => {
await auth.logout()
window.location.href = "/login"
await navigate({ to: "/login" })
}}
>
<LogOut className="size-4" />
Expand Down
7 changes: 5 additions & 2 deletions src/pages/callback/oauth-associate-complete-page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { useEffect } from "react"
import { useNavigate } from "@tanstack/react-router"

type Props = {
provider: "google" | "steam"
}

export function OAuthAssociateCompletePage({ provider }: Props) {
const navigate = useNavigate()

useEffect(() => {
window.location.href = `/profile?linked=${provider}`
}, [provider])
void navigate({ to: "/profile", search: { linked: provider } })
}, [provider, navigate])

return (
<div className="flex flex-1 items-center justify-center">
Expand Down
51 changes: 34 additions & 17 deletions src/pages/callback/oauth-complete-page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,50 @@
import { useEffect, useRef } from "react"
import { useNavigate } from "@tanstack/react-router"
import { api } from "@/api/api"
import { useAuth } from "@/lib/auth"

export function OAuthCompletePage() {
const auth = useAuth()
const navigate = useNavigate()
const calledRef = useRef(false)

useEffect(() => {
if (calledRef.current) return
calledRef.current = true

api
.get("auth/me")
.json<{ email: string | null; tos_accepted_at: string | null }>()
.then((user) => {
async function complete() {
try {
const user = await api
.get("auth/me")
.json<{ email: string | null; tos_accepted_at: string | null }>()
// Also refresh the AuthContext so the destination route's beforeLoad
// sees the new session. The direct read above gives us the user data
// for the routing decision below without waiting for the React
// render cycle to flush.
await auth.checkAuth()

// Steam OAuth users start with no email until they pass through
// /accept-terms (auth-api #36, auth-web #36) — route them there
// even if they previously accepted the old TOS-only gate.
// /accept-terms — route them there even if they previously accepted
// the old TOS-only gate.
if (!user.tos_accepted_at || !user.email) {
// Keep the redirect in localStorage — accept-terms page will use it
window.location.href = "/accept-terms"
} else {
const savedRedirect = localStorage.getItem("auth_redirect")
localStorage.removeItem("auth_redirect")
window.location.href = savedRedirect ?? "/profile"
await navigate({ to: "/accept-terms" })
return
}
const savedRedirect = localStorage.getItem("auth_redirect")
localStorage.removeItem("auth_redirect")
if (savedRedirect && savedRedirect.startsWith("http")) {
// Cross-origin redirect (consumer apps) — hard nav.
window.location.href = savedRedirect
return
}
})
.catch(() => {
window.location.href = "/login"
})
}, [])
await navigate({ to: savedRedirect ?? "/profile" })
} catch {
await navigate({ to: "/login" })
}
}

void complete()
}, [auth, navigate])

return (
<div className="flex flex-1 items-center justify-center">
Expand Down
20 changes: 16 additions & 4 deletions src/pages/login/login-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react"
import { Link, useSearch } from "@tanstack/react-router"
import { Link, useNavigate, useSearch } from "@tanstack/react-router"
import { LoaderCircle } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
Expand All @@ -15,8 +15,11 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { api, baseUrl } from "@/api/api"
import { getErrorMessage } from "@/lib/api-errors"
import { useAuth } from "@/lib/auth"

export function LoginPage() {
const auth = useAuth()
const navigate = useNavigate()
const search = useSearch({ from: "/login" })
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
Expand All @@ -33,9 +36,18 @@ export function LoginPage() {
body: new URLSearchParams({ username: email, password }),
headers: { "Content-Type": "application/x-www-form-urlencoded" },
})
// Hard redirect — reloads the page so AuthProvider picks up the new cookie cleanly.
// Client-side navigate has a race condition with the onUnauthorized handler.
window.location.href = redirect ?? "/profile"
const target = redirect ?? "/profile"
if (target.startsWith("http")) {
// Cross-origin redirect (e.g. from a consumer app like
// hera-streamer-…) — must be a hard nav since useNavigate only
// handles in-app routes.
window.location.href = target
return
}
// Refresh the AuthContext (reads /auth/me with the new cookie) before
// navigating, so /profile's beforeLoad sees isAuthenticated=true.
await auth.checkAuth()
await navigate({ to: target })
} catch (error) {
const message = await getErrorMessage(error, "Login failed")
toast.error(message)
Expand Down
6 changes: 5 additions & 1 deletion src/pages/profile/profile-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,11 @@ export function ProfilePage() {
try {
await api.delete("auth/me")
toast.success("Account deleted.")
window.location.href = "/login"
// Clear local auth state before navigating — /login's beforeLoad
// redirects authenticated users to /profile, so without this the
// user would bounce back to the page they just deleted from.
await auth.logout()
await navigate({ to: "/login" })
} catch (error) {
const message = await getErrorMessage(error, "Failed to delete account")
toast.error(message)
Expand Down
10 changes: 8 additions & 2 deletions src/pages/register/register-page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react"
import { Link } from "@tanstack/react-router"
import { Link, useNavigate } from "@tanstack/react-router"
import { LoaderCircle } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
Expand All @@ -16,9 +16,12 @@ import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { api } from "@/api/api"
import { getErrorMessage } from "@/lib/api-errors"
import { useAuth } from "@/lib/auth"
import { submitConsents, type ConsentInput } from "@/lib/consent"

export function RegisterPage() {
const auth = useAuth()
const navigate = useNavigate()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
Expand Down Expand Up @@ -66,7 +69,10 @@ export function RegisterPage() {
} catch {
// Swallow — user is signed up; they'll be prompted again on profile.
}
window.location.href = "/profile"
// Refresh the AuthContext (reads /auth/me with the new cookie) before
// navigating, so /profile's beforeLoad sees isAuthenticated=true.
await auth.checkAuth()
await navigate({ to: "/profile" })
} catch (error) {
const message = await getErrorMessage(error, "Registration failed")
toast.error(message)
Expand Down
Loading