Skip to content

Commit

Permalink
Merge pull request #141 from MurakawaTakuya/feat/notification-method
Browse files Browse the repository at this point in the history
通知の送信方法を変更
  • Loading branch information
MurakawaTakuya authored Jan 9, 2025
2 parents dddd9ba + 90078c5 commit 98dc256
Show file tree
Hide file tree
Showing 29 changed files with 6,209 additions and 2,091 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ ui-debug.log
*.cache
*.log
functions\lib

# PWA
public/workbox-*.js
public/sw.js
9 changes: 8 additions & 1 deletion functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ admin.initializeApp({
});

import goalRouter from "./routers/goalRouter";
import notificationRouter from "./routers/notificationRouter";
import postRouter from "./routers/postRouter";
import resultRouter from "./routers/resultRouter";
import userRouer from "./routers/userRouter";
Expand Down Expand Up @@ -52,7 +53,12 @@ const verifyAppCheckToken = async (
// Postmanを使うためにCloud FunctionsのApp Checkは開発環境では使用しない
if (process.env.NODE_ENV === "production") {
app.use((req, res, next) => {
verifyAppCheckToken(req, res, next);
if (req.headers.token === process.env.NOTIFICATION_KEY) {
// tasksからの/notificationの場合はスキップ
next();
} else {
verifyAppCheckToken(req, res, next);
}
});
}

Expand Down Expand Up @@ -93,6 +99,7 @@ app.use("/user", userRouer);
app.use("/goal", goalRouter);
app.use("/post", postRouter);
app.use("/result", resultRouter);
app.use("/notification", notificationRouter);

const region = "asia-northeast1";

Expand Down
114 changes: 114 additions & 0 deletions functions/src/routers/notificationRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import express, { Request, Response } from "express";
import admin from "firebase-admin";
import { logger } from "firebase-functions";
import { GoogleAuth } from "google-auth-library";

const router = express.Router();
const db = admin.firestore();

// POST: 通知を送信
router.post("/", async (req: Request, res: Response) => {
const token = req.headers.token;
if (!token) {
return res.status(401).json({ message: "Unauthorized" });
}
if (token !== process.env.NOTIFICATION_KEY) {
return res.status(401).json({ message: "Unauthorized" });
}

let goalId: string;
let marginTime: number;

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

if (!goalId || !marginTime) {
return res
.status(400)
.json({ message: "goalId and marginTime are required" });
}

try {
// goalIdからgoalを取得
const goalDoc = await db.collection("goal").doc(goalId).get();
const goalData = goalDoc.data();

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

// goalのuserIdからfcmTokenを取得
const userData = await db
.collection("user")
.doc(goalData.userId)
.get()
.then((doc) => doc.data());

if (!userData) {
return res.status(404).json({ message: "User not found" });
}

if (!userData.fcmToken) {
return res.status(404).json({ message: "FCM token not found" });
}

const auth = new GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
});
const accessToken = await auth.getAccessToken();

// 通知データを作成
const projectId = process.env.GCP_PROJECT_ID;
const postData = {
message: {
token: userData.fcmToken,
data: {
title: `${marginTime}分以内に目標を完了し写真をアップロードしましょう`,
body: goalData.text,
icon: "https://todo-real-c28fa.web.app/appIcon.svg",
},
},
};
const url = `https://fcm.googleapis.com/v1/projects/${projectId}/messages:send`;
const headers = {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
};
logger.info("Sending notification:", {
goalId: goalId,
userId: goalData.userId,
text: goalData.text,
fcmToken: userData.fcmToken,
});

// 通知を送信
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(postData),
});

if (!response.ok) {
const errorData = await response.json();
logger.error("httpRequest error:", errorData);
return res
.status(500)
.json({ message: "Error sending notification", error: errorData });
}

const responseData = await response.json();
logger.info("Notification sent successfully:", responseData);
} catch (error) {
logger.error("Error sending notification:", error);
return res
.status(500)
.json({ message: "Error sending notification", error });
}

return res.status(200).json({ message: "Notification sent successfully" });
});

export default router;
43 changes: 6 additions & 37 deletions functions/src/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { CloudTasksClient } from "@google-cloud/tasks";
import * as admin from "firebase-admin";
import { Buffer } from "buffer";
import { logger } from "firebase-functions";
import {
onDocumentCreated,
onDocumentDeleted,
} from "firebase-functions/v2/firestore";
import { GoogleAuth } from "google-auth-library";

const tasksClient = new CloudTasksClient();
const projectId = process.env.GCP_PROJECT_ID;
Expand Down Expand Up @@ -38,33 +37,23 @@ export const createTasksOnGoalCreate = onDocumentCreated(
goalData.deadline.toDate().getTime() - marginTime * 60 * 1000
);
const goalId = event.params.goalId;
const fcmToken = await getUserFcmToken(goalData.userId);
const postData = {
message: {
token: fcmToken, // 通知を受信する端末のトークン
data: {
title: `${marginTime}分以内に目標を完了し写真をアップロードしましょう!`,
body: goalData.text,
icon: "https://todo-real-c28fa.web.app/appIcon.svg",
},
},
goalId,
marginTime,
};
const queuePath = tasksClient.queuePath(projectId, region, queue);
const auth = new GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
});
const accessToken = await auth.getAccessToken();
const accessToken = process.env.NOTIFICATION_KEY;

await tasksClient.createTask({
parent: queuePath,
task: {
name: `${queuePath}/tasks/${goalId}`, // タスクの名前をgoalIdに設定
httpRequest: {
httpMethod: "POST",
url: `https://fcm.googleapis.com//v1/projects/${projectId}/messages:send`,
url: "https://firestore-okdtj725ta-an.a.run.app/notification",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
token: `${accessToken}`,
},
body: Buffer.from(JSON.stringify(postData)).toString("base64"),
},
Expand Down Expand Up @@ -138,23 +127,3 @@ export const deleteTasksOnPostCreate = onDocumentCreated(
}
}
);

const getUserFcmToken = async (userId: string) => {
const userData = await admin
.firestore()
.collection("user")
.doc(userId)
.get()
.then((doc) => doc.data());

if (!userData) {
throw new Error(`No user data found for userId:, ${userId}`);
}
if (!userData.fcmToken) {
throw new Error(`No FCM token found for userId:, ${userId}`);
}
if (userData.fcmToken === "") {
throw new Error(`FCM token is not stored for userId:, ${userId}`);
}
return userData.fcmToken;
};
3 changes: 2 additions & 1 deletion functions/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"strict": true,
"esModuleInterop": true,
"target": "es2017",
"moduleResolution": "node"
"moduleResolution": "node",
"skipLibCheck": true
},
"compileOnSave": true,
"include": ["src/*", "functions/src/**/*", "src/**/*.ts"]
Expand Down
13 changes: 10 additions & 3 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { NextConfig } from "next";
import nextPWA from "next-pwa";

const nextConfig: NextConfig = {
const withPWA = nextPWA({
dest: "public",
register: true,
skipWaiting: true,
buildExcludes: [/middleware-manifest.json$/],
});

const nextConfig = withPWA({
output: "export",
trailingSlash: true,
sassOptions: {
Expand All @@ -9,6 +16,6 @@ const nextConfig: NextConfig = {
images: {
unoptimized: true,
},
};
});

export default nextConfig;
Loading

0 comments on commit 98dc256

Please sign in to comment.