Bläddra i källkod

migrate to next 15 and react 19

Milo Schwartz 9 månader sedan
förälder
incheckning
ce19cc4ba4

+ 1 - 1
Dockerfile

@@ -22,7 +22,7 @@ RUN npm install --omit=dev
 
 COPY --from=builder /app/.next ./.next
 COPY --from=builder /app/dist ./dist
-COPY ./config/config.example.yml /app/dist/
+COPY ./config/config.example.yml ./
 COPY server/db/names.json /app/dist/names.json
 
 CMD ["npm", "start"]

+ 98 - 94
package.json

@@ -1,96 +1,100 @@
 {
-    "name": "@fossorial/pangolin",
-    "version": "0.1.0",
-    "private": true,
-    "type": "module",
-    "scripts": {
-        "dev": "ENVIRONMENT=dev tsx watch server/index.ts",
-        "db:generate": "drizzle-kit generate",
-        "db:push": "npx tsx server/db/migrate.ts",
-        "db:hydrate": "npx tsx scripts/hydrate.ts",
-        "db:studio": "drizzle-kit studio",
-        "build": "mkdir -p dist && next build && node scripts/esbuild.mjs -e server/index.ts -o dist/server.mjs",
-        "start": "ENVIRONMENT=prod node dist/server.mjs",
-        "email": "email dev --dir server/emails/templates --port 3002"
-    },
-    "dependencies": {
-        "@esbuild-plugins/tsconfig-paths": "0.1.2",
-        "@hookform/resolvers": "3.9.0",
-        "@node-rs/argon2": "1.8.3",
-        "@oslojs/crypto": "1.0.1",
-        "@oslojs/encoding": "1.1.0",
-        "@radix-ui/react-avatar": "1.1.1",
-        "@radix-ui/react-checkbox": "1.1.2",
-        "@radix-ui/react-dialog": "1.1.2",
-        "@radix-ui/react-dropdown-menu": "2.1.2",
-        "@radix-ui/react-icons": "1.3.0",
-        "@radix-ui/react-label": "2.1.0",
-        "@radix-ui/react-popover": "1.1.2",
-        "@radix-ui/react-radio-group": "1.2.1",
-        "@radix-ui/react-select": "2.1.2",
-        "@radix-ui/react-separator": "1.1.0",
-        "@radix-ui/react-slot": "1.1.0",
-        "@radix-ui/react-switch": "1.1.1",
-        "@radix-ui/react-toast": "1.2.2",
-        "@react-email/components": "0.0.25",
-        "@react-email/tailwind": "0.1.0",
-        "@tanstack/react-table": "8.20.5",
-        "axios": "1.7.7",
-        "better-sqlite3": "11.3.0",
-        "class-variance-authority": "0.7.0",
-        "clsx": "2.1.1",
-        "cmdk": "1.0.0",
-        "cookie-parser": "1.4.6",
-        "cors": "2.8.5",
-        "drizzle-orm": "0.33.0",
-        "esbuild": "0.20.1",
-        "esbuild-node-externals": "1.13.0",
-        "express": "4.21.0",
-        "express-rate-limit": "7.4.0",
-        "glob": "11.0.0",
-        "helmet": "7.1.0",
-        "http-errors": "2.0.0",
-        "input-otp": "1.2.4",
-        "js-yaml": "4.1.0",
-        "lucide-react": "0.447.0",
-        "moment": "2.30.1",
-        "next": "14.2.13",
-        "next-themes": "0.3.0",
-        "node-fetch": "3.3.2",
-        "nodemailer": "6.9.15",
-        "oslo": "1.2.1",
-        "react": "^18",
-        "react-dom": "^18",
-        "react-hook-form": "7.53.0",
-        "rebuild": "0.1.2",
-        "tailwind-merge": "2.5.3",
-        "tailwindcss-animate": "1.0.7",
-        "winston": "3.14.2",
-        "winston-daily-rotate-file": "5.0.0",
-        "yargs": "17.7.2",
-        "zod": "3.23.8",
-        "zod-validation-error": "3.4.0"
-    },
-    "devDependencies": {
-        "@dotenvx/dotenvx": "1.14.2",
-        "@types/better-sqlite3": "7.6.11",
-        "@types/cookie-parser": "1.4.7",
-        "@types/cors": "2.8.17",
-        "@types/express": "5.0.0",
-        "@types/js-yaml": "4.0.9",
-        "@types/node": "^20",
-        "@types/nodemailer": "6.4.16",
-        "@types/react": "^18",
-        "@types/react-dom": "^18",
-        "@types/yargs": "17.0.33",
-        "drizzle-kit": "0.24.2",
-        "eslint": "^8",
-        "eslint-config-next": "14.2.13",
-        "postcss": "^8",
-        "react-email": "3.0.1",
-        "tailwindcss": "^3.4.1",
-        "tsc-alias": "1.8.10",
-        "tsx": "4.19.1",
-        "typescript": "^5"
-    }
+  "name": "@fossorial/pangolin",
+  "version": "0.1.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "ENVIRONMENT=dev tsx watch server/index.ts",
+    "db:generate": "drizzle-kit generate",
+    "db:push": "npx tsx server/db/migrate.ts",
+    "db:hydrate": "npx tsx scripts/hydrate.ts",
+    "db:studio": "drizzle-kit studio",
+    "build": "mkdir -p dist && next build && node scripts/esbuild.mjs -e server/index.ts -o dist/server.mjs",
+    "start": "ENVIRONMENT=prod node dist/server.mjs",
+    "email": "email dev --dir server/emails/templates --port 3002"
+  },
+  "dependencies": {
+    "@esbuild-plugins/tsconfig-paths": "0.1.2",
+    "@hookform/resolvers": "3.9.0",
+    "@node-rs/argon2": "1.8.3",
+    "@oslojs/crypto": "1.0.1",
+    "@oslojs/encoding": "1.1.0",
+    "@radix-ui/react-avatar": "1.1.1",
+    "@radix-ui/react-checkbox": "1.1.2",
+    "@radix-ui/react-dialog": "1.1.2",
+    "@radix-ui/react-dropdown-menu": "2.1.2",
+    "@radix-ui/react-icons": "1.3.0",
+    "@radix-ui/react-label": "2.1.0",
+    "@radix-ui/react-popover": "1.1.2",
+    "@radix-ui/react-radio-group": "1.2.1",
+    "@radix-ui/react-select": "2.1.2",
+    "@radix-ui/react-separator": "1.1.0",
+    "@radix-ui/react-slot": "1.1.0",
+    "@radix-ui/react-switch": "1.1.1",
+    "@radix-ui/react-toast": "1.2.2",
+    "@react-email/components": "0.0.25",
+    "@react-email/tailwind": "0.1.0",
+    "@tanstack/react-table": "8.20.5",
+    "axios": "1.7.7",
+    "better-sqlite3": "11.3.0",
+    "class-variance-authority": "0.7.0",
+    "clsx": "2.1.1",
+    "cmdk": "1.0.0",
+    "cookie-parser": "1.4.6",
+    "cors": "2.8.5",
+    "drizzle-orm": "0.33.0",
+    "esbuild": "0.20.1",
+    "esbuild-node-externals": "1.13.0",
+    "express": "4.21.0",
+    "express-rate-limit": "7.4.0",
+    "glob": "11.0.0",
+    "helmet": "7.1.0",
+    "http-errors": "2.0.0",
+    "input-otp": "1.2.4",
+    "js-yaml": "4.1.0",
+    "lucide-react": "0.447.0",
+    "moment": "2.30.1",
+    "next": "15.0.1",
+    "next-themes": "0.3.0",
+    "node-fetch": "3.3.2",
+    "nodemailer": "6.9.15",
+    "oslo": "1.2.1",
+    "react": "19.0.0-rc-69d4b800-20241021",
+    "react-dom": "19.0.0-rc-69d4b800-20241021",
+    "react-hook-form": "7.53.0",
+    "rebuild": "0.1.2",
+    "tailwind-merge": "2.5.3",
+    "tailwindcss-animate": "1.0.7",
+    "winston": "3.14.2",
+    "winston-daily-rotate-file": "5.0.0",
+    "yargs": "17.7.2",
+    "zod": "3.23.8",
+    "zod-validation-error": "3.4.0"
+  },
+  "devDependencies": {
+    "@dotenvx/dotenvx": "1.14.2",
+    "@types/better-sqlite3": "7.6.11",
+    "@types/cookie-parser": "1.4.7",
+    "@types/cors": "2.8.17",
+    "@types/express": "5.0.0",
+    "@types/js-yaml": "4.0.9",
+    "@types/node": "^20",
+    "@types/nodemailer": "6.4.16",
+    "@types/react": "npm:types-react@19.0.0-rc.1",
+    "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
+    "@types/yargs": "17.0.33",
+    "drizzle-kit": "0.24.2",
+    "eslint": "^8",
+    "eslint-config-next": "15.0.1",
+    "postcss": "^8",
+    "react-email": "3.0.1",
+    "tailwindcss": "^3.4.1",
+    "tsc-alias": "1.8.10",
+    "tsx": "4.19.1",
+    "typescript": "^5"
+  },
+  "overrides": {
+    "@types/react": "npm:types-react@19.0.0-rc.1",
+    "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
+  }
 }

