diff --git a/src/app/AvatarMenu.tsx b/src/app/AvatarMenu.tsx index d3ee22c0..80391209 100644 --- a/src/app/AvatarMenu.tsx +++ b/src/app/AvatarMenu.tsx @@ -5,7 +5,7 @@ import { Button, Header, Menu, MenuItem, MenuSection, MenuTrigger, Popover } fro import AvatarIcon from '@/app/components/AvatarIcon' import MenuLinkItem from '@/app/components/MenuLinkItem' -import authClient from '@/lib/client/auth' +import authClient, { githubSignin } from '@/lib/client/auth' import { useThrowToBoundary } from '@/lib/client/util' import { useConfigCtx } from '@/lib/contexts' import { setIsAdmin } from '@/lib/server/actions' @@ -62,15 +62,7 @@ export default function AvatarMenu() { ) } else if (!session.isPending && cfg.hasGithubAuth) { - return ( - - ) + return } else { return <> } diff --git a/src/app/[userName]/unauthorized.tsx b/src/app/[userName]/unauthorized.tsx new file mode 100644 index 00000000..d181597f --- /dev/null +++ b/src/app/[userName]/unauthorized.tsx @@ -0,0 +1,22 @@ +'use client' + +import { githubSignin } from '@/lib/client/auth' +import { useThrowToBoundary } from '@/lib/client/util' +import { useConfigCtx } from '@/lib/contexts' + +export default function UnauthorizedProjects() { + const cfg = useConfigCtx() + const { throwToBoundary } = useThrowToBoundary() + + return ( +
+

Sign In Required

+

You must be signed in to view profiles or projects.

+ {cfg.hasGithubAuth && ( + + )} +
+ ) +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 00000000..2f8d0148 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,26 @@ +import Link from 'next/link' + +import { getUserSession } from '@/lib/server/auth' + +export default async function NotFound() { + const session = await getUserSession() + + return ( +
+

404 Not Found

+

Page Not Found.

+

+ {session && ( + <> + Return to your profile. + + )} + {!session && ( + <> + Return to the home page. + + )} +

+
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 223da15f..7fb60e45 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,7 +4,7 @@ import { type Route } from 'next' import Link from 'next/link' import { useSearchParams } from 'next/navigation' -import authClient from '@/lib/client/auth' +import authClient, { githubSignin } from '@/lib/client/auth' import { useServerAction, useThrowToBoundary } from '@/lib/client/util' import { type Config, useConfigCtx } from '@/lib/contexts' import { loginDevUser } from '@/lib/server/actions' @@ -34,7 +34,7 @@ export default function Root() { const session = authClient.useSession() const error = useSearchParams().get('error') const { throwToBoundary } = useThrowToBoundary() - const [_, devLoginAction] = useServerAction(loginDevUser, () => session.refetch()) + const [_, devLoginAction] = useServerAction(loginDevUser, () => window.location.reload()) return ( <> @@ -56,9 +56,7 @@ export default function Root() { className='login-link' disabled={!cfg.hasGithubAuth} title={!cfg.hasGithubAuth ? 'Ask your administrator to set up GitHub authentication.' : undefined} - onClick={() => { - authClient.signIn.social({ provider: 'github' }).catch(throwToBoundary) - }} + onClick={() => githubSignin().catch(throwToBoundary)} > GitHub diff --git a/src/lib/client/auth.ts b/src/lib/client/auth.ts index 01b773f5..8b096b13 100644 --- a/src/lib/client/auth.ts +++ b/src/lib/client/auth.ts @@ -3,9 +3,18 @@ import { createAuthClient } from 'better-auth/react' import type { AuthInstance } from '@/lib/server/auth' -export default createAuthClient({ +const authClient = createAuthClient({ // connection errors always throw, this makes non-ok status throw as well so they can be handled similarly fetchOptions: { throw: true }, // https://better-auth.com/docs/concepts/typescript#inferring-additional-fields-on-client plugins: [inferAdditionalFields()], }) + +export function githubSignin() { + return authClient.signIn.social({ + provider: 'github', + callbackURL: window.location.pathname, + }) +} + +export default authClient diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index f3dfa398..9f8cd8d3 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -212,15 +212,22 @@ export async function getAuth(): Promise { return g.__auth! } -/** Require an authenticated user. Throws `unauthorized()` if not logged in. */ -export async function requireAuth(): Promise { +/** Check for an authenticated user. Returns `null` if not logged in. */ +export async function getUserSession(): Promise { await io() const auth = await getAuth() const session = await auth.api.getSession({ headers: await headers() }) - if (!session) unauthorized() + if (!session) return null return session } +/** Require an authenticated user. Throws `unauthorized()` if not logged in. */ +export async function requireAuth(): Promise { + const auth = await getUserSession() + if (!auth) unauthorized() + return auth +} + /** Require an admin user. Throws `unauthorized()` if not logged in, `forbidden()` if not admin. */ export async function requireAdmin() { const session = await requireAuth() diff --git a/src/proxy.ts b/src/proxy.ts index 24655d23..1761e34f 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -1,8 +1,9 @@ import { type NextRequest, NextResponse } from 'next/server' +import { getUserSession } from './lib/server/auth' import { getConfig } from './lib/server/config' -export function proxy(request: NextRequest) { +export async function proxy(request: NextRequest) { const cfg = getConfig() if (!cfg.isSetupComplete) { const path = request.nextUrl.pathname @@ -14,6 +15,15 @@ export function proxy(request: NextRequest) { url.pathname = '/setup' return NextResponse.redirect(url) } + if (request.nextUrl.pathname === '/') { + const session = await getUserSession() + if (session) { + const url = request.nextUrl.clone() + url.pathname = `/${session.user.name}` + url.search = '' + return NextResponse.redirect(url) + } + } return NextResponse.next() }