Stability improvements + react + proper rate-limit
This commit is contained in:
parent
d636f25bf7
commit
e9cacf404d
9 changed files with 75 additions and 83 deletions
|
@ -5,7 +5,7 @@ A scrum poker tool that helps agile teams plan their sprints in real-time.
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
- Front-end framework: Nextjs
|
- Front-end framework: Nextjs
|
||||||
- Front-end library: Preact
|
- Front-end library: React
|
||||||
- Rendering method: SSR SPA
|
- Rendering method: SSR SPA
|
||||||
- Hosting: Vercel
|
- Hosting: Vercel
|
||||||
- Real-time pub/sub: Ably
|
- Real-time pub/sub: Ably
|
||||||
|
|
|
@ -12,17 +12,6 @@ const config = {
|
||||||
images: {
|
images: {
|
||||||
domains: ["avatars.githubusercontent.com", "lh3.googleusercontent.com"],
|
domains: ["avatars.githubusercontent.com", "lh3.googleusercontent.com"],
|
||||||
},
|
},
|
||||||
webpack: (config, { dev, isServer }) => {
|
|
||||||
if (!dev && !isServer) {
|
|
||||||
Object.assign(config.resolve.alias, {
|
|
||||||
"react/jsx-runtime.js": "preact/compat/jsx-runtime",
|
|
||||||
react: "preact/compat",
|
|
||||||
"react-dom/test-utils": "preact/test-utils",
|
|
||||||
"react-dom": "preact/compat",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -23,14 +23,14 @@
|
||||||
"@trpc/server": "10.35.0",
|
"@trpc/server": "10.35.0",
|
||||||
"@upstash/ratelimit": "^0.4.3",
|
"@upstash/ratelimit": "^0.4.3",
|
||||||
"@upstash/redis": "^1.22.0",
|
"@upstash/redis": "^1.22.0",
|
||||||
"ably": "^1.2.41",
|
"ably": "^1.2.42",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"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",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
"preact": "^10.16.0",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
"react-email": "^1.9.4",
|
"react-email": "^1.9.4",
|
||||||
"react-icons": "^4.10.1",
|
"react-icons": "^4.10.1",
|
||||||
"resend": "^0.17.1",
|
"resend": "^0.17.1",
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.44.0",
|
"@types/eslint": "^8.44.1",
|
||||||
"@types/json2csv": "^5.0.3",
|
"@types/json2csv": "^5.0.3",
|
||||||
"@types/node": "^20.4.4",
|
"@types/node": "^20.4.4",
|
||||||
"@types/react": "^18.2.16",
|
"@types/react": "^18.2.16",
|
||||||
|
|
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
|
@ -39,8 +39,8 @@ dependencies:
|
||||||
specifier: ^1.22.0
|
specifier: ^1.22.0
|
||||||
version: 1.22.0
|
version: 1.22.0
|
||||||
ably:
|
ably:
|
||||||
specifier: ^1.2.41
|
specifier: ^1.2.42
|
||||||
version: 1.2.41
|
version: 1.2.42
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.14
|
specifier: ^10.4.14
|
||||||
version: 10.4.14(postcss@8.4.27)
|
version: 10.4.14(postcss@8.4.27)
|
||||||
|
@ -56,12 +56,12 @@ dependencies:
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.27
|
specifier: ^8.4.27
|
||||||
version: 8.4.27
|
version: 8.4.27
|
||||||
preact:
|
|
||||||
specifier: ^10.16.0
|
|
||||||
version: 10.16.0
|
|
||||||
react:
|
react:
|
||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
|
react-dom:
|
||||||
|
specifier: 18.2.0
|
||||||
|
version: 18.2.0(react@18.2.0)
|
||||||
react-email:
|
react-email:
|
||||||
specifier: ^1.9.4
|
specifier: ^1.9.4
|
||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
|
@ -83,8 +83,8 @@ dependencies:
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/eslint':
|
'@types/eslint':
|
||||||
specifier: ^8.44.0
|
specifier: ^8.44.1
|
||||||
version: 8.44.0
|
version: 8.44.1
|
||||||
'@types/json2csv':
|
'@types/json2csv':
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3
|
version: 5.0.3
|
||||||
|
@ -132,7 +132,7 @@ packages:
|
||||||
react: '>=18.1.0'
|
react: '>=18.1.0'
|
||||||
react-dom: '>=18.1.0'
|
react-dom: '>=18.1.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
ably: 1.2.41
|
ably: 1.2.42
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -1087,8 +1087,8 @@ packages:
|
||||||
'@types/responselike': 1.0.0
|
'@types/responselike': 1.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@types/eslint@8.44.0:
|
/@types/eslint@8.44.1:
|
||||||
resolution: {integrity: sha512-gsF+c/0XOguWgaOgvFs+xnnRqt9GwgTvIks36WpE6ueeI4KCEHHd8K/CKHqhOqrJKsYH8m27kRzQEvWXAwXUTw==}
|
resolution: {integrity: sha512-XpNDc4Z5Tb4x+SW1MriMVeIsMoONHCkWFMkR/aPJbzEsxqHy+4Glu/BqTdPrApfDeMaXbtNh6bseNgl5KaWrSg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.1
|
'@types/estree': 1.0.1
|
||||||
'@types/json-schema': 7.0.12
|
'@types/json-schema': 7.0.12
|
||||||
|
@ -1382,8 +1382,8 @@ packages:
|
||||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/ably@1.2.41:
|
/ably@1.2.42:
|
||||||
resolution: {integrity: sha512-TE3PjtrclXFjfIALQhuTd1bEL7EAlS28AGFLaILeXmecXnBQE37WoC2EKZJIX7jRX31bwTHxUouje3WBEnhNKQ==}
|
resolution: {integrity: sha512-dUnza7cERLWaDa/2pLVXtU2PJoU5k/t6g9sQZI1dgWC5Vok39nE6tf/xH2Rat7PJs7pXl9hLpkg1AhS4Xwd2/w==}
|
||||||
engines: {node: '>=5.10.x'}
|
engines: {node: '>=5.10.x'}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ably/msgpack-js': 0.4.0
|
'@ably/msgpack-js': 0.4.0
|
||||||
|
@ -1686,7 +1686,7 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
caniuse-lite: 1.0.30001517
|
caniuse-lite: 1.0.30001517
|
||||||
electron-to-chromium: 1.4.469
|
electron-to-chromium: 1.4.470
|
||||||
node-releases: 2.0.13
|
node-releases: 2.0.13
|
||||||
update-browserslist-db: 1.0.11(browserslist@4.21.9)
|
update-browserslist-db: 1.0.11(browserslist@4.21.9)
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -2169,8 +2169,8 @@ packages:
|
||||||
semver: 7.5.4
|
semver: 7.5.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/electron-to-chromium@1.4.469:
|
/electron-to-chromium@1.4.470:
|
||||||
resolution: {integrity: sha512-HRN9XQjElxJBrdDky5iiUUr3eDwXGTg6Cp4IV8MuNc8VqMkYSneSnIe6poFKx9PsNzkudCgaWCBVxwDqirwQWQ==}
|
resolution: {integrity: sha512-zZM48Lmy2FKWgqyvsX9XK+J6FfP7aCDUFLmgooLJzA7v1agCs/sxSoBpTIwDLhmbhpx9yJIxj2INig/ncjJRqg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/emoji-regex@9.2.2:
|
/emoji-regex@9.2.2:
|
||||||
|
|
|
@ -2,12 +2,16 @@ import Ably from "ably";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
import type { EventType } from "../utils/types";
|
import type { EventType } from "../utils/types";
|
||||||
|
|
||||||
export const publishToChannel = async (
|
const ablyRest = new Ably.Rest(env.ABLY_PRIVATE_KEY);
|
||||||
ablyInstance: Ably.Types.RealtimePromise,
|
|
||||||
|
export const publishToChannel = (
|
||||||
channel: string,
|
channel: string,
|
||||||
event: EventType,
|
event: EventType,
|
||||||
message: string
|
message: string
|
||||||
) => {
|
) => {
|
||||||
const channelName = ablyInstance.channels.get(`${env.APP_ENV}-${channel}`);
|
try {
|
||||||
await channelName.publish(event, message);
|
ablyRest.channels.get(`${env.APP_ENV}-${channel}`).publish(event, message);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`❌❌❌ Failed to send message!`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
25
src/server/api/createTokenRequest.ts
Normal file
25
src/server/api/createTokenRequest.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import Ably from "ably/promises";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { env } from "~/env.mjs";
|
||||||
|
import { getServerSession } from "next-auth/next";
|
||||||
|
import { authOptions } from "../auth";
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
// Signed in
|
||||||
|
const client = new Ably.Realtime(env.ABLY_PRIVATE_KEY);
|
||||||
|
|
||||||
|
const tokenRequestData = await client.auth.createTokenRequest({
|
||||||
|
clientId: session.user.id,
|
||||||
|
});
|
||||||
|
res.status(200).json(tokenRequestData);
|
||||||
|
} else {
|
||||||
|
// Not Signed in
|
||||||
|
res.status(401);
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,8 +27,7 @@ export const roomRouter = createTRPCRouter({
|
||||||
await invalidateCache(`kv_roomcount_admin`);
|
await invalidateCache(`kv_roomcount_admin`);
|
||||||
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
||||||
|
|
||||||
await publishToChannel(
|
publishToChannel(
|
||||||
ctx.ably,
|
|
||||||
`${ctx.session.user.id}`,
|
`${ctx.session.user.id}`,
|
||||||
"ROOM_LIST_UPDATE",
|
"ROOM_LIST_UPDATE",
|
||||||
"CREATE"
|
"CREATE"
|
||||||
|
@ -202,12 +201,7 @@ export const roomRouter = createTRPCRouter({
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newRoom) {
|
if (newRoom) {
|
||||||
await publishToChannel(
|
publishToChannel(`${newRoom.id}`, "ROOM_UPDATE", "UPDATE");
|
||||||
ctx.ably,
|
|
||||||
`${newRoom.id}`,
|
|
||||||
"ROOM_UPDATE",
|
|
||||||
"UPDATE"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!newRoom;
|
return !!newRoom;
|
||||||
|
@ -228,19 +222,13 @@ export const roomRouter = createTRPCRouter({
|
||||||
await invalidateCache(`kv_votecount_admin`);
|
await invalidateCache(`kv_votecount_admin`);
|
||||||
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
||||||
|
|
||||||
await publishToChannel(
|
publishToChannel(
|
||||||
ctx.ably,
|
|
||||||
`${ctx.session.user.id}`,
|
`${ctx.session.user.id}`,
|
||||||
"ROOM_LIST_UPDATE",
|
"ROOM_LIST_UPDATE",
|
||||||
"DELETE"
|
"DELETE"
|
||||||
);
|
);
|
||||||
|
|
||||||
await publishToChannel(
|
publishToChannel(`${deletedRoom.id}`, "ROOM_UPDATE", "DELETE");
|
||||||
ctx.ably,
|
|
||||||
`${deletedRoom.id}`,
|
|
||||||
"ROOM_UPDATE",
|
|
||||||
"DELETE"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!deletedRoom;
|
return !!deletedRoom;
|
||||||
|
|
|
@ -100,12 +100,7 @@ export const voteRouter = createTRPCRouter({
|
||||||
await invalidateCache(`kv_votecount_admin`);
|
await invalidateCache(`kv_votecount_admin`);
|
||||||
await invalidateCache(`kv_votes_${input.roomId}`);
|
await invalidateCache(`kv_votes_${input.roomId}`);
|
||||||
|
|
||||||
await publishToChannel(
|
publishToChannel(`${vote.roomId}`, "VOTE_UPDATE", "UPDATE");
|
||||||
ctx.ably,
|
|
||||||
`${vote.roomId}`,
|
|
||||||
"VOTE_UPDATE",
|
|
||||||
"UPDATE"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!vote;
|
return !!vote;
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { type Session } from "next-auth";
|
||||||
|
|
||||||
import { getServerAuthSession } from "~/server/auth";
|
import { getServerAuthSession } from "~/server/auth";
|
||||||
import { prisma } from "~/server/db";
|
import { prisma } from "~/server/db";
|
||||||
import Ably from "ably";
|
|
||||||
|
|
||||||
type CreateContextOptions = {
|
type CreateContextOptions = {
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
|
@ -38,7 +37,6 @@ type CreateContextOptions = {
|
||||||
const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
return {
|
return {
|
||||||
session: opts.session,
|
session: opts.session,
|
||||||
ably: new Ably.Realtime.Promise(env.ABLY_PRIVATE_KEY),
|
|
||||||
prisma,
|
prisma,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -108,33 +106,26 @@ const enforceRouteProtection = t.middleware(async ({ ctx, next }) => {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const rateLimit = new Ratelimit({
|
||||||
const rateLimit = new Ratelimit({
|
redis: Redis.fromEnv(),
|
||||||
redis: Redis.fromEnv(),
|
limiter: Ratelimit.slidingWindow(
|
||||||
limiter: Ratelimit.slidingWindow(
|
Number(env.UPSTASH_RATELIMIT_REQUESTS),
|
||||||
Number(env.UPSTASH_RATELIMIT_REQUESTS),
|
`${Number(env.UPSTASH_RATELIMIT_SECONDS)}s`
|
||||||
`${Number(env.UPSTASH_RATELIMIT_SECONDS)}s`
|
),
|
||||||
),
|
analytics: true,
|
||||||
analytics: true,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
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" });
|
console.log(success);
|
||||||
|
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
ctx: {
|
ctx: {
|
||||||
session: { ...ctx.session, user: ctx.session.user },
|
session: { ...ctx.session, user: ctx.session.user },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch {
|
|
||||||
return next({
|
|
||||||
ctx: {
|
|
||||||
session: { ...ctx.session, user: ctx.session.user },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Reference in a new issue