Skip to content
Open
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
2 changes: 1 addition & 1 deletion backend/src/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import nodemailer from "nodemailer";

const router = express.Router();

const sendEmail = async (to: string, subject: string, text: string) => {
export const sendEmail = async (to: string, subject: string, text: string) => {
const transporter = nodemailer.createTransport({
host: process.env.EMAIL_HOST!,
port: Number(process.env.EMAIL_PORT!),
Expand Down
57 changes: 55 additions & 2 deletions backend/src/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1319,7 +1319,7 @@ export type User_Room_Bool_Exp = {

/** unique or primary key constraints on table "user_room" */
export enum User_Room_Constraint {
/** unique or primary key constraint on columns "user_uuid", "room_uuid" */
/** unique or primary key constraint on columns "room_uuid", "user_uuid" */
UserRoomPkey = 'user_room_pkey'
}

Expand Down Expand Up @@ -1547,13 +1547,35 @@ export type AddUserMutationVariables = Exact<{

export type AddUserMutation = { __typename?: 'mutation_root', insert_user_one?: { __typename?: 'user', uuid: any } | null };

export type DeleteUserMutationVariables = Exact<{
uuid: Scalars['uuid']['input'];
}>;


export type DeleteUserMutation = { __typename?: 'mutation_root', delete_user?: { __typename?: 'user_mutation_response', affected_rows: number } | null };

export type UpdateUserPasswordMutationVariables = Exact<{
uuid: Scalars['uuid']['input'];
password: Scalars['String']['input'];
}>;


export type UpdateUserPasswordMutation = { __typename?: 'mutation_root', update_user_by_pk?: { __typename?: 'user', uuid: any } | null };

export type GetUsersByUsernameQueryVariables = Exact<{
username: Scalars['String']['input'];
}>;


export type GetUsersByUsernameQuery = { __typename?: 'query_root', user: Array<{ __typename?: 'user', uuid: any, password: string }> };

export type GetUsersByUuidQueryVariables = Exact<{
uuid: Scalars['uuid']['input'];
}>;


export type GetUsersByUuidQuery = { __typename?: 'query_root', user: Array<{ __typename?: 'user', username: string, password: string }> };


export const AddMessageDocument = gql`
mutation addMessage($user_uuid: uuid!, $room_uuid: uuid!, $content: String!) {
Expand Down Expand Up @@ -1619,6 +1641,20 @@ export const AddUserDocument = gql`
}
}
`;
export const DeleteUserDocument = gql`
mutation deleteUser($uuid: uuid!) {
delete_user(where: {uuid: {_eq: $uuid}}) {
affected_rows
}
}
`;
export const UpdateUserPasswordDocument = gql`
mutation updateUserPassword($uuid: uuid!, $password: String!) {
update_user_by_pk(pk_columns: {uuid: $uuid}, _set: {password: $password}) {
uuid
}
}
`;
export const GetUsersByUsernameDocument = gql`
query getUsersByUsername($username: String!) {
user(where: {username: {_eq: $username}}) {
Expand All @@ -1627,6 +1663,14 @@ export const GetUsersByUsernameDocument = gql`
}
}
`;
export const GetUsersByUuidDocument = gql`
query getUsersByUuid($uuid: uuid!) {
user(where: {uuid: {_eq: $uuid}}) {
username
password
}
}
`;

export type SdkFunctionWrapper = <T>(action: (requestHeaders?:Record<string, string>) => Promise<T>, operationName: string, operationType?: string, variables?: any) => Promise<T>;

Expand Down Expand Up @@ -1656,9 +1700,18 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
addUser(variables: AddUserMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<AddUserMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<AddUserMutation>(AddUserDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'addUser', 'mutation', variables);
},
deleteUser(variables: DeleteUserMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<DeleteUserMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<DeleteUserMutation>(DeleteUserDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteUser', 'mutation', variables);
},
updateUserPassword(variables: UpdateUserPasswordMutationVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<UpdateUserPasswordMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<UpdateUserPasswordMutation>(UpdateUserPasswordDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'updateUserPassword', 'mutation', variables);
},
getUsersByUsername(variables: GetUsersByUsernameQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<GetUsersByUsernameQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetUsersByUsernameQuery>(GetUsersByUsernameDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getUsersByUsername', 'query', variables);
},
getUsersByUuid(variables: GetUsersByUuidQueryVariables, requestHeaders?: GraphQLClientRequestHeaders): Promise<GetUsersByUuidQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetUsersByUuidQuery>(GetUsersByUuidDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getUsersByUuid', 'query', variables);
}
};
}
export type Sdk = ReturnType<typeof getSdk>;
export type Sdk = ReturnType<typeof getSdk>;
87 changes: 87 additions & 0 deletions backend/src/user.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from "express";
import jwt from "jsonwebtoken";
import {sendEmail} from "./email";
import { sdk as graphql } from "./index";

interface userJWTPayload {
Expand Down Expand Up @@ -71,4 +72,90 @@ router.post("/register", async (req, res) => {
}
});

router.post("/forgot-password", async (req, res) => {
const { username } = req.body;

// 1. 鲁棒性检查:输入参数检查
if (!username) {
return res.status(422).json({ error: "Missing username." });
}

try {
const queryResult = await graphql.getUsersByUsername({ username });

// 2. 鲁棒性检查:防止用户枚举攻击
if (queryResult.user.length === 0) {
return res.status(200).json({ message: "If a user with that username exists, a password reset link has been sent." });
}

const user = queryResult.user[0];
const payload: userJWTPayload = {
uuid: user.uuid,
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": ["admin", "user"],
"x-hasura-default-role": "user",
},
};
const token = jwt.sign(payload, process.env.JWT_SECRET!, {
expiresIn: "1h", // Token 有效期1小时
});
const resetLink = `http://localhost:8888/user/reset-password?token=${token}`;

// 调用 sendEmail 函数
await sendEmail(
username,
"密码重置",
`请点击以下链接重置你的密码:${resetLink}`
);

// 同样,为防止用户枚举,即使发送失败也返回成功信息
return res.status(200).json({ message: "If a user with that username exists, a password reset link has been sent." });
} catch (err) {
console.error("Error in /forgot-password:", err);
// 增加一个更通用的服务器错误返回
return res.status(500).json({ error: "Internal server error." });
}
});


// 新增:重置密码路由
router.post("/reset-password", async (req, res) => {
const { token, newPassword } = req.body;

// 1. 鲁棒性检查:输入参数检查
if (!token || !newPassword) {
return res.status(422).json({ error: "Missing token or new password." });
}
// 2. 鲁棒性检查:新密码的复杂度检查
if (newPassword.length < 8) {
return res.status(400).json({ error: "New password must be at least 8 characters long." });
}

try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as userJWTPayload;
const uuid = payload.uuid;

// 3. 鲁棒性检查:验证token中的用户ID是否存在于数据库
const userQueryResult = await graphql.getUsersByUuid({ uuid });
if (userQueryResult.user.length === 0) {
// 如果用户不存在,返回通用错误,不暴露用户ID
return res.status(401).json({ error: "Unauthorized: Invalid or expired token." });
}

await graphql.updateUserPassword({ uuid: uuid, password: newPassword });

return res.status(200).json({ message: "Password has been successfully reset." });
} catch (err) {
console.error("Error in /reset-password:", err);
if (err instanceof jwt.TokenExpiredError) {
return res.status(401).json({ error: "Unauthorized: Token has expired." });
}
// 处理其他JWT验证错误(如无效签名等)
if (err instanceof jwt.JsonWebTokenError) {
return res.status(401).json({ error: "Unauthorized: Invalid token." });
}
return res.status(500).json({ error: "Internal server error." });
}
});

export default router;
Loading