diff --git a/apps/client/app/api-client/sdk.gen.ts b/apps/client/app/api-client/sdk.gen.ts index b0e3093..64432dd 100644 --- a/apps/client/app/api-client/sdk.gen.ts +++ b/apps/client/app/api-client/sdk.gen.ts @@ -68,6 +68,10 @@ export const register = (options?: Options return (options?.client ?? _heyApiClient).post({ url: "/api/v1/auth/register", ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, }); }; @@ -78,6 +82,10 @@ export const login = (options?: Options({ url: "/api/v1/auth/login", ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, }); }; diff --git a/apps/client/app/api-client/types.gen.ts b/apps/client/app/api-client/types.gen.ts index 1949eeb..9085ca5 100644 --- a/apps/client/app/api-client/types.gen.ts +++ b/apps/client/app/api-client/types.gen.ts @@ -1,7 +1,10 @@ // This file is auto-generated by @hey-api/openapi-ts export type RegisterData = { - body?: never; + body?: { + password: string; + username: string; + }; path?: never; query?: never; url: "/api/v1/auth/register"; @@ -30,7 +33,10 @@ export type RegisterResponses = { export type RegisterResponse = RegisterResponses[keyof RegisterResponses]; export type LoginData = { - body?: never; + body?: { + password: string; + username: string; + }; path?: never; query?: never; url: "/api/v1/auth/login"; diff --git a/apps/client/app/components/layout.tsx b/apps/client/app/components/layout.tsx index 5342d18..cf9152c 100644 --- a/apps/client/app/components/layout.tsx +++ b/apps/client/app/components/layout.tsx @@ -1,8 +1,35 @@ -import { Outlet } from "react-router"; +import { useMutation } from "@tanstack/react-query"; +import { Outlet, useNavigate } from "react-router"; +import { toast } from "sonner"; import { cn } from "~/lib/utils"; import { AppBreadcrumb } from "./app-breadcrumb"; +import { Button } from "./ui/button"; +import { logoutMutation } from "~/api-client/@tanstack/react-query.gen"; +import type { Route } from "./+types/layout"; +import { appContext } from "~/context"; +import { authMiddleware } from "~/middleware/auth"; + +export const clientMiddleware = [authMiddleware]; + +export async function clientLoader({ context }: Route.LoaderArgs) { + const ctx = context.get(appContext); + return ctx; +} + +export default function Layout({ loaderData }: Route.ComponentProps) { + const navigate = useNavigate(); + + const logout = useMutation({ + ...logoutMutation(), + onSuccess: async () => { + navigate("/login", { replace: true }); + }, + onError: (error) => { + console.error(error); + toast.error("Logout failed"); + }, + }); -export default function Layout() { return (
- +
+ + {loaderData.user && ( +
+ Welcome, {loaderData.user?.username} + +
+ )} +
diff --git a/apps/client/app/context.ts b/apps/client/app/context.ts new file mode 100644 index 0000000..5d3973a --- /dev/null +++ b/apps/client/app/context.ts @@ -0,0 +1,12 @@ +import { createContext } from "react-router"; +import type { User } from "./lib/types"; + +type AppContext = { + user: User | null; + hasUsers: boolean; +}; + +export const appContext = createContext({ + user: null, + hasUsers: false, +}); diff --git a/apps/client/app/lib/types.ts b/apps/client/app/lib/types.ts index 033e686..07f3024 100644 --- a/apps/client/app/lib/types.ts +++ b/apps/client/app/lib/types.ts @@ -1,5 +1,7 @@ -import type { GetVolumeResponse } from "~/api-client"; +import type { GetMeResponse, GetVolumeResponse } from "~/api-client"; export type Volume = GetVolumeResponse["volume"]; export type StatFs = GetVolumeResponse["statfs"]; export type VolumeStatus = Volume["status"]; + +export type User = GetMeResponse["user"]; diff --git a/apps/client/app/middleware/auth.ts b/apps/client/app/middleware/auth.ts new file mode 100644 index 0000000..c85397f --- /dev/null +++ b/apps/client/app/middleware/auth.ts @@ -0,0 +1,18 @@ +import { redirect, type MiddlewareFunction } from "react-router"; +import { getMe, getStatus } from "~/api-client"; +import { appContext } from "~/context"; + +export const authMiddleware: MiddlewareFunction = async ({ context }) => { + const session = await getMe(); + + if (!session.data?.user.id) { + const status = await getStatus(); + if (!status.data?.hasUsers) { + throw redirect("/register"); + } + + throw redirect("/login"); + } + + context.set(appContext, { user: session.data.user, hasUsers: true }); +}; diff --git a/apps/client/app/routes.ts b/apps/client/app/routes.ts index b71113b..b03d061 100644 --- a/apps/client/app/routes.ts +++ b/apps/client/app/routes.ts @@ -1,5 +1,7 @@ import { index, layout, type RouteConfig, route } from "@react-router/dev/routes"; export default [ + route("onboarding", "./routes/onboarding.tsx"), + route("login", "./routes/login.tsx"), layout("./components/layout.tsx", [index("./routes/home.tsx"), route("volumes/:name", "./routes/details.tsx")]), ] satisfies RouteConfig; diff --git a/apps/client/app/routes/login.tsx b/apps/client/app/routes/login.tsx new file mode 100644 index 0000000..3c3d0dc --- /dev/null +++ b/apps/client/app/routes/login.tsx @@ -0,0 +1,86 @@ +import { useMutation } from "@tanstack/react-query"; +import { useId, useState } from "react"; +import { useNavigate } from "react-router"; +import { toast } from "sonner"; +import { loginMutation } from "~/api-client/@tanstack/react-query.gen"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; + +export default function LoginPage() { + const navigate = useNavigate(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const usernameId = useId(); + const passwordId = useId(); + + const login = useMutation({ + ...loginMutation(), + onSuccess: async () => { + navigate("/"); + }, + onError: (error) => { + console.error(error); + toast.error("Login failed"); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!username.trim() || !password.trim()) { + toast.error("Username and password are required"); + return; + } + + login.mutate({ + body: { + username: username.trim(), + password: password.trim(), + }, + }); + }; + + return ( +
+ + + Welcome Back + Sign in to your account + + +
+
+ + setUsername(e.target.value)} + disabled={login.isPending} + autoFocus + required + /> +
+
+ + setPassword(e.target.value)} + disabled={login.isPending} + required + /> +
+ +
+
+
+
+ ); +} diff --git a/apps/client/app/routes/onboarding.tsx b/apps/client/app/routes/onboarding.tsx new file mode 100644 index 0000000..a9b1efc --- /dev/null +++ b/apps/client/app/routes/onboarding.tsx @@ -0,0 +1,106 @@ +import { useMutation } from "@tanstack/react-query"; +import { useId, useState } from "react"; +import { useNavigate } from "react-router"; +import { toast } from "sonner"; +import { registerMutation } from "~/api-client/@tanstack/react-query.gen"; +import { Button } from "~/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; + +export default function OnboardingPage() { + const navigate = useNavigate(); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const usernameId = useId(); + const passwordId = useId(); + const confirmPasswordId = useId(); + + const register = useMutation({ + ...registerMutation(), + onSuccess: async () => { + toast.success("Admin user created successfully!"); + navigate("/"); + }, + onError: (error) => { + console.error(error); + toast.error("Failed to create admin user"); + }, + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!username.trim() || !password.trim()) { + toast.error("Username and password are required"); + return; + } + + if (password !== confirmPassword) { + toast.error("Passwords do not match"); + return; + } + + register.mutate({ + body: { + username: username.trim(), + password: password.trim(), + }, + }); + }; + + return ( +
+ + + Welcome to IronMount + Create the admin user to get started + + +
+
+ + setUsername(e.target.value)} + disabled={register.isPending} + autoFocus + required + /> +
+
+ + setPassword(e.target.value)} + disabled={register.isPending} + required + /> +
+
+ + setConfirmPassword(e.target.value)} + disabled={register.isPending} + required + /> +
+ +
+
+
+
+ ); +} diff --git a/apps/client/package.json b/apps/client/package.json index 314b2a8..1b0a986 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -36,14 +36,14 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.62.0", - "react-router": "^7.7.1", + "react-router": "^7.9.3", "recharts": "2.15.4", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "yaml": "^2.8.1" }, "devDependencies": { - "@react-router/dev": "^7.7.1", + "@react-router/dev": "^7.9.3", "@tailwindcss/vite": "^4.1.4", "@types/node": "^20", "@types/prismjs": "^1.26.5", diff --git a/apps/client/react-router.config.ts b/apps/client/react-router.config.ts index a98a3df..46aff4a 100644 --- a/apps/client/react-router.config.ts +++ b/apps/client/react-router.config.ts @@ -3,4 +3,7 @@ import type { Config } from "@react-router/dev/config"; export default { ssr: false, buildDirectory: "dist", + future: { + v8_middleware: true, + }, } satisfies Config; diff --git a/apps/server/src/modules/auth/auth.controller.ts b/apps/server/src/modules/auth/auth.controller.ts index 33f13ea..19ea5c5 100644 --- a/apps/server/src/modules/auth/auth.controller.ts +++ b/apps/server/src/modules/auth/auth.controller.ts @@ -1,7 +1,16 @@ -import { arktypeValidator } from "@hono/arktype-validator"; +import { validator } from "hono-openapi"; + import { Hono } from "hono"; import { deleteCookie, getCookie, setCookie } from "hono/cookie"; -import { getMeDto, loginBodySchema, loginDto, logoutDto, registerBodySchema, registerDto } from "./auth.dto"; +import { + getMeDto, + getStatusDto, + loginBodySchema, + loginDto, + logoutDto, + registerBodySchema, + registerDto, +} from "./auth.dto"; import { authService } from "./auth.service"; const COOKIE_NAME = "session_id"; @@ -13,24 +22,23 @@ const COOKIE_OPTIONS = { }; export const authController = new Hono() - .post("/register", registerDto, arktypeValidator("json", registerBodySchema), async (c) => { + .post("/register", registerDto, validator("json", registerBodySchema), async (c) => { const body = c.req.valid("json"); try { - const { user } = await authService.register(body.username, body.password); + const { user, sessionId } = await authService.register(body.username, body.password); - return c.json( - { - message: "User registered successfully", - user: { id: user.id, username: user.username }, - }, - 201, - ); + setCookie(c, COOKIE_NAME, sessionId, { + ...COOKIE_OPTIONS, + expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + }); + + return c.json({ message: "User registered successfully", user: { id: user.id, username: user.username } }, 201); } catch (error) { return c.json({ message: error instanceof Error ? error.message : "Registration failed" }, 400); } }) - .post("/login", loginDto, arktypeValidator("json", loginBodySchema), async (c) => { + .post("/login", loginDto, validator("json", loginBodySchema), async (c) => { const body = c.req.valid("json"); try { @@ -76,4 +84,8 @@ export const authController = new Hono() return c.json({ user: session.user, }); + }) + .get("/status", getStatusDto, async (c) => { + const hasUsers = await authService.hasUsers(); + return c.json({ hasUsers }); }); diff --git a/apps/server/src/modules/auth/auth.dto.ts b/apps/server/src/modules/auth/auth.dto.ts index 8b45034..ca18418 100644 --- a/apps/server/src/modules/auth/auth.dto.ts +++ b/apps/server/src/modules/auth/auth.dto.ts @@ -93,5 +93,25 @@ export const getMeDto = describeRoute({ }, }); +const statusResponseSchema = type({ + hasUsers: "boolean", +}); + +export const getStatusDto = describeRoute({ + description: "Get authentication system status", + operationId: "getStatus", + tags: ["Auth"], + responses: { + 200: { + description: "Authentication system status", + content: { + "application/json": { + schema: resolver(statusResponseSchema), + }, + }, + }, + }, +}); + export type LoginBody = typeof loginBodySchema.infer; export type RegisterBody = typeof registerBodySchema.infer; diff --git a/apps/server/src/modules/auth/auth.service.ts b/apps/server/src/modules/auth/auth.service.ts index 822b635..20ee8db 100644 --- a/apps/server/src/modules/auth/auth.service.ts +++ b/apps/server/src/modules/auth/auth.service.ts @@ -10,10 +10,10 @@ export class AuthService { * Register a new user with username and password */ async register(username: string, password: string) { - const [existingUser] = await db.select().from(usersTable).where(eq(usersTable.username, username)); + const [existingUser] = await db.select().from(usersTable); if (existingUser) { - throw new Error("Username already exists"); + throw new Error("Admin user already exists"); } const passwordHash = await Bun.password.hash(password, { @@ -29,8 +29,16 @@ export class AuthService { } logger.info(`User registered: ${username}`); + const sessionId = crypto.randomUUID(); + const expiresAt = new Date(Date.now() + SESSION_DURATION); - return { user: { id: user.id, username: user.username, createdAt: user.createdAt } }; + await db.insert(sessionsTable).values({ + id: sessionId, + userId: user.id, + expiresAt, + }); + + return { user: { id: user.id, username: user.username, createdAt: user.createdAt }, sessionId }; } /** @@ -118,6 +126,14 @@ export class AuthService { logger.info(`Cleaned up ${result.length} expired sessions`); } } + + /** + * Check if any users exist in the system + */ + async hasUsers(): Promise { + const [user] = await db.select({ id: usersTable.id }).from(usersTable).limit(1); + return !!user; + } } export const authService = new AuthService(); diff --git a/bun.lock b/bun.lock index 762adff..0135679 100644 --- a/bun.lock +++ b/bun.lock @@ -38,14 +38,14 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-hook-form": "^7.62.0", - "react-router": "^7.7.1", + "react-router": "^7.9.3", "recharts": "2.15.4", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "yaml": "^2.8.1", }, "devDependencies": { - "@react-router/dev": "^7.7.1", + "@react-router/dev": "^7.9.3", "@tailwindcss/vite": "^4.1.4", "@types/node": "^20", "@types/prismjs": "^1.26.5", @@ -378,7 +378,7 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@react-router/dev": ["@react-router/dev@7.8.1", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "7.8.1", "@vitejs/plugin-rsc": "0.4.11", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "set-cookie-parser": "^2.6.0", "tinyglobby": "^0.2.14", "valibot": "^0.41.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.8.1", "react-router": "^7.8.1", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-ESFe7DbMvCvl7e8N7L9NmI64VJGNCc60/VX1DUZYw/jFfzA5098/6D1aUojcxyVYBbMbVTfw0xmEvD4CsJzy1Q=="], + "@react-router/dev": ["@react-router/dev@7.9.3", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.7", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", "@react-router/node": "7.9.3", "@remix-run/node-fetch-server": "^0.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", "dedent": "^1.5.3", "es-module-lexer": "^1.3.1", "exit-hook": "2.2.1", "isbot": "^5.1.11", "jsesc": "3.0.2", "lodash": "^4.17.21", "pathe": "^1.1.2", "picocolors": "^1.1.1", "prettier": "^3.6.2", "react-refresh": "^0.14.0", "semver": "^7.3.7", "tinyglobby": "^0.2.14", "valibot": "^0.41.0", "vite-node": "^3.2.2" }, "peerDependencies": { "@react-router/serve": "^7.9.3", "@vitejs/plugin-rsc": "*", "react-router": "^7.9.3", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" }, "optionalPeers": ["@react-router/serve", "@vitejs/plugin-rsc", "typescript", "wrangler"], "bin": { "react-router": "bin.js" } }, "sha512-oPaO+OpvCo/rNTJrRipHSp31/K4It19PE5A24x21FlYlemPTe3fbGX/kyC2+8au/abXbvzNHfRbuIBD/rfojmA=="], "@react-router/express": ["@react-router/express@7.8.1", "", { "dependencies": { "@react-router/node": "7.8.1" }, "peerDependencies": { "express": "^4.17.1 || ^5", "react-router": "7.8.1", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-Oq+l1eOex6TE1uAixM177YGF0yhYCqMoqvLQIjAGz4bfpCui6UewSDR6FSBNm+vub2OB06B5ARk6W4BOzf2ZcQ=="], @@ -386,6 +386,8 @@ "@react-router/serve": ["@react-router/serve@7.8.1", "", { "dependencies": { "@react-router/express": "7.8.1", "@react-router/node": "7.8.1", "compression": "^1.7.4", "express": "^4.19.2", "get-port": "5.1.1", "morgan": "^1.10.0", "source-map-support": "^0.5.21" }, "peerDependencies": { "react-router": "7.8.1" }, "bin": { "react-router-serve": "bin.js" } }, "sha512-GjevrjDesWI4r3p7/pr6oBZGyozQVlz+ePAJ+IEJ+ZsFgto1qb01p6fdDdywcMpgNZokmorrj3m3cfyQJIfrvg=="], + "@remix-run/node-fetch-server": ["@remix-run/node-fetch-server@0.9.0", "", {}, "sha512-SoLMv7dbH+njWzXnOY6fI08dFMI5+/dQ+vY3n8RnnbdG7MdJEgiP28Xj/xWlnRnED/aB6SFw56Zop+LbmaaKqA=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.41", "", { "os": "android", "cpu": "arm64" }, "sha512-Edflndd9lU7JVhVIvJlZhdCj5DkhYDJPIRn4Dx0RUdfc8asP9xHOI5gMd8MesDDx+BJpdIT/uAmVTearteU/mQ=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.41", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XGCzqfjdk7550PlyZRTBKbypXrB7ATtXhw/+bjtxnklLQs0mKP/XkQVOKyn9qGKSlvH8I56JLYryVxl0PCvSNw=="], @@ -556,8 +558,6 @@ "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], - "@vitejs/plugin-rsc": ["@vitejs/plugin-rsc@0.4.11", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.7.0", "es-module-lexer": "^1.7.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.17", "periscopic": "^4.0.2", "turbo-stream": "^3.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "react": "*", "react-dom": "*", "vite": "*" } }, "sha512-+4H4wLi+Y9yF58znBfKgGfX8zcqUGt8ngnmNgzrdGdF1SVz7EO0sg7WnhK5fFVHt6fUxsVEjmEabsCWHKPL1Tw=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -770,8 +770,6 @@ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -864,8 +862,6 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], @@ -1022,8 +1018,6 @@ "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], - "periscopic": ["periscopic@4.0.2", "", { "dependencies": { "@types/estree": "*", "is-reference": "^3.0.2", "zimmerframe": "^1.0.0" } }, "sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], @@ -1074,7 +1068,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.8.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA=="], + "react-router": ["react-router@7.9.3", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg=="], "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], @@ -1190,7 +1184,7 @@ "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], - "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -1210,8 +1204,6 @@ "turbo-linux-arm64": ["turbo-linux-arm64@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ=="], - "turbo-stream": ["turbo-stream@3.1.0", "", {}, "sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A=="], - "turbo-windows-64": ["turbo-windows-64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg=="], "turbo-windows-arm64": ["turbo-windows-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q=="], @@ -1262,8 +1254,6 @@ "vite-tsconfig-paths": ["vite-tsconfig-paths@5.1.4", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w=="], - "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], - "which": ["which@3.0.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg=="], "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], @@ -1288,8 +1278,6 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "zimmerframe": ["zimmerframe@1.1.2", "", {}, "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w=="], - "zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -1310,6 +1298,14 @@ "@npmcli/git/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + "@react-router/dev/@react-router/node": ["@react-router/node@7.9.3", "", { "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, "peerDependencies": { "react-router": "7.9.3", "typescript": "^5.1.0" }, "optionalPeers": ["typescript"] }, "sha512-+OvWxPPUgouOshw85QlG0J6yFJM0GMCCpXqPj38IcveeFLlP7ppOAEkOi7RBFrDvg7vSUtCEBDnsbuDCvxUPJg=="], + + "@react-router/express/react-router": ["react-router@7.8.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA=="], + + "@react-router/node/react-router": ["react-router@7.8.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA=="], + + "@react-router/serve/react-router": ["react-router@7.8.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA=="], + "@scalar/types/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], @@ -1328,8 +1324,6 @@ "@types/ssh2/@types/node": ["@types/node@18.19.127", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA=="], - "@vitejs/plugin-rsc/@mjackson/node-fetch-server": ["@mjackson/node-fetch-server@0.7.0", "", {}, "sha512-un8diyEBKU3BTVj3GzlTPA1kIjCkGdD+AMYQy31Gf9JCkfoZzwgJ79GUtHrF2BN3XPNMLpubbzPcxys+a3uZEw=="], - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -1384,8 +1378,6 @@ "tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - "vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "vite-node/vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="], @@ -1488,6 +1480,8 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "vite-node/vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + "giget/tar/minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], } }