Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"dockerode": "4.0.9",
"dotenv": "17.3.1",
"express": "4.22.1",
"graphql": "16.13.1",
"express-rate-limit": "^8.3.1",
"graphql": "16.12.0",
"graphql-request": "6.1.0",
"html-to-text": "9.0.5",
"ioredis": "5.9.3",
Expand Down
10 changes: 10 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import noticeRoute from "./routes/notice";
import courseRouter from "./routes/course";
import shareRouter from "./routes/share";
import llmRouter from "./routes/llm";
import rateLimit from "express-rate-limit";

const app = express();

Expand All @@ -30,6 +31,14 @@ const whitelist =
? ["https://eesast.com", "https://docs.eesast.com", "http://localhost:3000"]
: ["http://localhost:3000"];

const globalRateLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minutes
max: 1000, // limit each IP to 1000 requests per windowMs
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
message: "Too many requests from this IP, please try again later.",
});

app.use(
cors({
origin: function (origin, callback) {
Expand All @@ -46,6 +55,7 @@ app.use(logger(process.env.NODE_ENV === "production" ? "combined" : "dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(globalRateLimiter);
app.use("/static", staticRouter);
app.use("/user", userRouter);
app.use("/emails", emailRouter);
Expand Down
62 changes: 36 additions & 26 deletions src/middlewares/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import jwt from "jsonwebtoken";
import { gql } from "graphql-request";
import { client } from "..";


export interface JwtUserPayload {
uuid: string;
role: string;
Expand Down Expand Up @@ -64,18 +63,21 @@ const anonymous_user: UserInfo = {
* Middleware: validate user authorizations; reject if necessary
*/
const authenticate: (
acceptableRoles?: string[]
acceptableRoles?: string[],
) => (req: Request, res: Response, next: NextFunction) => Response | void = (
acceptableRoles
acceptableRoles,
) => {
return (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.get("Authorization");
if(!authHeader) {
if (!acceptableRoles || acceptableRoles.length === 0 || acceptableRoles.includes("anonymous")) {
if (!authHeader) {
if (
!acceptableRoles ||
acceptableRoles.length === 0 ||
acceptableRoles.includes("anonymous")
) {
req.auth = { user: anonymous_user };
return next();
}
else {
} else {
return res.status(401).send("401 Unauthorized: Missing Token");
}
}
Expand All @@ -93,32 +95,40 @@ const authenticate: (
const users: any = await client.request(
gql`
query MyQuery($uuid: uuid!) {
users(where: {uuid: {_eq: $uuid}}) {
uuid,
username,
password,
role,
realname,
email,
phone,
student_no,
department,
class,
tsinghua_email,
github_id,
users(where: { uuid: { _eq: $uuid } }) {
uuid
username
password
role
realname
email
phone
student_no
department
class
tsinghua_email
github_id
}
}
`,
{
uuid: payload.uuid
}
)
uuid: payload.uuid,
},
);
const user = users.users[0];
req.auth = { user: user ?? anonymous_user };
if (!acceptableRoles || acceptableRoles.length === 0 || acceptableRoles.includes(user.role)) {
// console.log(user);
// console.log(acceptableRoles);

if (user === undefined || user?.role === undefined) {
return res.status(401).send("401 Unauthorized: Permission denied");
} else if (
!acceptableRoles ||
acceptableRoles.length === 0 ||
acceptableRoles.includes(user.role)
) {
return next();
}
else {
} else {
return res.status(401).send("401 Unauthorized: Permission denied");
}
} catch (err) {
Expand Down
99 changes: 98 additions & 1 deletion src/routes/application.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import bcrypt from "bcrypt";
import express from "express";
import express, { Request } from "express";
import rateLimit from "express-rate-limit";
import { gql } from "graphql-request";
import { client } from "..";
import * as HnrHasFunc from "../hasura/honor";
Expand All @@ -24,6 +25,71 @@ import * as validator from "../helpers/validate";

const router = express.Router();

const queryLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1分钟
max: 40, // 每分钟最多20次查询
message: { error: "查询过于频繁,请1分钟后再试" },
standardHeaders: true,
legacyHeaders: false,
keyGenerator: (req: Request) => (req as any).auth?.user?.uuid as string,
skip: (req: Request) => {
const role = (req as any).auth?.user?.role;
return role === "counselor" || role === "teacher";
},
});

// const submitLimiter = rateLimit({
// windowMs: 10 * 60 * 1000,
// max: 3,
// message: { error: "提交尝试过多,请10分钟后再试" },
// keyGenerator: (req) => (req as any).auth?.user?.uuid as string,
// skip: (req) => {
// const role = (req as any).auth?.user?.role;
// return role === "counselor" || role === "teacher";
// },
// });

const requestCounts = new Map();

function checkRateLimit(
user_uuid: string,
role: string,
windowMs: number,
maxRequests: number,
): { allowed: boolean; message?: string; currentCount?: number } {
// 跳过辅导员和老师
if (role === "counselor" || role === "teacher") {
return { allowed: true };
}

const now = Date.now();

if (!requestCounts.has(user_uuid)) {
requestCounts.set(user_uuid, { count: 0, lastReset: now });
}

const userStats = requestCounts.get(user_uuid)!;

// 检查是否需要重置
if (now - userStats.lastReset > windowMs) {
userStats.count = 0;
userStats.lastReset = now;
}

userStats.count++;
console.log(`用户 ${user_uuid} 在本分钟内已进行 ${userStats.count} 次查询`);

if (userStats.count >= maxRequests) {
return {
allowed: false,
message: "查询过于频繁,请1分钟后再试",
currentCount: userStats.count,
};
}

return { allowed: true };
}

interface IMentor {
uuid: string; // 导师uuid
name: string; // 导师姓名
Expand Down Expand Up @@ -496,6 +562,15 @@ router.get(
const role: string = req.auth.user.role;

if (role === "student") {
const limitResult = checkRateLimit(user_uuid, role, 1 * 60 * 1000, 20);
if (!limitResult.allowed) {
return res.status(429).json({
error: limitResult.message,
currentCount: limitResult.currentCount,
limit: 20,
});
}

const application_query: any = await client.request(
gql`
query MyQuery($student_uuid: uuid!) {
Expand Down Expand Up @@ -851,6 +926,7 @@ router.get(
// 获取新生信息
router.get(
"/info/mentor/freshmen",
queryLimiter,
authenticate(["student", "counselor"]),
async (req, res) => {
try {
Expand All @@ -860,6 +936,7 @@ router.get(
if (role === "student") {
const student_no: string = req.auth.user.student_no;
const realname: string = req.auth.user.realname;
console.log(student_no);
const freshman_query: any = await client.request(
gql`
query MyQuery(
Expand Down Expand Up @@ -1340,6 +1417,16 @@ router.post(
const id: string = req.body.id;
const statement: string = req.body.statement;
const user_uuid: string = req.auth.user.uuid;
const role: string = req.auth.user.role;

const limitResult = checkRateLimit(user_uuid, role, 1 * 60 * 1000, 20);
if (!limitResult.allowed) {
return res.status(429).json({
error: limitResult.message,
currentCount: limitResult.currentCount,
limit: 20,
});
}

if (statement.length === 0) {
return res.status(400).send("Error: Invalid statement");
Expand Down Expand Up @@ -1971,6 +2058,16 @@ router.put(
const user_uuid: string = req.auth.user.uuid;
const student_no: string = req.auth.user.student_no;
const realname: string = req.auth.user.realname;
const role: string = req.auth.user.role;

const limitResult = checkRateLimit(user_uuid, role, 1 * 60 * 1000, 20);
if (!limitResult.allowed) {
return res.status(429).json({
error: limitResult.message,
currentCount: limitResult.currentCount,
limit: 20,
});
}

if (statement.length === 0) {
return res.status(400).send("Error: Invalid statement");
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,13 @@ eventemitter3@^5.0.1:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.4.tgz#a86d66170433712dde814707ac52b5271ceb1feb"
integrity sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==

express-rate-limit@^8.3.1:
version "8.3.1"
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.3.1.tgz#0aaba098eadd40f6737f30a98e6b16fa1a29edfb"
integrity sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==
dependencies:
ip-address "10.1.0"

express@4.22.1:
version "4.22.1"
resolved "https://registry.yarnpkg.com/express/-/express-4.22.1.tgz#1de23a09745a4fffdb39247b344bb5eaff382069"
Expand Down Expand Up @@ -1922,6 +1929,11 @@ ioredis@5.9.3:
redis-parser "^3.0.0"
standard-as-callback "^2.1.0"

ip-address@10.1.0:
version "10.1.0"
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4"
integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==

ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
Expand Down
Loading