feat(auth): Add authorize page
This commit is contained in:
10
README.md
10
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:
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
17
app/page.tsx
17
app/page.tsx
@@ -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">
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
33
components/server/authorize.tsx
Normal file
33
components/server/authorize.tsx
Normal 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 };
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user