Merge pull request #35 from atridadl/dev
2.0.0 F-f-f-f-f-faster! No more Next-Auth holding me down! ✨ Auth moved from next-auth -> Clerk ✨ ORM moved from Prisma to Drizzle ✨ Major version number is one whole number higher! Wow!
This commit is contained in:
commit
0d2005a8e5
32 changed files with 1743 additions and 2102 deletions
|
@ -8,9 +8,9 @@ UPSTASH_REDIS_EXPIRY_SECONDS=""
|
|||
UPSTASH_RATELIMIT_REQUESTS=""
|
||||
UPSTASH_RATELIMIT_SECONDS=""
|
||||
|
||||
#Next Auth Core
|
||||
NEXTAUTH_SECRET=""
|
||||
NEXTAUTH_URL=""
|
||||
#Auth
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
|
||||
CLERK_SECRET_KEY=""
|
||||
|
||||
# Next Auth Github Provider
|
||||
GITHUB_CLIENT_ID=""
|
||||
|
|
12
drizzle.config.ts
Normal file
12
drizzle.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import type { Config } from "drizzle-kit";
|
||||
import "dotenv/config";
|
||||
|
||||
export default {
|
||||
schema: "./src/server/schema.ts",
|
||||
out: "./drizzle/generated",
|
||||
driver: "mysql2",
|
||||
breakpoints: true,
|
||||
dbCredentials: {
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
},
|
||||
} satisfies Config;
|
|
@ -10,7 +10,11 @@ const config = {
|
|||
defaultLocale: "en",
|
||||
},
|
||||
images: {
|
||||
domains: ["avatars.githubusercontent.com", "lh3.googleusercontent.com"],
|
||||
domains: [
|
||||
"avatars.githubusercontent.com",
|
||||
"lh3.googleusercontent.com",
|
||||
"img.clerk.com",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
25
package.json
25
package.json
|
@ -1,34 +1,35 @@
|
|||
{
|
||||
"name": "sprintpadawan",
|
||||
"version": "1.2.7",
|
||||
"version": "2.0.0",
|
||||
"description": "Plan. Sprint. Repeat.",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"serv": "NEXTAUTH_URL=http://localhost:3000 && next dev",
|
||||
"serv": "next dev",
|
||||
"dev": "pnpm serv",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ably-labs/react-hooks": "^2.1.1",
|
||||
"@auth/prisma-adapter": "^1.0.1",
|
||||
"@prisma/client": "5.1.1",
|
||||
"@clerk/nextjs": "^4.23.2",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@planetscale/database": "^1.10.0",
|
||||
"@react-email/components": "^0.0.7",
|
||||
"@tanstack/react-query": "^4.32.6",
|
||||
"@trpc/client": "10.37.1",
|
||||
"@trpc/next": "10.37.1",
|
||||
"@trpc/react-query": "10.37.1",
|
||||
"@trpc/server": "10.37.1",
|
||||
"@unkey/api": "^0.5.0",
|
||||
"@unkey/api": "^0.6.9",
|
||||
"@upstash/ratelimit": "^0.4.3",
|
||||
"@upstash/redis": "^1.22.0",
|
||||
"ably": "^1.2.42",
|
||||
"ably": "^1.2.43",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-orm": "^0.28.2",
|
||||
"json2csv": "6.0.0-alpha.2",
|
||||
"next": "^13.4.13",
|
||||
"next-auth": "^4.22.5",
|
||||
"nextjs-cors": "^2.1.2",
|
||||
"postcss": "^8.4.27",
|
||||
"react": "18.2.0",
|
||||
|
@ -44,14 +45,14 @@
|
|||
"devDependencies": {
|
||||
"@types/eslint": "^8.44.2",
|
||||
"@types/json2csv": "^5.0.3",
|
||||
"@types/node": "^20.4.9",
|
||||
"@types/react": "^18.2.19",
|
||||
"@types/node": "^20.4.10",
|
||||
"@types/react": "^18.2.20",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
"daisyui": "^3.5.1",
|
||||
"eslint": "^8.46.0",
|
||||
"drizzle-kit": "^0.19.12",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-config-next": "^13.4.13",
|
||||
"prisma": "5.1.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
|
|
1682
pnpm-lock.yaml
generated
1682
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,109 +0,0 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
relationMode = "prisma"
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
rooms Room[]
|
||||
votes Vote[]
|
||||
logs Log[]
|
||||
isAdmin Boolean @default(false)
|
||||
isVIP Boolean @default(false)
|
||||
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
model Room {
|
||||
id String @id @unique @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
userId String
|
||||
roomName String
|
||||
storyName String
|
||||
visible Boolean
|
||||
votes Vote[]
|
||||
scale String
|
||||
logs Log[]
|
||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Vote {
|
||||
id String @id @unique @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
userId String
|
||||
roomId String
|
||||
value String
|
||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, roomId])
|
||||
@@index([roomId])
|
||||
}
|
||||
|
||||
model Log {
|
||||
id String @id @unique @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
userId String
|
||||
roomId String
|
||||
scale String
|
||||
votes Json
|
||||
roomName String
|
||||
storyName String
|
||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([roomId])
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { signIn, signOut, useSession } from "next-auth/react";
|
||||
import { UserButton, useUser } from "@clerk/nextjs";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -9,19 +9,22 @@ interface NavbarProps {
|
|||
}
|
||||
|
||||
const Navbar = ({ title }: NavbarProps) => {
|
||||
const { data: sessionData, status: sessionStatus } = useSession();
|
||||
const { isLoaded, isSignedIn } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
const navigationMenu = () => {
|
||||
if (sessionStatus === "authenticated" && router.pathname !== "/dashboard") {
|
||||
if (router.pathname !== "/dashboard" && isSignedIn) {
|
||||
return (
|
||||
<Link className="btn btn-secondary btn-outline mx-2" href="/dashboard">
|
||||
Dashboard
|
||||
</Link>
|
||||
);
|
||||
} else if (sessionStatus === "unauthenticated") {
|
||||
} else if (!isSignedIn) {
|
||||
return (
|
||||
<button className="btn btn-secondary" onClick={() => void signIn()}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => void router.push("/sign-in")}
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
);
|
||||
|
@ -51,7 +54,7 @@ const Navbar = ({ title }: NavbarProps) => {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
{sessionStatus === "loading" ? (
|
||||
{!isLoaded ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
</div>
|
||||
|
@ -59,53 +62,7 @@ const Navbar = ({ title }: NavbarProps) => {
|
|||
navigationMenu()
|
||||
)}
|
||||
|
||||
{sessionData?.user.image && (
|
||||
<div className="flex-none gap-2">
|
||||
<div className="dropdown dropdown-end">
|
||||
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
||||
<div className="w-10 rounded-full">
|
||||
<Image
|
||||
src={sessionData.user.image}
|
||||
alt="Profile picture."
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="mt-3 p-2 shadow menu menu-compact dropdown-content bg-base-100 rounded-box z-50"
|
||||
>
|
||||
<li>
|
||||
<Link
|
||||
about="Profile Page"
|
||||
href="/profile"
|
||||
className="justify-between"
|
||||
>
|
||||
Profile
|
||||
</Link>
|
||||
</li>
|
||||
{sessionData.user.isAdmin && (
|
||||
<li>
|
||||
<Link
|
||||
about="Admin Page"
|
||||
href="/admin"
|
||||
className="justify-between"
|
||||
>
|
||||
Admin
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary btn-sm text-center whitespace-nowrap"
|
||||
onClick={() => void signOut({ callbackUrl: "/" })}
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<UserButton afterSignOutUrl="/" />
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
|
||||
import { useState } from "react";
|
||||
import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5";
|
||||
import { env } from "~/env.mjs";
|
||||
import { api } from "~/utils/api";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
const RoomList = () => {
|
||||
const { data: sessionData } = useSession();
|
||||
const { isSignedIn, user } = useUser();
|
||||
|
||||
configureAbly({
|
||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
||||
clientId: sessionData?.user.id,
|
||||
clientId: user?.id,
|
||||
recover: (_, cb) => {
|
||||
cb(true);
|
||||
},
|
||||
});
|
||||
|
||||
const [] = useChannel(
|
||||
`${env.NEXT_PUBLIC_APP_ENV}-${sessionData ? sessionData.user.id : ""}`,
|
||||
`${env.NEXT_PUBLIC_APP_ENV}-${user?.id}`,
|
||||
() => void refetchRoomsFromDb()
|
||||
);
|
||||
|
||||
|
@ -27,7 +26,7 @@ const RoomList = () => {
|
|||
|
||||
const { data: roomsFromDb, refetch: refetchRoomsFromDb } =
|
||||
api.room.getAll.useQuery(undefined, {
|
||||
enabled: sessionData?.user !== undefined,
|
||||
enabled: isSignedIn,
|
||||
});
|
||||
|
||||
const createRoom = api.room.create.useMutation({});
|
||||
|
@ -43,7 +42,7 @@ const RoomList = () => {
|
|||
const deleteRoom = api.room.delete.useMutation({});
|
||||
|
||||
const deleteRoomHandler = (roomId: string) => {
|
||||
if (sessionData) {
|
||||
if (isSignedIn) {
|
||||
deleteRoom.mutate({ id: roomId });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
|
||||
import { env } from "~/env.mjs";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
const Stats = () => {
|
||||
configureAbly({
|
||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
||||
recover: (_, cb) => {
|
||||
cb(true);
|
||||
},
|
||||
});
|
||||
|
||||
const [] = useChannel(
|
||||
`${env.NEXT_PUBLIC_APP_ENV}-stats`,
|
||||
() => void refetchData()
|
||||
);
|
||||
|
||||
const {
|
||||
data: usersCount,
|
||||
isLoading: usersCountLoading,
|
||||
isFetching: usersCountFetching,
|
||||
refetch: refetchUsersCount,
|
||||
} = api.rest.userCount.useQuery();
|
||||
const {
|
||||
data: roomsCount,
|
||||
isLoading: roomsCountLoading,
|
||||
isFetching: roomsCountFetching,
|
||||
refetch: refetchRoomsCount,
|
||||
} = api.rest.roomCount.useQuery();
|
||||
const {
|
||||
data: votesCount,
|
||||
isLoading: votesCountLoading,
|
||||
isFetching: votesCountFetching,
|
||||
refetch: refetchVotesCount,
|
||||
} = api.rest.voteCount.useQuery();
|
||||
|
||||
const refetchData = async () => {
|
||||
await Promise.all([
|
||||
refetchUsersCount(),
|
||||
refetchRoomsCount(),
|
||||
refetchVotesCount(),
|
||||
]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="stats stats-horizontal shadow bg-neutral m-4">
|
||||
<div className="stat">
|
||||
<div className="stat-title">Users</div>
|
||||
<div className="stat-value">
|
||||
{usersCountLoading || usersCountFetching ? (
|
||||
<span className="loading loading-infinity loading-lg"></span>
|
||||
) : (
|
||||
<>{usersCount ? usersCount : "0"}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat">
|
||||
<div className="stat-title">Rooms</div>
|
||||
<div className="stat-value">
|
||||
{roomsCountLoading || roomsCountFetching ? (
|
||||
<span className="loading loading-infinity loading-lg"></span>
|
||||
) : (
|
||||
<>{roomsCount ? roomsCount : "0"}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat">
|
||||
<div className="stat-title">Votes</div>
|
||||
<div className="stat-value">
|
||||
{votesCountLoading || votesCountFetching ? (
|
||||
<span className="loading loading-infinity loading-lg"></span>
|
||||
) : (
|
||||
<>{votesCount ? votesCount : "0"}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stats;
|
18
src/env.mjs
18
src/env.mjs
|
@ -12,17 +12,6 @@ const server = z.object({
|
|||
UPSTASH_RATELIMIT_REQUESTS: z.string(),
|
||||
UPSTASH_RATELIMIT_SECONDS: z.string(),
|
||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||
NEXTAUTH_SECRET:
|
||||
process.env.NODE_ENV === "production"
|
||||
? z.string().min(1)
|
||||
: z.string().min(1).optional(),
|
||||
NEXTAUTH_URL: z.preprocess(
|
||||
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
|
||||
// Since NextAuth.js automatically uses the VERCEL_URL if present.
|
||||
(str) => process.env.VERCEL_URL ?? str,
|
||||
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
|
||||
process.env.VERCEL ? z.string().min(1) : z.string().url()
|
||||
),
|
||||
GITHUB_CLIENT_ID: z.string(),
|
||||
GITHUB_CLIENT_SECRET: z.string(),
|
||||
GOOGLE_CLIENT_ID: z.string(),
|
||||
|
@ -31,6 +20,7 @@ const server = z.object({
|
|||
APP_ENV: z.string(),
|
||||
RESEND_API_KEY: z.string(),
|
||||
UNKEY_ROOT_KEY: z.string(),
|
||||
CLERK_SECRET_KEY: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -40,6 +30,7 @@ const server = z.object({
|
|||
const client = z.object({
|
||||
NEXT_PUBLIC_ABLY_PUBLIC_KEY: z.string(),
|
||||
NEXT_PUBLIC_APP_ENV: z.string(),
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -56,8 +47,6 @@ const processEnv = {
|
|||
UPSTASH_RATELIMIT_REQUESTS: process.env.UPSTASH_RATELIMIT_REQUESTS,
|
||||
UPSTASH_RATELIMIT_SECONDS: process.env.UPSTASH_RATELIMIT_SECONDS,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
||||
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||
|
@ -68,6 +57,9 @@ const processEnv = {
|
|||
NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV,
|
||||
RESEND_API_KEY: process.env.RESEND_API_KEY,
|
||||
UNKEY_ROOT_KEY: process.env.UNKEY_ROOT_KEY,
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
||||
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
||||
};
|
||||
|
||||
// Don't touch the part below
|
||||
|
|
9
src/middleware.ts
Normal file
9
src/middleware.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { authMiddleware } from "@clerk/nextjs";
|
||||
|
||||
export default authMiddleware({
|
||||
publicRoutes: ["/", "/api/(.*)"],
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
|
||||
};
|
|
@ -1,56 +1,23 @@
|
|||
import { type Session } from "next-auth";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { type AppType } from "next/app";
|
||||
import { ClerkProvider } from "@clerk/nextjs";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Footer from "~/components/Footer";
|
||||
import Navbar from "~/components/Navbar";
|
||||
import "~/styles/globals.css";
|
||||
|
||||
const MyApp: AppType<{ session: Session | null }> = ({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
}) => {
|
||||
const [pageLoading, setPageLoading] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.events.on("routeChangeStart", () => {
|
||||
setPageLoading(true);
|
||||
});
|
||||
|
||||
router.events.on("routeChangeComplete", () => {
|
||||
setPageLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", () => {
|
||||
setPageLoading(true);
|
||||
});
|
||||
|
||||
router.events.off("routeChangeComplete", () => {
|
||||
setPageLoading(false);
|
||||
});
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||
return (
|
||||
<SessionProvider session={ session }>
|
||||
<ClerkProvider {...pageProps}>
|
||||
<div className="block h-[100%]">
|
||||
<Navbar title="Sprint Padawan" />
|
||||
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
|
||||
{ pageLoading ? (
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
) : (
|
||||
<Component { ...pageProps } />
|
||||
) }
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
</SessionProvider>
|
||||
</ClerkProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,270 +0,0 @@
|
|||
import { type GetServerSideProps, type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { AiOutlineClear } from "react-icons/ai";
|
||||
import { FaShieldAlt } from "react-icons/fa";
|
||||
import { IoTrashBinOutline } from "react-icons/io5";
|
||||
import { SiGithub, SiGoogle } from "react-icons/si";
|
||||
import { GiStarFormation } from "react-icons/gi";
|
||||
import { api } from "~/utils/api";
|
||||
import { getServerAuthSession } from "../../server/auth";
|
||||
import Stats from "~/components/Stats";
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
// Redirect to login if not signed in
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!session.user.isAdmin) {
|
||||
ctx.res.statusCode = 403;
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
// Return session if logged in
|
||||
return {
|
||||
props: { session },
|
||||
};
|
||||
};
|
||||
|
||||
const Admin: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sprint Padawan - Admin</title>
|
||||
<meta name="description" content="Plan. Sprint. Repeat." />
|
||||
</Head>
|
||||
<div className="flex flex-col items-center justify-center text-center px-4 py-16 ">
|
||||
<div className="flex flex-col items-center">
|
||||
<AdminBody />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Admin;
|
||||
|
||||
const AdminBody = () => {
|
||||
const {
|
||||
data: users,
|
||||
isLoading: usersLoading,
|
||||
isFetching: usersFetching,
|
||||
refetch: refetchUsers,
|
||||
} = api.user.getAll.useQuery();
|
||||
|
||||
const getProviders = (user: {
|
||||
createdAt: Date;
|
||||
accounts: {
|
||||
provider: string;
|
||||
}[];
|
||||
sessions: {
|
||||
id: string;
|
||||
}[];
|
||||
id: string;
|
||||
isAdmin: boolean;
|
||||
isVIP: boolean;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
}) => {
|
||||
return user.accounts.map((account) => {
|
||||
return account.provider;
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUserMutation = api.user.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchData();
|
||||
},
|
||||
});
|
||||
|
||||
const clearSessionsByUserMutation = api.session.deleteAllByUserId.useMutation(
|
||||
{
|
||||
onSuccess: async () => {
|
||||
await refetchData();
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const clearSessionsMutation = api.session.deleteAll.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchData();
|
||||
},
|
||||
});
|
||||
|
||||
const setAdminMutation = api.user.setAdmin.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchData();
|
||||
},
|
||||
});
|
||||
|
||||
const setVIPMutation = api.user.setVIP.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchData();
|
||||
},
|
||||
});
|
||||
|
||||
const deleteUserHandler = async (userId: string) => {
|
||||
await deleteUserMutation.mutateAsync({ userId });
|
||||
};
|
||||
|
||||
const clearSessionsByUserHandler = async (userId: string) => {
|
||||
await clearSessionsByUserMutation.mutateAsync({ userId });
|
||||
};
|
||||
|
||||
const clearSessionsHandler = async () => {
|
||||
await clearSessionsMutation.mutateAsync();
|
||||
};
|
||||
|
||||
const setAdmin = async (userId: string, value: boolean) => {
|
||||
await setAdminMutation.mutateAsync({ userId, value });
|
||||
};
|
||||
|
||||
const setVIP = async (userId: string, value: boolean) => {
|
||||
await setVIPMutation.mutateAsync({ userId, value });
|
||||
};
|
||||
|
||||
const refetchData = async () => {
|
||||
await Promise.all([refetchUsers()]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-4xl font-bold">Admin Panel</h1>
|
||||
|
||||
<Stats />
|
||||
|
||||
{usersFetching ? (
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
) : (
|
||||
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
||||
<button
|
||||
className="btn btn-primary m-2"
|
||||
onClick={() => void clearSessionsHandler()}
|
||||
>
|
||||
Delete All Sessions
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => void refetchData()}
|
||||
>
|
||||
Re-fetch
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card max-w-[80vw] bg-neutral shadow-xl m-4">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">Users:</h2>
|
||||
|
||||
{usersLoading || usersFetching ? (
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
) : (
|
||||
<div className="overflow-x-scroll">
|
||||
<table className="table text-center">
|
||||
{/* head */}
|
||||
<thead>
|
||||
<tr className="border-white">
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Created At</th>
|
||||
<th># Sessions</th>
|
||||
<th>Providers</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="">
|
||||
{users
|
||||
?.sort((user1, user2) =>
|
||||
user2.createdAt > user1.createdAt ? 1 : -1
|
||||
)
|
||||
.map((user) => {
|
||||
return (
|
||||
<tr key={user.id} className="hover">
|
||||
<td className="max-w-[100px] break-words">
|
||||
{user.id}
|
||||
</td>
|
||||
|
||||
<td className="max-w-[100px] break-normal">
|
||||
{user.name}
|
||||
</td>
|
||||
<td className="max-w-[100px] break-normal">
|
||||
{user.createdAt.toLocaleDateString()}
|
||||
</td>
|
||||
<td className="max-w-[100px] break-normal">
|
||||
{user.sessions.length}
|
||||
</td>
|
||||
<td className="max-w-[100px] break-normal">
|
||||
{getProviders(user).includes("google") && (
|
||||
<SiGoogle className="text-xl m-1 inline-block hover:text-secondary" />
|
||||
)}
|
||||
{getProviders(user).includes("github") && (
|
||||
<SiGithub className="text-xl m-1 inline-block hover:text-secondary" />
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<button className="m-2">
|
||||
{user.isAdmin ? (
|
||||
<FaShieldAlt
|
||||
className="text-xl inline-block text-primary"
|
||||
onClick={() => void setAdmin(user.id, false)}
|
||||
/>
|
||||
) : (
|
||||
<FaShieldAlt
|
||||
className="text-xl inline-block"
|
||||
onClick={() => void setAdmin(user.id, true)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<button className="m-2">
|
||||
{user.isVIP ? (
|
||||
<GiStarFormation
|
||||
className="text-xl inline-block text-secondary"
|
||||
onClick={() => void setVIP(user.id, false)}
|
||||
/>
|
||||
) : (
|
||||
<GiStarFormation
|
||||
className="text-xl inline-block"
|
||||
onClick={() => void setVIP(user.id, true)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="m-2"
|
||||
onClick={() =>
|
||||
void clearSessionsByUserHandler(user.id)
|
||||
}
|
||||
>
|
||||
<AiOutlineClear className="text-xl inline-block hover:text-warning" />
|
||||
</button>
|
||||
<button
|
||||
className="m-2"
|
||||
onClick={() => void deleteUserHandler(user.id)}
|
||||
>
|
||||
<IoTrashBinOutline className="text-xl inline-block hover:text-error" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
import NextAuth from "next-auth";
|
||||
import { authOptions } from "~/server/auth";
|
||||
|
||||
export default NextAuth(authOptions);
|
|
@ -10,9 +10,9 @@ export default createNextApiHandler({
|
|||
onError:
|
||||
env.NODE_ENV === "development"
|
||||
? ({ path, error }) => {
|
||||
console.error(
|
||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { GetServerSideProps, NextPage } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
|
||||
import RoomList from "~/components/RoomList";
|
||||
|
@ -7,27 +6,8 @@ import RoomList from "~/components/RoomList";
|
|||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaShieldAlt } from "react-icons/fa";
|
||||
import { getServerAuthSession } from "~/server/auth";
|
||||
import { GiStarFormation } from "react-icons/gi";
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
// Redirect to login if not signed in
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Return session if logged in
|
||||
return {
|
||||
props: { session },
|
||||
};
|
||||
};
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
|
@ -46,23 +26,27 @@ const Home: NextPage = () => {
|
|||
export default Home;
|
||||
|
||||
const HomePageBody = () => {
|
||||
const { data: sessionData } = useSession();
|
||||
const { isLoaded, user } = useUser();
|
||||
const [joinRoomTextBox, setJoinRoomTextBox] = useState<string>("");
|
||||
const [tabIndex, setTabIndex] = useState<number>();
|
||||
|
||||
useEffect(() => {
|
||||
const tabIndexLocal = localStorage.getItem(`dashboardTabIndex`);
|
||||
setTabIndex(tabIndexLocal !== null ? Number(tabIndexLocal) : 0);
|
||||
}, [tabIndex, sessionData]);
|
||||
}, [tabIndex, user]);
|
||||
|
||||
return (
|
||||
return !isLoaded ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-4xl font-bold mx-auto">
|
||||
Hi, {sessionData?.user.name}!{" "}
|
||||
{sessionData?.user.isAdmin && (
|
||||
Hi, {user?.fullName}!{" "}
|
||||
{(user?.publicMetadata.isAdmin as boolean | undefined) && (
|
||||
<FaShieldAlt className="inline-block text-primary" />
|
||||
)}
|
||||
{sessionData?.user.isVIP && (
|
||||
{(user?.publicMetadata.isVIP as boolean | undefined) && (
|
||||
<GiStarFormation className="inline-block text-secondary" />
|
||||
)}
|
||||
</h1>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import Stats from "~/components/Stats";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
|
@ -55,13 +54,6 @@ const HomePageBody = () => {
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card card-compact bg-secondary text-black font-bold text-left">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">Stats:</h2>
|
||||
<Stats />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,205 +0,0 @@
|
|||
import { type GetServerSideProps, type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
|
||||
import { signIn, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaShieldAlt } from "react-icons/fa";
|
||||
import { SiGithub, SiGoogle } from "react-icons/si";
|
||||
import { api } from "~/utils/api";
|
||||
import { getServerAuthSession } from "../../server/auth";
|
||||
import { GiStarFormation } from "react-icons/gi";
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
// Redirect to login if not signed in
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Return session if logged in
|
||||
return {
|
||||
props: { session },
|
||||
};
|
||||
};
|
||||
|
||||
const Profile: NextPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Sprint Padawan - Profile</title>
|
||||
<meta name="description" content="Plan. Sprint. Repeat." />
|
||||
</Head>
|
||||
<div className="flex flex-col items-center justify-center text-center gap-12 px-4 py-16 ">
|
||||
<div className="flex flex-col items-center">
|
||||
<ProfileBody />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
||||
|
||||
const ProfileBody = () => {
|
||||
const { data: sessionData } = useSession();
|
||||
const [nameText, setNameText] = useState<string>("");
|
||||
const router = useRouter();
|
||||
|
||||
const { data: providers, isLoading: providersLoading } =
|
||||
api.user.getProviders.useQuery();
|
||||
|
||||
const deleteUserMutation = api.user.delete.useMutation({});
|
||||
const saveUserMutation = api.user.save.useMutation({});
|
||||
|
||||
const deleteCurrentUser = async () => {
|
||||
await deleteUserMutation.mutateAsync();
|
||||
(document.querySelector("#delete-user-modal") as HTMLInputElement).checked =
|
||||
false;
|
||||
router.reload();
|
||||
};
|
||||
|
||||
const saveUser = async () => {
|
||||
await saveUserMutation.mutateAsync({
|
||||
name: nameText,
|
||||
});
|
||||
router.reload();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNameText(sessionData?.user.name || "");
|
||||
}, [sessionData]);
|
||||
|
||||
if (sessionData) {
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="delete-user-modal"
|
||||
className="modal-toggle"
|
||||
/>
|
||||
<div className="modal modal-bottom sm:modal-middle">
|
||||
<div className="modal-box flex-col flex text-center justify-center items-center">
|
||||
<label
|
||||
htmlFor="delete-user-modal"
|
||||
className="btn btn-sm btn-circle absolute right-2 top-2"
|
||||
>
|
||||
✕
|
||||
</label>
|
||||
|
||||
<h3 className="font-bold text-lg text-error">
|
||||
This action will delete ALL data associated with your account. The
|
||||
same GitHub Account can be used, but none of your existing data
|
||||
will be available. If you are sure, please confirm below:
|
||||
</h3>
|
||||
|
||||
<div className="modal-action">
|
||||
<label
|
||||
htmlFor="delete-user-modal"
|
||||
className="btn btn-error"
|
||||
onClick={() => void deleteCurrentUser()}
|
||||
>
|
||||
I am sure!
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card w-90 bg-neutral shadow-xl">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">Profile:</h2>
|
||||
{sessionData.user.image && (
|
||||
<Image
|
||||
className="mx-auto"
|
||||
src={sessionData.user.image}
|
||||
alt="Profile picture."
|
||||
height={100}
|
||||
width={100}
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-row flex-wrap items-center text-center justify-center gap-4">
|
||||
{sessionData.user.isAdmin && (
|
||||
<div className="tooltip tooltip-primary" data-tip="Admin">
|
||||
<FaShieldAlt className="text-xl text-primary" />
|
||||
</div>
|
||||
)}
|
||||
{sessionData.user.isVIP && (
|
||||
<div className="tooltip tooltip-secondary" data-tip="VIP">
|
||||
<GiStarFormation className="inline-block text-xl text-secondary" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{providersLoading ? (
|
||||
<div className="mx-auto">
|
||||
<span className="loading loading-dots loading-lg"></span>{" "}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-auto">
|
||||
<button
|
||||
className={`btn btn-square btn-outline mx-2`}
|
||||
disabled={providers?.includes("github")}
|
||||
onClick={() => void signIn("github")}
|
||||
>
|
||||
<SiGithub />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`btn btn-square btn-outline mx-2`}
|
||||
disabled={providers?.includes("google")}
|
||||
onClick={() => void signIn("google")}
|
||||
>
|
||||
<SiGoogle />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sessionData.user.name && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
className="input input-bordered"
|
||||
value={nameText}
|
||||
onChange={(event) => setNameText(event.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sessionData.user.email && (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Email"
|
||||
className="input input-bordered"
|
||||
value={sessionData.user.email}
|
||||
disabled
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => void saveUser()}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
Save Account
|
||||
</button>
|
||||
|
||||
{/* <button className="btn btn-error">Delete Account</button> */}
|
||||
|
||||
<label htmlFor="delete-user-modal" className="btn btn-error">
|
||||
Delete Account
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return <h1>Error getting login session!</h1>;
|
||||
}
|
||||
};
|
|
@ -1,10 +1,9 @@
|
|||
import { type GetServerSideProps, type NextPage } from "next";
|
||||
import { type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { EventTypes } from "~/utils/types";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
IoCheckmarkCircleOutline,
|
||||
|
@ -17,10 +16,7 @@ import {
|
|||
IoSaveOutline,
|
||||
} from "react-icons/io5";
|
||||
import { GiStarFormation } from "react-icons/gi";
|
||||
import { z } from "zod";
|
||||
import { api } from "~/utils/api";
|
||||
import { getServerAuthSession } from "../../server/auth";
|
||||
|
||||
import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks";
|
||||
import Link from "next/link";
|
||||
import { FaShieldAlt } from "react-icons/fa";
|
||||
|
@ -28,27 +24,10 @@ import { RiVipCrownFill } from "react-icons/ri";
|
|||
import { env } from "~/env.mjs";
|
||||
import { downloadCSV } from "~/utils/helpers";
|
||||
import type { PresenceItem } from "~/utils/types";
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
// Redirect to login if not signed in
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Return session if logged in
|
||||
return {
|
||||
props: { session },
|
||||
};
|
||||
};
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
const Room: NextPage = () => {
|
||||
const { isSignedIn } = useUser();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
@ -57,7 +36,13 @@ const Room: NextPage = () => {
|
|||
<meta http-equiv="Cache-control" content="no-cache" />
|
||||
</Head>
|
||||
<div className="flex flex-col items-center justify-center text-center gap-2">
|
||||
<RoomBody />
|
||||
{!isSignedIn ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
</div>
|
||||
) : (
|
||||
<RoomBody />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -66,9 +51,9 @@ const Room: NextPage = () => {
|
|||
export default Room;
|
||||
|
||||
const RoomBody = ({}) => {
|
||||
const { data: sessionData } = useSession();
|
||||
const { isSignedIn, user } = useUser();
|
||||
const { query } = useRouter();
|
||||
const roomId = z.string().parse(query.id);
|
||||
const roomId = query.id as string;
|
||||
|
||||
const [storyNameText, setStoryNameText] = useState<string>("");
|
||||
const [roomScale, setRoomScale] = useState<string>("");
|
||||
|
@ -85,7 +70,7 @@ const RoomBody = ({}) => {
|
|||
|
||||
configureAbly({
|
||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
||||
clientId: sessionData?.user.id,
|
||||
clientId: user?.id,
|
||||
recover: (_, cb) => {
|
||||
cb(true);
|
||||
},
|
||||
|
@ -108,11 +93,11 @@ const RoomBody = ({}) => {
|
|||
const [presenceData] = usePresence<PresenceItem>(
|
||||
`${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
|
||||
{
|
||||
name: sessionData?.user.name || "",
|
||||
image: sessionData?.user.image || "",
|
||||
client_id: sessionData?.user.id || "",
|
||||
isAdmin: sessionData?.user.isAdmin || false,
|
||||
isVIP: sessionData?.user.isVIP || false,
|
||||
name: user?.fullName || "",
|
||||
image: user?.imageUrl || "",
|
||||
client_id: user?.id || "",
|
||||
isAdmin: (user?.publicMetadata.isAdmin as boolean | undefined) || false,
|
||||
isVIP: (user?.publicMetadata.isVIP as boolean | undefined) || false,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -129,19 +114,16 @@ const RoomBody = ({}) => {
|
|||
|
||||
// Init story name
|
||||
useEffect(() => {
|
||||
if (sessionData && roomFromDb) {
|
||||
if (isSignedIn && roomFromDb) {
|
||||
setStoryNameText(roomFromDb.storyName || "");
|
||||
setRoomScale(roomFromDb.scale || "ERROR");
|
||||
}
|
||||
}, [roomFromDb, roomId, sessionData]);
|
||||
}, [roomFromDb, roomId, isSignedIn, user]);
|
||||
|
||||
// Helper functions
|
||||
const getVoteForCurrentUser = () => {
|
||||
if (roomFromDb && sessionData) {
|
||||
return (
|
||||
votesFromDb &&
|
||||
votesFromDb.find((vote) => vote.userId === sessionData.user.id)
|
||||
);
|
||||
if (roomFromDb && isSignedIn) {
|
||||
return votesFromDb && votesFromDb.find((vote) => vote.userId === user.id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -183,16 +165,16 @@ const RoomBody = ({}) => {
|
|||
})
|
||||
.concat({
|
||||
id: "LATEST",
|
||||
createdAt: new Date(),
|
||||
userId: roomFromDb.owner.id,
|
||||
created_at: new Date(),
|
||||
userId: roomFromDb.userId,
|
||||
roomId: roomFromDb.id,
|
||||
scale: roomScale,
|
||||
votes: votesFromDb.map((vote) => {
|
||||
return {
|
||||
name: vote.owner.name,
|
||||
value: vote.value,
|
||||
};
|
||||
}),
|
||||
room: roomFromDb,
|
||||
roomName: roomFromDb.roomName,
|
||||
storyName: storyNameText,
|
||||
});
|
||||
|
@ -365,111 +347,108 @@ const RoomBody = ({}) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{sessionData &&
|
||||
!!roomFromDb &&
|
||||
roomFromDb.userId === sessionData.user.id && (
|
||||
<>
|
||||
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
|
||||
<div className="card-body flex flex-col flex-wrap">
|
||||
<h2 className="card-title mx-auto">Room Settings</h2>
|
||||
{isSignedIn && !!roomFromDb && roomFromDb.userId === user.id && (
|
||||
<>
|
||||
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
|
||||
<div className="card-body flex flex-col flex-wrap">
|
||||
<h2 className="card-title mx-auto">Room Settings</h2>
|
||||
|
||||
<label className="label mx-auto">
|
||||
{"Vote Scale (Comma Separated):"}{" "}
|
||||
</label>
|
||||
<label className="label mx-auto">
|
||||
{"Vote Scale (Comma Separated):"}{" "}
|
||||
</label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Scale (Comma Separated)"
|
||||
className="input input-bordered m-auto"
|
||||
value={roomScale}
|
||||
onChange={(event) => {
|
||||
setRoomScale(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Scale (Comma Separated)"
|
||||
className="input input-bordered m-auto"
|
||||
value={roomScale}
|
||||
onChange={(event) => {
|
||||
setRoomScale(event.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<label className="label mx-auto">{"Story Name:"} </label>
|
||||
<label className="label mx-auto">{"Story Name:"} </label>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Story Name"
|
||||
className="input input-bordered m-auto"
|
||||
value={storyNameText}
|
||||
onChange={(event) => {
|
||||
setStoryNameText(event.target.value);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Story Name"
|
||||
className="input input-bordered m-auto"
|
||||
value={storyNameText}
|
||||
onChange={(event) => {
|
||||
setStoryNameText(event.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
||||
<div>
|
||||
<button
|
||||
onClick={() => saveRoom(!roomFromDb.visible, false)}
|
||||
className="btn btn-primary inline-flex"
|
||||
>
|
||||
{roomFromDb.visible ? (
|
||||
<>
|
||||
<IoEyeOffOutline className="text-xl mr-1" />
|
||||
Hide
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IoEyeOutline className="text-xl mr-1" />
|
||||
Show
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
onClick={() =>
|
||||
saveRoom(
|
||||
false,
|
||||
true,
|
||||
roomFromDb.storyName === storyNameText ||
|
||||
votesFromDb?.length === 0
|
||||
? false
|
||||
: true
|
||||
)
|
||||
}
|
||||
className="btn btn-primary inline-flex"
|
||||
disabled={
|
||||
[...new Set(roomScale.split(","))].filter(
|
||||
(item) => item !== ""
|
||||
).length <= 1
|
||||
}
|
||||
>
|
||||
{roomFromDb.storyName === storyNameText ||
|
||||
votesFromDb?.length === 0 ? (
|
||||
<>
|
||||
<IoReloadOutline className="text-xl mr-1" /> Reset
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IoSaveOutline className="text-xl mr-1" /> Save
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{votesFromDb &&
|
||||
(roomFromDb.logs.length > 0 ||
|
||||
votesFromDb.length > 0) && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => downloadLogs()}
|
||||
className="btn btn-primary inline-flex hover:animate-pulse"
|
||||
>
|
||||
<>
|
||||
<IoDownloadOutline className="text-xl" />
|
||||
</>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
||||
<div>
|
||||
<button
|
||||
onClick={() => saveRoom(!roomFromDb.visible, false)}
|
||||
className="btn btn-primary inline-flex"
|
||||
>
|
||||
{roomFromDb.visible ? (
|
||||
<>
|
||||
<IoEyeOffOutline className="text-xl mr-1" />
|
||||
Hide
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IoEyeOutline className="text-xl mr-1" />
|
||||
Show
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
onClick={() =>
|
||||
saveRoom(
|
||||
false,
|
||||
true,
|
||||
roomFromDb.storyName === storyNameText ||
|
||||
votesFromDb?.length === 0
|
||||
? false
|
||||
: true
|
||||
)
|
||||
}
|
||||
className="btn btn-primary inline-flex"
|
||||
disabled={
|
||||
[...new Set(roomScale.split(","))].filter(
|
||||
(item) => item !== ""
|
||||
).length <= 1
|
||||
}
|
||||
>
|
||||
{roomFromDb.storyName === storyNameText ||
|
||||
votesFromDb?.length === 0 ? (
|
||||
<>
|
||||
<IoReloadOutline className="text-xl mr-1" /> Reset
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<IoSaveOutline className="text-xl mr-1" /> Save
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{votesFromDb &&
|
||||
(roomFromDb.logs.length > 0 || votesFromDb.length > 0) && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => downloadLogs()}
|
||||
className="btn btn-primary inline-flex hover:animate-pulse"
|
||||
>
|
||||
<>
|
||||
<IoDownloadOutline className="text-xl" />
|
||||
</>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
// Room does not exist
|
||||
|
|
17
src/pages/sign-in/[[...index]].tsx
Normal file
17
src/pages/sign-in/[[...index]].tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { SignIn } from "@clerk/nextjs";
|
||||
|
||||
const SignInPage = () => (
|
||||
<div style={styles}>
|
||||
<SignIn path="/sign-in" routing="path" signUpUrl="/sign-up" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SignInPage;
|
||||
|
||||
const styles = {
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
};
|
17
src/pages/sign-up/[[...index]].tsx
Normal file
17
src/pages/sign-up/[[...index]].tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { SignUp } from "@clerk/nextjs";
|
||||
|
||||
const SignUpPage = () => (
|
||||
<div style={styles}>
|
||||
<SignUp path="/sign-up" routing="path" signInUrl="/sign-in" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SignUpPage;
|
||||
|
||||
const styles = {
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
};
|
|
@ -1,7 +1,5 @@
|
|||
import { roomRouter } from "~/server/api/routers/room";
|
||||
import { createTRPCRouter } from "~/server/api/trpc";
|
||||
import { sessionRouter } from "./routers/session";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { voteRouter } from "./routers/vote";
|
||||
import { restRouter } from "./routers/rest";
|
||||
|
||||
|
@ -13,8 +11,6 @@ import { restRouter } from "./routers/rest";
|
|||
export const appRouter = createTRPCRouter({
|
||||
room: roomRouter,
|
||||
vote: voteRouter,
|
||||
user: userRouter,
|
||||
session: sessionRouter,
|
||||
rest: restRouter,
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import { z } from "zod";
|
|||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { fetchCache, setCache } from "~/server/redis";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { rooms, votes } from "~/server/schema";
|
||||
|
||||
export const restRouter = createTRPCRouter({
|
||||
dbWarmer: publicProcedure
|
||||
|
@ -13,7 +15,7 @@ export const restRouter = createTRPCRouter({
|
|||
.query(async ({ ctx, input }) => {
|
||||
const isValidKey = await validateApiKey(input.key);
|
||||
if (isValidKey) {
|
||||
await ctx.prisma.verificationToken.findMany();
|
||||
await ctx.db.query.votes.findMany();
|
||||
return "Toasted the DB";
|
||||
} else {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
@ -30,7 +32,11 @@ export const restRouter = createTRPCRouter({
|
|||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
} else {
|
||||
const votesCount = await ctx.prisma.vote.count();
|
||||
const votesResult = (
|
||||
await ctx.db.select({ count: sql<number>`count(*)` }).from(votes)
|
||||
)[0];
|
||||
|
||||
const votesCount = votesResult ? Number(votesResult.count) : 0;
|
||||
|
||||
await setCache(`kv_votecount`, votesCount);
|
||||
|
||||
|
@ -38,24 +44,6 @@ export const restRouter = createTRPCRouter({
|
|||
}
|
||||
}),
|
||||
|
||||
userCount: publicProcedure
|
||||
.meta({ openapi: { method: "GET", path: "/rest/users/count" } })
|
||||
.input(z.void())
|
||||
.output(z.number())
|
||||
.query(async ({ ctx }) => {
|
||||
const cachedResult = await fetchCache<number>(`kv_usercount`);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
} else {
|
||||
const usersCount = await ctx.prisma.user.count();
|
||||
|
||||
await setCache(`kv_usercount`, usersCount);
|
||||
|
||||
return usersCount;
|
||||
}
|
||||
}),
|
||||
|
||||
roomCount: publicProcedure
|
||||
.meta({ openapi: { method: "GET", path: "/rest/rooms/count" } })
|
||||
.input(z.void())
|
||||
|
@ -66,7 +54,11 @@ export const restRouter = createTRPCRouter({
|
|||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
} else {
|
||||
const roomsCount = await ctx.prisma.room.count();
|
||||
const roomsResult = (
|
||||
await ctx.db.select({ count: sql<number>`count(*)` }).from(rooms)
|
||||
)[0];
|
||||
|
||||
const roomsCount = roomsResult ? Number(roomsResult.count) : 0;
|
||||
|
||||
await setCache(`kv_roomcount`, roomsCount);
|
||||
|
||||
|
|
|
@ -3,7 +3,10 @@ import { publishToChannel } from "~/server/ably";
|
|||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
|
||||
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
|
||||
import { logs, rooms, votes } from "~/server/schema";
|
||||
import { EventTypes } from "~/utils/types";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export const roomRouter = createTRPCRouter({
|
||||
// Create
|
||||
|
@ -14,57 +17,52 @@ export const roomRouter = createTRPCRouter({
|
|||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.session) {
|
||||
const room = await ctx.prisma.room.create({
|
||||
data: {
|
||||
userId: ctx.session.user.id,
|
||||
roomName: input.name,
|
||||
storyName: "First Story!",
|
||||
scale: "0.5,1,2,3,5,8",
|
||||
visible: false,
|
||||
},
|
||||
});
|
||||
if (room) {
|
||||
await invalidateCache(`kv_roomcount`);
|
||||
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
||||
const room = await ctx.db.insert(rooms).values({
|
||||
id: createId(),
|
||||
userId: ctx.auth.userId,
|
||||
roomName: input.name,
|
||||
storyName: "First Story!",
|
||||
scale: "0.5,1,2,3,5,8",
|
||||
visible: false,
|
||||
});
|
||||
|
||||
await publishToChannel(
|
||||
`${ctx.session.user.id}`,
|
||||
EventTypes.ROOM_LIST_UPDATE,
|
||||
JSON.stringify(room)
|
||||
);
|
||||
const success = room.rowsAffected > 0;
|
||||
if (room) {
|
||||
await invalidateCache(`kv_roomcount`);
|
||||
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
|
||||
|
||||
await publishToChannel(
|
||||
`stats`,
|
||||
EventTypes.STATS_UPDATE,
|
||||
JSON.stringify(room)
|
||||
);
|
||||
}
|
||||
// happy path
|
||||
return !!room;
|
||||
await publishToChannel(
|
||||
`${ctx.auth.userId}`,
|
||||
EventTypes.ROOM_LIST_UPDATE,
|
||||
JSON.stringify(room)
|
||||
);
|
||||
|
||||
await publishToChannel(
|
||||
`stats`,
|
||||
EventTypes.STATS_UPDATE,
|
||||
JSON.stringify(room)
|
||||
);
|
||||
}
|
||||
|
||||
// clinically depressed path
|
||||
return false;
|
||||
return success;
|
||||
}),
|
||||
|
||||
// Get One
|
||||
get: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(({ ctx, input }) => {
|
||||
return ctx.prisma.room.findUnique({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
logs: true,
|
||||
roomName: true,
|
||||
storyName: true,
|
||||
visible: true,
|
||||
scale: true,
|
||||
owner: true,
|
||||
return ctx.db.query.rooms.findFirst({
|
||||
where: eq(rooms.id, input.id),
|
||||
with: {
|
||||
logs: {
|
||||
with: {
|
||||
room: true,
|
||||
},
|
||||
},
|
||||
votes: {
|
||||
with: {
|
||||
room: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
@ -77,23 +75,16 @@ export const roomRouter = createTRPCRouter({
|
|||
createdAt: Date;
|
||||
roomName: string;
|
||||
}[]
|
||||
>(`kv_roomlist_${ctx.session.user.id}`);
|
||||
>(`kv_roomlist_${ctx.auth.userId}`);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
} else {
|
||||
const roomList = await ctx.prisma.room.findMany({
|
||||
where: {
|
||||
userId: ctx.session.user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
roomName: true,
|
||||
},
|
||||
const roomList = await ctx.db.query.rooms.findMany({
|
||||
where: eq(rooms.userId, ctx.auth.userId),
|
||||
});
|
||||
|
||||
await setCache(`kv_roomlist_${ctx.session.user.id}`, roomList);
|
||||
await setCache(`kv_roomlist_${ctx.auth.userId}`, roomList);
|
||||
|
||||
return roomList;
|
||||
}
|
||||
|
@ -114,119 +105,88 @@ export const roomRouter = createTRPCRouter({
|
|||
.mutation(async ({ ctx, input }) => {
|
||||
if (input.reset) {
|
||||
if (input.log) {
|
||||
const oldRoom = await ctx.prisma.room.findUnique({
|
||||
where: {
|
||||
id: input.roomId,
|
||||
},
|
||||
select: {
|
||||
roomName: true,
|
||||
storyName: true,
|
||||
scale: true,
|
||||
votes: {
|
||||
select: {
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
const oldRoom = await ctx.db.query.rooms.findFirst({
|
||||
where: eq(rooms.id, input.roomId),
|
||||
with: {
|
||||
votes: true,
|
||||
logs: true,
|
||||
},
|
||||
});
|
||||
|
||||
oldRoom &&
|
||||
(await ctx.prisma.log.create({
|
||||
data: {
|
||||
userId: ctx.session.user.id,
|
||||
roomId: input.roomId,
|
||||
scale: oldRoom.scale,
|
||||
votes: oldRoom.votes.map((vote) => {
|
||||
return {
|
||||
name: vote.owner.name,
|
||||
value: vote.value,
|
||||
};
|
||||
}),
|
||||
roomName: oldRoom.roomName,
|
||||
storyName: oldRoom.storyName,
|
||||
},
|
||||
(await ctx.db.insert(logs).values({
|
||||
id: createId(),
|
||||
userId: ctx.auth.userId,
|
||||
roomId: input.roomId,
|
||||
scale: oldRoom.scale,
|
||||
votes: oldRoom.votes.map((vote) => {
|
||||
return {
|
||||
name: vote.userId,
|
||||
value: vote.value,
|
||||
};
|
||||
}),
|
||||
roomName: oldRoom.roomName,
|
||||
storyName: oldRoom.storyName,
|
||||
}));
|
||||
}
|
||||
|
||||
await ctx.prisma.vote.deleteMany({
|
||||
where: {
|
||||
roomId: input.roomId,
|
||||
},
|
||||
});
|
||||
await ctx.db.delete(votes).where(eq(votes.roomId, input.roomId));
|
||||
|
||||
await invalidateCache(`kv_votes_${input.roomId}`);
|
||||
}
|
||||
|
||||
const newRoom = await ctx.prisma.room.update({
|
||||
where: {
|
||||
id: input.roomId,
|
||||
},
|
||||
data: {
|
||||
const newRoom = await ctx.db
|
||||
.update(rooms)
|
||||
.set({
|
||||
storyName: input.name,
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
visible: input.visible,
|
||||
scale: [...new Set(input.scale.split(","))]
|
||||
.filter((item) => item !== "")
|
||||
.toString(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
roomName: true,
|
||||
storyName: true,
|
||||
visible: true,
|
||||
scale: true,
|
||||
votes: {
|
||||
select: {
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
.where(eq(rooms.id, input.roomId));
|
||||
|
||||
if (newRoom) {
|
||||
const success = newRoom.rowsAffected > 0;
|
||||
|
||||
if (success) {
|
||||
await publishToChannel(
|
||||
`${newRoom.id}`,
|
||||
`${input.roomId}`,
|
||||
EventTypes.ROOM_UPDATE,
|
||||
JSON.stringify(newRoom)
|
||||
);
|
||||
}
|
||||
|
||||
return !!newRoom;
|
||||
return success;
|
||||
}),
|
||||
|
||||
// Delete One
|
||||
delete: protectedProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const deletedRoom = await ctx.prisma.room.delete({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
const deletedRoom = await ctx.db
|
||||
.delete(rooms)
|
||||
.where(eq(rooms.id, input.id));
|
||||
|
||||
const success = deletedRoom.rowsAffected > 0;
|
||||
|
||||
if (success) {
|
||||
await ctx.db.delete(votes).where(eq(votes.roomId, input.id));
|
||||
|
||||
await ctx.db.delete(logs).where(eq(logs.roomId, input.id));
|
||||
|
||||
if (deletedRoom) {
|
||||
await invalidateCache(`kv_roomcount`);
|
||||
await invalidateCache(`kv_votecount`);
|
||||
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
||||
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
|
||||
|
||||
await publishToChannel(
|
||||
`${ctx.session.user.id}`,
|
||||
`${ctx.auth.userId}`,
|
||||
EventTypes.ROOM_LIST_UPDATE,
|
||||
JSON.stringify(deletedRoom)
|
||||
);
|
||||
|
||||
await publishToChannel(
|
||||
`${deletedRoom.id}`,
|
||||
`${input.id}`,
|
||||
EventTypes.ROOM_UPDATE,
|
||||
JSON.stringify(deletedRoom)
|
||||
);
|
||||
|
@ -238,6 +198,6 @@ export const roomRouter = createTRPCRouter({
|
|||
);
|
||||
}
|
||||
|
||||
return !!deletedRoom;
|
||||
return success;
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import { z } from "zod";
|
||||
import { adminProcedure, createTRPCRouter } from "~/server/api/trpc";
|
||||
import { invalidateCache } from "~/server/redis";
|
||||
|
||||
export const sessionRouter = createTRPCRouter({
|
||||
deleteAllByUserId: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const sessions = await ctx.prisma.session.deleteMany({
|
||||
where: {
|
||||
userId: input.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!!sessions) {
|
||||
await invalidateCache(`kv_userlist_admin`);
|
||||
}
|
||||
|
||||
return !!sessions;
|
||||
}),
|
||||
deleteAll: adminProcedure.mutation(async ({ ctx }) => {
|
||||
const sessions = await ctx.prisma.session.deleteMany();
|
||||
|
||||
if (!!sessions) {
|
||||
await invalidateCache(`kv_userlist_admin`);
|
||||
}
|
||||
|
||||
return !!sessions;
|
||||
}),
|
||||
});
|
|
@ -1,194 +0,0 @@
|
|||
import type { User } from "@prisma/client";
|
||||
import { Resend } from "resend";
|
||||
import { z } from "zod";
|
||||
import { Goodbye } from "~/components/templates/Goodbye";
|
||||
import { env } from "~/env.mjs";
|
||||
import { publishToChannel } from "~/server/ably";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "~/server/api/trpc";
|
||||
|
||||
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
|
||||
import { EventTypes } from "~/utils/types";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
getProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||
const providers = await ctx.prisma.user.findUnique({
|
||||
where: {
|
||||
id: ctx.session.user.id,
|
||||
},
|
||||
select: {
|
||||
accounts: {
|
||||
select: {
|
||||
provider: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return providers?.accounts.map((account) => {
|
||||
return account.provider;
|
||||
});
|
||||
}),
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
const cachedResult = await fetchCache<
|
||||
{
|
||||
accounts: {
|
||||
provider: string;
|
||||
}[];
|
||||
sessions: {
|
||||
id: string;
|
||||
}[];
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
isAdmin: boolean;
|
||||
isVIP: boolean;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
}[]
|
||||
>(`kv_userlist_admin`);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult.map((user) => {
|
||||
return {
|
||||
...user,
|
||||
createdAt: new Date(user.createdAt),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const users = await ctx.prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
isAdmin: true,
|
||||
isVIP: true,
|
||||
createdAt: true,
|
||||
email: true,
|
||||
sessions: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
accounts: {
|
||||
select: {
|
||||
provider: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await setCache(`${env.APP_ENV}_kv_userlist_admin`, users);
|
||||
|
||||
return users;
|
||||
}
|
||||
}),
|
||||
delete: protectedProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
userId: z.string(),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
let user: User;
|
||||
if (input?.userId && ctx.session.user.isAdmin) {
|
||||
user = await ctx.prisma.user.delete({
|
||||
where: {
|
||||
id: input.userId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
user = await ctx.prisma.user.delete({
|
||||
where: {
|
||||
id: ctx.session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!!user && user.name && user.email) {
|
||||
await resend.emails.send({
|
||||
from: "Sprint Padawan <no-reply@sprintpadawan.dev>",
|
||||
to: user.email,
|
||||
subject: "Sorry to see you go... 😭",
|
||||
react: Goodbye({ name: user.name }),
|
||||
});
|
||||
|
||||
await invalidateCache(`kv_usercount`);
|
||||
await invalidateCache(`kv_userlist_admin`);
|
||||
|
||||
await publishToChannel(
|
||||
`stats`,
|
||||
EventTypes.STATS_UPDATE,
|
||||
JSON.stringify(user)
|
||||
);
|
||||
}
|
||||
|
||||
return !!user;
|
||||
}),
|
||||
save: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await ctx.prisma.user.update({
|
||||
where: {
|
||||
id: ctx.session.user.id,
|
||||
},
|
||||
data: {
|
||||
name: input.name,
|
||||
},
|
||||
});
|
||||
|
||||
return !!user;
|
||||
}),
|
||||
setAdmin: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
value: z.boolean(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await ctx.prisma.user.update({
|
||||
where: {
|
||||
id: input.userId,
|
||||
},
|
||||
data: {
|
||||
isAdmin: input.value,
|
||||
},
|
||||
});
|
||||
|
||||
await invalidateCache(`kv_userlist_admin`);
|
||||
|
||||
return !!user;
|
||||
}),
|
||||
|
||||
setVIP: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
value: z.boolean(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await ctx.prisma.user.update({
|
||||
where: {
|
||||
id: input.userId,
|
||||
},
|
||||
data: {
|
||||
isVIP: input.value,
|
||||
},
|
||||
});
|
||||
|
||||
await invalidateCache(`kv_userlist_admin`);
|
||||
|
||||
return !!user;
|
||||
}),
|
||||
});
|
|
@ -1,10 +1,12 @@
|
|||
import { z } from "zod";
|
||||
import { publishToChannel } from "~/server/ably";
|
||||
|
||||
import type { Room } from "@prisma/client";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
|
||||
import { EventTypes } from "~/utils/types";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { votes } from "~/server/schema";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
export const voteRouter = createTRPCRouter({
|
||||
getAllByRoomId: protectedProcedure
|
||||
|
@ -12,14 +14,10 @@ export const voteRouter = createTRPCRouter({
|
|||
.query(async ({ ctx, input }) => {
|
||||
const cachedResult = await fetchCache<
|
||||
{
|
||||
value: string;
|
||||
room: Room;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
value: string;
|
||||
created_at: Date;
|
||||
userId: string;
|
||||
owner: {
|
||||
name: string | null;
|
||||
};
|
||||
roomId: string;
|
||||
}[]
|
||||
>(`kv_votes_${input.roomId}`);
|
||||
|
@ -27,23 +25,8 @@ export const voteRouter = createTRPCRouter({
|
|||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
} else {
|
||||
const votesByRoomId = await ctx.prisma.vote.findMany({
|
||||
where: {
|
||||
roomId: input.roomId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
room: true,
|
||||
roomId: true,
|
||||
userId: true,
|
||||
value: true,
|
||||
},
|
||||
const votesByRoomId = await ctx.db.query.votes.findMany({
|
||||
where: eq(votes.roomId, input.roomId),
|
||||
});
|
||||
|
||||
await setCache(`kv_votes_${input.roomId}`, votesByRoomId);
|
||||
|
@ -54,42 +37,34 @@ export const voteRouter = createTRPCRouter({
|
|||
set: protectedProcedure
|
||||
.input(z.object({ value: z.string(), roomId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const vote = await ctx.prisma.vote.upsert({
|
||||
where: {
|
||||
userId_roomId: {
|
||||
roomId: input.roomId,
|
||||
userId: ctx.session.user.id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
const updateResult = await ctx.db
|
||||
.update(votes)
|
||||
.set({
|
||||
value: input.value,
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
roomId: input.roomId,
|
||||
},
|
||||
update: {
|
||||
value: input.value,
|
||||
userId: ctx.session.user.id,
|
||||
roomId: input.roomId,
|
||||
},
|
||||
select: {
|
||||
value: true,
|
||||
userId: true,
|
||||
roomId: true,
|
||||
id: true,
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
.where(eq(votes.userId, ctx.auth.userId));
|
||||
|
||||
if (vote) {
|
||||
let success = updateResult.rowsAffected > 0;
|
||||
|
||||
if (!success) {
|
||||
const vote = await ctx.db.insert(votes).ignore().values({
|
||||
id: createId(),
|
||||
value: input.value,
|
||||
userId: ctx.auth.userId,
|
||||
roomId: input.roomId,
|
||||
});
|
||||
|
||||
success = vote.rowsAffected > 0;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
await invalidateCache(`kv_votecount`);
|
||||
await invalidateCache(`kv_votes_${input.roomId}`);
|
||||
|
||||
await publishToChannel(
|
||||
`${vote.roomId}`,
|
||||
`${input.roomId}`,
|
||||
EventTypes.VOTE_UPDATE,
|
||||
input.value
|
||||
);
|
||||
|
@ -97,10 +72,10 @@ export const voteRouter = createTRPCRouter({
|
|||
await publishToChannel(
|
||||
`stats`,
|
||||
EventTypes.STATS_UPDATE,
|
||||
JSON.stringify(vote)
|
||||
JSON.stringify(success)
|
||||
);
|
||||
}
|
||||
|
||||
return !!vote;
|
||||
return success;
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,81 +1,71 @@
|
|||
/**
|
||||
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
||||
* 1. You want to modify request context (see Part 1).
|
||||
* 2. You want to create a new middleware or type of procedure (see Part 3).
|
||||
* 1. You want to modify request context (see Part 1)
|
||||
* 2. You want to create a new middleware or type of procedure (see Part 3)
|
||||
*
|
||||
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
||||
* need to use are documented accordingly near the end.
|
||||
* tl;dr - this is where all the tRPC server stuff is created and plugged in.
|
||||
* The pieces you will need to use are documented accordingly near the end
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. CONTEXT
|
||||
*
|
||||
* This section defines the "contexts" that are available in the backend API.
|
||||
* This section defines the "contexts" that are available in the backend API
|
||||
*
|
||||
* These allow you to access things like the database, the session, etc, when
|
||||
* processing a request
|
||||
*
|
||||
* These allow you to access things when processing a request, like the database, the session, etc.
|
||||
*/
|
||||
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import { type Session } from "next-auth";
|
||||
import { getAuth } from "@clerk/nextjs/server";
|
||||
import type {
|
||||
SignedInAuthObject,
|
||||
SignedOutAuthObject,
|
||||
} from "@clerk/nextjs/api";
|
||||
|
||||
import { getServerAuthSession } from "~/server/auth";
|
||||
import { prisma } from "~/server/db";
|
||||
|
||||
type CreateContextOptions = {
|
||||
session: Session | null;
|
||||
ip: string | undefined;
|
||||
};
|
||||
import { db } from "../db";
|
||||
|
||||
interface AuthContext {
|
||||
auth: SignedInAuthObject | SignedOutAuthObject;
|
||||
}
|
||||
/**
|
||||
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
|
||||
* it from here.
|
||||
* This helper generates the "internals" for a tRPC context. If you need to use
|
||||
* it, you can export it from here
|
||||
*
|
||||
* Examples of things you may need it for:
|
||||
* - testing, so we don't have to mock Next.js' req/res
|
||||
* - tRPC's `createSSGHelpers`, where we don't have req/res
|
||||
*
|
||||
* - testing, so we dont have to mock Next.js' req/res
|
||||
* - trpc's `createSSGHelpers` where we don't have req/res
|
||||
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
|
||||
*/
|
||||
const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||
const createInnerTRPCContext = ({ auth }: AuthContext) => {
|
||||
return {
|
||||
session: opts.session,
|
||||
ip: opts.ip,
|
||||
prisma,
|
||||
auth,
|
||||
db,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the actual context you will use in your router. It will be used to process every request
|
||||
* that goes through your tRPC endpoint.
|
||||
*
|
||||
* @see https://trpc.io/docs/context
|
||||
* This is the actual context you'll use in your router. It will be used to
|
||||
* process every request that goes through your tRPC endpoint
|
||||
* @link https://trpc.io/docs/context
|
||||
*/
|
||||
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||
const { req, res } = opts;
|
||||
|
||||
// Get the session from the server using the getServerSession wrapper function
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
|
||||
return createInnerTRPCContext({
|
||||
ip: req.socket.remoteAddress,
|
||||
session,
|
||||
});
|
||||
export const createTRPCContext = (opts: CreateNextContextOptions) => {
|
||||
return createInnerTRPCContext({ auth: getAuth(opts.req) });
|
||||
};
|
||||
|
||||
/**
|
||||
* 2. INITIALIZATION
|
||||
*
|
||||
* This is where the tRPC API is initialized, connecting the context and transformer.
|
||||
* This is where the trpc api is initialized, connecting the context and
|
||||
* transformer
|
||||
*/
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import type { OpenApiMeta } from "trpc-openapi";
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import superjson from "superjson";
|
||||
import { env } from "~/env.mjs";
|
||||
import { Redis } from "@upstash/redis";
|
||||
import type { OpenApiMeta } from "trpc-openapi";
|
||||
|
||||
const t = initTRPC
|
||||
.meta<OpenApiMeta>()
|
||||
.context<typeof createTRPCContext>()
|
||||
.meta<OpenApiMeta>()
|
||||
.create({
|
||||
transformer: superjson,
|
||||
errorFormatter({ shape }) {
|
||||
|
@ -83,91 +73,37 @@ const t = initTRPC
|
|||
},
|
||||
});
|
||||
|
||||
// check if the user is signed in, otherwise through a UNAUTHORIZED CODE
|
||||
const isAuthed = t.middleware(({ next, ctx }) => {
|
||||
if (!ctx.auth.userId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
auth: ctx.auth,
|
||||
},
|
||||
});
|
||||
});
|
||||
/**
|
||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||
*
|
||||
* These are the pieces you use to build your tRPC API. You should import these a lot in the
|
||||
* "/src/server/api/routers" directory.
|
||||
* These are the pieces you use to build your tRPC API. You should import these
|
||||
* a lot in the /src/server/api/routers folder
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is how you create new routers and sub-routers in your tRPC API.
|
||||
*
|
||||
* This is how you create new routers and subrouters in your tRPC API
|
||||
* @see https://trpc.io/docs/router
|
||||
*/
|
||||
export const createTRPCRouter = t.router;
|
||||
|
||||
/**
|
||||
* Public (unauthenticated) procedure
|
||||
* Public (unauthed) procedure
|
||||
*
|
||||
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
|
||||
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||
* are logged in.
|
||||
* This is the base piece you use to build new queries and mutations on your
|
||||
* tRPC API. It does not guarantee that a user querying is authorized, but you
|
||||
* can still access user session data if they are logged in
|
||||
*/
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||
const enforceAuthSession = t.middleware(async ({ ctx, next }) => {
|
||||
// Auth
|
||||
if (!ctx.session || !ctx.session.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const rateLimit = new Ratelimit({
|
||||
redis: Redis.fromEnv(),
|
||||
limiter: Ratelimit.slidingWindow(
|
||||
Number(env.UPSTASH_RATELIMIT_REQUESTS),
|
||||
`${Number(env.UPSTASH_RATELIMIT_SECONDS)}s`
|
||||
),
|
||||
analytics: true,
|
||||
});
|
||||
|
||||
const { success } = await rateLimit.limit(
|
||||
`${env.APP_ENV}_${ctx.session?.user.id}`
|
||||
);
|
||||
|
||||
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const enforceAdminRole = t.middleware(async ({ ctx, next }) => {
|
||||
if (!ctx.session || !ctx.session.user || !ctx.session?.user.isAdmin)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
session: { ...ctx.session, user: ctx.session.user },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// const enforceApiToken = t.middleware(async ({ ctx, next, path }) => {
|
||||
// const res = await unkey.keys.verify({
|
||||
// key: ""
|
||||
// })
|
||||
// if (!ctx.session || !ctx.session.user || !ctx.session?.user.isAdmin)
|
||||
// throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// return next({
|
||||
// ctx: {
|
||||
// session: { ...ctx.session, user: ctx.session.user },
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
|
||||
/**
|
||||
* Protected (authenticated) procedure
|
||||
*
|
||||
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
|
||||
* the session is valid and guarantees `ctx.session.user` is not null.
|
||||
*
|
||||
* @see https://trpc.io/docs/procedures
|
||||
*/
|
||||
export const protectedProcedure = t.procedure.use(enforceAuthSession);
|
||||
|
||||
export const adminProcedure = t.procedure.use(enforceAdminRole);
|
||||
export const protectedProcedure = t.procedure.use(isAuthed);
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import { type GetServerSidePropsContext } from "next";
|
||||
import {
|
||||
getServerSession,
|
||||
type DefaultSession,
|
||||
type NextAuthOptions,
|
||||
} from "next-auth";
|
||||
import GithubProvider from "next-auth/providers/github";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import { Resend } from "resend";
|
||||
import { env } from "~/env.mjs";
|
||||
import { prisma } from "~/server/db";
|
||||
import { Welcome } from "../components/templates/Welcome";
|
||||
import { invalidateCache } from "./redis";
|
||||
import { publishToChannel } from "./ably";
|
||||
import { EventTypes } from "~/utils/types";
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
isAdmin: boolean;
|
||||
isVIP: boolean;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
interface User {
|
||||
isAdmin: boolean;
|
||||
isVIP: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
|
||||
*
|
||||
* @see https://next-auth.js.org/configuration/options
|
||||
*/
|
||||
export const authOptions: NextAuthOptions = {
|
||||
callbacks: {
|
||||
session({ session, user }) {
|
||||
if (session.user) {
|
||||
session.user.id = user.id;
|
||||
session.user.isAdmin = user.isAdmin;
|
||||
session.user.isVIP = user.isVIP;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
events: {
|
||||
async createUser({ user }) {
|
||||
if (user && user.name && user.email) {
|
||||
await resend.sendEmail({
|
||||
from: "no-reply@sprintpadawan.dev",
|
||||
to: user.email,
|
||||
subject: "🎉 Welcome to Sprint Padawan! 🎉",
|
||||
//@ts-ignore: IDK why this doesn't work...
|
||||
|
||||
react: Welcome({ name: user.name }),
|
||||
});
|
||||
await invalidateCache(`kv_userlist_admin`);
|
||||
await invalidateCache(`kv_usercount`);
|
||||
|
||||
await publishToChannel(
|
||||
`stats`,
|
||||
EventTypes.STATS_UPDATE,
|
||||
JSON.stringify(user)
|
||||
);
|
||||
}
|
||||
},
|
||||
async signIn({}) {
|
||||
await invalidateCache(`kv_userlist_admin`);
|
||||
},
|
||||
async signOut() {
|
||||
await invalidateCache(`kv_userlist_admin`);
|
||||
},
|
||||
},
|
||||
// @ts-ignore This adapter should work...
|
||||
adapter: PrismaAdapter(prisma),
|
||||
providers: [
|
||||
GithubProvider({
|
||||
clientId: env.GITHUB_CLIENT_ID,
|
||||
clientSecret: env.GITHUB_CLIENT_SECRET,
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
/**
|
||||
* ...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
|
||||
*/
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
|
||||
*
|
||||
* @see https://next-auth.js.org/configuration/nextjs
|
||||
*/
|
||||
export const getServerAuthSession = (ctx: {
|
||||
req: GetServerSidePropsContext["req"];
|
||||
res: GetServerSidePropsContext["res"];
|
||||
}) => {
|
||||
return getServerSession(ctx.req, ctx.res, authOptions);
|
||||
};
|
|
@ -1,14 +1,11 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { drizzle } from "drizzle-orm/planetscale-serverless";
|
||||
import { connect } from "@planetscale/database";
|
||||
import { env } from "~/env.mjs";
|
||||
import * as schema from "~/server/schema";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
|
||||
// create the connection
|
||||
const connection = connect({
|
||||
url: env.DATABASE_URL,
|
||||
});
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ||
|
||||
new PrismaClient({
|
||||
log:
|
||||
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||
});
|
||||
|
||||
if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
export const db = drizzle(connection, { schema });
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { Redis } from "@upstash/redis";
|
||||
import https from "https";
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
export const redis = Redis.fromEnv({
|
||||
agent: new https.Agent({ keepAlive: true }),
|
||||
});
|
||||
export const redis = Redis.fromEnv();
|
||||
|
||||
export const setCache = async <T>(key: string, value: T) => {
|
||||
try {
|
||||
|
@ -20,14 +17,8 @@ export const setCache = async <T>(key: string, value: T) => {
|
|||
export const fetchCache = async <T>(key: string) => {
|
||||
try {
|
||||
const result = await redis.get(`${env.APP_ENV}_${key}`);
|
||||
if (result) {
|
||||
console.log("CACHE HIT");
|
||||
} else {
|
||||
console.log("CACHE MISS");
|
||||
}
|
||||
return result as T;
|
||||
} catch {
|
||||
console.log("CACHE ERROR");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
65
src/server/schema.ts
Normal file
65
src/server/schema.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import {
|
||||
timestamp,
|
||||
mysqlTable,
|
||||
varchar,
|
||||
boolean,
|
||||
json,
|
||||
} from "drizzle-orm/mysql-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
|
||||
export const rooms = mysqlTable("Room", {
|
||||
id: varchar("id", { length: 255 }).notNull().primaryKey(),
|
||||
created_at: timestamp("created_at", {
|
||||
mode: "date",
|
||||
fsp: 3,
|
||||
}).defaultNow(),
|
||||
userId: varchar("userId", { length: 255 }).notNull(),
|
||||
roomName: varchar("roomName", { length: 255 }),
|
||||
storyName: varchar("storyName", { length: 255 }),
|
||||
visible: boolean("visible").default(false).notNull(),
|
||||
scale: varchar("scale", { length: 255 }).default("0.5,1,2,3,5").notNull(),
|
||||
});
|
||||
|
||||
export const roomsRelations = relations(rooms, ({ many }) => ({
|
||||
votes: many(votes),
|
||||
logs: many(logs),
|
||||
}));
|
||||
|
||||
export const votes = mysqlTable("Vote", {
|
||||
id: varchar("id", { length: 255 }).notNull().primaryKey(),
|
||||
created_at: timestamp("created_at", {
|
||||
mode: "date",
|
||||
fsp: 3,
|
||||
}).defaultNow(),
|
||||
userId: varchar("userId", { length: 255 }).notNull(),
|
||||
roomId: varchar("roomId", { length: 255 }).notNull(),
|
||||
value: varchar("value", { length: 255 }).notNull(),
|
||||
});
|
||||
|
||||
export const votesRelations = relations(votes, ({ one }) => ({
|
||||
room: one(rooms, {
|
||||
fields: [votes.roomId],
|
||||
references: [rooms.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const logs = mysqlTable("Log", {
|
||||
id: varchar("id", { length: 255 }).notNull().primaryKey(),
|
||||
created_at: timestamp("created_at", {
|
||||
mode: "date",
|
||||
fsp: 3,
|
||||
}).defaultNow(),
|
||||
userId: varchar("userId", { length: 255 }).notNull(),
|
||||
roomId: varchar("roomId", { length: 255 }).notNull(),
|
||||
scale: varchar("scale", { length: 255 }),
|
||||
votes: json("votes"),
|
||||
roomName: varchar("roomName", { length: 255 }),
|
||||
storyName: varchar("storyName", { length: 255 }),
|
||||
});
|
||||
|
||||
export const logsRelations = relations(logs, ({ one }) => ({
|
||||
room: one(rooms, {
|
||||
fields: [logs.roomId],
|
||||
references: [rooms.id],
|
||||
}),
|
||||
}));
|
Loading…
Add table
Reference in a new issue