diff --git a/server/index.ts b/server/index.ts index 120e1400..83756c29 100644 --- a/server/index.ts +++ b/server/index.ts @@ -76,6 +76,9 @@ app.prepare().then(() => { `Internal server is running on http://localhost:${internalPort}`, ); }); + + internalServer.use(notFoundMiddleware) + internalServer.use(errorHandlerMiddleware); }); declare global { diff --git a/server/routers/badger/index.ts b/server/routers/badger/index.ts new file mode 100644 index 00000000..f7622030 --- /dev/null +++ b/server/routers/badger/index.ts @@ -0,0 +1 @@ +export * from "./verifyUser"; diff --git a/server/routers/badger/verifyUser.ts b/server/routers/badger/verifyUser.ts new file mode 100644 index 00000000..15d0c9ad --- /dev/null +++ b/server/routers/badger/verifyUser.ts @@ -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; + +export type VerifyUserResponse = { + valid: boolean; +}; + +export async function verifyUser( + req: Request, + res: Response, + next: NextFunction, +): Promise { + 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(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", + ), + ); + } +} diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 63e35dc2..dfcce4eb 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -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; diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index c05b6937..bae3122e 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -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, }, }, }, diff --git a/src/api/index.ts b/src/api/index.ts index 30e2e5b8..42ece4ef 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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", diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 8f27333c..84ef337f 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -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(null); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - email: "", - password: "", - }, - }); - - async function onSubmit(values: z.infer) { - const { email, password } = values; - const res = await api - .post("/auth/login", { - email, - password, - }) - .catch((e) => { - setError( - e.response?.data?.message || - "An error occurred while logging in", - ); - }); + if (user) { + redirect("/"); } return ( - - - Login - - Enter your credentials to access your dashboard - - - -
- - ( - - Email - - - - - - )} - /> - ( - - Password - - - - - - )} - /> - {error && ( - - - {error} - - )} - - - -
- - - -
+ <> + + ); } + +export default Page; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e8aa54f8..a7a57eb9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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; diff --git a/src/app/page.tsx b/src/app/page.tsx index 430093a3..7ea646f1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 ( + <> + +

You're logged in!

+
+ + ); } diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx new file mode 100644 index 00000000..9e5f9b22 --- /dev/null +++ b/src/components/LoginForm.tsx @@ -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(null); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: "", + password: "", + }, + }); + + async function onSubmit(values: z.infer) { + const { email, password } = values; + const res = await api + .post("/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 ( + + + Secure Login + + Enter your credentials to access your dashboard + + + +
+ + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + {error && ( + + + {error} + + )} + + + +
+
+ ); +} diff --git a/src/contexts/userContext.ts b/src/contexts/userContext.ts new file mode 100644 index 00000000..9eb8f229 --- /dev/null +++ b/src/contexts/userContext.ts @@ -0,0 +1,3 @@ +import { createContext } from "react"; + +export const UserContext = createContext<{ id: string } | null>(null); diff --git a/src/hooks/useUserContext.ts b/src/hooks/useUserContext.ts new file mode 100644 index 00000000..8d9e011c --- /dev/null +++ b/src/hooks/useUserContext.ts @@ -0,0 +1,7 @@ +import { UserContext } from "@app/contexts/userContext"; +import { useContext } from "react"; + +export function useUserContext() { + const user = useContext(UserContext); + return user; +} diff --git a/src/lib/verifySession.ts b/src/lib/verifySession.ts new file mode 100644 index 00000000..ea47b51b --- /dev/null +++ b/src/lib/verifySession.ts @@ -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; +} diff --git a/src/providers/LandingProvider.tsx b/src/providers/LandingProvider.tsx new file mode 100644 index 00000000..215f871e --- /dev/null +++ b/src/providers/LandingProvider.tsx @@ -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 {children}; +} + +export default LandingProvider;