+ 1 - 1
server/config.ts

@@ -31,7 +31,7 @@ const environmentSchema = z.object({
     email: z
         .object({
             smtp_host: z.string().optional(),
-            smtp_port: z.number().optional(),
+            smtp_port: z.number().positive().gt(0).lte(65535).optional(),
             smtp_user: z.string().optional(),
             smtp_pass: z.string().optional(),
             no_reply: z.string().email().optional(),

+ 3 - 2
src/api/cookies.ts

@@ -1,7 +1,8 @@
 import { cookies } from "next/headers";
 
-export function authCookieHeader() {
-    const sessionId = cookies().get("session")?.value ?? null;
+export async function authCookieHeader() {
+    const allCookies = await cookies();
+    const sessionId = allCookies.get("session")?.value ?? null;
     return {
         headers: {
             Cookie: `session=${sessionId}`

+ 12 - 7
src/app/[orgId]/layout.tsx

@@ -40,23 +40,28 @@ const topNavItems = [
 
 interface ConfigurationLaytoutProps {
     children: React.ReactNode;
-    params: { orgId: string };
+    params: Promise<{ orgId: string }>;
 }
 
-export default async function ConfigurationLaytout({
-    children,
-    params,
-}: ConfigurationLaytoutProps) {
+export default async function ConfigurationLaytout(
+    props: ConfigurationLaytoutProps
+) {
+    const params = await props.params;
+
+    const { children } = props;
+
     const user = await verifySession();
 
     if (!user) {
         redirect("/auth/login");
     }
 
+    const cookie = await authCookieHeader();
+
     try {
         await internal.get<AxiosResponse<GetOrgResponse>>(
             `/org/${params.orgId}`,
-            authCookieHeader(),
+            cookie
         );
     } catch {
         redirect(`/`);
@@ -66,7 +71,7 @@ export default async function ConfigurationLaytout({
     try {
         const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
             `/orgs`,
-            authCookieHeader(),
+            cookie
         );
         if (res && res.data.data.orgs) {
             orgs = res.data.data.orgs;

+ 3 - 2
src/app/[orgId]/page.tsx

@@ -1,10 +1,11 @@
 import { redirect } from "next/navigation";
 
 type OrgPageProps = {
-    params: { orgId: string };
+    params: Promise<{ orgId: string }>;
 };
 
-export default async function Page({ params }: OrgPageProps) {
+export default async function Page(props: OrgPageProps) {
+    const params = await props.params;
     redirect(`/${params.orgId}/sites`);
 
     return <></>;

+ 9 - 6
src/app/[orgId]/resources/[resourceId]/layout.tsx

@@ -22,20 +22,23 @@ export const metadata: Metadata = {
 
 interface SettingsLayoutProps {
     children: React.ReactNode;
-    params: { resourceId: string; orgId: string };
+    params: Promise<{ resourceId: string; orgId: string }>;
 }
 
-export default async function SettingsLayout({
-    children,
-    params,
-}: SettingsLayoutProps) {
+export default async function SettingsLayout(props: SettingsLayoutProps) {
+    const params = await props.params;
+
+    const {
+        children
+    } = props;
+
     let resource = null;
 
     if (params.resourceId !== "create") {
         try {
             const res = await internal.get<AxiosResponse<GetResourceResponse>>(
                 `/resource/${params.resourceId}`,
-                authCookieHeader(),
+                await authCookieHeader(),
             );
             resource = res.data.data;
         } catch {

+ 6 - 5
src/app/[orgId]/resources/[resourceId]/page.tsx

@@ -3,11 +3,12 @@ import { Separator } from "@/components/ui/separator";
 import { CreateResourceForm } from "./components/CreateResource";
 import { GeneralForm } from "./components/GeneralForm";
 
-export default function SettingsPage({
-    params,
-}: {
-    params: { resourceId: string };
-}) {
+export default async function SettingsPage(
+    props: {
+        params: Promise<{ resourceId: string }>;
+    }
+) {
+    const params = await props.params;
     const isCreate = params.resourceId === "create";
 
     return (

+ 7 - 6
src/app/[orgId]/resources/[resourceId]/targets/page.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { useEffect, useState } from "react";
+import { useEffect, useState, use } from "react";
 import { PlusCircle, Trash2, Server, Globe, Cpu } from "lucide-react";
 import { Button } from "@/components/ui/button";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -25,11 +25,12 @@ const isValidIPAddress = (ip: string) => {
     return ipv4Regex.test(ip);
 };
 
-export default function ReverseProxyTargets({
-    params,
-}: {
-    params: { resourceId: string };
-}) {
+export default function ReverseProxyTargets(
+    props: {
+        params: Promise<{ resourceId: string }>;
+    }
+) {
+    const params = use(props.params);
     const [targets, setTargets] = useState<ListTargetsResponse["targets"]>([]);
     const [nextId, setNextId] = useState(1);
     const [ipError, setIpError] = useState("");

+ 4 - 3
src/app/[orgId]/resources/page.tsx

@@ -5,15 +5,16 @@ import { AxiosResponse } from "axios";
 import { ListResourcesResponse } from "@server/routers/resource";
 
 type ResourcesPageProps = {
-    params: { orgId: string };
+    params: Promise<{ orgId: string }>;
 };
 
-export default async function Page({ params }: ResourcesPageProps) {
+export default async function Page(props: ResourcesPageProps) {
+    const params = await props.params;
     let resources: ListResourcesResponse["resources"] = [];
     try {
         const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
             `/org/${params.orgId}/resources`,
-            authCookieHeader(),
+            await authCookieHeader(),
         );
         resources = res.data.data.resources;
     } catch (e) {

+ 9 - 6
src/app/[orgId]/sites/[niceId]/layout.tsx

@@ -22,20 +22,23 @@ import { ClientLayout } from "./components/ClientLayout";
 
 interface SettingsLayoutProps {
     children: React.ReactNode;
-    params: { niceId: string; orgId: string };
+    params: Promise<{ niceId: string; orgId: string }>;
 }
 
-export default async function SettingsLayout({
-    children,
-    params,
-}: SettingsLayoutProps) {
+export default async function SettingsLayout(props: SettingsLayoutProps) {
+    const params = await props.params;
+
+    const {
+        children
+    } = props;
+
     let site = null;
 
     if (params.niceId !== "create") {
         try {
             const res = await internal.get<AxiosResponse<GetSiteResponse>>(
                 `/org/${params.orgId}/site/${params.niceId}`,
-                authCookieHeader(),
+                await authCookieHeader(),
             );
             site = res.data.data;
         } catch {

+ 6 - 5
src/app/[orgId]/sites/[niceId]/page.tsx

@@ -3,11 +3,12 @@ import { Separator } from "@/components/ui/separator";
 import { CreateSiteForm } from "./components/CreateSite";
 import { GeneralForm } from "./components/GeneralForm";
 
-export default function SettingsPage({
-    params,
-}: {
-    params: { niceId: string };
-}) {
+export default async function SettingsPage(
+    props: {
+        params: Promise<{ niceId: string }>;
+    }
+) {
+    const params = await props.params;
     const isCreate = params.niceId === "create";
 
     return (

+ 4 - 3
src/app/[orgId]/sites/page.tsx

@@ -5,15 +5,16 @@ import { AxiosResponse } from "axios";
 import SitesTable, { SiteRow } from "./components/SitesTable";
 
 type SitesPageProps = {
-    params: { orgId: string };
+    params: Promise<{ orgId: string }>;
 };
 
-export default async function Page({ params }: SitesPageProps) {
+export default async function Page(props: SitesPageProps) {
+    const params = await props.params;
     let sites: ListSitesResponse["sites"] = [];
     try {
         const res = await internal.get<AxiosResponse<ListSitesResponse>>(
             `/org/${params.orgId}/sites`,
-            authCookieHeader(),
+            await authCookieHeader(),
         );
         sites = res.data.data.sites;
     } catch (e) {

+ 6 - 5
src/app/auth/login/page.tsx

@@ -3,11 +3,12 @@ import { verifySession } from "@app/lib/auth/verifySession";
 import Link from "next/link";
 import { redirect } from "next/navigation";
 
-export default async function Page({
-    searchParams,
-}: {
-    searchParams: { [key: string]: string | string[] | undefined };
-}) {
+export default async function Page(
+    props: {
+        searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+    }
+) {
+    const searchParams = await props.searchParams;
     const user = await verifySession();
 
     if (user) {

+ 6 - 5
src/app/auth/signup/page.tsx

@@ -3,11 +3,12 @@ import { verifySession } from "@app/lib/auth/verifySession";
 import Link from "next/link";
 import { redirect } from "next/navigation";
 
-export default async function Page({
-    searchParams,
-}: {
-    searchParams: { [key: string]: string | string[] | undefined };
-}) {
+export default async function Page(
+    props: {
+        searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+    }
+) {
+    const searchParams = await props.searchParams;
     const user = await verifySession();
 
     if (user) {

+ 6 - 5
src/app/auth/verify-email/page.tsx

@@ -2,11 +2,12 @@ import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm";
 import { verifySession } from "@app/lib/auth/verifySession";
 import { redirect } from "next/navigation";
 
-export default async function Page({
-    searchParams,
-}: {
-    searchParams: { [key: string]: string | string[] | undefined };
-}) {
+export default async function Page(
+    props: {
+        searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+    }
+) {
+    const searchParams = await props.searchParams;
     const user = await verifySession();
 
     if (!user) {

+ 1 - 1
src/app/layout.tsx

@@ -29,7 +29,7 @@ export default async function RootLayout({
         try {
             const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
                 `/orgs`,
-                authCookieHeader(),
+                await authCookieHeader(),
             );
             if (res && res.data.data.orgs) {
                 orgs = res.data.data.orgs;

+ 1 - 1
src/app/page.tsx

@@ -20,7 +20,7 @@ export default async function Page() {
     try {
         const res = await internal.get<AxiosResponse<ListOrgsResponse>>(
             `/orgs`,
-            authCookieHeader(),
+            await authCookieHeader(),
         );
         if (res && res.data.data.orgs) {
             orgs = res.data.data.orgs;

+ 4 - 4
src/lib/auth/verifySession.ts

@@ -2,13 +2,13 @@ import { internal } from "@app/api";
 import { authCookieHeader } from "@app/api/cookies";
 import { GetUserResponse } from "@server/routers/user";
 import { AxiosResponse } from "axios";
-import { cookies } from "next/headers";
 
 export async function verifySession(): Promise<GetUserResponse | null> {
-    const sessionId = cookies().get("session")?.value ?? null;
-
     try {
-        const res = await internal.get<AxiosResponse<GetUserResponse>>("/user", authCookieHeader());
+        const res = await internal.get<AxiosResponse<GetUserResponse>>(
+            "/user",
+            await authCookieHeader()
+        );
 
         return res.data.data;
     } catch {

+ 9 - 25
tsconfig.json

@@ -1,10 +1,6 @@
 {
     "compilerOptions": {
-        "lib": [
-            "dom",
-            "dom.iterable",
-            "esnext"
-        ],
+        "lib": ["dom", "dom.iterable", "esnext"],
         "allowJs": true,
         "skipLibCheck": true,
         "strict": true,
@@ -18,29 +14,17 @@
         "incremental": true,
         "baseUrl": "src",
         "paths": {
-            "@server/*": [
-                "../server/*"
-            ],
-            "@app/*": [
-                "*"
-            ],
-            "@/*": [
-                "./*"
-            ]
+            "@server/*": ["../server/*"],
+            "@app/*": ["*"],
+            "@/*": ["./*"]
         },
         "plugins": [
             {
                 "name": "next"
             }
-        ]
+        ],
+        "target": "ES2017"
     },
-    "include": [
-        "next-env.d.ts",
-        "**/*.ts",
-        "**/*.tsx",
-        ".next/types/**/*.ts"
-    ],
-    "exclude": [
-        "node_modules"
-    ]
-}
+    "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+    "exclude": ["node_modules"]
+}