Merge pull request #31 from atridadl/dev

Package updates
This commit is contained in:
Atridad Lahiji 2023-08-03 12:57:20 -06:00 committed by GitHub
commit dec3ba1334
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 536 additions and 199 deletions

View file

@ -14,13 +14,13 @@
"dependencies": { "dependencies": {
"@ably-labs/react-hooks": "^2.1.1", "@ably-labs/react-hooks": "^2.1.1",
"@auth/prisma-adapter": "^1.0.1", "@auth/prisma-adapter": "^1.0.1",
"@prisma/client": "5.0.0", "@prisma/client": "5.1.1",
"@react-email/components": "^0.0.7", "@react-email/components": "^0.0.7",
"@tanstack/react-query": "^4.32.0", "@tanstack/react-query": "^4.32.1",
"@trpc/client": "10.36.0", "@trpc/client": "10.37.1",
"@trpc/next": "10.36.0", "@trpc/next": "10.37.1",
"@trpc/react-query": "10.36.0", "@trpc/react-query": "10.37.1",
"@trpc/server": "10.36.0", "@trpc/server": "10.37.1",
"@upstash/ratelimit": "^0.4.3", "@upstash/ratelimit": "^0.4.3",
"@upstash/redis": "^1.22.0", "@upstash/redis": "^1.22.0",
"ably": "^1.2.42", "ably": "^1.2.42",
@ -28,6 +28,7 @@
"json2csv": "6.0.0-alpha.2", "json2csv": "6.0.0-alpha.2",
"next": "^13.4.12", "next": "^13.4.12",
"next-auth": "^4.22.3", "next-auth": "^4.22.3",
"nextjs-cors": "^2.1.2",
"postcss": "^8.4.27", "postcss": "^8.4.27",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@ -36,19 +37,20 @@
"resend": "^0.17.2", "resend": "^0.17.2",
"sharp": "^0.32.4", "sharp": "^0.32.4",
"superjson": "1.13.1", "superjson": "1.13.1",
"trpc-openapi": "^1.2.0",
"zod": "^3.21.4" "zod": "^3.21.4"
}, },
"devDependencies": { "devDependencies": {
"@types/eslint": "^8.44.1", "@types/eslint": "^8.44.1",
"@types/json2csv": "^5.0.3", "@types/json2csv": "^5.0.3",
"@types/node": "^20.4.5", "@types/node": "^20.4.6",
"@types/react": "^18.2.17", "@types/react": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.0", "@typescript-eslint/parser": "^6.2.1",
"daisyui": "^3.5.0", "daisyui": "^3.5.1",
"eslint": "^8.46.0", "eslint": "^8.46.0",
"eslint-config-next": "^13.4.12", "eslint-config-next": "^13.4.12",
"prisma": "5.0.0", "prisma": "5.1.1",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^5.1.6" "typescript": "^5.1.6"
}, },

549
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { createOpenApiNextHandler } from "trpc-openapi";
import cors from "nextjs-cors";
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Setup CORS
await cors(req, res);
// Handle incoming OpenAPI requests
return createOpenApiNextHandler({
router: appRouter,
createContext: createTRPCContext,
})(req, res);
};
export default handler;

View file

@ -0,0 +1,10 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { openApiDocument } from "../../server/openapi";
// Respond with our OpenAPI schema
const handler = (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).send(openApiDocument);
};
export default handler;

View file

@ -1,5 +1,4 @@
import { createNextApiHandler } from "@trpc/server/adapters/next"; import { createNextApiHandler } from "@trpc/server/adapters/next";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import { appRouter } from "~/server/api/root"; import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc"; import { createTRPCContext } from "~/server/api/trpc";

View file

