Skip to content

Commit

Permalink
Merge pull request #132 from MurakawaTakuya/feat/misc-storage-skeleto…
Browse files Browse the repository at this point in the history
…n-reload-goal

QOL update
  • Loading branch information
MurakawaTakuya authored Dec 29, 2024
2 parents ecc2e76 + 90315c2 commit dd7df81
Show file tree
Hide file tree
Showing 26 changed files with 417 additions and 197 deletions.
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

0 comments on commit dd7df81

Please sign in to comment.