Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QOL update #132

Merged
merged 6 commits into from
Dec 29, 2024
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
14 changes: 8 additions & 6 deletions Documents/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ API is provided by Firebase Cloud Functions. Database is provided by Firestore.
"text": "hoge fuga",
"post": {
"userId": "Vlx6GCtq90ag3lxgh0pcCKGp5ba0",
"storedURL": "hogehoge URL",
"storedId": "0f9a84ed-8ae8-44b0-a6f5-5ac5ca517948",
"text": "数学の勉強したよ^^",
"submittedAt": {
"_seconds": 1735603199,
Expand Down Expand Up @@ -151,14 +151,14 @@ API is provided by Firebase Cloud Functions. Database is provided by Firestore.
- Body (form-data)
- goalId: string
- text: string
- storedURL: string (画像のストレージパス、/post/{storedURL}/image)
- storedId: string (画像のストレージパス、/post/{storedId}/image)
- submittedAt: Date
- Example
```json
{
"goalId": "RXlHJiv3GtpzSDHhfljS",
"text": "今日は勉強をがんばった",
"storedURL": "hogehoge URL",
"storedId": "0f9a84ed-8ae8-44b0-a6f5-5ac5ca517948",
"submittedAt": "2024-12-31T23:59:59.000Z"
}
```
Expand All @@ -182,7 +182,7 @@ API is provided by Firebase Cloud Functions. Database is provided by Firestore.
"goalId": "9fgWJA6wMN54EkxIC2WD",
"userId": "IK0Zc2hoUYaYjXoqzmCl",
"text": "今日は勉強をがんばった",
"storedURL": "hogehoge URL",
"storedId": "0f9a84ed-8ae8-44b0-a6f5-5ac5ca517948",
"goalId": "RXlHJiv3GtpzSDHhfljS",
"submittedAt": "2024-12-31T23:59:59.000Z"
}
Expand All @@ -201,8 +201,10 @@ Use Create Post API to update post.
- URL: /result/:?userId
- Empty userId will return all results.
- Parameters
- limit?: number - The maximum number of results to return.(Default is 50)
- limit?: number - The maximum number of results to return. (Default is 50)
- offset?: number - The number of results to skip before starting to collect the result set.
- onlyPending?: boolean - If true, only pending goals will be returned. (Default is false)
- onlyCompleted?: boolean - If true, only completed or failed goals will be returned. (Default is false)
- Method: GET
- Response
```json
Expand All @@ -215,7 +217,7 @@ Use Create Post API to update post.
"text": "Duolingoやる",
"post": {
"text": "フランス語したよ",
"storedURL": "hogehoge URL",
"storedId": "0f9a84ed-8ae8-44b0-a6f5-5ac5ca517948",
"submittedAt": "2024-12-28T09:45:10.718Z"
},
"userData": {
Expand Down
20 changes: 15 additions & 5 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import serviceAccount from "./serviceAccountKey.json";

admin.initializeApp({
credential: admin.credential.cert(serviceAccount as admin.ServiceAccount),
storageBucket: "todo-real-c28fa.appspot.com",
storageBucket: "todo-real-c28fa.firebasestorage.app",
});

import goalRouter from "./routers/goalRouter";
Expand Down Expand Up @@ -61,26 +61,36 @@ if (process.env.NODE_ENV === "production") {
});
}

// 10分間で最大300回に制限
// 10分間で最大100回に制限
app.use(
rateLimit({
windowMs: 10 * 60 * 1000,
max: 300,
max: 100,
keyGenerator: (req) => {
const key = req.headers["x-forwarded-for"] || req.ip || "unknown";
return Array.isArray(key) ? key[0] : key;
},
handler: (req, res) => {
return res
.status(429)
.json({ message: "Too many requests, please try again later." });
},
})
);
// 1時間で最大1000回に制限
// 1時間で最大300回に制限
app.use(
rateLimit({
windowMs: 60 * 60 * 1000,
max: 1000,
max: 300,
keyGenerator: (req) => {
const key = req.headers["x-forwarded-for"] || req.ip || "unknown";
return Array.isArray(key) ? key[0] : key;
},
handler: (req, res) => {
return res
.status(429)
.json({ message: "Too many requests, please try again later." });
},
})
);

Expand Down
35 changes: 28 additions & 7 deletions functions/src/routers/goalRouter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express, { Request, Response } from "express";
import admin from "firebase-admin";
import { logger } from "firebase-functions";
import { Goal, GoalWithId } from "./types";
import { Goal, GoalWithId } from "../types";

const router = express.Router();
const db = admin.firestore();
Expand Down Expand Up @@ -147,14 +147,35 @@ router.put("/:goalId", async (req: Request, res: Response) => {

// DELETE: 目標を削除
router.delete("/:goalId", async (req: Request, res: Response) => {
const goalId = req.params.goalId;
try {
const goalId = req.params.goalId;

if (!goalId) {
return res.status(400).json({ message: "Goal ID is required" });
}
if (!goalId) {
return res.status(400).json({ message: "Goal ID is required" });
}

try {
await db.collection("goal").doc(goalId).delete();
const goalRef = db.collection("goal").doc(goalId);
const goalDoc = await goalRef.get();

if (!goalDoc.exists) {
return res.status(404).json({ message: "Goal not found" });
}

// storageから画像を削除
const storedId = goalDoc.data()?.post?.storedId;
if (storedId) {
try {
const bucket = admin.storage().bucket();
const file = bucket.file(`post/${storedId}`);
await file.delete();
logger.info("Image deleted successfully:", storedId);
} catch (error) {
logger.error("Error deleting image:", error);
return res.status(500).json({ message: "Error deleting image" });
}
}

await goalRef.delete();
return res.json({ message: "Goal deleted successfully", goalId });
} catch (error) {
logger.error(error);
Expand Down
42 changes: 30 additions & 12 deletions functions/src/routers/postRouter.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import express, { Request, Response } from "express";
import admin from "firebase-admin";
import { logger } from "firebase-functions";
import { updateStreak } from "./status";
import { PostWithGoalId } from "./types";
import { updateStreak } from "../status";
import { PostWithGoalId } from "../types";

const router = express.Router();
const db = admin.firestore();
Expand All @@ -25,7 +25,7 @@ router.get("/", async (req: Request, res: Response) => {
goalId: goalDoc.id,
userId: goalData.userId,
text: goalData.post.text,
storedURL: goalData.post.storedURL,
storedId: goalData.post.storedId,
submittedAt: goalData.post.submittedAt.toDate(),
});
}
Expand Down Expand Up @@ -69,7 +69,7 @@ router.get("/:userId", async (req: Request, res: Response) => {
goalId: goalDoc.id,
userId: goalData.userId,
text: goalData.post.text,
storedURL: goalData.post.storedURL,
storedId: goalData.post.storedId,
submittedAt: goalData.post.submittedAt.toDate(),
});
}
Expand All @@ -86,19 +86,19 @@ router.get("/:userId", async (req: Request, res: Response) => {
router.post("/", async (req: Request, res: Response) => {
let goalId: PostWithGoalId["goalId"];
let text: PostWithGoalId["text"];
let storedURL: PostWithGoalId["storedURL"];
let storedId: PostWithGoalId["storedId"];
let submittedAt: PostWithGoalId["submittedAt"];

try {
({ goalId, text = "", storedURL, submittedAt } = req.body);
({ goalId, text = "", storedId, submittedAt } = req.body);
} catch (error) {
logger.error(error);
return res.status(400).json({ message: "Invalid request body" });
}

if (!goalId || !storedURL || !submittedAt) {
if (!goalId || !storedId || !submittedAt) {
return res.status(400).json({
message: "userId, storedURL, goalId, and submittedAt are required",
message: "userId, storedId, goalId, and submittedAt are required",
});
}

Expand All @@ -119,7 +119,7 @@ router.post("/", async (req: Request, res: Response) => {
await goalRef.update({
post: {
text,
storedURL,
storedId,
submittedAt: new Date(submittedAt),
},
});
Expand All @@ -135,18 +135,36 @@ router.post("/", async (req: Request, res: Response) => {

// DELETE: 投稿を削除
router.delete("/:goalId", async (req: Request, res: Response) => {
const goalId = req.params.goalId;

try {
const goalId = req.params.goalId;

if (!goalId) {
return res.status(400).json({ message: "Goal ID is required" });
}

const goalRef = db.collection("goal").doc(goalId);
const goalDoc = await goalRef.get();

if (!goalDoc.exists) {
return res.status(404).json({ message: "Goal not found" });
}

// Storageから画像を削除
const storedId = goalDoc.data()?.post?.storedId;
if (storedId) {
try {
const bucket = admin.storage().bucket();
const file = bucket.file(`post/${storedId}`);
await file.delete();
logger.info("Image deleted successfully:", storedId);
} catch (error) {
logger.error("Error deleting image:", error);
return res.status(500).json({ message: "Error deleting image" });
}
}

await goalRef.update({
post: admin.firestore.FieldValue.delete(),
post: null,
});

return res.json({ message: "Post deleted successfully" });
Expand Down
Loading
Loading