@ -20,35 +20,35 @@ export default Home;
const HomePageBody: React.FC = () => { const HomePageBody: React.FC = () => {
return ( return (
<> <>
<h1 className="text-6xl font-bold"> <h1 className="text-3xl sm:text-6xl font-bold">
Sprint{ " " } Sprint{" "}
<span className="bg-gradient-to-br from-pink-600 to-cyan-400 bg-clip-text text-transparent box-decoration-clone"> <span className="bg-gradient-to-br from-pink-600 to-cyan-400 bg-clip-text text-transparent box-decoration-clone">
Padawan Padawan
</span> </span>
</h1> </h1>
<h2 className="my-4 text-3xl font-bold"> <h2 className="my-4 text-xl sm:text-3xl font-bold">
A{ " " } A{" "}
<span className="bg-gradient-to-br from-pink-600 to-pink-400 bg-clip-text text-transparent box-decoration-clone"> <span className="bg-gradient-to-br from-pink-600 to-pink-400 bg-clip-text text-transparent box-decoration-clone">
scrum poker{ " " } scrum poker{" "}
</span>{ " " } </span>{" "}
tool that helps{ " " } tool that helps{" "}
<span className="bg-gradient-to-br from-purple-600 to-purple-400 bg-clip-text text-transparent box-decoration-clone"> <span className="bg-gradient-to-br from-purple-600 to-purple-400 bg-clip-text text-transparent box-decoration-clone">
agile teams{ " " } agile teams{" "}
</span>{ " " } </span>{" "}
plan their sprints in{ " " } plan their sprints in{" "}
<span className="bg-gradient-to-br from-cyan-600 to-cyan-400 bg-clip-text text-transparent box-decoration-clone"> <span className="bg-gradient-to-br from-cyan-600 to-cyan-400 bg-clip-text text-transparent box-decoration-clone">
real-time real-time
</span> </span>
. .
</h2> </h2>
<div className="card bg-secondary text-black font-bold text-left"> <div className="card card-compact bg-secondary text-black font-bold text-left">
<div className="card-body"> <div className="card-body">
<h2 className="card-title">Features:</h2> <h2 className="card-title">Features:</h2>
<ul> <ul>
<li>🚀 Real-time votes!</li> <li>🚀 Real-time votes!</li>
<li>🚀 Granular control of room name and vote scale!</li> <li>🚀 Customizable room name and vote scale!</li>
<li>🚀 CSV Reports for every room!</li> <li>🚀 CSV Reports for every room!</li>
<li>🚀 100% free and open-source... forever!</li> <li>🚀 100% free and open-source... forever!</li>
</ul> </ul>

View file

@ -1,6 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { publishToChannel } from "~/server/ably"; import { publishToChannel } from "~/server/ably";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import {
createTRPCRouter,
protectedProcedure,
adminProcedure,
} from "~/server/api/trpc";
import { fetchCache, invalidateCache, setCache } from "~/server/redis"; import { fetchCache, invalidateCache, setCache } from "~/server/redis";
@ -92,7 +96,7 @@ export const roomRouter = createTRPCRouter({
} }
}), }),
countAll: protectedProcedure.query(async ({ ctx }) => { countAll: adminProcedure.query(async ({ ctx }) => {
const cachedResult = await fetchCache<number>(`kv_roomcount_admin`); const cachedResult = await fetchCache<number>(`kv_roomcount_admin`);
if (cachedResult) { if (cachedResult) {

View file

@ -1,9 +1,9 @@
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { adminProcedure, createTRPCRouter } from "~/server/api/trpc";
import { invalidateCache } from "~/server/redis"; import { invalidateCache } from "~/server/redis";
export const sessionRouter = createTRPCRouter({ export const sessionRouter = createTRPCRouter({
deleteAllByUserId: protectedProcedure deleteAllByUserId: adminProcedure
.input( .input(
z.object({ z.object({
userId: z.string(), userId: z.string(),
@ -22,7 +22,7 @@ export const sessionRouter = createTRPCRouter({
return !!sessions; return !!sessions;
}), }),
deleteAll: protectedProcedure.mutation(async ({ ctx }) => { deleteAll: adminProcedure.mutation(async ({ ctx }) => {
const sessions = await ctx.prisma.session.deleteMany(); const sessions = await ctx.prisma.session.deleteMany();
if (!!sessions) { if (!!sessions) {

View file

@ -3,14 +3,18 @@ import { Resend } from "resend";
import { z } from "zod"; import { z } from "zod";
import { Goodbye } from "~/components/templates/Goodbye"; import { Goodbye } from "~/components/templates/Goodbye";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "~/server/api/trpc";
import { fetchCache, invalidateCache, setCache } from "~/server/redis"; import { fetchCache, invalidateCache, setCache } from "~/server/redis";
const resend = new Resend(process.env.RESEND_API_KEY); const resend = new Resend(process.env.RESEND_API_KEY);
export const userRouter = createTRPCRouter({ export const userRouter = createTRPCRouter({
countAll: protectedProcedure.query(async ({ ctx }) => { countAll: adminProcedure.query(async ({ ctx }) => {
const cachedResult = await fetchCache<number>(`kv_usercount_admin`); const cachedResult = await fetchCache<number>(`kv_usercount_admin`);
if (cachedResult) { if (cachedResult) {
@ -150,7 +154,7 @@ export const userRouter = createTRPCRouter({
return !!user; return !!user;
}), }),
setAdmin: protectedProcedure setAdmin: adminProcedure
.input( .input(
z.object({ z.object({
userId: z.string(), userId: z.string(),
@ -172,7 +176,7 @@ export const userRouter = createTRPCRouter({
return !!user; return !!user;
}), }),
setVIP: protectedProcedure setVIP: adminProcedure
.input( .input(
z.object({ z.object({
userId: z.string(), userId: z.string(),

View file

@ -2,11 +2,19 @@ import { z } from "zod";
import { publishToChannel } from "~/server/ably"; import { publishToChannel } from "~/server/ably";
import type { Room } from "@prisma/client"; import type { Room } from "@prisma/client";
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
} from "~/server/api/trpc";
import { fetchCache, invalidateCache, setCache } from "~/server/redis"; import { fetchCache, invalidateCache, setCache } from "~/server/redis";
export const voteRouter = createTRPCRouter({ export const voteRouter = createTRPCRouter({
countAll: protectedProcedure.query(async ({ ctx }) => { countAll: adminProcedure
.input(z.void())
.output(z.number())
.meta({ openapi: { method: "GET", path: "/votes/count" } })
.query(async ({ ctx }) => {
const cachedResult = await fetchCache<number>(`kv_votecount_admin`); const cachedResult = await fetchCache<number>(`kv_votecount_admin`);
if (cachedResult) { if (cachedResult) {

View file

@ -22,6 +22,7 @@ import { prisma } from "~/server/db";
type CreateContextOptions = { type CreateContextOptions = {
session: Session | null; session: Session | null;
ip: string | undefined;
}; };
/** /**
@ -37,6 +38,7 @@ type CreateContextOptions = {
const createInnerTRPCContext = (opts: CreateContextOptions) => { const createInnerTRPCContext = (opts: CreateContextOptions) => {
return { return {
session: opts.session, session: opts.session,
ip: opts.ip,
prisma, prisma,
}; };
}; };
@ -54,6 +56,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const session = await getServerAuthSession({ req, res }); const session = await getServerAuthSession({ req, res });
return createInnerTRPCContext({ return createInnerTRPCContext({
ip: req.socket.remoteAddress,
session, session,
}); });
}; };
@ -64,17 +67,21 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* 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 { initTRPC, TRPCError } from "@trpc/server";
import type { OpenApiMeta } from "trpc-openapi";
import { Ratelimit } from "@upstash/ratelimit"; import { Ratelimit } from "@upstash/ratelimit";
import superjson from "superjson"; import superjson from "superjson";
import { env } from "~/env.mjs"; import { env } from "~/env.mjs";
import { Redis } from "@upstash/redis"; import { Redis } from "@upstash/redis";
const t = initTRPC.context<typeof createTRPCContext>().create({ const t = initTRPC
.meta<OpenApiMeta>()
.context<typeof createTRPCContext>()
.create({
transformer: superjson, transformer: superjson,
errorFormatter({ shape }) { errorFormatter({ shape }) {
return shape; return shape;
}, },
}); });
/** /**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
@ -100,7 +107,7 @@ export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure; export const publicProcedure = t.procedure;
/** Reusable middleware that enforces users are logged in before running the procedure. */ /** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceRouteProtection = t.middleware(async ({ ctx, next }) => { const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
// Auth // Auth
if (!ctx.session || !ctx.session.user) { if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" }); throw new TRPCError({ code: "UNAUTHORIZED" });
@ -116,7 +123,7 @@ const enforceRouteProtection = t.middleware(async ({ ctx, next }) => {
}); });
const { success } = await rateLimit.limit( const { success } = await rateLimit.limit(
`${env.APP_ENV}_${ctx.session.user.id}` `${env.APP_ENV}_${ctx.session?.user.id}`
); );
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" }); if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
@ -128,6 +135,17 @@ const enforceRouteProtection = t.middleware(async ({ ctx, next }) => {
}); });
}); });
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 },
},
});
});
/** /**
* Protected (authenticated) procedure * Protected (authenticated) procedure
* *
@ -136,4 +154,6 @@ const enforceRouteProtection = t.middleware(async ({ ctx, next }) => {
* *
* @see https://trpc.io/docs/procedures * @see https://trpc.io/docs/procedures
*/ */
export const protectedProcedure = t.procedure.use(enforceRouteProtection); export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
export const adminProcedure = t.procedure.use(enforceAdminRole);

12
src/server/openapi.ts Normal file
View file

@ -0,0 +1,12 @@
import { generateOpenApiDocument } from 'trpc-openapi';
import { appRouter } from './api/root';
// Generate OpenAPI schema document
export const openApiDocument = generateOpenApiDocument(appRouter, {
title: 'Example CRUD API',
description: 'OpenAPI compliant REST API built using tRPC with Next.js',
version: '1.0.0',
baseUrl: 'http://localhost:3000/api',
docsUrl: 'https://github.com/jlalmes/trpc-openapi',
});