From 962aa542b0f139c5efa031afcbc9936ac73ce743 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Sun, 1 Feb 2026 22:23:46 +0800 Subject: [PATCH 1/7] add export button for project and payment table --- src/app/dashboard/admin/@tools/export.tsx | 30 +++++------------------ src/app/profile/[id]/profile.tsx | 2 +- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/app/dashboard/admin/@tools/export.tsx b/src/app/dashboard/admin/@tools/export.tsx index 2fc42097f..b264d2143 100644 --- a/src/app/dashboard/admin/@tools/export.tsx +++ b/src/app/dashboard/admin/@tools/export.tsx @@ -25,35 +25,17 @@ export default function ExportButton({ data, label }: ExportButtonProps) { return } - const headers = Object.keys(data[0] as Record) - const escapeCSV = (value: unknown): string => { + const headers = Object.keys(data[0] as Record).join(",") + const escapeCSV = (value: unknown) => { if (value == null) return "" - - let str: string - if (typeof value === "object") { - if (value instanceof Date) { - str = value.toISOString() - } else { - // Stringify JSON objects/arrays - str = JSON.stringify(value) - } - } else { - str = String(value) - } - - // Escape CSV: wrap in quotes if contains comma, quote, or newline - // and escape internal quotes by doubling them - if (/[",\n\r]/.test(str)) { + const str = String(value) + if (/[",\n]/.test(str)) { return `"${str.replace(/"/g, '""')}"` } return str } - - const rows = data - .map((row) => headers.map((key) => escapeCSV((row as Record)[key])).join(",")) - .join("\n") - - const csv = headers.join(",") + "\n" + rows + const rows = data.map((row) => Object.values(row).map(escapeCSV).join(",")).join("\n") + const csv = headers + "\n" + rows const blob = new Blob([csv], { type: "text/csv" }) const url = window.URL.createObjectURL(blob) diff --git a/src/app/profile/[id]/profile.tsx b/src/app/profile/[id]/profile.tsx index 84c77ca25..4053f0bc4 100644 --- a/src/app/profile/[id]/profile.tsx +++ b/src/app/profile/[id]/profile.tsx @@ -146,7 +146,7 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => { user.membership_expiry && (
Your membership will expire on{" "} - + {format(new Date(String(user.membership_expiry)), "dd MMMM yyyy")}
From 992c2d6971964672aaac493b801fc9b3b4cebad0 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Wed, 4 Feb 2026 23:28:20 +0800 Subject: [PATCH 2/7] migration --- .env.example | 8 +- drizzle.config.ts | 4 +- package.json | 2 + pnpm-lock.yaml | 81 +++++++++++++++---- src/app/dashboard/admin/@tools/export.tsx | 4 + src/app/dashboard/admin/@users/table.tsx | 4 +- src/app/dashboard/admin/layout.tsx | 15 ++-- src/app/profile/[id]/profile.tsx | 2 +- src/middleware.ts | 33 ++------ .../routers/admin/analytics/get-payments.ts | 2 +- .../admin/analytics/get-users-per-day.ts | 4 +- .../api/routers/admin/projects/get-all.ts | 2 +- .../routers/admin/projects/get-projects.ts | 4 +- src/server/api/routers/admin/users/get-all.ts | 2 +- .../api/routers/admin/users/update-email.ts | 1 - .../routers/projects/get-application-open.ts | 2 +- .../api/routers/projects/get-projects.ts | 10 +-- src/server/api/routers/projects/get-public.ts | 2 +- src/server/db/drizzle.ts | 9 +++ src/server/db/index.ts | 10 ++- src/server/db/schema.ts | 12 +-- 21 files changed, 130 insertions(+), 83 deletions(-) create mode 100644 src/server/db/drizzle.ts diff --git a/.env.example b/.env.example index 1fefa2a95..72d084e1f 100644 --- a/.env.example +++ b/.env.example @@ -37,4 +37,10 @@ UPSTASH_REDIS_REST_TOKEN=upstash-redis-token NEXT_PUBLIC_MAPBOX_API=mapbox-key # Cron (openssl rand -hex 32) -CRON_SECRET=some-random-secret \ No newline at end of file +CRON_SECRET=some-random-secret + +# Supabase +SUPABASE_URL= +SUPABASE_PUBLISHABLE_DEFAULT_KEY= +SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts index 7a6c2ad59..1a0565012 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -1,7 +1,5 @@ import { defineConfig } from "drizzle-kit" -import { getXataClient } from "~/server/db/xata" - export default defineConfig({ out: "./src/server/db/migrations", schema: "./src/server/db/schema.ts", @@ -9,6 +7,6 @@ export default defineConfig({ dialect: "postgresql", tablesFilter: ["cfc-website_*"], dbCredentials: { - url: getXataClient().sql.connectionString, + url: process.env.SUPABASE_DB_URL!, }, }) diff --git a/package.json b/package.json index 18ccda0e3..ae5c3183d 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "next": "14.2.26", "next-themes": "0.3.0", "pg": "^8.14.1", + "pg-native": "^3.5.2", "react": "18.3.1", "react-day-picker": "8.10.0", "react-dom": "18.3.1", @@ -96,6 +97,7 @@ "@types/eslint": "8.56.6", "@types/mapbox-gl": "3.1.0", "@types/node": "20.12.2", + "@types/pg": "^8.16.0", "@types/react": "18.3.3", "@types/react-dom": "18.2.23", "@typescript-eslint/eslint-plugin": "7.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cfd4796d3..5a4e40dd9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,7 +141,7 @@ importers: version: 0.30.1(typescript@5.4.3) '@xata.io/drizzle': specifier: 0.0.24 - version: 0.0.24(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3))(typescript@5.4.3) + version: 0.0.24(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)))(typescript@5.4.3) class-variance-authority: specifier: 0.7.0 version: 0.7.0 @@ -156,10 +156,10 @@ importers: version: 3.6.0 drizzle-orm: specifier: 0.41.0 - version: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3) + version: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)) drizzle-zod: specifier: ^0.7.1 - version: 0.7.1(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3))(zod@3.22.4) + version: 0.7.1(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)))(zod@3.22.4) framer-motion: specifier: 11.0.24 version: 11.0.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -174,7 +174,10 @@ importers: version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pg: specifier: ^8.14.1 - version: 8.16.3 + version: 8.16.3(pg-native@3.5.2) + pg-native: + specifier: ^3.5.2 + version: 3.5.2 react: specifier: 18.3.1 version: 18.3.1 @@ -245,6 +248,9 @@ importers: '@types/node': specifier: 20.12.2 version: 20.12.2 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 '@types/react': specifier: 18.3.3 version: 18.3.3 @@ -2808,6 +2814,9 @@ packages: '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} @@ -3585,6 +3594,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bn.js@5.2.2: resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} @@ -4515,6 +4527,9 @@ packages: resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} engines: {node: '>= 12'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -5179,6 +5194,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libpq@1.8.15: + resolution: {integrity: sha512-4lSWmly2Nsj3LaTxxtFmJWuP3Kx+0hYHEd+aNrcXEWT0nKWaPd9/QZPiMkkC680zeALFGHQdQWjBvnilL+vgWA==} + lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} @@ -5439,6 +5457,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nan@2.22.2: + resolution: {integrity: sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -5692,6 +5713,9 @@ packages: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} + pg-native@3.5.2: + resolution: {integrity: sha512-3oi+KVil86Vngo4H0IlhBaYSJWdcu8t2f1Y4TkQoQi5oZ9bNeYECGqW3oSGx69mjSZYHoC3h+3jYtqzRgndn5A==} + pg-pool@3.10.1: resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} peerDependencies: @@ -9755,7 +9779,13 @@ snapshots: '@types/pg-pool@2.0.6': dependencies: - '@types/pg': 8.6.1 + '@types/pg': 8.16.0 + + '@types/pg@8.16.0': + dependencies: + '@types/node': 20.12.2 + pg-protocol: 1.10.3 + pg-types: 2.2.0 '@types/pg@8.6.1': dependencies: @@ -10481,10 +10511,10 @@ snapshots: dependencies: typescript: 5.4.3 - '@xata.io/drizzle@0.0.24(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3))(typescript@5.4.3)': + '@xata.io/drizzle@0.0.24(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)))(typescript@5.4.3)': dependencies: '@xata.io/client': 0.30.1(typescript@5.4.3) - drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3) + drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)) transitivePeerDependencies: - typescript @@ -10799,6 +10829,10 @@ snapshots: binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + bn.js@5.2.2: {} borsh@0.7.0: @@ -11287,17 +11321,17 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3): + drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)): optionalDependencies: '@opentelemetry/api': 1.9.0 - '@types/pg': 8.6.1 + '@types/pg': 8.16.0 '@xata.io/client': 0.30.1(typescript@5.4.3) gel: 2.2.0 - pg: 8.16.3 + pg: 8.16.3(pg-native@3.5.2) - drizzle-zod@0.7.1(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3))(zod@3.22.4): + drizzle-zod@0.7.1(drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)))(zod@3.22.4): dependencies: - drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3) + drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(@xata.io/client@0.30.1(typescript@5.4.3))(gel@2.2.0)(pg@8.16.3(pg-native@3.5.2)) zod: 3.22.4 dunder-proto@1.0.1: @@ -11788,6 +11822,8 @@ snapshots: dependencies: tslib: 2.8.1 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -12525,6 +12561,11 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libpq@1.8.15: + dependencies: + bindings: 1.5.0 + nan: 2.22.2 + lighthouse-logger@1.4.2: dependencies: debug: 2.6.9 @@ -12918,6 +12959,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nan@2.22.2: {} + nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -13176,9 +13219,14 @@ snapshots: pg-int8@1.0.1: {} - pg-pool@3.10.1(pg@8.16.3): + pg-native@3.5.2: + dependencies: + libpq: 1.8.15 + pg-types: 2.2.0 + + pg-pool@3.10.1(pg@8.16.3(pg-native@3.5.2)): dependencies: - pg: 8.16.3 + pg: 8.16.3(pg-native@3.5.2) pg-protocol@1.10.3: {} @@ -13190,15 +13238,16 @@ snapshots: postgres-date: 1.0.7 postgres-interval: 1.2.0 - pg@8.16.3: + pg@8.16.3(pg-native@3.5.2): dependencies: pg-connection-string: 2.9.1 - pg-pool: 3.10.1(pg@8.16.3) + pg-pool: 3.10.1(pg@8.16.3(pg-native@3.5.2)) pg-protocol: 1.10.3 pg-types: 2.2.0 pgpass: 1.0.5 optionalDependencies: pg-cloudflare: 1.2.7 + pg-native: 3.5.2 pgpass@1.0.5: dependencies: diff --git a/src/app/dashboard/admin/@tools/export.tsx b/src/app/dashboard/admin/@tools/export.tsx index b264d2143..2711b55be 100644 --- a/src/app/dashboard/admin/@tools/export.tsx +++ b/src/app/dashboard/admin/@tools/export.tsx @@ -28,6 +28,10 @@ export default function ExportButton({ data, label }: ExportButtonProps) { const headers = Object.keys(data[0] as Record).join(",") const escapeCSV = (value: unknown) => { if (value == null) return "" + if (value instanceof Date) return value.toISOString() + if (typeof value === "string" && !isNaN(Date.parse(value))) { + return new Date(value).toISOString() + } const str = String(value) if (/[",\n]/.test(str)) { return `"${str.replace(/"/g, '""')}"` diff --git a/src/app/dashboard/admin/@users/table.tsx b/src/app/dashboard/admin/@users/table.tsx index 5adb01847..c0844a6a2 100644 --- a/src/app/dashboard/admin/@users/table.tsx +++ b/src/app/dashboard/admin/@users/table.tsx @@ -66,7 +66,7 @@ import { api } from "~/trpc/react" import ExportButton from "../@tools/export" import AddUserForm from "./form" -type DisplayColumn = Omit +type DisplayColumn = Omit export interface TableProps { data: Array @@ -167,7 +167,7 @@ const columns = (updateRole: ({ id, role }: UpdateUserRoleFunctionProps) => void id: "Date joined", header: "Date joined", cell: (cell) => {cell.getValue()}, - accessorFn: (user) => format(user.createdAt, "Pp", { locale: enAU }), + accessorFn: (user) => format(user.created_at, "Pp", { locale: enAU }), }, { id: "Membership expiry", diff --git a/src/app/dashboard/admin/layout.tsx b/src/app/dashboard/admin/layout.tsx index b2de060aa..87c29705b 100644 --- a/src/app/dashboard/admin/layout.tsx +++ b/src/app/dashboard/admin/layout.tsx @@ -1,14 +1,10 @@ -"use client" - import * as React from "react" -import type { ImperativePanelHandle } from "react-resizable-panels" -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "~/components/ui/resizable" -import { Separator } from "~/components/ui/separator" import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs" +import NotFound from "~/app/not-found" import type { PropsWithChildren } from "~/lib/types" -import { cn } from "~/lib/utils" +import { api } from "~/trpc/server" interface AdminDashLayoutProps extends PropsWithChildren { users: React.ReactNode @@ -18,7 +14,12 @@ interface AdminDashLayoutProps extends PropsWithChildren { tools: React.ReactNode } -const Layout = ({ children, ...props }: AdminDashLayoutProps) => { +const Layout = async ({ children, ...props }: AdminDashLayoutProps) => { + const user = await api.users.getCurrent.query() + if (!["admin", "committee"].includes(user?.role ?? "")) { + return + } + const sidebarItems = [ { text: "Users", icon: "group", component: props.users }, { text: "Projects", icon: "devices", component: props.projects }, diff --git a/src/app/profile/[id]/profile.tsx b/src/app/profile/[id]/profile.tsx index 4053f0bc4..84c77ca25 100644 --- a/src/app/profile/[id]/profile.tsx +++ b/src/app/profile/[id]/profile.tsx @@ -146,7 +146,7 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => { user.membership_expiry && (
Your membership will expire on{" "} - + {format(new Date(String(user.membership_expiry)), "dd MMMM yyyy")}
diff --git a/src/middleware.ts b/src/middleware.ts index d5b7acdfb..d1f311d9d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,36 +1,17 @@ -import { auth, clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server" -import { eq } from "drizzle-orm" +import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server" -import { db } from "./server/db" -import { User } from "./server/db/schema" - -const adminRoles = ["admin", "committee"] - -const isAdminPage = createRouteMatcher(["/dashboard/admin(.*)"]) const isProtectedPage = createRouteMatcher(["/dashboard(.*)", "/profile/settings(.*)"]) const isAuthPage = createRouteMatcher(["/join(.*)"]) export default clerkMiddleware(async (auth, req) => { - const session = await auth() - const clerkId = session.userId - - if (isAdminPage(req) && clerkId) { - const user = await db.query.User.findFirst({ - where: eq(User.clerk_id, clerkId), - }) - - if (!adminRoles.includes(user?.role ?? "")) { - // non-existent clerk role so we go to 404 page cleanly - await auth.protect({ - role: "lmfaooo", - }) - } - } - + // Only require authentication for protected pages if (isProtectedPage(req)) { await auth.protect() } + // Redirect authenticated users away from /join + const session = await auth() + const clerkId = session.userId if (isAuthPage(req) && clerkId) { return Response.redirect(new URL("/dashboard", req.url)) } @@ -39,11 +20,7 @@ export default clerkMiddleware(async (auth, req) => { export const config = { matcher: [ "/", - // Skip Next.js internals and all static files, unless found in search params "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", - // // Always run for API routes - // "/(api|trpc)(.*)", - // Ignore trpc routes because they are handled by the server "/api(.*)", ], } diff --git a/src/server/api/routers/admin/analytics/get-payments.ts b/src/server/api/routers/admin/analytics/get-payments.ts index ee2ce741e..496481f77 100644 --- a/src/server/api/routers/admin/analytics/get-payments.ts +++ b/src/server/api/routers/admin/analytics/get-payments.ts @@ -5,7 +5,7 @@ import { Payment } from "~/server/db/schema" export const getAllPayments = adminProcedure.query(async ({ ctx }) => { const paymentList = await ctx.db.query.Payment.findMany({ - orderBy: [desc(Payment.createdAt), desc(Payment.id)], + orderBy: [desc(Payment.created_at), desc(Payment.id)], }) return paymentList diff --git a/src/server/api/routers/admin/analytics/get-users-per-day.ts b/src/server/api/routers/admin/analytics/get-users-per-day.ts index b2fd49b3c..03e978aff 100644 --- a/src/server/api/routers/admin/analytics/get-users-per-day.ts +++ b/src/server/api/routers/admin/analytics/get-users-per-day.ts @@ -20,7 +20,7 @@ export const getUsersPerDay = adminProcedure.query(async ({ ctx }) => { count: sql`count(*)`.mapWith(Number), }) .from(User) - .where(between(User.createdAt, twoMonths, new Date())) + .where(between(User.created_at, twoMonths, new Date())) .groupBy( sql`extract(day from created_at)`, sql`extract(month from created_at)`, @@ -39,7 +39,7 @@ export const getUsersPerDay = adminProcedure.query(async ({ ctx }) => { count: sql`count(*)`.mapWith(Number), }) .from(User) - .where(and(isNotNull(User.role), between(User.createdAt, twoMonths, new Date()))) + .where(and(isNotNull(User.role), between(User.created_at, twoMonths, new Date()))) .groupBy( sql`extract(day from created_at)`, sql`extract(month from created_at)`, diff --git a/src/server/api/routers/admin/projects/get-all.ts b/src/server/api/routers/admin/projects/get-all.ts index 6ccbd0478..94324cb01 100644 --- a/src/server/api/routers/admin/projects/get-all.ts +++ b/src/server/api/routers/admin/projects/get-all.ts @@ -5,7 +5,7 @@ import { Project } from "~/server/db/schema" export const getAllProjects = adminProcedure.query(async ({ ctx }) => { const projectList = await ctx.db.query.Project.findMany({ - orderBy: [desc(Project.createdAt), desc(Project.id)], + orderBy: [desc(Project.created_at), desc(Project.id)], }) return projectList diff --git a/src/server/api/routers/admin/projects/get-projects.ts b/src/server/api/routers/admin/projects/get-projects.ts index ae2ea89c1..bc1739580 100644 --- a/src/server/api/routers/admin/projects/get-projects.ts +++ b/src/server/api/routers/admin/projects/get-projects.ts @@ -35,7 +35,7 @@ export const getProjects = adminProcedure }, where: name ? eq(Project.name, name) : undefined, - orderBy: [desc(Project.createdAt), desc(Project.id)], + orderBy: [desc(Project.created_at), desc(Project.id)], }) return projectList @@ -71,7 +71,7 @@ export const getPublicProjects = adminProcedure }, where: eq(Project.is_public, is_public), - orderBy: [desc(Project.createdAt), desc(Project.id)], + orderBy: [desc(Project.created_at), desc(Project.id)], }) return projectList diff --git a/src/server/api/routers/admin/users/get-all.ts b/src/server/api/routers/admin/users/get-all.ts index f08e09616..7f9e3bece 100644 --- a/src/server/api/routers/admin/users/get-all.ts +++ b/src/server/api/routers/admin/users/get-all.ts @@ -5,7 +5,7 @@ import { User } from "~/server/db/schema" export const getAll = adminProcedure.query(async ({ ctx }) => { const userList = await ctx.db.query.User.findMany({ - orderBy: [desc(User.createdAt), desc(User.id)], + orderBy: [desc(User.created_at), desc(User.id)], }) return userList diff --git a/src/server/api/routers/admin/users/update-email.ts b/src/server/api/routers/admin/users/update-email.ts index 3acf3cefa..c03151ad0 100644 --- a/src/server/api/routers/admin/users/update-email.ts +++ b/src/server/api/routers/admin/users/update-email.ts @@ -4,7 +4,6 @@ import { eq } from "drizzle-orm" import { z } from "zod" import { adminProcedure } from "~/server/api/trpc" -import { db } from "~/server/db" import { User } from "~/server/db/schema" export const updateEmail = adminProcedure diff --git a/src/server/api/routers/projects/get-application-open.ts b/src/server/api/routers/projects/get-application-open.ts index 13786aa61..d11855904 100644 --- a/src/server/api/routers/projects/get-application-open.ts +++ b/src/server/api/routers/projects/get-application-open.ts @@ -7,7 +7,7 @@ import { Project } from "~/server/db/schema" export const getApplicationOpen = protectedRatedProcedure(Ratelimit.fixedWindow(60, "30s")).query(async ({ ctx }) => { const projectList = await ctx.db.query.Project.findMany({ where: (project, { eq }) => eq(project.is_application_open, true), - orderBy: [desc(Project.createdAt), desc(Project.id)], + orderBy: [desc(Project.created_at), desc(Project.id)], }) return projectList diff --git a/src/server/api/routers/projects/get-projects.ts b/src/server/api/routers/projects/get-projects.ts index 4687a6a22..06690923e 100644 --- a/src/server/api/routers/projects/get-projects.ts +++ b/src/server/api/routers/projects/get-projects.ts @@ -1,6 +1,6 @@ import { TRPCError } from "@trpc/server" import { Ratelimit } from "@upstash/ratelimit" -import { and, arrayContains, eq } from "drizzle-orm" +import { and, arrayContains, eq, sql } from "drizzle-orm" import { z } from "zod" import { protectedRatedProcedure, publicRatedProcedure } from "~/server/api/trpc" @@ -41,14 +41,14 @@ export const getProjectByName = publicRatedProcedure(Ratelimit.fixedWindow(60, " export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60, "30s")) .input(z.object({ user: z.string(), isPublic: z.boolean().optional() })) .query(async ({ input, ctx }) => { - const conditions = [] + let whereClause = sql`TRUE` if (input.user) { - conditions.push(arrayContains(Project.members, [input.user])) + whereClause = sql`${whereClause} AND ${Project.members} ILIKE '%${input.user}%'` } if (typeof input.isPublic === "boolean") { - conditions.push(eq(Project.is_public, input.isPublic)) + whereClause = sql`${whereClause} AND ${Project.is_public} = ${input.isPublic}` } const projectData = await ctx.db.query.Project.findMany({ @@ -70,7 +70,7 @@ export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60 application_url: true, is_public: true, }, - where: conditions.length > 0 ? and(...conditions) : undefined, + where: whereClause, }) return projectData diff --git a/src/server/api/routers/projects/get-public.ts b/src/server/api/routers/projects/get-public.ts index d1748e159..7175cc082 100644 --- a/src/server/api/routers/projects/get-public.ts +++ b/src/server/api/routers/projects/get-public.ts @@ -14,7 +14,7 @@ export const getPublic = publicRatedProcedure(Ratelimit.fixedWindow(60, "30s")). }, where: (project, { eq }) => eq(project.is_public, true), - orderBy: [desc(Project.createdAt), desc(Project.id)], + orderBy: [desc(Project.created_at), desc(Project.id)], }) return projectList diff --git a/src/server/db/drizzle.ts b/src/server/db/drizzle.ts new file mode 100644 index 000000000..800c7ad14 --- /dev/null +++ b/src/server/db/drizzle.ts @@ -0,0 +1,9 @@ +// src/server/db/drizzle.ts +import { drizzle } from "drizzle-orm/node-postgres" +import { Pool } from "pg" + +const pool = new Pool({ + connectionString: process.env.SUPABASE_DB_URL, +}) + +export const db = drizzle(pool) diff --git a/src/server/db/index.ts b/src/server/db/index.ts index 2a9dd6d97..d3646f385 100644 --- a/src/server/db/index.ts +++ b/src/server/db/index.ts @@ -1,8 +1,10 @@ -import { drizzle } from "drizzle-orm/xata-http" +import { drizzle } from "drizzle-orm/node-postgres" +import { Pool } from "pg" import * as schema from "./schema" -import { getXataClient } from "./xata" -const xata = getXataClient() +const pool = new Pool({ + connectionString: process.env.SUPABASE_DB_URL, +}) -export const db = drizzle(xata, { schema }) +export const db = drizzle(pool, { schema }) diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index b4667f96d..73b096e40 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -37,10 +37,10 @@ export const User = pgTable( role: roleEnum("role"), square_customer_id: varchar("square_customer_id", { length: 32 }).unique().notNull(), membership_expiry: timestamp("membership_expiry"), - createdAt: timestamp("created_at") + created_at: timestamp("created_at") .$default(() => new Date()) .notNull(), - updatedAt: timestamp("updated_at").$onUpdate(() => new Date()), + updated_at: timestamp("updated_at").$onUpdate(() => new Date()), // `reminder_pending` id a Flag used to send reminders for membership renewal bc Resend can only send 100 emails per day reminder_pending: boolean("reminder_pending").default(false).notNull(), }, @@ -62,10 +62,10 @@ export const Payment = pgTable( label: varchar("label", { length: 256 }).notNull(), event_id: varchar("event_id", { length: 32 }), // TODO: link when events are implemented - createdAt: timestamp("created_at") + created_at: timestamp("created_at") .$default(() => new Date()) .notNull(), - updatedAt: timestamp("updated_at").$onUpdate(() => new Date()), + updated_at: timestamp("updated_at").$onUpdate(() => new Date()), }, (payment) => [index("user_id_idx").on(payment.user_id), index("event_id_idx").on(payment.event_id)], ) @@ -105,10 +105,10 @@ export const Project = pgTable( is_application_open: boolean("is_application_open").default(false).notNull(), // whether the project is receiving applications or not application_url: varchar("application_url", { length: 256 }), // link to the application form is_public: boolean("is_public").default(false).notNull(), // means they are visible on projects page - createdAt: timestamp("created_at") + created_at: timestamp("created_at") .$default(() => new Date()) .notNull(), - updatedAt: timestamp("updated_at").$onUpdate(() => new Date()), + updated_at: timestamp("updated_at").$onUpdate(() => new Date()), }, (project) => [index("name_idx").on(project.name)], ) From e79b6f335351662b12f129cf58869eb37d39e226 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Fri, 6 Feb 2026 21:06:36 +0800 Subject: [PATCH 3/7] some fix and env var change --- .env.example | 9 ++-- README.md | 17 +++++--- src/app/dashboard/(root)/page.tsx | 2 +- src/app/dashboard/admin/@analytics/count.tsx | 7 ++-- src/app/dashboard/admin/@tools/tool-card.tsx | 42 +++++++++++++++++++ .../dashboard/admin/@tools/update-email.tsx | 32 +++----------- src/app/profile/[id]/profile.tsx | 2 +- src/components/payment/online/index.tsx | 4 +- src/env.js | 6 +++ .../routers/admin/analytics/get-payments.ts | 2 +- .../admin/analytics/get-project-count.ts | 13 ++++++ .../routers/admin/analytics/get-user-count.ts | 4 +- .../api/routers/admin/analytics/index.ts | 2 + .../routers/admin/projects/get-projects.ts | 2 +- .../api/routers/projects/get-projects.ts | 3 +- src/server/db/xata.ts | 33 --------------- 16 files changed, 97 insertions(+), 83 deletions(-) create mode 100644 src/app/dashboard/admin/@tools/tool-card.tsx create mode 100644 src/server/api/routers/admin/analytics/get-project-count.ts delete mode 100644 src/server/db/xata.ts diff --git a/.env.example b/.env.example index 72d084e1f..c56055c86 100644 --- a/.env.example +++ b/.env.example @@ -39,8 +39,9 @@ NEXT_PUBLIC_MAPBOX_API=mapbox-key # Cron (openssl rand -hex 32) CRON_SECRET=some-random-secret +# Resend +RESEND_API_KEY= + # Supabase -SUPABASE_URL= -SUPABASE_PUBLISHABLE_DEFAULT_KEY= -SUPABASE_ANON_KEY= -SUPABASE_SERVICE_ROLE_KEY= \ No newline at end of file +SUPABASE_DB_URL= +SUPABASE_ANON_KEY= \ No newline at end of file diff --git a/README.md b/README.md index 36842d214..5c1d41fd3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ Bootstrapped using the create-t3-app using: - [Drizzle ORM](https://orm.drizzle.team/docs/overview) - [Clerk authentication](https://clerk.com/docs) - [Vercel](https://vercel.com) -- [Xata](https://app.xata.io) +- [Supabase](https://supabase.com/) +- Deprecated: [Xata lite](https://app.xata.io) We use [pnpm](https://pnpm.io/) as the package manager for this project. @@ -32,22 +33,26 @@ pnpm i -E pnpm upgrade -L # Database migration -pnpm drizzle-kit pull -pnpm drizzle-kit generate -pnpm drizzle-kit migrate +npx drizzle-kit generate +npx drizzle-kit push ``` +### Notes + +- UTC time +- When you make changes to the DB, please back up the data first. + ## Planned Features ### Project Integration Enhancements -- Profiles will soon showcase the projects that users have contributed to, highlighting their work and collaborations. +- Profiles showcase the projects that users have contributed to, highlighting their work and collaborations. - Applications to join our seasonal Summer and Winter projects will be directly accessible through the site for our members. - Committee members and Administrators will gain the ability to upload project specifics directly to the platform. ### User Profile Enhancements -- The platform will support multiple role assignments for users, offering tailored experiences based on their involvement and interests. +- The platform supports multiple role assignments for users, offering tailored experiences based on their involvement and interests. - Enhanced profile customization options will be introduced. ### Event Participation and Management diff --git a/src/app/dashboard/(root)/page.tsx b/src/app/dashboard/(root)/page.tsx index 358404c21..c3df3556e 100644 --- a/src/app/dashboard/(root)/page.tsx +++ b/src/app/dashboard/(root)/page.tsx @@ -13,7 +13,7 @@ export default async function Dashboard() { Here you can see our upcoming projects and the projects you have participated in. You can apply for new projects here if we link the application form.

-
+
diff --git a/src/app/dashboard/admin/@analytics/count.tsx b/src/app/dashboard/admin/@analytics/count.tsx index 1c40161bf..3a9f7e002 100644 --- a/src/app/dashboard/admin/@analytics/count.tsx +++ b/src/app/dashboard/admin/@analytics/count.tsx @@ -1,5 +1,3 @@ -import Link from "next/link" - import { api } from "~/trpc/server" interface CardProps { @@ -25,13 +23,14 @@ const Card = (props: CardProps) => { const Count = async () => { const count = await api.admin.analytics.getUserCount.query() + const projectCount = await api.admin.analytics.getProjectCount.query() return ( <> - - + + {/* */} ) } diff --git a/src/app/dashboard/admin/@tools/tool-card.tsx b/src/app/dashboard/admin/@tools/tool-card.tsx new file mode 100644 index 000000000..361bb762b --- /dev/null +++ b/src/app/dashboard/admin/@tools/tool-card.tsx @@ -0,0 +1,42 @@ +import * as React from "react" + +import { Button } from "~/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "~/components/ui/dialog" + +type ToolCardProps = { + title: string + children: React.ReactNode +} + +function ToolCard({ title, children }: ToolCardProps) { + return ( +
+
+

{title}

+
+
+ + + + + + + {title} + + + {children} + + +
+
+ ) +} + +export default ToolCard diff --git a/src/app/dashboard/admin/@tools/update-email.tsx b/src/app/dashboard/admin/@tools/update-email.tsx index 02e91785b..e3d709e48 100644 --- a/src/app/dashboard/admin/@tools/update-email.tsx +++ b/src/app/dashboard/admin/@tools/update-email.tsx @@ -5,20 +5,14 @@ import { useForm } from "react-hook-form" import { z } from "zod" import { Button } from "~/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "~/components/ui/dialog" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "~/components/ui/form" import { Input } from "~/components/ui/input" import { toast } from "~/components/ui/use-toast" import { api } from "~/trpc/react" +import ToolCard from "./tool-card" + const formSchema = z.object({ userId: z.string(), oldEmail: z.string().email(), @@ -35,25 +29,9 @@ const defaultValues: FormSchema = { const UpdateEmail = () => { return ( -
-
-

Update Email

-
-
- - - - - - - Update user email - - - - - -
-
+ + + ) } diff --git a/src/app/profile/[id]/profile.tsx b/src/app/profile/[id]/profile.tsx index 84c77ca25..7ebe3b0b4 100644 --- a/src/app/profile/[id]/profile.tsx +++ b/src/app/profile/[id]/profile.tsx @@ -146,7 +146,7 @@ const ProfilePage = ({ id, currentUser }: ProfilePageProps) => { user.membership_expiry && (
Your membership will expire on{" "} - + {format(new Date(String(user.membership_expiry)), "dd MMMM yyyy")}
diff --git a/src/components/payment/online/index.tsx b/src/components/payment/online/index.tsx index e5236644a..c372393ea 100644 --- a/src/components/payment/online/index.tsx +++ b/src/components/payment/online/index.tsx @@ -16,7 +16,7 @@ import { type RouterOutputs } from "~/trpc/shared" const Card = dynamic(() => import("./card"), { ssr: false, - loading: () => , + loading: () => , }) const GooglePay = dynamic(() => import("./google-pay"), { ssr: false, @@ -206,7 +206,7 @@ const OnlinePaymentForm = ({ ) : ( - + ) } diff --git a/src/env.js b/src/env.js index 11507a5a1..fb77e45af 100644 --- a/src/env.js +++ b/src/env.js @@ -14,6 +14,9 @@ export const env = createEnv({ UPSTASH_REDIS_REST_URL: z.string().url(), UPSTASH_REDIS_REST_TOKEN: z.string(), CRON_SECRET: z.string(), + RESEND_API_KEY: z.string(), + SUPABASE_DB_URL: z.string().url(), + SUPABASE_ANON_KEY: z.string(), }, /** @@ -53,6 +56,9 @@ export const env = createEnv({ NEXT_PUBLIC_CLERK_SIGN_IN_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL, NEXT_PUBLIC_CLERK_SIGN_UP_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL, CRON_SECRET: process.env.CRON_SECRET, + RESEND_API_KEY: process.env.RESEND_API_KEY, + SUPABASE_DB_URL: process.env.SUPABASE_DB_URL, + SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY, }, /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially diff --git a/src/server/api/routers/admin/analytics/get-payments.ts b/src/server/api/routers/admin/analytics/get-payments.ts index 496481f77..8a13cb45d 100644 --- a/src/server/api/routers/admin/analytics/get-payments.ts +++ b/src/server/api/routers/admin/analytics/get-payments.ts @@ -1,4 +1,4 @@ -import { desc } from "drizzle-orm" +import { desc, sql } from "drizzle-orm" import { adminProcedure } from "~/server/api/trpc" import { Payment } from "~/server/db/schema" diff --git a/src/server/api/routers/admin/analytics/get-project-count.ts b/src/server/api/routers/admin/analytics/get-project-count.ts new file mode 100644 index 000000000..e6f9c1b80 --- /dev/null +++ b/src/server/api/routers/admin/analytics/get-project-count.ts @@ -0,0 +1,13 @@ +import { eq, sql } from "drizzle-orm" + +import { adminProcedure } from "~/server/api/trpc" +import { Project } from "~/server/db/schema" + +export const getProjectCount = adminProcedure.query(async ({ ctx }) => { + const [result] = await ctx.db + .select({ count: sql`count(*)`.mapWith(Number) }) + .from(Project) + .where(eq(Project.is_public, true)) + + return { publicProjects: result?.count ?? 0 } +}) diff --git a/src/server/api/routers/admin/analytics/get-user-count.ts b/src/server/api/routers/admin/analytics/get-user-count.ts index fc1ff31b3..7decc31af 100644 --- a/src/server/api/routers/admin/analytics/get-user-count.ts +++ b/src/server/api/routers/admin/analytics/get-user-count.ts @@ -1,4 +1,4 @@ -import { isNotNull, sql } from "drizzle-orm" +import { eq, sql } from "drizzle-orm" import { adminProcedure } from "~/server/api/trpc" import { User } from "~/server/db/schema" @@ -9,7 +9,7 @@ export const getUserCount = adminProcedure.query(async ({ ctx }) => { ctx.db .select({ count: sql`count(*)`.mapWith(Number) }) .from(User) - .where(isNotNull(User.role)), + .where(eq(User.role, "member")), ]) return { diff --git a/src/server/api/routers/admin/analytics/index.ts b/src/server/api/routers/admin/analytics/index.ts index 75d2fd0f3..726604003 100644 --- a/src/server/api/routers/admin/analytics/index.ts +++ b/src/server/api/routers/admin/analytics/index.ts @@ -2,6 +2,7 @@ import { createTRPCRouter } from "~/server/api/trpc" import { getGenderStatistics } from "./get-gender-statistics" import { getAllPayments } from "./get-payments" +import { getProjectCount } from "./get-project-count" import { getUserCount } from "./get-user-count" import { getUsersPerDay } from "./get-users-per-day" @@ -10,4 +11,5 @@ export const analyticsAdminRouter = createTRPCRouter({ getUsersPerDay, getGenderStatistics, getAllPayments, + getProjectCount, }) diff --git a/src/server/api/routers/admin/projects/get-projects.ts b/src/server/api/routers/admin/projects/get-projects.ts index bc1739580..12138ba92 100644 --- a/src/server/api/routers/admin/projects/get-projects.ts +++ b/src/server/api/routers/admin/projects/get-projects.ts @@ -1,5 +1,5 @@ import { desc } from "drizzle-orm" -import { and, eq } from "drizzle-orm" +import { eq } from "drizzle-orm" import { z } from "zod" import { adminProcedure } from "~/server/api/trpc" diff --git a/src/server/api/routers/projects/get-projects.ts b/src/server/api/routers/projects/get-projects.ts index 06690923e..118cf5d56 100644 --- a/src/server/api/routers/projects/get-projects.ts +++ b/src/server/api/routers/projects/get-projects.ts @@ -44,7 +44,8 @@ export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60 let whereClause = sql`TRUE` if (input.user) { - whereClause = sql`${whereClause} AND ${Project.members} ILIKE '%${input.user}%'` + const searchPattern = `%${input.user}%` + whereClause = sql`${whereClause} AND ${Project.members} ILIKE ${searchPattern}` } if (typeof input.isPublic === "boolean") { diff --git a/src/server/db/xata.ts b/src/server/db/xata.ts deleted file mode 100644 index 50f317f61..000000000 --- a/src/server/db/xata.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { buildClient } from "@xata.io/client" -import type { BaseClientOptions, SchemaInference } from "@xata.io/client" - -import { env } from "~/env" - -const tables = [] as const - -export type SchemaTables = typeof tables -export type InferredTypes = SchemaInference - -// eslint-disable-next-line -export type DatabaseSchema = {} - -const DatabaseClient = buildClient() - -const defaultOptions = { - databaseURL: env.XATA_DATABASE_URL, -} - -export class XataClient extends DatabaseClient { - constructor(options?: BaseClientOptions) { - super({ ...defaultOptions, ...options }, tables) - } -} - -let instance: XataClient | undefined = undefined - -export const getXataClient = () => { - if (instance) return instance - - instance = new XataClient() - return instance -} From e178a00593e57b83e2914324d8c21eb5cfcbcae0 Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Fri, 6 Feb 2026 23:20:39 +0800 Subject: [PATCH 4/7] fix db type issue --- src/app/dashboard/admin/@tools/export.tsx | 32 +++++++++++++------ src/app/dashboard/admin/@tools/page.tsx | 3 -- .../dashboard/admin/@tools/update-email.tsx | 4 ++- .../api/routers/admin/users/update-email.ts | 17 ++++++++-- .../api/routers/projects/get-projects.ts | 9 +++--- src/server/api/routers/users/update-email.ts | 5 +++ src/server/db/drizzle.ts | 9 ------ 7 files changed, 50 insertions(+), 29 deletions(-) delete mode 100644 src/server/db/drizzle.ts diff --git a/src/app/dashboard/admin/@tools/export.tsx b/src/app/dashboard/admin/@tools/export.tsx index 2711b55be..2fc42097f 100644 --- a/src/app/dashboard/admin/@tools/export.tsx +++ b/src/app/dashboard/admin/@tools/export.tsx @@ -25,21 +25,35 @@ export default function ExportButton({ data, label }: ExportButtonProps) { return } - const headers = Object.keys(data[0] as Record).join(",") - const escapeCSV = (value: unknown) => { + const headers = Object.keys(data[0] as Record) + const escapeCSV = (value: unknown): string => { if (value == null) return "" - if (value instanceof Date) return value.toISOString() - if (typeof value === "string" && !isNaN(Date.parse(value))) { - return new Date(value).toISOString() + + let str: string + if (typeof value === "object") { + if (value instanceof Date) { + str = value.toISOString() + } else { + // Stringify JSON objects/arrays + str = JSON.stringify(value) + } + } else { + str = String(value) } - const str = String(value) - if (/[",\n]/.test(str)) { + + // Escape CSV: wrap in quotes if contains comma, quote, or newline + // and escape internal quotes by doubling them + if (/[",\n\r]/.test(str)) { return `"${str.replace(/"/g, '""')}"` } return str } - const rows = data.map((row) => Object.values(row).map(escapeCSV).join(",")).join("\n") - const csv = headers + "\n" + rows + + const rows = data + .map((row) => headers.map((key) => escapeCSV((row as Record)[key])).join(",")) + .join("\n") + + const csv = headers.join(",") + "\n" + rows const blob = new Blob([csv], { type: "text/csv" }) const url = window.URL.createObjectURL(blob) diff --git a/src/app/dashboard/admin/@tools/page.tsx b/src/app/dashboard/admin/@tools/page.tsx index 38fc1e1ac..9b839e930 100644 --- a/src/app/dashboard/admin/@tools/page.tsx +++ b/src/app/dashboard/admin/@tools/page.tsx @@ -12,9 +12,6 @@ export default async function AdminUserTable() {

Tools

-
- -
diff --git a/src/app/dashboard/admin/@tools/update-email.tsx b/src/app/dashboard/admin/@tools/update-email.tsx index e3d709e48..5f6366f08 100644 --- a/src/app/dashboard/admin/@tools/update-email.tsx +++ b/src/app/dashboard/admin/@tools/update-email.tsx @@ -27,9 +27,11 @@ const defaultValues: FormSchema = { newEmail: "", } +// deprecated, only for admin to update user email +// users should use the updateEmail procedure in users router to update their own email const UpdateEmail = () => { return ( - + ) diff --git a/src/server/api/routers/admin/users/update-email.ts b/src/server/api/routers/admin/users/update-email.ts index c03151ad0..8b18111f1 100644 --- a/src/server/api/routers/admin/users/update-email.ts +++ b/src/server/api/routers/admin/users/update-email.ts @@ -1,11 +1,13 @@ import { clerkClient } from "@clerk/nextjs/server" import { TRPCError } from "@trpc/server" -import { eq } from "drizzle-orm" +import { eq, sql } from "drizzle-orm" import { z } from "zod" import { adminProcedure } from "~/server/api/trpc" -import { User } from "~/server/db/schema" +import { Project, User } from "~/server/db/schema" +// deprecated, only for admin to update user email +// users should use the updateEmail procedure in users router to update their own email export const updateEmail = adminProcedure .input(z.object({ userId: z.string(), oldEmail: z.string().email(), newEmail: z.string().email() })) .mutation(async ({ ctx, input }) => { @@ -13,18 +15,23 @@ export const updateEmail = adminProcedure const user_data = await ctx.db.query.User.findFirst({ where: eq(User.email, input.oldEmail), }) + if (!user_data) { throw new TRPCError({ code: "NOT_FOUND", message: `User with email: ${input.oldEmail} does not exist` }) } + const user_email_data = await ctx.db.query.User.findFirst({ where: eq(User.email, input.newEmail), }) + if (user_email_data) { throw new TRPCError({ code: "FORBIDDEN", message: `User with email: ${input.newEmail} already exist` }) } + const user = await ctx.db.query.User.findFirst({ where: eq(User.id, input.userId), }) + if (!user) { throw new TRPCError({ code: "NOT_FOUND", message: `User with id: ${input.userId} does not exist` }) } @@ -46,6 +53,12 @@ export const updateEmail = adminProcedure console.error(err) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Failed to update email" }) } + await ctx.db + .update(Project) + .set({ + members: sql`array_replace(${Project.members}, ${input.oldEmail}, ${input.newEmail})`, + }) + .where(sql`${input.oldEmail} = ANY(${Project.members})`) return user }) diff --git a/src/server/api/routers/projects/get-projects.ts b/src/server/api/routers/projects/get-projects.ts index 118cf5d56..6bc49ba81 100644 --- a/src/server/api/routers/projects/get-projects.ts +++ b/src/server/api/routers/projects/get-projects.ts @@ -41,15 +41,14 @@ export const getProjectByName = publicRatedProcedure(Ratelimit.fixedWindow(60, " export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60, "30s")) .input(z.object({ user: z.string(), isPublic: z.boolean().optional() })) .query(async ({ input, ctx }) => { - let whereClause = sql`TRUE` + const conditions = [] if (input.user) { - const searchPattern = `%${input.user}%` - whereClause = sql`${whereClause} AND ${Project.members} ILIKE ${searchPattern}` + conditions.push(arrayContains(Project.members, [input.user])) } if (typeof input.isPublic === "boolean") { - whereClause = sql`${whereClause} AND ${Project.is_public} = ${input.isPublic}` + conditions.push(eq(Project.is_public, input.isPublic)) } const projectData = await ctx.db.query.Project.findMany({ @@ -71,7 +70,7 @@ export const getProjectByUser = protectedRatedProcedure(Ratelimit.fixedWindow(60 application_url: true, is_public: true, }, - where: whereClause, + where: conditions.length > 0 ? and(...conditions) : undefined, }) return projectData diff --git a/src/server/api/routers/users/update-email.ts b/src/server/api/routers/users/update-email.ts index 18a7c84ef..98d7f6e92 100644 --- a/src/server/api/routers/users/update-email.ts +++ b/src/server/api/routers/users/update-email.ts @@ -20,18 +20,23 @@ export const updateEmail = protectedRatedProcedure(Ratelimit.fixedWindow(4, "30s const user_data = await ctx.db.query.User.findFirst({ where: eq(User.email, input.oldEmail), }) + if (!user_data) { throw new TRPCError({ code: "NOT_FOUND", message: `User with email: ${input.oldEmail} does not exist` }) } + const user_email_data = await ctx.db.query.User.findFirst({ where: eq(User.email, input.newEmail), }) + if (user_email_data) { throw new TRPCError({ code: "FORBIDDEN", message: `User with email: ${input.newEmail} already exist` }) } + const user = await ctx.db.query.User.findFirst({ where: eq(User.id, input.userId), }) + if (!user) { throw new TRPCError({ code: "NOT_FOUND", message: `User with id: ${input.userId} does not exist` }) } diff --git a/src/server/db/drizzle.ts b/src/server/db/drizzle.ts deleted file mode 100644 index 800c7ad14..000000000 --- a/src/server/db/drizzle.ts +++ /dev/null @@ -1,9 +0,0 @@ -// src/server/db/drizzle.ts -import { drizzle } from "drizzle-orm/node-postgres" -import { Pool } from "pg" - -const pool = new Pool({ - connectionString: process.env.SUPABASE_DB_URL, -}) - -export const db = drizzle(pool) From c05ced849d330a3d87d17758ade9ca31db06df7d Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Fri, 6 Feb 2026 23:29:08 +0800 Subject: [PATCH 5/7] small edits --- src/middleware.ts | 4 ++++ src/server/api/routers/projects/get-projects.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/middleware.ts b/src/middleware.ts index d1f311d9d..9a12d65e4 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -20,7 +20,11 @@ export default clerkMiddleware(async (auth, req) => { export const config = { matcher: [ "/", + // Skip Next.js internals and all static files, unless found in search params "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", + // // Always run for API routes + // "/(api|trpc)(.*)", + // Ignore trpc routes because they are handled by the server "/api(.*)", ], } diff --git a/src/server/api/routers/projects/get-projects.ts b/src/server/api/routers/projects/get-projects.ts index 6bc49ba81..4687a6a22 100644 --- a/src/server/api/routers/projects/get-projects.ts +++ b/src/server/api/routers/projects/get-projects.ts @@ -1,6 +1,6 @@ import { TRPCError } from "@trpc/server" import { Ratelimit } from "@upstash/ratelimit" -import { and, arrayContains, eq, sql } from "drizzle-orm" +import { and, arrayContains, eq } from "drizzle-orm" import { z } from "zod" import { protectedRatedProcedure, publicRatedProcedure } from "~/server/api/trpc" From b53474b02975125f6e559065d9bb83fa21ccf20b Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Fri, 6 Feb 2026 23:43:37 +0800 Subject: [PATCH 6/7] fix project member preview api permission --- src/app/projects/(default)/db-project.tsx | 2 +- src/server/api/routers/admin/users/index.ts | 2 -- .../api/routers/{admin => }/users/get-name-by-email.ts | 5 +++-- src/server/api/routers/users/index.ts | 2 ++ 4 files changed, 6 insertions(+), 5 deletions(-) rename src/server/api/routers/{admin => }/users/get-name-by-email.ts (67%) diff --git a/src/app/projects/(default)/db-project.tsx b/src/app/projects/(default)/db-project.tsx index 8a19e4994..4303aa12e 100644 --- a/src/app/projects/(default)/db-project.tsx +++ b/src/app/projects/(default)/db-project.tsx @@ -53,7 +53,7 @@ export const Impact = ({ impact, ...props }: { impact?: string[]; className?: st ) export default function DBProject({ data }: DBProjectProps) { - const { data: users } = api.admin.users.getNamesByEmails.useQuery({ emails: data.members ? data.members : [] }) + const { data: users } = api.users.getNamesByEmails.useQuery({ emails: data.members ? data.members : [] }) let icon = "devices" // default "devices" if (data.type === "Mobile application") { diff --git a/src/server/api/routers/admin/users/index.ts b/src/server/api/routers/admin/users/index.ts index cd3f7b885..1a83a3eef 100644 --- a/src/server/api/routers/admin/users/index.ts +++ b/src/server/api/routers/admin/users/index.ts @@ -2,7 +2,6 @@ import { createTRPCRouter } from "~/server/api/trpc" import { createManual } from "./create-manual" import { getAll } from "./get-all" -import { getNamesByEmails } from "./get-name-by-email" import { updateEmail } from "./update-email" import { updateRole } from "./update-role" @@ -11,5 +10,4 @@ export const usersAdminRouter = createTRPCRouter({ getAll, updateRole, updateEmail, - getNamesByEmails, }) diff --git a/src/server/api/routers/admin/users/get-name-by-email.ts b/src/server/api/routers/users/get-name-by-email.ts similarity index 67% rename from src/server/api/routers/admin/users/get-name-by-email.ts rename to src/server/api/routers/users/get-name-by-email.ts index b9a469f55..a4bc4032d 100644 --- a/src/server/api/routers/admin/users/get-name-by-email.ts +++ b/src/server/api/routers/users/get-name-by-email.ts @@ -1,10 +1,11 @@ +import { Ratelimit } from "@upstash/ratelimit" import { inArray } from "drizzle-orm" import { z } from "zod" -import { adminProcedure } from "~/server/api/trpc" +import { publicRatedProcedure } from "~/server/api/trpc" import { User } from "~/server/db/schema" -export const getNamesByEmails = adminProcedure +export const getNamesByEmails = publicRatedProcedure(Ratelimit.fixedWindow(4, "30s")) .input(z.object({ emails: z.array(z.string().email()) })) .query(async ({ ctx, input }) => { const users = await ctx.db.query.User.findMany({ diff --git a/src/server/api/routers/users/index.ts b/src/server/api/routers/users/index.ts index c52fb16ff..1bc9527be 100644 --- a/src/server/api/routers/users/index.ts +++ b/src/server/api/routers/users/index.ts @@ -3,6 +3,7 @@ import { createTRPCRouter } from "~/server/api/trpc" import { create } from "./create" import { get } from "./get" import { getCurrent } from "./get-current" +import { getNamesByEmails } from "./get-name-by-email" import { update } from "./update" import { updateEmail } from "./update-email" @@ -12,4 +13,5 @@ export const usersRouter = createTRPCRouter({ get, update, updateEmail, + getNamesByEmails, }) From 2c8251d3019bcb9f4b61454a644675a52deafdab Mon Sep 17 00:00:00 2001 From: ErikaKK <491649804@qq.com> Date: Fri, 6 Feb 2026 23:53:28 +0800 Subject: [PATCH 7/7] edit docs --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5c1d41fd3..fec8f684b 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,9 @@ pnpm i -E pnpm upgrade -L # Database migration -npx drizzle-kit generate -npx drizzle-kit push +pnpm drizzle-kit pull +pnpm drizzle-kit generate +pnpm drizzle-kit push ``` ### Notes