commit
dec3ba1334
12 changed files with 536 additions and 199 deletions
26
package.json
26
package.json
|
@ -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
549
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
19
src/pages/api/[...trpc].ts
Normal file
19
src/pages/api/[...trpc].ts
Normal 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;
|
10
src/pages/api/openapi.json.ts
Normal file
10
src/pages/api/openapi.json.ts
Normal 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;
|
|
@ -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";
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
12
src/server/openapi.ts
Normal 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',
|
||||
});
|
Loading…
Add table
Reference in a new issue