diff --git a/.env.example b/.env.example index 1fefa2a95..c56055c86 100644 --- a/.env.example +++ b/.env.example @@ -37,4 +37,11 @@ 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 + +# Resend +RESEND_API_KEY= + +# Supabase +SUPABASE_DB_URL= +SUPABASE_ANON_KEY= \ No newline at end of file diff --git a/README.md b/README.md index 36842d214..fec8f684b 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. @@ -34,20 +35,25 @@ pnpm upgrade -L # Database migration pnpm drizzle-kit pull pnpm drizzle-kit generate -pnpm drizzle-kit migrate +pnpm 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/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/(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/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/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..5f6366f08 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(), @@ -33,27 +27,13 @@ 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 ( -
-
-

Update Email

-
-
- - - - - - - Update user email - - - - - -
-
+ + + ) } 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 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/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/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/middleware.ts b/src/middleware.ts index d5b7acdfb..9a12d65e4 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)) } diff --git a/src/server/api/routers/admin/analytics/get-payments.ts b/src/server/api/routers/admin/analytics/get-payments.ts index ee2ce741e..8a13cb45d 100644 --- a/src/server/api/routers/admin/analytics/get-payments.ts +++ b/src/server/api/routers/admin/analytics/get-payments.ts @@ -1,11 +1,11 @@ -import { desc } from "drizzle-orm" +import { desc, sql } from "drizzle-orm" import { adminProcedure } from "~/server/api/trpc" 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-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/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/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-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..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" @@ -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/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/update-email.ts b/src/server/api/routers/admin/users/update-email.ts index 3acf3cefa..8b18111f1 100644 --- a/src/server/api/routers/admin/users/update-email.ts +++ b/src/server/api/routers/admin/users/update-email.ts @@ -1,12 +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 { db } from "~/server/db" -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 }) => { @@ -14,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` }) } @@ -47,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-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-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/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, }) 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/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)], ) 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 -}