feat(auth): Add authorize page

This commit is contained in:
Björn Benouarets
2026-01-21 06:40:53 +01:00
parent 74232ad2d2
commit 9e7841ee35
8 changed files with 90 additions and 38 deletions

View File

@@ -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: Create a `.env.local` file in the root directory:
```bash ```bash
SECNEX_API_HOST=http://localhost:3001 SECNEX_AUTH_API_HOST=http://localhost:3001
SECNEX_API_KEY=your_api_key_here SECNEX_AUTH_API_KEY=your_api_key_here
``` ```
### Installation ### Installation
@@ -243,7 +243,7 @@ The `permissions.json` file defines OAuth scope permissions:
- Server Actions use `"use server"` directive - Server Actions use `"use server"` directive
- Route Handlers allow cookie modification from Server Components - Route Handlers allow cookie modification from Server Components
- Cookies are HTTP-only for additional security - 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) - API credentials should be stored in environment variables (not hardcoded)
## Tech Stack ## 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: Make sure to set the following environment variables in your deployment:
- `SECNEX_API_HOST` - Your SecNex API host URL - `SECNEX_AUTH_API_HOST` - Your SecNex API host URL
- `SECNEX_API_KEY` - Your SecNex API key - `SECNEX_AUTH_API_KEY` - Your SecNex API key
## License ## License

View File

@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
export async function GET() { 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" }); return NextResponse.json({ success: false, message: "SecNex API host or key is not set" });
} }
const cookieStore = await cookies(); const cookieStore = await cookies();
@@ -12,12 +12,12 @@ export async function GET() {
return NextResponse.json({ success: false, message: "No token found" }); return NextResponse.json({ success: false, message: "No token found" });
} }
console.log("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", method: "POST",
body: JSON.stringify({ token: token.value }), body: JSON.stringify({ token: token.value }),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${process.env.SECNEX_API_KEY}`, "Authorization": `Bearer ${process.env.SECNEX_AUTH_API_KEY}`,
}, },
}); });
if (!response.ok) { if (!response.ok) {

View File

@@ -3,7 +3,7 @@ import { cookies } from "next/headers";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
export async function GET() { 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" }); return NextResponse.json({ success: false, message: "SecNex API host or key is not set" });
} }
const cookieStore = await cookies(); const cookieStore = await cookies();
@@ -14,12 +14,12 @@ export async function GET() {
} }
try { 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", method: "POST",
body: JSON.stringify({ token: token.value }), body: JSON.stringify({ token: token.value }),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${process.env.SECNEX_API_KEY}`, "Authorization": `Bearer ${process.env.SECNEX_AUTH_API_KEY}`,
}, },
}); });

View File

@@ -5,23 +5,40 @@ import { cookies } from "next/headers";
import { AuthorizeContainer } from "@/components/core/authorize"; 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<AuthorizeParams> }) {
const params = await searchParams; const params = await searchParams;
const cookieStore = await cookies(); const cookieStore = await cookies();
const token = cookieStore.get("token"); 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; if (!token) {
const redirect_uri = params.redirect_uri as string; redirect(`/?returnTo=/authorize?${queryString}`);
const response_type = params.response_type as string || "code"; }
const scope = params.scope as string || "profile email";
return ( return (
<div className="flex justify-center items-center h-screen"> <div className="flex justify-center items-center h-screen">
<AuthorizeContainer applicationName="SecNex" applicationUrl="https://secnex.io" client_id={client_id} redirect_uri={redirect_uri} response_type={response_type} scope={scope} /> <AuthorizeContainer
applicationName="SecNex"
applicationUrl="https://secnex.io"
client_id={params.client_id || ""}
redirect_uri={params.redirect_uri || ""}
response_type={params.response_type || "code"}
scope={params.scope || "profile email"}
returnTo={`/authorize?${queryString}`}
/>
</div> </div>
) )
} }

View File

@@ -1,13 +1,26 @@
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { LoginContainer, LoginSuccessContainer } from "@/components/core/login-form"; 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<HomeParams>
}) {
const params = await searchParams;
const cookieStore = await cookies(); const cookieStore = await cookies();
const token = cookieStore.get("token"); 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) { if (token) {
return ( return (
<div className="flex justify-center items-center h-screen"> <div className="flex justify-center items-center h-screen">

View File

@@ -15,7 +15,7 @@ import { Separator } from "@/components/ui/separator";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { ShieldCheck } from "lucide-react"; 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"; import permissions from "@/permissions.json";
@@ -27,6 +27,7 @@ export interface AuthorizeContainerProps {
applicationName: string; applicationName: string;
applicationUrl: string; applicationUrl: string;
applicationLogo?: string; applicationLogo?: string;
returnTo: string;
} }
export interface AuthorizeLoadingProps { export interface AuthorizeLoadingProps {
@@ -98,18 +99,6 @@ export const AuthorizeContainer = (props: AuthorizeContainerProps) => {
const router = useRouter(); 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(() => { const scopePermissions = React.useMemo(() => {
return props.scope.split(" ").map((scope) => { return props.scope.split(" ").map((scope) => {
return { return {

View File

@@ -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<AuthorizeResponse> => {
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 };
};

View File

@@ -3,18 +3,18 @@
import { cookies } from "next/headers"; import { cookies } from "next/headers";
export const login = async (username: string, password: string): Promise<{ success: boolean, message: string, token?: string }> => { 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" }; return { success: false, message: "SecNex API host or key is not set" };
} }
const cookieStore = await cookies(); const cookieStore = await cookies();
try { try {
const response = await fetch(`${process.env.SECNEX_API_HOST}/login`, { const response = await fetch(`${process.env.SECNEX_AUTH_API_HOST}/login`, {
method: "POST", method: "POST",
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password }),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": `Bearer ${process.env.SECNEX_API_KEY}`, "Authorization": `Bearer ${process.env.SECNEX_AUTH_API_KEY}`,
}, },
}); });
const dataResponse = await response.json(); const dataResponse = await response.json();