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

View file

@ -20,14 +20,14 @@ export default Home;
const HomePageBody: React.FC = () => {
return (
<>
<h1 className="text-6xl font-bold">
<h1 className="text-3xl sm:text-6xl font-bold">
Sprint{" "}
<span className="bg-gradient-to-br from-pink-600 to-cyan-400 bg-clip-text text-transparent box-decoration-clone">
Padawan
</span>
</h1>
<h2 className="my-4 text-3xl font-bold">
<h2 className="my-4 text-xl sm:text-3xl font-bold">
A{" "}
<span className="bg-gradient-to-br from-pink-600 to-pink-400 bg-clip-text text-transparent box-decoration-clone">
scrum poker{" "}
@ -43,12 +43,12 @@ const HomePageBody: React.FC = () => {
.
</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">
<h2 className="card-title">Features:</h2>
<ul>
<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>🚀 100% free and open-source... forever!</li>
</ul>

View file

@ -1,6 +1,10 @@
import { z } from "zod";
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";
@ -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`);
if (cachedResult) {

View file

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

View file

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

View file

@ -2,11 +2,19 @@ import { z } from "zod";
import { publishToChannel } from "~/server/ably";
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";
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`);
if (cachedResult) {

View file

@ -22,6 +22,7 @@ import { prisma } from "~/server/db";
type CreateContextOptions = {
session: Session | null;
ip: string | undefined;
};
/**
@ -37,6 +38,7 @@ type CreateContextOptions = {
const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
ip: opts.ip,
prisma,
};
};
@ -54,6 +56,7 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const session = await getServerAuthSession({ req, res });
return createInnerTRPCContext({
ip: req.socket.remoteAddress,
session,
});
};
@ -64,12 +67,16 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => {
* 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";
const t = initTRPC.context<typeof createTRPCContext>().create({
const t = initTRPC
.meta<OpenApiMeta>()
.context<typeof createTRPCContext>()
.create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
@ -100,7 +107,7 @@ export const createTRPCRouter = t.router;
export const publicProcedure = t.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
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
@ -116,7 +123,7 @@ const enforceRouteProtection = t.middleware(async ({ ctx, next }) => {
});
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" });
@ -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
*
@ -136,4 +154,6 @@ const enforceRouteProtection = t.middleware(async ({ ctx, next }) => {
*
* @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',
});