and spliited
diff --git a/src/app/_components/expense/paid-by.tsx b/src/app/_components/expense/paid-by.tsx
index 15950d4..f31acce 100644
--- a/src/app/_components/expense/paid-by.tsx
+++ b/src/app/_components/expense/paid-by.tsx
@@ -8,20 +8,16 @@ import {
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { DialogClose } from "@radix-ui/react-dialog";
-import type { PublicUser } from "next-auth";
import Avatar from "@/components/avatar";
import { Icons } from "@/components/icons";
import { cn } from "@/lib/utils";
import PaidByCustomSplit from "./paid-by-custom-split";
import { useExpenseStore } from "@/lib/store/expense-store";
+import type { User } from "@/server/db/schema";
/* saved result is an array of objects with user and amount */
-export default function PaidByInput({
- sessionUser,
-}: {
- sessionUser: PublicUser;
-}) {
+export default function PaidByInput({ sessionUser }: { sessionUser: User }) {
const amount = useExpenseStore((state) => state.amount);
const payments = useExpenseStore((state) => state.payments);
const setPayments = useExpenseStore((state) => state.setPayments);
diff --git a/src/app/_components/install-prompt.tsx b/src/app/_components/install-prompt.tsx
deleted file mode 100644
index 64ee3de..0000000
--- a/src/app/_components/install-prompt.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-// "use client";
-// import React, { useEffect, useState } from "react";
-// export function usePwaInstallStatus() {
-// const [isStandalone, setIsStandalone] = useState(false);
-// const [isIOS, setIsIOS] = useState(false);
-// const [isAndroid, setIsAndroid] = useState(false);
-
-// useEffect(() => {
-// const userAgent =
-// navigator.userAgent || navigator.vendor || (window as any).opera;
-
-// // iOS detection
-// const iOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
-// setIsIOS(iOS);
-
-// // Android detection
-// const android = /android/i.test(userAgent);
-// setIsAndroid(android);
-
-// // PWA "standalone" mode detection (works for both iOS & Android)
-// const isInStandaloneMode =
-// window.matchMedia("(display-mode: standalone)").matches ||
-// (navigator as any).standalone === true; // for iOS Safari
-
-// setIsStandalone(isInStandaloneMode);
-// }, []);
-
-// return { isIOS, isAndroid, isStandalone };
-// }
-
-// function InstallPrompt() {
-// const { isIOS, isAndroid, isStandalone } = usePwaInstallStatus();
-// const [deferredPrompt, setDeferredPrompt] = useState
(null);
-
-// useEffect(() => {
-// const handler = (e: any) => {
-// e.preventDefault();
-// setDeferredPrompt(e);
-// };
-
-// window.addEventListener("beforeinstallprompt", handler);
-
-// return () => {
-// window.removeEventListener("beforeinstallprompt", handler);
-// };
-// }, []);
-
-// const handleInstallClick = () => {
-// console.log("install clicked");
-
-// if (deferredPrompt) {
-// deferredPrompt.prompt();
-// deferredPrompt.userChoice.then((choiceResult: any) => {
-// if (choiceResult.outcome === "accepted") {
-// console.log("User accepted the A2HS prompt");
-// } else {
-// console.log("User dismissed the A2HS prompt");
-// }
-// setDeferredPrompt(null);
-// });
-// }
-// };
-// if (isStandalone) {
-// return null; // Already running as a PWA, no need to show install prompt
-// }
-
-// return (
-//
-// {isIOS &&
Tap "Share" and "Add to Home Screen" to install the app.
}
-// {isAndroid &&
}
-//
-// );
-// }
-
-// export default function PWAWrapper({
-// children,
-// }: {
-// children: React.ReactNode;
-// }) {
-// return (
-// <>
-//
-// {children}
-// >
-// );
-// }
diff --git a/src/app/_components/user-dropdown.tsx b/src/app/_components/user-dropdown.tsx
deleted file mode 100644
index f65029f..0000000
--- a/src/app/_components/user-dropdown.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from "react";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import type { User } from "next-auth";
-import Avatar from "@/components/avatar";
-import { LogOut } from "lucide-react";
-import { Button } from "@/components/ui/button";
-import { Icons } from "@/components/icons";
-import Link from "next/link";
-
-export default function UserDropdown({ user }: { user: User }) {
- return (
-
-
-
-
-
- My Account
-
-
-
-
- Profile
-
-
-
-
-
-
- Billing
-
-
-
-
-
-
- Logout
-
-
-
-
-
- );
-}
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
deleted file mode 100644
index 63cd05d..0000000
--- a/src/app/api/auth/[...nextauth]/route.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { handlers } from "@/server/auth";
-
-export const { GET, POST } = handlers;
diff --git a/src/app/api/webhooks/route.ts b/src/app/api/webhooks/route.ts
new file mode 100644
index 0000000..be5a6ed
--- /dev/null
+++ b/src/app/api/webhooks/route.ts
@@ -0,0 +1,43 @@
+import { env } from "@/env";
+import { db } from "@/server/db";
+import { users } from "@/server/db/schema";
+import { verifyWebhook } from "@clerk/nextjs/webhooks";
+import { eq } from "drizzle-orm";
+
+export async function POST(req: Request) {
+ try {
+ const evt = await verifyWebhook(req, {
+ signingSecret: env.CLERK_WEBHOOK_SIGNING_SECRET,
+ });
+
+ const eventType = evt.type;
+ switch (eventType) {
+ case "user.created":
+ await db.insert(users).values({
+ id: evt.data.id,
+ name: evt.data.first_name ?? evt.data.last_name,
+ image: evt.data.image_url,
+ });
+ break;
+ case "user.updated":
+ await db
+ .update(users)
+ .set({
+ name: evt.data.first_name ?? evt.data.last_name,
+ image: evt.data.image_url,
+ })
+ .where(eq(users.id, evt.data.id));
+ break;
+ case "user.deleted":
+ await db.delete(users).where(eq(users.id, evt.data.id!));
+ break;
+ default:
+ console.log("Unknown event type:", eventType);
+ break;
+ }
+ return new Response("Webhook received", { status: 200 });
+ } catch (err) {
+ console.error("Error verifying webhook:", err);
+ return new Response("Error verifying webhook", { status: 400 });
+ }
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9e9b0f0..ffc1702 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -3,8 +3,7 @@ import "@/styles/globals.css";
import type { Metadata } from "next";
import { Geist } from "next/font/google";
import { Toaster } from "@/components/ui/sonner";
-import { TRPCReactProvider } from "@/trpc/react";
-import { ThemeProvider } from "@/components/theme-provider";
+import Providers from "./providers";
// import PWAWrapper from "./_components/install-prompt";
export const metadata: Metadata = {
@@ -24,19 +23,10 @@ export default function RootLayout({
return (
-
-
- {/*
- */}
- {children}
-
-
-
+
+ {children}
+
+
);
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
new file mode 100644
index 0000000..2469bf1
--- /dev/null
+++ b/src/app/providers.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import { TRPCReactProvider } from "@/trpc/react";
+import { ThemeProvider } from "@/components/theme-provider";
+
+import { ClerkProvider } from "@clerk/nextjs";
+import { dark } from "@clerk/themes";
+
+function Providers({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+export default Providers;
diff --git a/src/components/header.tsx b/src/components/header.tsx
index a330d10..b4dfd54 100644
--- a/src/components/header.tsx
+++ b/src/components/header.tsx
@@ -12,9 +12,12 @@ export default function Header({
return (
{text}
-
-
{children}
+
+ {/* Glow effect */}
+ {glow && (
+
+ )}
);
}
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 154301d..636c60a 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -4,6 +4,7 @@ type IconName =
| "logo"
| "home"
| "wallet"
+ | "brokenWallet"
| "add"
| "group"
| "friends"
@@ -81,6 +82,23 @@ export const Icons: Record = {
);
},
+ brokenWallet(props) {
+ return (
+
+ );
+ },
add(props) {
return (
);
diff --git a/src/env.js b/src/env.js
index 0a23897..cf59bca 100644
--- a/src/env.js
+++ b/src/env.js
@@ -2,51 +2,59 @@ import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
- /**
- * Specify your server-side environment variables schema here. This way you can ensure the app
- * isn't built with invalid env vars.
- */
- server: {
- AUTH_SECRET:
- process.env.NODE_ENV === "production"
- ? z.string()
- : z.string().optional(),
- AUTH_DISCORD_ID: z.string(),
- AUTH_DISCORD_SECRET: z.string(),
- DATABASE_URL: z.string().url(),
- NODE_ENV: z
- .enum(["development", "test", "production"])
- .default("development"),
- },
+ /**
+ * Specify your server-side environment variables schema here. This way you can ensure the app
+ * isn't built with invalid env vars.
+ */
+ server: {
+ // AUTH_SECRET:
+ // process.env.NODE_ENV === "production"
+ // ? z.string()
+ // : z.string().optional(),
+ // AUTH_DISCORD_ID: z.string(),
+ // AUTH_DISCORD_SECRET: z.string(),
+ DATABASE_URL: z.string().url(),
+ NODE_ENV: z
+ .enum(["development", "test", "production"])
+ .default("development"),
+ CLERK_SECRET_KEY: z.string(),
+ CLERK_WEBHOOK_SIGNING_SECRET: z.string(),
+ },
- /**
- * Specify your client-side environment variables schema here. This way you can ensure the app
- * isn't built with invalid env vars. To expose them to the client, prefix them with
- * `NEXT_PUBLIC_`.
- */
- client: {
- // NEXT_PUBLIC_CLIENTVAR: z.string(),
- },
+ /**
+ * Specify your client-side environment variables schema here. This way you can ensure the app
+ * isn't built with invalid env vars. To expose them to the client, prefix them with
+ * `NEXT_PUBLIC_`.
+ */
+ client: {
+ // NEXT_PUBLIC_CLIENTVAR: z.string(),
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(),
+ },
- /**
- * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
- * middlewares) or client-side so we need to destruct manually.
- */
- runtimeEnv: {
- AUTH_SECRET: process.env.AUTH_SECRET,
- AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
- AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
- DATABASE_URL: process.env.DATABASE_URL,
- NODE_ENV: process.env.NODE_ENV,
- },
- /**
- * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
- * useful for Docker builds.
- */
- skipValidation: !!process.env.SKIP_ENV_VALIDATION,
- /**
- * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
- * `SOME_VAR=''` will throw an error.
- */
- emptyStringAsUndefined: true,
+ /**
+ * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
+ * middlewares) or client-side so we need to destruct manually.
+ */
+ runtimeEnv: {
+ // AUTH_SECRET: process.env.AUTH_SECRET,
+ // AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
+ // AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
+ NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
+ process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
+ CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
+ CLERK_WEBHOOK_SIGNING_SECRET: process.env.CLERK_WEBHOOK_SIGNING_SECRET,
+
+ DATABASE_URL: process.env.DATABASE_URL,
+ NODE_ENV: process.env.NODE_ENV,
+ },
+ /**
+ * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
+ * useful for Docker builds.
+ */
+ skipValidation: !!process.env.SKIP_ENV_VALIDATION,
+ /**
+ * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
+ * `SOME_VAR=''` will throw an error.
+ */
+ emptyStringAsUndefined: true,
});
diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts
index a224338..c0c6f95 100644
--- a/src/lib/utils/index.ts
+++ b/src/lib/utils/index.ts
@@ -16,3 +16,5 @@ export function debounce
void>(
timeoutId = setTimeout(() => func(...args), delay);
};
}
+
+export const getAmount = (amount: number) => `${amount.toFixed(2)}€`;
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..908e900
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,21 @@
+import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
+
+const isProtectedRoute = createRouteMatcher([
+ "/",
+ "/friend",
+ "/expense",
+ "/group",
+ "/add",
+]);
+
+export default clerkMiddleware(async (auth, req) => {
+ if (isProtectedRoute(req)) await auth.protect();
+});
+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)(.*)",
+ ],
+};
diff --git a/src/server/api/root.ts b/src/server/api/root.ts
index 72453a3..e481ef0 100644
--- a/src/server/api/root.ts
+++ b/src/server/api/root.ts
@@ -1,6 +1,5 @@
-import { expenseRouter } from "@/server/api/routers/expense";
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
-import { friendRouter } from "./routers/friend";
+import { expenseRouter, friendRouter, userRouter } from "./routers";
/**
* This is the primary router for your server.
@@ -10,6 +9,7 @@ import { friendRouter } from "./routers/friend";
export const appRouter = createTRPCRouter({
expense: expenseRouter,
friend: friendRouter,
+ user: userRouter,
});
// export type definition of API
diff --git a/src/server/api/routers/expense.ts b/src/server/api/routers/expense.ts
index f1ad7d3..c5c3d08 100644
--- a/src/server/api/routers/expense.ts
+++ b/src/server/api/routers/expense.ts
@@ -5,17 +5,41 @@ import { expenses, expenseSplits, type ExpenseSplit } from "@/server/db/schema";
import { expenseSchema, expenseSplitSchema } from "@/lib/validations/expense";
import { eq, or } from "drizzle-orm";
+function restructureExpenseSplits(expenseSplits: Array) {
+ const expensesMap = new Map();
+
+ expenseSplits.forEach((split) => {
+ const expenseId = split.expenseId;
+ const expense = split.expense;
+
+ if (!expensesMap.has(expenseId)) {
+ expensesMap.set(expenseId, {
+ ...expense,
+ splits: [],
+ });
+ }
+
+ expensesMap.get(expenseId).splits.push(split);
+ });
+
+ return Array.from(expensesMap.values());
+}
+
export const expenseRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
- return await ctx.db.query.expenseSplits.findMany({
+ const splits = await ctx.db.query.expenseSplits.findMany({
where: or(
- eq(expenseSplits.owedFromId, ctx.session.user.id),
- eq(expenseSplits.owedToId, ctx.session.user.id)
+ eq(expenseSplits.owedFromId, ctx.auth.userId),
+ eq(expenseSplits.owedToId, ctx.auth.userId)
),
with: {
+ owedFrom: true,
+ owedTo: true,
expense: true,
},
});
+ const expenses = restructureExpenseSplits(splits);
+ return expenses;
}),
create: protectedProcedure
.input(
@@ -34,7 +58,7 @@ export const expenseRouter = createTRPCRouter({
const [expense] = await ctx.db
.insert(expenses)
.values({
- createdById: ctx.session.user.id,
+ createdById: ctx.auth.userId,
...input.expense,
amount: input.expense.amount.toString(),
})
diff --git a/src/server/api/routers/friend.ts b/src/server/api/routers/friend.ts
index 930a2d4..5c16541 100644
--- a/src/server/api/routers/friend.ts
+++ b/src/server/api/routers/friend.ts
@@ -10,8 +10,8 @@ export const friendRouter = createTRPCRouter({
getAll: protectedProcedure.query(async ({ ctx }) => {
const friends = await ctx.db.query.friendships.findMany({
where: or(
- eq(friendships.userOneId, ctx.session.user.id),
- eq(friendships.userTwoId, ctx.session.user.id)
+ eq(friendships.userOneId, ctx.auth.userId),
+ eq(friendships.userTwoId, ctx.auth.userId)
),
with: {
userOne: true,
@@ -21,8 +21,8 @@ export const friendRouter = createTRPCRouter({
return friends.map((f) => ({
id: f.id,
status: f.status,
- requestedBy: ctx.session.user.id === f.userOneId ? "me" : "them",
- user: ctx.session.user.id === f.userOneId ? f.userTwo : f.userOne,
+ requestedBy: ctx.auth.userId === f.userOneId ? "me" : "them",
+ user: ctx.auth.userId === f.userOneId ? f.userTwo : f.userOne,
}));
}),
getPendingFriendRequests: protectedProcedure.query(
@@ -30,8 +30,8 @@ export const friendRouter = createTRPCRouter({
await ctx.db.query.friendships.findMany({
where: and(
or(
- eq(friendships.userOneId, ctx.session.user.id),
- eq(friendships.userTwoId, ctx.session.user.id)
+ eq(friendships.userOneId, ctx.auth.userId),
+ eq(friendships.userTwoId, ctx.auth.userId)
),
eq(friendships.status, "pending")
),
@@ -40,7 +40,7 @@ export const friendRouter = createTRPCRouter({
search: protectedProcedure
.input(z.object({ search: z.string() }))
.query(async ({ ctx, input }) => {
- const userId = ctx.session.user.id;
+ const userId = ctx.auth.userId;
const friendIds = await ctx.db.query.friendships.findMany({
where: and(
or(
@@ -91,7 +91,7 @@ export const friendRouter = createTRPCRouter({
.input(z.object({ userId: z.string() }))
.mutation(async ({ ctx, input }) => {
await ctx.db.insert(friendships).values({
- userOneId: ctx.session.user.id,
+ userOneId: ctx.auth.userId,
userTwoId: input.userId,
status: "pending",
});
diff --git a/src/server/api/routers/index.ts b/src/server/api/routers/index.ts
new file mode 100644
index 0000000..8d75cc1
--- /dev/null
+++ b/src/server/api/routers/index.ts
@@ -0,0 +1,3 @@
+export { friendRouter } from "./friend";
+export { expenseRouter } from "./expense";
+export { userRouter } from "./user";
diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts
index 5daf9ae..32be565 100644
--- a/src/server/api/routers/user.ts
+++ b/src/server/api/routers/user.ts
@@ -1,14 +1,12 @@
-import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
+import { eq } from "drizzle-orm";
+import { createTRPCRouter, protectedProcedure } from "../trpc";
+import { users } from "@/server/db/schema";
export const userRouter = createTRPCRouter({
- getAll: protectedProcedure.query(
- async ({ ctx }) =>
- await ctx.db.query.users.findMany({
- columns: {
- id: true,
- name: true,
- image: true,
- },
- })
- ),
+ getSessionUser: protectedProcedure.query(async ({ ctx }) => {
+ const user = await ctx.db.query.users.findFirst({
+ where: eq(users.id, ctx.auth.userId),
+ });
+ return user;
+ }),
});
diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts
index ab1ce47..ce434e6 100644
--- a/src/server/api/trpc.ts
+++ b/src/server/api/trpc.ts
@@ -11,8 +11,8 @@ import { TRPCError, initTRPC } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
-import { auth } from "@/server/auth";
import { db } from "@/server/db";
+import { auth } from "@clerk/nextjs/server";
/**
* 1. CONTEXT
@@ -27,13 +27,13 @@ import { db } from "@/server/db";
* @see https://trpc.io/docs/server/context
*/
export const createTRPCContext = async (opts: { headers: Headers }) => {
- const session = await auth();
+ const clerkAuth = await auth();
- return {
- db,
- session,
- ...opts,
- };
+ return {
+ db,
+ auth: clerkAuth,
+ ...opts,
+ };
};
/**
@@ -44,17 +44,17 @@ export const createTRPCContext = async (opts: { headers: Headers }) => {
* errors on the backend.
*/
const t = initTRPC.context().create({
- transformer: superjson,
- errorFormatter({ shape, error }) {
- return {
- ...shape,
- data: {
- ...shape.data,
- zodError:
- error.cause instanceof ZodError ? error.cause.flatten() : null,
- },
- };
- },
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
});
/**
@@ -85,20 +85,20 @@ export const createTRPCRouter = t.router;
* network latency that would occur in production but not in local development.
*/
const timingMiddleware = t.middleware(async ({ next, path }) => {
- const start = Date.now();
+ const start = Date.now();
- if (t._config.isDev) {
- // artificial delay in dev
- const waitMs = Math.floor(Math.random() * 400) + 100;
- await new Promise((resolve) => setTimeout(resolve, waitMs));
- }
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
- const result = await next();
+ const result = await next();
- const end = Date.now();
- console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+ const end = Date.now();
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
- return result;
+ return result;
});
/**
@@ -119,15 +119,15 @@ export const publicProcedure = t.procedure.use(timingMiddleware);
* @see https://trpc.io/docs/procedures
*/
export const protectedProcedure = t.procedure
- .use(timingMiddleware)
- .use(({ ctx, next }) => {
- if (!ctx.session?.user) {
- throw new TRPCError({ code: "UNAUTHORIZED" });
- }
- return next({
- ctx: {
- // infers the `session` as non-nullable
- session: { ...ctx.session, user: ctx.session.user },
- },
- });
- });
+ .use(timingMiddleware)
+ .use(({ ctx, next }) => {
+ if (!ctx.auth?.userId) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
+ return next({
+ ctx: {
+ // infers the `session` as non-nullable
+ auth: { ...ctx.auth, userId: ctx.auth.userId },
+ },
+ });
+ });
diff --git a/src/server/auth/config.ts b/src/server/auth/config.ts
deleted file mode 100644
index 3d3774e..0000000
--- a/src/server/auth/config.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { DrizzleAdapter } from "@auth/drizzle-adapter";
-import type { DefaultSession, NextAuthConfig, User } from "next-auth";
-import DiscordProvider from "next-auth/providers/discord";
-
-import { db } from "@/server/db";
-import {
- accounts,
- sessions,
- users,
- verificationTokens,
-} from "@/server/db/schema";
-
-/**
- * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
- * object and keep type safety.
- *
- * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
- */
-declare module "next-auth" {
- interface Session extends DefaultSession {
- user: {
- id: string;
- // ...other properties
- // role: UserRole;
- } & DefaultSession["user"];
- }
-
- // interface User {
- // // ...other properties
- // // role: UserRole;
- // }
- type PublicUser = Pick;
-}
-
-/**
- * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
- *
- * @see https://next-auth.js.org/configuration/options
- */
-export const authConfig = {
- trustHost: true,
- providers: [
- DiscordProvider,
- /**
- * ...add more providers here.
- *
- * Most other providers require a bit more work than the Discord provider. For example, the
- * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
- * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
- *
- * @see https://next-auth.js.org/providers/github
- */
- ],
- adapter: DrizzleAdapter(db, {
- usersTable: users,
- accountsTable: accounts,
- sessionsTable: sessions,
- verificationTokensTable: verificationTokens,
- }),
-
- callbacks: {
- session: ({ session, user }) => ({
- ...session,
- user: {
- ...session.user,
- id: user.id,
- },
- }),
- },
-} satisfies NextAuthConfig;
diff --git a/src/server/auth/index.ts b/src/server/auth/index.ts
deleted file mode 100644
index 76c146d..0000000
--- a/src/server/auth/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import NextAuth from "next-auth";
-import { cache } from "react";
-
-import { authConfig } from "./config";
-
-const { auth: uncachedAuth, handlers, signIn, signOut } = NextAuth(authConfig);
-
-const auth = cache(uncachedAuth);
-
-export { auth, handlers, signIn, signOut };
diff --git a/src/server/db/index.ts b/src/server/db/index.ts
index 5e4fea9..011e0af 100644
--- a/src/server/db/index.ts
+++ b/src/server/db/index.ts
@@ -9,7 +9,7 @@ import * as schema from "./schema";
* update.
*/
const globalForDb = globalThis as unknown as {
- conn: postgres.Sql | undefined;
+ conn: postgres.Sql | undefined;
};
const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts
index 72e6f28..fd4c41a 100644
--- a/src/server/db/schema.ts
+++ b/src/server/db/schema.ts
@@ -1,8 +1,6 @@
import { relations, sql } from "drizzle-orm";
-import { index, pgTableCreator, primaryKey } from "drizzle-orm/pg-core";
-import type { User } from "next-auth";
-import type { AdapterAccount } from "next-auth/adapters";
-
+import { index, pgTableCreator } from "drizzle-orm/pg-core";
+import { createId as createCuid2 } from "@paralleldrive/cuid2";
/**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects.
@@ -11,17 +9,16 @@ import type { AdapterAccount } from "next-auth/adapters";
*/
export const createTable = pgTableCreator((name) => `betterwise_${name}`);
+const createId = () => createCuid2();
+
export const expenses = createTable(
"expense",
(d) => ({
- id: d
- .uuid()
- .primaryKey()
- .default(sql`gen_random_uuid()`),
- groupId: d.uuid(),
- friendId: d.uuid(),
+ id: d.varchar().primaryKey().$defaultFn(createId),
+ groupId: d.varchar(),
+ friendId: d.varchar(),
createdById: d
- .uuid()
+ .varchar()
.notNull()
.references(() => users.id),
amount: d.numeric({ scale: 2 }),
@@ -61,20 +58,17 @@ export type Expense = typeof expenses.$inferSelect & {
export const expenseSplits = createTable(
"expense_split",
(d) => ({
- id: d
- .uuid()
- .primaryKey()
- .default(sql`gen_random_uuid()`),
+ id: d.varchar().primaryKey().$defaultFn(createId),
expenseId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => expenses.id, { onDelete: "cascade" }),
owedToId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => users.id),
owedFromId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => users.id),
amount: d.numeric({ scale: 2 }),
@@ -105,22 +99,20 @@ export const expenseSplitRelations = relations(expenseSplits, ({ one }) => ({
export type ExpenseSplit = typeof expenseSplits.$inferSelect & {
expense?: Expense;
paidBy?: User;
- owedBy?: User;
+ owedTo?: User;
+ owedFrom?: User;
};
export const settlements = createTable(
"settlement",
(d) => ({
- id: d
- .uuid()
- .primaryKey()
- .default(sql`gen_random_uuid()`),
+ id: d.varchar().primaryKey().$defaultFn(createId),
payerId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => users.id),
receiverId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => users.id),
amount: d.numeric().notNull(),
@@ -153,18 +145,16 @@ export type Settlement = typeof settlements.$inferSelect & {
};
// Groups Table
+
export const groups = createTable(
"group",
(d) => ({
- id: d
- .uuid()
- .primaryKey()
- .default(sql`gen_random_uuid()`),
+ id: d.varchar().primaryKey().$defaultFn(createId),
name: d.varchar({ length: 255 }).notNull(),
createdById: d
- .uuid()
+ .varchar()
.notNull()
- .references(() => users.id, { onDelete: "cascade" }),
+ .references(() => users.id),
createdAt: d
.timestamp({ withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
@@ -190,16 +180,13 @@ export type Group = typeof groups.$inferSelect & {
export const groupMembers = createTable(
"group_member",
(d) => ({
- id: d
- .uuid()
- .primaryKey()
- .default(sql`gen_random_uuid()`),
+ id: d.varchar().primaryKey().$defaultFn(createId),
groupId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => groups.id, { onDelete: "cascade" }),
userId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
joinedAt: d
@@ -233,19 +220,17 @@ export type GroupMember = typeof groupMembers.$inferSelect & {
};
// Friendships Table (Tracks friend relationships)
+
export const friendships = createTable(
"friendship",
(d) => ({
- id: d
- .uuid()
- .primaryKey()
- .default(sql`gen_random_uuid()`),
+ id: d.varchar().primaryKey().$defaultFn(createId),
userOneId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
userTwoId: d
- .uuid()
+ .varchar()
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
status: d
@@ -281,76 +266,11 @@ export type Friendship = typeof friendships.$inferSelect & {
};
export const users = createTable("user", (d) => ({
- id: d
- .uuid()
- .primaryKey()
- .default(sql`gen_random_uuid()`),
+ id: d.varchar().notNull().primaryKey().unique(),
name: d.varchar({ length: 255 }),
- email: d.varchar({ length: 255 }).notNull(),
- emailVerified: d
- .timestamp({
- mode: "date",
- withTimezone: true,
- })
- .default(sql`CURRENT_TIMESTAMP`),
image: d.varchar({ length: 255 }),
}));
-export const usersRelations = relations(users, ({ many }) => ({
- accounts: many(accounts),
-}));
+export type User = typeof users.$inferSelect;
-export const accounts = createTable(
- "account",
- (d) => ({
- userId: d
- .uuid()
- .notNull()
- .references(() => users.id),
- type: d.varchar({ length: 255 }).$type().notNull(),
- provider: d.varchar({ length: 255 }).notNull(),
- providerAccountId: d.varchar({ length: 255 }).notNull(),
- refresh_token: d.text(),
- access_token: d.text(),
- expires_at: d.integer(),
- token_type: d.varchar({ length: 255 }),
- scope: d.varchar({ length: 255 }),
- id_token: d.text(),
- session_state: d.varchar({ length: 255 }),
- }),
- (t) => [
- primaryKey({ columns: [t.provider, t.providerAccountId] }),
- index("account_user_id_idx").on(t.userId),
- ]
-);
-
-export const accountsRelations = relations(accounts, ({ one }) => ({
- user: one(users, { fields: [accounts.userId], references: [users.id] }),
-}));
-
-export const sessions = createTable(
- "session",
- (d) => ({
- sessionToken: d.varchar({ length: 255 }).notNull().primaryKey(),
- userId: d
- .uuid()
- .notNull()
- .references(() => users.id),
- expires: d.timestamp({ mode: "date", withTimezone: true }).notNull(),
- }),
- (t) => [index("t_user_id_idx").on(t.userId)]
-);
-
-export const sessionsRelations = relations(sessions, ({ one }) => ({
- user: one(users, { fields: [sessions.userId], references: [users.id] }),
-}));
-
-export const verificationTokens = createTable(
- "verification_token",
- (d) => ({
- identifier: d.varchar({ length: 255 }).notNull(),
- token: d.varchar({ length: 255 }).notNull(),
- expires: d.timestamp({ mode: "date", withTimezone: true }).notNull(),
- }),
- (t) => [primaryKey({ columns: [t.identifier, t.token] })]
-);
+// export const usersRelations = relations(users, ({ many }) => ({}));