From 9e7841ee358d4272436024372487d4d9d3db3128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Benouarets?= Date: Wed, 21 Jan 2026 06:40:53 +0100 Subject: [PATCH] feat(auth): Add authorize page --- README.md | 10 +++++----- app/api/logout/route.tsx | 6 +++--- app/api/session/route.ts | 6 +++--- app/authorize/page.tsx | 35 ++++++++++++++++++++++++--------- app/page.tsx | 17 ++++++++++++++-- components/core/authorize.tsx | 15 ++------------ components/server/authorize.tsx | 33 +++++++++++++++++++++++++++++++ components/server/login.tsx | 6 +++--- 8 files changed, 90 insertions(+), 38 deletions(-) create mode 100644 components/server/authorize.tsx diff --git a/README.md b/README.md index d17f36f..3fbcecb 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,8 @@ Error: Cookies can only be modified in a Server Action or Route Handler. Create a `.env.local` file in the root directory: ```bash -SECNEX_API_HOST=http://localhost:3001 -SECNEX_API_KEY=your_api_key_here +SECNEX_AUTH_API_HOST=http://localhost:3001 +SECNEX_AUTH_API_KEY=your_api_key_here ``` ### Installation @@ -243,7 +243,7 @@ The `permissions.json` file defines OAuth scope permissions: - Server Actions use `"use server"` directive - Route Handlers allow cookie modification from Server Components - Cookies are HTTP-only for additional security -- The SecNex API must be running at the configured `SECNEX_API_HOST` +- The SecNex API must be running at the configured `SECNEX_AUTH_API_HOST` - API credentials should be stored in environment variables (not hardcoded) ## Tech Stack @@ -273,8 +273,8 @@ For more information, see the [Next.js Deployment Documentation](https://nextjs. Make sure to set the following environment variables in your deployment: -- `SECNEX_API_HOST` - Your SecNex API host URL -- `SECNEX_API_KEY` - Your SecNex API key +- `SECNEX_AUTH_API_HOST` - Your SecNex API host URL +- `SECNEX_AUTH_API_KEY` - Your SecNex API key ## License diff --git a/app/api/logout/route.tsx b/app/api/logout/route.tsx index 741d005..488c96b 100644 --- a/app/api/logout/route.tsx +++ b/app/api/logout/route.tsx @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; export async function GET() { - if (!process.env.SECNEX_API_HOST || !process.env.SECNEX_API_KEY) { + if (!process.env.SECNEX_AUTH_API_HOST || !process.env.SECNEX_AUTH_API_KEY) { return NextResponse.json({ success: false, message: "SecNex API host or key is not set" }); } const cookieStore = await cookies(); @@ -12,12 +12,12 @@ export async function GET() { return NextResponse.json({ success: false, message: "No token found" }); } console.log("Token found"); - const response = await fetch(`${process.env.SECNEX_API_HOST}/logout`, { + const response = await fetch(`${process.env.SECNEX_AUTH_API_HOST}/logout`, { method: "POST", body: JSON.stringify({ token: token.value }), headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${process.env.SECNEX_API_KEY}`, + "Authorization": `Bearer ${process.env.SECNEX_AUTH_API_KEY}`, }, }); if (!response.ok) { diff --git a/app/api/session/route.ts b/app/api/session/route.ts index 997eb11..91f85c0 100644 --- a/app/api/session/route.ts +++ b/app/api/session/route.ts @@ -3,7 +3,7 @@ import { cookies } from "next/headers"; import { revalidatePath } from "next/cache"; export async function GET() { - if (!process.env.SECNEX_API_HOST || !process.env.SECNEX_API_KEY) { + if (!process.env.SECNEX_AUTH_API_HOST || !process.env.SECNEX_AUTH_API_KEY) { return NextResponse.json({ success: false, message: "SecNex API host or key is not set" }); } const cookieStore = await cookies(); @@ -14,12 +14,12 @@ export async function GET() { } try { - const response = await fetch(`${process.env.SECNEX_API_HOST}/session/info`, { + const response = await fetch(`${process.env.SECNEX_AUTH_API_HOST}/session/info`, { method: "POST", body: JSON.stringify({ token: token.value }), headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${process.env.SECNEX_API_KEY}`, + "Authorization": `Bearer ${process.env.SECNEX_AUTH_API_KEY}`, }, }); diff --git a/app/authorize/page.tsx b/app/authorize/page.tsx index 99882b6..b9d9ca4 100644 --- a/app/authorize/page.tsx +++ b/app/authorize/page.tsx @@ -5,23 +5,40 @@ import { cookies } from "next/headers"; import { AuthorizeContainer } from "@/components/core/authorize"; -export default async function AuthorizePage({ searchParams }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { +export interface AuthorizeParams { + client_id?: string, + response_type?: string, + redirect_uri?: string, + scope?: string, + state?: string, +} + +export default async function AuthorizePage({ searchParams }: { searchParams: Promise }) { const params = await searchParams; const cookieStore = await cookies(); const token = cookieStore.get("token"); - if (!token) { - redirect("/"); - } + + + const queryString = new URLSearchParams( + Object.entries(params).filter(([, v]) => v !== undefined) as [string, string][] + ).toString(); - const client_id = params.client_id as string; - const redirect_uri = params.redirect_uri as string; - const response_type = params.response_type as string || "code"; - const scope = params.scope as string || "profile email"; + if (!token) { + redirect(`/?returnTo=/authorize?${queryString}`); + } return (
- +
) } \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 78a9043..ebbc170 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,13 +1,26 @@ import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; import { LoginContainer, LoginSuccessContainer } from "@/components/core/login-form"; -// Get the url before redirect to this page +export interface HomeParams { + returnTo?: string; +} -export default async function Home() { +export default async function Home({ + searchParams +}: { + searchParams: Promise +}) { + const params = await searchParams; const cookieStore = await cookies(); const token = cookieStore.get("token"); + // If token exists and we came from a redirect, go back + if (token && params.returnTo) { + redirect(params.returnTo); + } + if (token) { return (
diff --git a/components/core/authorize.tsx b/components/core/authorize.tsx index 5358cc8..abf7c40 100644 --- a/components/core/authorize.tsx +++ b/components/core/authorize.tsx @@ -15,7 +15,7 @@ import { Separator } from "@/components/ui/separator"; import { Spinner } from "@/components/ui/spinner"; import { ShieldCheck } from "lucide-react"; -import { IconExternalLink, IconHomeFilled, IconUserCircle, IconUser, IconMail, IconChevronRight, IconCheck } from "@tabler/icons-react"; +import { IconExternalLink, IconHomeFilled, IconUserCircle, IconChevronRight, IconCheck } from "@tabler/icons-react"; import permissions from "@/permissions.json"; @@ -27,6 +27,7 @@ export interface AuthorizeContainerProps { applicationName: string; applicationUrl: string; applicationLogo?: string; + returnTo: string; } export interface AuthorizeLoadingProps { @@ -98,18 +99,6 @@ export const AuthorizeContainer = (props: AuthorizeContainerProps) => { const router = useRouter(); - // const handleLogout = async () => { - // const response = await fetch("/api/logout"); - // const data = await response.json(); - // if (data.success) { - // toast.success(data.message); - // router.refresh(); - // } else { - // toast.error(data.message); - // } - // }; - - // Get for each scope the permission name and description const scopePermissions = React.useMemo(() => { return props.scope.split(" ").map((scope) => { return { diff --git a/components/server/authorize.tsx b/components/server/authorize.tsx new file mode 100644 index 0000000..8d25e48 --- /dev/null +++ b/components/server/authorize.tsx @@ -0,0 +1,33 @@ +"use server"; + +export interface AuthorizeParams { + client_id: string; + redirect_uri: string; + response_type: string; + scope: string; + state: string; +} + +export interface AuthorizeResponse { + success: boolean; + message: string; + code?: string; + state?: string; +} + +export const authorize = async (params: AuthorizeParams, token: string): Promise => { + if (!process.env.SECNEX_OAUTH2_API_HOST) { + return { success: false, message: "SecNex OAuth2 API host is not set" }; + } + + const response = await fetch(`${process.env.SECNEX_OAUTH2_API_HOST}/authorize`, { + method: "POST", + body: JSON.stringify(params), + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }, + }); + const data = await response.json(); + return { success: true, message: data.message, code: data.code, state: data.state }; +}; \ No newline at end of file diff --git a/components/server/login.tsx b/components/server/login.tsx index dee58c8..1e188ea 100644 --- a/components/server/login.tsx +++ b/components/server/login.tsx @@ -3,18 +3,18 @@ import { cookies } from "next/headers"; export const login = async (username: string, password: string): Promise<{ success: boolean, message: string, token?: string }> => { - if (!process.env.SECNEX_API_HOST || !process.env.SECNEX_API_KEY) { + if (!process.env.SECNEX_AUTH_API_HOST || !process.env.SECNEX_AUTH_API_KEY) { return { success: false, message: "SecNex API host or key is not set" }; } const cookieStore = await cookies(); try { - const response = await fetch(`${process.env.SECNEX_API_HOST}/login`, { + const response = await fetch(`${process.env.SECNEX_AUTH_API_HOST}/login`, { method: "POST", body: JSON.stringify({ username, password }), headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${process.env.SECNEX_API_KEY}`, + "Authorization": `Bearer ${process.env.SECNEX_AUTH_API_KEY}`, }, }); const dataResponse = await response.json();