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}
+
+
+
+
+
+ )
+}
+
+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
-
-
-
-
-
+
+
+
)
}
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
-}