Skip to content

Commit

Permalink
Merge branch 'main' into create-my-content
Browse files Browse the repository at this point in the history
  • Loading branch information
MurakawaTakuya committed Dec 12, 2024
2 parents 845002a + f09bec8 commit 948a33c
Show file tree
Hide file tree
Showing 21 changed files with 861 additions and 457 deletions.
134 changes: 41 additions & 93 deletions functions/package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
},
"main": "lib/index.js",
"dependencies": {
"@google-cloud/tasks": "^5.5.1",
"@types/multer": "^1.4.12",
"express": "^4.21.1",
"express": "^4.21.2",
"express-rate-limit": "^7.4.1",
"firebase-admin": "^12.7.0",
"firebase-functions": "^6.1.0",
"firebase-functions": "^6.1.1",
"helmet": "^8.0.0",
"multer": "^1.4.5-lts.1"
},
Expand Down
16 changes: 15 additions & 1 deletion functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import resultRouter from "./routers/resultRouter";
import userRouer from "./routers/userRouter";

const app = express();
app.set("trust proxy", 1);
app.use(helmet());
app.use(cors());
app.use(express.json());
Expand Down Expand Up @@ -50,7 +51,14 @@ const verifyAppCheckToken = async (

// Postmanを使うためにCloud FunctionsのApp Checkは開発環境では使用しない
if (process.env.NODE_ENV === "production") {
app.use(verifyAppCheckToken);
app.use((req, res, next) => {
// /sendNotificationと/receiveTestは別の認証を使用するのでApp Checkを使用しない
if (req.path !== "/sendNotification" && req.path !== "/receiveTest") {
verifyAppCheckToken(req, res, next);
} else {
next();
}
});
}

// 10分間で最大300回に制限
Expand Down Expand Up @@ -83,3 +91,9 @@ export const helloWorld = onRequest({ region: region }, (req, res) => {
export const firestore = onRequest({ region: region }, async (req, res) => {
app(req, res);
});

export {
createTasksOnGoalCreate,
deleteTasksOnGoalDelete,
deleteTasksOnPostCreate,
} from "./tasks";
156 changes: 156 additions & 0 deletions functions/src/tasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { CloudTasksClient } from "@google-cloud/tasks";
import * as admin from "firebase-admin";
import * as logger from "firebase-functions/logger";
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;
const region = "asia-northeast1";
const queue = "deadline-notification-queue";

export const createTasksOnGoalCreate = onDocumentCreated(
{ region: region, document: "goal/{goalId}" },
async (event) => {
// production以外はスキップ(Cloud Tasksがエミュレーターで実行できないため)
if (process.env.NODE_ENV !== "production") {
return;
}

if (!projectId) {
logger.info("GCP_PROJECT_ID is not defined.");
return;
}

if (!event.data) {
logger.info("No data found in event.");
return;
}

try {
const goalData = event.data.data();
const marginTime = 2;
// 期限のmarginTime分前にタスクを設定
const deadline = new Date(
goalData.deadline.toDate().getTime() - marginTime * 60 * 1000
);
const goalId = event.params.goalId;
const fcmToken = await getUserFcmToken(goalData.userId);
const postData = {
message: {
token: fcmToken, // 通知を受信する端末のトークン
notification: {
title: `${marginTime}分以内に目標を完了し写真をアップロードしましょう!`,
body: goalData.text,
},
},
};
const queuePath = tasksClient.queuePath(projectId, region, queue);
const auth = new GoogleAuth({
scopes: ["https://www.googleapis.com/auth/cloud-platform"],
});
const accessToken = await auth.getAccessToken();

await tasksClient.createTask({
parent: queuePath,
task: {
name: `${queuePath}/tasks/${goalId}`, // タスクの名前をgoalIdに設定
httpRequest: {
httpMethod: "POST",
url: `https://fcm.googleapis.com//v1/projects/${projectId}/messages:send`,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
body: Buffer.from(JSON.stringify(postData)).toString("base64"),
},
scheduleTime: { seconds: Math.floor(deadline.getTime() / 1000) },
},
});
logger.info("Task created for goalId:", goalId);
} catch (error) {
logger.info("Error scheduling task:", error);
}
}
);

export const deleteTasksOnGoalDelete = onDocumentDeleted(
{ region: region, document: "goal/{goalId}" },
async (event) => {
if (process.env.NODE_ENV !== "production") {
return;
}

if (!projectId) {
logger.info("GCP_PROJECT_ID is not defined.");
return;
}

if (!event.data) {
logger.info("No data found in event.");
return;
}

try {
const goalId = event.params.goalId;
const queuePath = tasksClient.queuePath(projectId, region, queue);
const taskName = `${queuePath}/tasks/${goalId}`; // goalIdが名前になっているタスクを削除
await tasksClient.deleteTask({ name: taskName });
logger.info("Task deleted for goalId:", goalId);
} catch (error) {
logger.info("Error deleting task:", error);
}
}
);

export const deleteTasksOnPostCreate = onDocumentCreated(
{
region: region,
document: "post/{postId}",
},
async (event) => {
if (process.env.NODE_ENV !== "production") {
return;
}

if (!projectId) {
logger.info("GCP_PROJECT_ID is not defined.");
return;
}

if (!event.data) {
logger.info("No data found in event.");
return;
}

try {
const postData = event.data.data();
const goalId = postData.goalId;
const queuePath = tasksClient.queuePath(projectId, region, queue);
const taskName = `${queuePath}/tasks/${goalId}`; // goalIdが名前になっているタスクを削除
await tasksClient.deleteTask({ name: taskName });
logger.info("Task deleted for goalId:", goalId);
} catch (error) {
logger.info("Error deleting task:", error);
}
}
);

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}`);
}
return userData.fcmToken;
};
15 changes: 11 additions & 4 deletions src/Components/GoalModal/GoalModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { appCheckToken, functionsEndpoint } from "@/app/firebase";
import { Goal } from "@/types/types";
import { useUser } from "@/utils/UserContext";
import { Add } from "@mui/icons-material";
import SendIcon from "@mui/icons-material/Send";
import {
Button,
DialogContent,
Expand All @@ -24,7 +25,7 @@ export default function GoalModal() {
event.preventDefault();

const postData: Goal = {
userId: user ? user.uid : "",
userId: user?.uid as string,
text: text,
deadline: new Date(dueDate),
};
Expand Down Expand Up @@ -79,7 +80,7 @@ export default function GoalModal() {
color="primary"
startDecorator={<Add />}
onClick={() => setOpen(true)}
disabled={!user || user?.loginType === "Guest"}
disabled={!user || user?.loginType === "Guest" || !user?.isMailVerified}
>
Create Goal
</Button>
Expand All @@ -93,9 +94,10 @@ export default function GoalModal() {
<ModalDialog
aria-labelledby="create-goal-title"
aria-describedby="create-goal-description"
sx={{ width: "90%", maxWidth: 400 }}
>
<DialogTitle>目標を作成</DialogTitle>
<DialogContent>自分の目標を入力してください.</DialogContent>
<DialogContent>達成したい内容と期限を入力してください</DialogContent>
<form onSubmit={handleSubmit}>
<Stack spacing={2} sx={{ mt: 2 }}>
<Input
Expand All @@ -122,7 +124,12 @@ export default function GoalModal() {
type="submit"
variant="solid"
color="primary"
disabled={!user || user?.loginType === "Guest"}
disabled={
!user ||
user?.loginType === "Guest" ||
!user?.isMailVerified
}
endDecorator={<SendIcon />}
>
Create Goal
</Button>
Expand Down
103 changes: 103 additions & 0 deletions src/Components/NameUpdate/NameUpdate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client";
import { appCheckToken, auth, functionsEndpoint } from "@/app/firebase";
import { useUser } from "@/utils/UserContext";
import {
DialogContent,
DialogTitle,
Input,
Modal,
ModalDialog,
} from "@mui/joy";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { updateProfile } from "firebase/auth";
import React, { useState } from "react";

export default function NameUpdate({
open,
setOpen,
}: {
open: boolean;
setOpen: (open: boolean) => void;
}) {
const [newName, setNewName] = useState("");
const { user } = useUser();
const handleNameUpdate = async (event: React.FormEvent) => {
event.preventDefault();

const response = await fetch(`${functionsEndpoint}/user/${user?.uid}`, {
method: "PUT",
headers: {
"X-Firebase-AppCheck": appCheckToken,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: newName }),
});

// Firebase Authentication の displayName を更新
if (auth.currentUser) {
await updateProfile(auth.currentUser, { displayName: newName });
console.log("Firebase displayName updated successfully");
} else {
console.error("Failed to update displayName: No authenticated user");
}

if (!response.ok) {
console.error("Failed to update name");
} else {
console.log("Name updated successfully");
setNewName("");
setOpen(false);
}
};

// 以下のJoy UIによるエラーを無効化
// Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release. Error Component Stack
try {
const consoleError = console.error;
console.error = (...args) => {
if (args[0]?.includes("Accessing element.ref was removed")) {
return;
}
consoleError(...args);
};
} catch {
console.error("Failed to disable Joy UI error");
}

return (
<Modal open={open} onClose={() => setOpen(false)} keepMounted disablePortal>
<ModalDialog
aria-labelledby="update-name-title"
aria-describedby="update-name-description"
>
<DialogTitle id="update-name-title">名前を変更</DialogTitle>
<DialogContent id="update-name-description">
新しい名前を入力してください.
</DialogContent>
<form onSubmit={handleNameUpdate}>
<Stack spacing={2} sx={{ mt: 2 }}>
<Input
placeholder="New Name"
value={newName}
onChange={(e) => setNewName(e.target.value)}
required
/>
<Stack direction="row" spacing={1} justifyContent="flex-end">
<Button
variant="outlined"
color="primary"
onClick={() => setOpen(false)}
>
Cancel
</Button>
<Button type="submit" variant="contained" color="primary">
Update Name
</Button>
</Stack>
</Stack>
</form>
</ModalDialog>
</Modal>
);
}
Loading

0 comments on commit 948a33c

Please sign in to comment.