mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-12 15:04:53 +02:00
add login portal and traefik middleware auth for testing redirect login
This commit is contained in:
parent
0838679120
commit
87c4fc798f
14 changed files with 285 additions and 130 deletions
|
@ -76,6 +76,9 @@ app.prepare().then(() => {
|
||||||
`Internal server is running on http://localhost:${internalPort}`,
|
`Internal server is running on http://localhost:${internalPort}`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
internalServer.use(notFoundMiddleware)
|
||||||
|
internalServer.use(errorHandlerMiddleware);
|
||||||
});
|
});
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
1
server/routers/badger/index.ts
Normal file
1
server/routers/badger/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from "./verifyUser";
|
61
server/routers/badger/verifyUser.ts
Normal file
61
server/routers/badger/verifyUser.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import lucia from "@server/auth";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { response } from "@server/utils/response";
|
||||||
|
|
||||||
|
export const verifyUserBody = z.object({
|
||||||
|
sessionId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type VerifyUserBody = z.infer<typeof verifyUserBody>;
|
||||||
|
|
||||||
|
export type VerifyUserResponse = {
|
||||||
|
valid: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function verifyUser(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
): Promise<any> {
|
||||||
|
const parsedBody = verifyUserBody.safeParse(req.query);
|
||||||
|
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sessionId } = parsedBody.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { session, user } = await lucia.validateSession(sessionId);
|
||||||
|
|
||||||
|
if (!session || !user) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Invalid session"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<VerifyUserResponse>(res, {
|
||||||
|
data: { valid: true },
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Access allowed",
|
||||||
|
status: HttpCode.OK,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to check user",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import * as gerbil from "@server/routers/gerbil";
|
import * as gerbil from "@server/routers/gerbil";
|
||||||
|
import * as badger from "@server/routers/badger";
|
||||||
import * as traefik from "@server/routers/traefik";
|
import * as traefik from "@server/routers/traefik";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
@ -14,10 +15,15 @@ internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
|
||||||
|
|
||||||
// Gerbil routes
|
// Gerbil routes
|
||||||
const gerbilRouter = Router();
|
const gerbilRouter = Router();
|
||||||
|
internalRouter.use("/gerbil", gerbilRouter);
|
||||||
|
|
||||||
gerbilRouter.get("/get-config", gerbil.getConfig);
|
gerbilRouter.get("/get-config", gerbil.getConfig);
|
||||||
gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
|
gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
|
||||||
|
|
||||||
internalRouter.use("/gerbil", gerbilRouter);
|
// Badger routes
|
||||||
|
const badgerRouter = Router();
|
||||||
|
internalRouter.use("/badger", badgerRouter);
|
||||||
|
|
||||||
|
internalRouter.get("/verify-user", badger.verifyUser)
|
||||||
|
|
||||||
export default internalRouter;
|
export default internalRouter;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { DynamicTraefikConfig } from "./configSchema";
|
||||||
import { and, like, eq } from "drizzle-orm";
|
import { and, like, eq } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import env from "@server/environment";
|
||||||
|
|
||||||
export async function traefikConfigProvider(_: Request, res: Response) {
|
export async function traefikConfigProvider(_: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
|
@ -36,10 +37,8 @@ export function buildTraefikConfig(
|
||||||
[middlewareName]: {
|
[middlewareName]: {
|
||||||
plugin: {
|
plugin: {
|
||||||
[middlewareName]: {
|
[middlewareName]: {
|
||||||
// These are temporary values
|
apiBaseUrl: "http://localhost:3001/api/v1",
|
||||||
apiAddress:
|
appBaseUrl: env.BASE_URL,
|
||||||
"http://host.docker.internal:3001/api/v1/badger",
|
|
||||||
validToken: "abc123",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const baseURL = `${window.location.protocol}//${window.location.host}/api/v1`;
|
// const baseURL = `${window.location.protocol}//${window.location.host}/api/v1`;
|
||||||
|
|
||||||
export const api = axios.create({
|
export const api = axios.create({
|
||||||
baseURL,
|
baseURL: "http://localhost:3000/api/v1",
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
|
@ -1,127 +1,23 @@
|
||||||
"use client";
|
import LoginForm from "@app/components/LoginForm";
|
||||||
|
import { verifySession } from "@app/lib/verifySession";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { useState } from "react";
|
export async function Page({
|
||||||
import { useForm } from "react-hook-form";
|
searchParams,
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
}: {
|
||||||
import * as z from "zod";
|
searchParams: { [key: string]: string | string[] | undefined };
|
||||||
import { Button } from "@/components/ui/button";
|
}) {
|
||||||
import { Input } from "@/components/ui/input";
|
const { user } = await verifySession();
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
|
|
||||||
import { LoginResponse } from "@server/routers/auth";
|
|
||||||
import { api } from "@app/api";
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
if (user) {
|
||||||
email: z.string().email({ message: "Invalid email address" }),
|
redirect("/");
|
||||||
password: z
|
|
||||||
.string()
|
|
||||||
.min(8, { message: "Password must be at least 8 characters" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function LoginForm() {
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof formSchema>) {
|
|
||||||
const { email, password } = values;
|
|
||||||
const res = await api
|
|
||||||
.post<LoginResponse>("/auth/login", {
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
setError(
|
|
||||||
e.response?.data?.message ||
|
|
||||||
"An error occurred while logging in",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-md mx-auto">
|
<>
|
||||||
<CardHeader>
|
<LoginForm redirect={searchParams.redirect as string} />
|
||||||
<CardTitle>Login</CardTitle>
|
</>
|
||||||
<CardDescription>
|
|
||||||
Enter your credentials to access your dashboard
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Email</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter your email"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Enter your password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<ExclamationTriangleIcon className="h-4 w-4" />
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-center">
|
|
||||||
<Button variant="link">Forgot password?</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const metadata: Metadata = {
|
||||||
|
|
||||||
const font = Roboto({ subsets: ["latin"], style: "normal", weight: "400" });
|
const font = Roboto({ subsets: ["latin"], style: "normal", weight: "400" });
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
export default function Page() {
|
import { verifySession } from "@app/lib/verifySession";
|
||||||
return <></>;
|
import { LandingProvider } from "@app/providers/LandingProvider";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
const { user } = await verifySession();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
redirect("/auth/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LandingProvider user={user}>
|
||||||
|
<p>You're logged in!</p>
|
||||||
|
</LandingProvider>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
140
src/components/LoginForm.tsx
Normal file
140
src/components/LoginForm.tsx
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import * as z from "zod";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
|
||||||
|
import { LoginResponse } from "@server/routers/auth";
|
||||||
|
import { api } from "@app/api";
|
||||||
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
type LoginFormProps = {
|
||||||
|
redirect?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
email: z.string().email({ message: "Invalid email address" }),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(8, { message: "Password must be at least 8 characters" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function LoginForm({ redirect }: LoginFormProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
const { email, password } = values;
|
||||||
|
const res = await api
|
||||||
|
.post<LoginResponse>("/auth/login", {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
setError(
|
||||||
|
e.response?.data?.message ||
|
||||||
|
"An error occurred while logging in",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
setError(null);
|
||||||
|
if (redirect && typeof redirect === "string") {
|
||||||
|
window.location.href = redirect;
|
||||||
|
} else {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-md mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Secure Login</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your credentials to access your dashboard
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter your email"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<ExclamationTriangleIcon className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
3
src/contexts/userContext.ts
Normal file
3
src/contexts/userContext.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { createContext } from "react";
|
||||||
|
|
||||||
|
export const UserContext = createContext<{ id: string } | null>(null);
|
7
src/hooks/useUserContext.ts
Normal file
7
src/hooks/useUserContext.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { UserContext } from "@app/contexts/userContext";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
export function useUserContext() {
|
||||||
|
const user = useContext(UserContext);
|
||||||
|
return user;
|
||||||
|
}
|
8
src/lib/verifySession.ts
Normal file
8
src/lib/verifySession.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import lucia from "@server/auth";
|
||||||
|
|
||||||
|
export async function verifySession() {
|
||||||
|
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
|
||||||
|
const session = await lucia.validateSession(sessionId || "");
|
||||||
|
return session;
|
||||||
|
}
|
15
src/providers/LandingProvider.tsx
Normal file
15
src/providers/LandingProvider.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { UserContext } from "@app/contexts/userContext";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
type LandingProviderProps = {
|
||||||
|
user: any;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LandingProvider({ user, children }: LandingProviderProps) {
|
||||||
|
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LandingProvider;
|
Loading…
Add table
Add a link
Reference in a new issue