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
16 changes: 0 additions & 16 deletions backend/.local.env.template

This file was deleted.

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
28 changes: 28 additions & 0 deletions backend/src/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import multer from "multer";
import fs from "fs";
import path from "path";
import authenticate from "./authenticate";
import { sdk as graphql } from "./index";

const router = express.Router();

Expand Down Expand Up @@ -74,4 +75,31 @@ router.get("/download", authenticate, (req, res) => {
}
});

router.post("/delete", authenticate, (req, res) => {
const { room, filename } = req.body;
if (!room || !filename) {
return res.status(422).send("422 Unprocessable Entity: Missing room or filename");
}
const filePath = path.resolve(baseDir, room as string, filename as string);
try {
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
} else {
return res.status(404).send("404 Not Found: File does not exist");
}
// Optionally remove DB references if schema tracks files. Attempt safe call.
try {
if ((graphql as any).delete_message) {
// no-op: placeholder for custom deletion logic if needed
}
} catch (err) {
console.error("GraphQL cleanup failed", err);
}
return res.sendStatus(200);
} catch (err) {
console.error(err);
return res.sendStatus(500);
}
});

export default router;
91 changes: 91 additions & 0 deletions backend/src/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import express from "express";
import jwt from "jsonwebtoken";
import { sdk as graphql } from "./index";
import { GraphQLClient } from "graphql-request";
import { sendEmail } from "./email";
import authenticate from "./authenticate";

interface userJWTPayload {
uuid: string;
Expand Down Expand Up @@ -71,4 +74,92 @@ router.post("/register", async (req, res) => {
}
});

router.post("/change-password/request", async (req, res) => {
const { username } = req.body;
if (!username) {
return res.status(422).send("422 Unprocessable Entity: Missing username");
}
try {
const queryResult = await graphql.getUsersByUsername({ username: username });
if (queryResult.user.length === 0) {
// Do not reveal whether user exists to avoid user enumeration
return res.sendStatus(200);
}
const user = queryResult.user[0];
const payload = { uuid: user.uuid, username };
const token = jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: "1h" });
const site = process.env.SITE_URL || `http://localhost:8888`;
const resetLink = `${site}/user/change-password?action=token&token=${token}`;
const text = `You (or someone else) requested a password reset. Use the link below to reset your password (valid 1 hour):\n\n${resetLink}`;
try {
await sendEmail(username, "Password reset request", text);
return res.sendStatus(200);
} catch (err) {
console.error("Failed to send reset email", err);
return res.sendStatus(500);
}
} catch (err) {
console.error(err);
return res.sendStatus(500);
}
});

router.post("/change-password/action", async (req, res) => {
const { token, newPassword } = req.body;
if (!token || !newPassword) {
return res.status(422).send("422 Unprocessable Entity: Missing token or newPassword");
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET! as string) as any;
if (!decoded || !decoded.uuid) {
return res.status(401).send("401 Unauthorized: Invalid token");
}
const userUuid = decoded.uuid;
try {
const client = new GraphQLClient(process.env.HASURA_GRAPHQL_ENDPOINT!, {
headers: {
"Content-Type": "application/json",
"x-hasura-admin-secret": process.env.HASURA_GRAPHQL_ADMIN_SECRET || "",
},
});
const mutation = `mutation updateUserPassword($uuid: uuid!, $password: String!) {\n update_user_by_pk(pk_columns: {uuid: $uuid}, _set: {password: $password}) { uuid }\n}`;
await client.request(mutation, { uuid: userUuid, password: newPassword });
return res.sendStatus(200);
} catch (err) {
console.error(err);
return res.sendStatus(500);
}
} catch (err) {
console.error(err);
return res.status(401).send("401 Unauthorized: Token expired or invalid");
}
});

router.get("/delete", authenticate, async (req, res) => {
// Expect Authorization header handled by authenticate middleware
const authHeader = req.get("Authorization")!;
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET! as string) as any;
const userUuid = decoded.uuid;
try {
const client = new GraphQLClient(process.env.HASURA_GRAPHQL_ENDPOINT!, {
headers: {
"Content-Type": "application/json",
"x-hasura-admin-secret": process.env.HASURA_GRAPHQL_ADMIN_SECRET || "",
},
});
const mutation = `mutation deleteUserByPk($uuid: uuid!) {\n delete_user_by_pk(uuid: $uuid) { uuid }\n}`;
await client.request(mutation, { uuid: userUuid });
return res.sendStatus(200);
} catch (err) {
console.error(err);
return res.sendStatus(500);
}
} catch (err) {
console.error(err);
return res.status(401).send("401 Unauthorized: Invalid token");
}
});

export default router;
2 changes: 0 additions & 2 deletions database/.local.env.template

This file was deleted.

2 changes: 2 additions & 0 deletions tutorials/04-Backend.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ yarn # 是 yarn install 的简写

大家需要根据自己情况填写变量值,删去多余注释,并将`.local.env.template`更名为`.local.env`(或新建文件复制过去),即可被项目正确识别。

> 注意:切勿将 `.local.env` 提交到 Git。仓库中已提供一个快速检查脚本 `backend/scripts/check_env.ps1`,可在 `backend` 目录运行以验证必需变量已正确设置。

此外,有关`JWT_SECRET`的说明详见下方的“注意事项”。

### 运行方式
Expand Down