add login portal and traefik middleware auth for testing redirect login

This commit is contained in:
Milo Schwartz 2024-10-06 18:08:26 -04:00
parent 0838679120
commit 87c4fc798f
No known key found for this signature in database
14 changed files with 285 additions and 130 deletions

View file

@ -76,6 +76,9 @@ app.prepare().then(() => {
`Internal server is running on http://localhost:${internalPort}`,
);
});
internalServer.use(notFoundMiddleware)
internalServer.use(errorHandlerMiddleware);
});
declare global {

View file

@ -0,0 +1 @@
export * from "./verifyUser";

View 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",
),
);
}
}

View file

@ -1,5 +1,6 @@
import { Router } from "express";
import * as gerbil from "@server/routers/gerbil";
import * as badger from "@server/routers/badger";
import * as traefik from "@server/routers/traefik";
import HttpCode from "@server/types/HttpCode";
@ -14,10 +15,15 @@ internalRouter.get("/traefik-config", traefik.traefikConfigProvider);
// Gerbil routes
const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter);
gerbilRouter.get("/get-config", gerbil.getConfig);
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;

View file

@ -5,6 +5,7 @@ import { DynamicTraefikConfig } from "./configSchema";
import { and, like, eq } from "drizzle-orm";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import env from "@server/environment";
export async function traefikConfigProvider(_: Request, res: Response) {
try {
@ -36,10 +37,8 @@ export function buildTraefikConfig(
[middlewareName]: {
plugin: {
[middlewareName]: {
// These are temporary values
apiAddress:
"http://host.docker.internal:3001/api/v1/badger",
validToken: "abc123",
apiBaseUrl: "http://localhost:3001/api/v1",
appBaseUrl: env.BASE_URL,
},
},
},

View file

@ -1,9 +1,9 @@
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({
baseURL,
baseURL: "http://localhost:3000/api/v1",
timeout: 10000,
headers: {
"Content-Type": "application/json",

View file

@ -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";
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,
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";
export async function Page({
searchParams,
}: {
searchParams: { [key: string]: string | string[] | undefined };
}) {
const { user } = await verifySession();
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() {
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",
);
});
if (user) {
redirect("/");
}
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<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>
<>
<LoginForm redirect={searchParams.redirect as string} />
</>
);
}
export default Page;

View file

@ -9,7 +9,7 @@ export const metadata: Metadata = {
const font = Roboto({ subsets: ["latin"], style: "normal", weight: "400" });
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;

View file

@ -1,3 +1,19 @@
export default function Page() {
return <></>;
import { verifySession } from "@app/lib/verifySession";
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>
</>
);
}

View 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>
);
}

View file

@ -0,0 +1,3 @@
import { createContext } from "react";
export const UserContext = createContext<{ id: string } | null>(null);

View 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
View 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;
}

View 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;