diff --git a/ALUM/ALUM.xcodeproj/project.pbxproj b/ALUM/ALUM.xcodeproj/project.pbxproj index fba26e47..1a43c2dc 100644 --- a/ALUM/ALUM.xcodeproj/project.pbxproj +++ b/ALUM/ALUM.xcodeproj/project.pbxproj @@ -341,6 +341,7 @@ 0748208D29712921004AF547 /* ALUM */, 0748208C29712921004AF547 /* Products */, 0799ACEE2A0102E400EEAFA2 /* Recovered References */, + 2A06083E2A0E3128007F38D6 /* Frameworks */, ); sourceTree = ""; }; @@ -537,6 +538,13 @@ path = Services; sourceTree = ""; }; + 2A06083E2A0E3128007F38D6 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; 97992B5D29A6E7E200701CC7 /* SignUpPage */ = { isa = PBXGroup; children = ( diff --git a/ALUM/ALUM/ALUM.entitlements b/ALUM/ALUM/ALUM.entitlements index 0c67376e..903def2a 100644 --- a/ALUM/ALUM/ALUM.entitlements +++ b/ALUM/ALUM/ALUM.entitlements @@ -1,5 +1,8 @@ - + + aps-environment + development + diff --git a/ALUM/ALUM/ALUMApp.swift b/ALUM/ALUM/ALUMApp.swift index 27ada38b..4f7067c9 100644 --- a/ALUM/ALUM/ALUMApp.swift +++ b/ALUM/ALUM/ALUMApp.swift @@ -9,14 +9,9 @@ import SwiftUI import FirebaseCore import FirebaseFirestore import FirebaseAuth - -class AppDelegate: NSObject, UIApplicationDelegate { - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - FirebaseApp.configure() - return true - } -} +import Firebase +import UserNotifications +import FirebaseMessaging extension View { func dismissKeyboardOnDrag() -> some View { @@ -38,3 +33,90 @@ struct ALUMApp: App { } } } + +class AppDelegate: NSObject, UIApplicationDelegate { + let gcmMessageIDKey = "gcm.message_id" + @ObservedObject var currentUser: CurrentUserModel = CurrentUserModel.shared + + func application(_ application: UIApplication, didFinishLaunchingWithOptions + launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + FirebaseApp.configure() + + Messaging.messaging().delegate = self + + if #available(iOS 10.0, *) { + // For iOS 10 display notification (sent via APNS) + UNUserNotificationCenter.current().delegate = self + + let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] + UNUserNotificationCenter.current().requestAuthorization( + options: authOptions, + completionHandler: {_, _ in } + ) + } else { + let settings: UIUserNotificationSettings = + UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil) + application.registerUserNotificationSettings(settings) + } + + application.registerForRemoteNotifications() + return true + } + + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + + if let messageID = userInfo[gcmMessageIDKey] { + print("Message ID: \(messageID)") + } + + print(userInfo) + + completionHandler(UIBackgroundFetchResult.newData) + } +} + +extension AppDelegate: MessagingDelegate { + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + currentUser.fcmToken = fcmToken + } +} + +@available(iOS 10, *) +extension AppDelegate: UNUserNotificationCenterDelegate { + + // Receive displayed notifications for iOS 10 devices. + func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: + @escaping (UNNotificationPresentationOptions) -> Void) { + let userInfo = notification.request.content.userInfo + + if let messageID = userInfo[gcmMessageIDKey] { + print("Message ID: \(messageID)") + } + + print(userInfo) + + // Change this to your preferred presentation option + completionHandler([[.banner, .badge, .sound]]) + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + Messaging.messaging().apnsToken = deviceToken + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + let userInfo = response.notification.request.content.userInfo + + if let messageID = userInfo[gcmMessageIDKey] { + print("Message ID from userNotificationCenter didReceive: \(messageID)") + } + + print(userInfo) + + completionHandler() + } +} diff --git a/ALUM/ALUM/Info.plist b/ALUM/ALUM/Info.plist index e9b3de4f..79c22756 100644 --- a/ALUM/ALUM/Info.plist +++ b/ALUM/ALUM/Info.plist @@ -9,5 +9,12 @@ Metropolis-ExtraBoldItalic.otf Metropolis-Thin.otf + UIBackgroundModes + + fetch + remote-notification + + UIUserInterfaceStyle + Light diff --git a/ALUM/ALUM/Models/CurrentUserModel.swift b/ALUM/ALUM/Models/CurrentUserModel.swift index 1a9d2797..4243209d 100644 --- a/ALUM/ALUM/Models/CurrentUserModel.swift +++ b/ALUM/ALUM/Models/CurrentUserModel.swift @@ -13,9 +13,14 @@ enum UserRole { case mentee } +struct FCMToken: Codable { + var fcmToken: String +} + class CurrentUserModel: ObservableObject { static let shared = CurrentUserModel() + @Published var fcmToken: String? @Published var isLoading: Bool @Published var uid: String? @Published var role: UserRole? @@ -31,6 +36,7 @@ class CurrentUserModel: ObservableObject { @Published var pairedMenteeId: String? init() { + self.fcmToken = nil self.isLoading = true self.isLoggedIn = false self.uid = nil @@ -61,6 +67,7 @@ class CurrentUserModel: ObservableObject { self.setCurrentUser(isLoading: false, isLoggedIn: false, uid: nil, role: nil) } else { try await self.setFromFirebaseUser(user: user!) + try await sendFcmToken(fcmToken: fcmToken!) } } catch { // in case setFromFirebaseUser fails, just make the user login again @@ -153,4 +160,31 @@ class CurrentUserModel: ObservableObject { } return userStatus } + + func sendFcmTokenHelper(fcmToken: String) { + Task { + do { + try await sendFcmToken(fcmToken: fcmToken) + } catch { + print("Error in sending FCM Token") + } + } + } + + func sendFcmToken(fcmToken: String) async throws { + print(fcmToken) + print(self.uid) + var tokenToSend: FCMToken = FCMToken(fcmToken: fcmToken) + let route = APIRoute.patchUser(userId: self.uid ?? "") + var request = try await route.createURLRequest() + guard let jsonData = try? JSONEncoder().encode(tokenToSend) else { + DispatchQueue.main.async { + CurrentUserModel.shared.showInternalError.toggle() + } + throw AppError.internalError(.jsonParsingError, message: "Failed to Encode Data") + } + request.httpBody = jsonData + let responseData = try await ServiceHelper.shared.sendRequestWithSafety(route: route, request: request) + print("SUCCESS - \(route.label)") + } } diff --git a/ALUM/ALUM/Services/APIConfig.swift b/ALUM/ALUM/Services/APIConfig.swift index 5ce6c6b6..f8721b4a 100644 --- a/ALUM/ALUM/Services/APIConfig.swift +++ b/ALUM/ALUM/Services/APIConfig.swift @@ -32,6 +32,7 @@ enum APIRoute { case getMentee(userId: String) case postMentor case postMentee + case patchUser(userId: String) case getCalendly case getNote(noteId: String) @@ -65,87 +66,97 @@ enum APIRoute { return URLString.sessions case .postSession: return URLString.sessions + case .getCalendly: + return URLString.calendly + case .patchUser(let userId): + return [URLString.user, userId].joined(separator: "/") case .deleteSession(sessionId: let sessionId): return [URLString.sessions, sessionId].joined(separator: "/") case .patchSession(sessionId: let sessionId): return [URLString.sessions, sessionId].joined(separator: "/") - case .getCalendly: - return URLString.calendly } } - var method: String { - switch self { - case .getSelf, .getMentee, .getMentor, .getNote, .getSession, .getSessions, .getCalendly: - return "GET" - case .postMentor, .postMentee, .postSession: - return "POST" - case .deleteSession: - return "DELETE" - case .patchNote, .patchSession: - return "PATCH" - } + var method: String { + switch self { + case .getSelf, .getMentee, .getMentor, .getNote, .getSession, .getSessions, .getCalendly: + return "GET" + case .postMentor, .postMentee, .postSession: + return "POST" + case .patchNote, .patchUser, .patchSession: + return "PATCH" + case .deleteSession: + return "DELETE" } + } - var requireAuth: Bool { - switch self { - case .getSelf, .getMentor, .getMentee, .getNote, .patchNote, .getSession, - .getSessions, .postSession, .getCalendly, .deleteSession, - .patchSession: - return true - case .postMentee, .postMentor: - return false - } + var requireAuth: Bool { + switch self { + case + .getSelf, + .getMentor, + .getMentee, + .getNote, + .patchNote, + .getSession, + .getSessions, + .postSession, + .getCalendly, + .patchUser, + .patchSession, + .deleteSession: + return true + case .postMentee, .postMentor: + return false } + } - func createURLRequest() async throws -> URLRequest { - return try await ServiceHelper.shared.createRequest( - urlString: self.url, - method: self.method, - requireAuth: self.requireAuth - ) - } + func createURLRequest() async throws -> URLRequest { + return try await ServiceHelper.shared.createRequest( + urlString: self.url, + method: self.method, + requireAuth: self.requireAuth + ) + } - var label: String { - return "\(self.method) \(self.url)" - } + var label: String { + return "\(self.method) \(self.url)" + } - var successCode: Int { - switch self { - case .getSelf, .getMentor, .getMentee, .getNote, .patchNote, - .getSession, .getSessions, .getCalendly, - .deleteSession, .patchSession: - return 200 // 200 Ok - case .postMentor, .postMentee, .postSession: - return 201 // 201 Created - } + var successCode: Int { + switch self { + case .getSelf, .getMentor, .getMentee, .getNote, .patchNote, + .getSession, .getSessions, .getCalendly, .patchUser, .deleteSession, .patchSession: + return 200 // 200 Ok + case .postMentor, .postMentee, .postSession: + return 201 // 201 Created } + } - func getAppError(statusCode: Int, message: String) -> AppError { - let labeledMessage = "\(self.label) - \(message)" - let errorMap: [Int: AppError] + func getAppError(statusCode: Int, message: String) -> AppError { + let labeledMessage = "\(self.label) - \(message)" + let errorMap: [Int: AppError] - switch self { - case .getSelf, .getMentor, .getMentee, .getNote, .patchNote, - .getSession, .getSessions, .getCalendly, - .deleteSession, .patchSession: - errorMap = [ - 401: AppError.actionable(.authenticationError, message: labeledMessage), - 400: AppError.internalError(.invalidRequest, message: labeledMessage), - 404: AppError.internalError(.invalidRequest, message: labeledMessage) - ] - case .postSession: + switch self { + case .getSelf, .getMentor, .getMentee, .getNote, .patchNote, + .getSession, .getSessions, .getCalendly, .patchUser, + .deleteSession, .patchSession: + errorMap = [ + 401: AppError.actionable(.authenticationError, message: labeledMessage), + 400: AppError.internalError(.invalidRequest, message: labeledMessage), + 404: AppError.internalError(.invalidRequest, message: labeledMessage) + ] + case .postSession: errorMap = [ 400: AppError.internalError(.invalidRequest, message: labeledMessage) ] - case .postMentor, .postMentee: - // message will be displayed to user so no label here - errorMap = [ - 400: AppError.actionable(.invalidInput, message: message) - ] - } - - let error = errorMap[statusCode] ?? AppError.internalError(.unknownError, message: labeledMessage) - return error + case .postMentor, .postMentee: + errorMap = [ + 400: AppError.internalError(.invalidRequest, message: message) + ] } + + let error = errorMap[statusCode] ?? AppError.internalError(.unknownError, message: labeledMessage) + return error + } } diff --git a/backend/package-lock.json b/backend/package-lock.json index 507ed739..a97ec103 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -19,6 +19,7 @@ "firebase-functions": "^4.4.1", "mongoose": "^6.8.4", "multer": "^1.4.5-lts.1", + "node-schedule": "^2.1.1", "pug": "^3.0.2", "sharp": "^0.31.3" }, @@ -28,6 +29,7 @@ "@types/mongoose": "^5.11.97", "@types/multer": "^1.4.7", "@types/node": "^18.11.18", + "@types/node-schedule": "^2.1.0", "@types/sharp": "^0.31.1", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.48.1", @@ -2202,6 +2204,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "node_modules/@types/node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -3338,6 +3349,17 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/cron-parser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.8.1.tgz", + "integrity": "sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5750,6 +5772,11 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "optional": true }, + "node_modules/long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -5784,6 +5811,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==" }, + "node_modules/luxon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", + "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==", + "engines": { + "node": ">=12" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -6209,6 +6244,19 @@ "node": ">= 6.13.0" } }, + "node_modules/node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", + "dependencies": { + "cron-parser": "^4.2.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7325,6 +7373,11 @@ "npm": ">= 3.0.0" } }, + "node_modules/sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -10209,6 +10262,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "@types/node-schedule": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/node-schedule/-/node-schedule-2.1.0.tgz", + "integrity": "sha512-NiTwl8YN3v/1YCKrDFSmCTkVxFDylueEqsOFdgF+vPsm+AlyJKGAo5yzX1FiOxPsZiN6/r8gJitYx2EaSuBmmg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -11037,6 +11099,14 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "cron-parser": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.8.1.tgz", + "integrity": "sha512-jbokKWGcyU4gl6jAfX97E1gDpY12DJ1cLJZmoDzaAln/shZ+S3KBFBuA2Q6WeUN4gJf/8klnV1EfvhA2lK5IRQ==", + "requires": { + "luxon": "^3.2.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -12852,6 +12922,11 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "optional": true }, + "long-timeout": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", + "integrity": "sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -12885,6 +12960,11 @@ } } }, + "luxon": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz", + "integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==" + }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -13204,6 +13284,16 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, + "node-schedule": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/node-schedule/-/node-schedule-2.1.1.tgz", + "integrity": "sha512-OXdegQq03OmXEjt2hZP33W2YPs/E5BcFQks46+G2gAxs4gHOIVD1u7EqlYLYSKsaIpyKCK9Gbk0ta1/gjRSMRQ==", + "requires": { + "cron-parser": "^4.2.0", + "long-timeout": "0.1.1", + "sorted-array-functions": "^1.3.0" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -14004,6 +14094,11 @@ "smart-buffer": "^4.2.0" } }, + "sorted-array-functions": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", + "integrity": "sha512-2sqgzeFlid6N4Z2fUQ1cvFmTOLRi/sEDzSQ0OKYchqgoPmQBVyM3959qYx3fpS6Esef80KjmpgPeEr028dP3OA==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/backend/package.json b/backend/package.json index 81402383..057941b9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -26,6 +26,7 @@ "firebase-functions": "^4.4.1", "mongoose": "^6.8.4", "multer": "^1.4.5-lts.1", + "node-schedule": "^2.1.1", "pug": "^3.0.2", "sharp": "^0.31.3" }, @@ -35,6 +36,7 @@ "@types/mongoose": "^5.11.97", "@types/multer": "^1.4.7", "@types/node": "^18.11.18", + "@types/node-schedule": "^2.1.0", "@types/sharp": "^0.31.1", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.48.1", diff --git a/backend/src/app.ts b/backend/src/app.ts index 3992b12e..334d7380 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -4,6 +4,7 @@ import { json } from "body-parser"; import { onRequest } from "firebase-functions/v2/https"; import { userRouter } from "./routes/user"; +import { selfRouter } from "./routes/self"; import { notesRouter } from "./routes/notes"; import { sessionsRouter } from "./routes/sessions"; import { mongoURI, port } from "./config"; @@ -11,6 +12,7 @@ import { imageRouter } from "./routes/image"; import { errorHandler } from "./errors/handler"; import { calendlyPage } from "./routes/calendlyPage"; +import { startUpcomingSessionCronJob, startPostSessionCronJob } from "./services/notifications"; /** * Express server application class. * @description Will later contain the routing system. @@ -29,6 +31,7 @@ mongoose.connect(mongoURI, {}, () => { server.app.use(json()); server.app.set("view engine", "pug"); server.app.use(userRouter); +server.app.use(selfRouter); server.app.use(sessionsRouter); server.app.use(notesRouter); server.app.use(imageRouter); @@ -38,4 +41,6 @@ server.app.use(errorHandler); // This handler is reached whenever there is some // make server listen on some port server.app.listen(port, () => console.log(`> Listening on port ${port}`)); // eslint-disable-line no-console +startUpcomingSessionCronJob(); +startPostSessionCronJob(); export const firebaseApp = onRequest(server.app); diff --git a/backend/src/errors/service.ts b/backend/src/errors/service.ts index 44c47c2f..08d4ae8a 100644 --- a/backend/src/errors/service.ts +++ b/backend/src/errors/service.ts @@ -19,9 +19,12 @@ const NOTE_WAS_NOT_FOUND = "Note was not found"; const NOTE_WAS_NOT_SAVED = "Note was not saved"; const INVALID_URI = "Calendly URI is invalid. Check formatting of URI string"; const ERROR_GETTING_EVENT_DATA = "There was an error retrieving the calendly event data"; + const ERROR_DELETING_EVENT = "There was an error deleting a calendly event"; const INVALID_ROLE_WAS_FOUND = "Allowed user roles for this context is mentor and mentee only"; +const ERROR_SENDING_NOTIFICATION = "There was an error sending the notification."; + export class ServiceError extends CustomError { static IMAGE_NOT_SAVED = new ServiceError(0, 404, IMAGE_NOT_SAVED); @@ -47,9 +50,11 @@ export class ServiceError extends CustomError { static MENTOR_WAS_NOT_SAVED = new ServiceError(11, 404, MENTOR_WAS_NOT_SAVED); - static MENTEE_WAS_NOT_SAVED = new ServiceError(11, 404, MENTEE_WAS_NOT_SAVED); + static MENTEE_WAS_NOT_SAVED = new ServiceError(12, 404, MENTEE_WAS_NOT_SAVED); + + static INVALID_ROLE_WAS_FOUND = new ServiceError(13, 404, INVALID_ROLE_WAS_FOUND); - static INVALID_ROLE_WAS_FOUND = new ServiceError(11, 404, INVALID_ROLE_WAS_FOUND); + static ERROR_DELETING_EVENT = new ServiceError(14, 404, ERROR_DELETING_EVENT); - static ERROR_DELETING_EVENT = new ServiceError(7, 404, ERROR_DELETING_EVENT); + static ERROR_SENDING_NOTIFICATION = new ServiceError(15, 404, ERROR_SENDING_NOTIFICATION); } diff --git a/backend/src/models/mentee.ts b/backend/src/models/mentee.ts index c3ac50ce..47014c81 100644 --- a/backend/src/models/mentee.ts +++ b/backend/src/models/mentee.ts @@ -10,6 +10,7 @@ interface MenteeInterface { careerInterests: string[]; mentorshipGoal: string; pairingId: string; + fcmToken: string; status: UserStatusType; } @@ -22,6 +23,7 @@ interface MenteeDoc extends mongoose.Document { careerInterests: string[]; mentorshipGoal: string; pairingId: string; + fcmToken: string; status: UserStatusType; } @@ -71,6 +73,10 @@ const MenteeSchema = new mongoose.Schema({ enum: ["paired", "approved", "under review"], required: true, }, + fcmToken: { + type: String, + required: true, + }, }); const Mentee = mongoose.model("Mentee", MenteeSchema); diff --git a/backend/src/models/mentor.ts b/backend/src/models/mentor.ts index 45f883e3..11cadd9f 100644 --- a/backend/src/models/mentor.ts +++ b/backend/src/models/mentor.ts @@ -22,6 +22,7 @@ interface MentorInterface { status: UserStatusType; personalAccessToken: string; location: string; + fcmToken: string; } interface MentorDoc extends mongoose.Document { @@ -40,6 +41,7 @@ interface MentorDoc extends mongoose.Document { status: UserStatusType; personalAccessToken: string; location: string; + fcmToken: string; } interface MentorModelInterface extends mongoose.Model { @@ -112,6 +114,10 @@ const mentorSchema = new mongoose.Schema({ type: String, required: true, }, + fcmToken: { + type: String, + required: true, + }, }); const Mentor = mongoose.model("Mentor", mentorSchema); diff --git a/backend/src/models/session.ts b/backend/src/models/session.ts index 90499e5d..b0d9adf3 100644 --- a/backend/src/models/session.ts +++ b/backend/src/models/session.ts @@ -18,6 +18,8 @@ interface SessionInterface { preSessionCompleted: boolean; postSessionMentorCompleted: boolean; postSessionMenteeCompleted: boolean; + upcomingSessionNotifSent: boolean; + postSessionNotifSent: boolean; } export interface SessionDoc extends mongoose.Document { @@ -33,6 +35,8 @@ export interface SessionDoc extends mongoose.Document { preSessionCompleted: boolean; postSessionMentorCompleted: boolean; postSessionMenteeCompleted: boolean; + upcomingSessionNotifSent: boolean; + postSessionNotifSent: boolean; } interface SessionModelInterface extends mongoose.Model { @@ -88,6 +92,14 @@ const SessionSchema = new mongoose.Schema({ type: Boolean, required: true, }, + upcomingSessionNotifSent: { + type: Boolean, + required: true, + }, + postSessionNotifSent: { + type: Boolean, + required: true, + }, }); const Session = mongoose.model("Session", SessionSchema); diff --git a/backend/src/routes/self.ts b/backend/src/routes/self.ts new file mode 100644 index 00000000..a5caa885 --- /dev/null +++ b/backend/src/routes/self.ts @@ -0,0 +1,134 @@ +/** + * This file contains routes that will be used for generic users + * i.e. not separate for mentee and mentor. + */ +import express, { NextFunction, Request, Response } from "express"; +// import { Infer } from "caketype"; +import mongoose from "mongoose"; +import { validateReqBodyWithCake } from "../middleware/validation"; +import { Mentee, Mentor } from "../models"; +import { getMenteeId, getMentorId, updateFCMToken } from "../services/user"; +import { UpdateUserCake } from "../types"; +import { InternalError } from "../errors/internal"; +import { ServiceError } from "../errors/service"; +import { verifyAuthToken } from "../middleware/auth"; +import { CustomError } from "../errors"; +import { getUpcomingSession, getLastSession } from "../services/session"; + +const router = express.Router(); + +// type UpdateUserType = Infer +router.patch( + "/user/:userId", + [verifyAuthToken], + validateReqBodyWithCake(UpdateUserCake), + async (req: Request, res: Response, next: NextFunction) => { + try { + console.log("Updating FCM Token"); + const userId = req.params.userId; + if (!mongoose.Types.ObjectId.isValid(userId)) { + throw ServiceError.INVALID_MONGO_ID; + } + console.log("user id is valid"); + const role = req.body.role; + const updatedToken = req.body.fcmToken; + console.log("got information"); + await updateFCMToken(updatedToken, userId, role); + res.status(200).json({ + message: "Success", + }); + } catch (e) { + next(e); + } + } +); + +/** + * Route to setup mobile app for any logged in user (mentor or mentee) + * + * This route returns the following + * If user is a mentor, + * menteeIds, status, upcomingSessionId + * + * If user is mentee, + * mentorId, status, upcomingSessionId + */ +router.get( + "/user/me", + [verifyAuthToken], + async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.body.uid; + const role = req.body.role; + console.log(`GET /user/me uid - ${req.body.uid}`); + + const getUpcomingSessionPromise = getUpcomingSession(userId, role); + const getPastSessionPromise = getLastSession(userId, role); + if (role === "mentee") { + // GET mentee document + const mentee = await Mentee.findById(userId); + if (!mentee) { + throw ServiceError.MENTEE_WAS_NOT_FOUND; + } + + if (mentee.status !== "paired") { + res.status(200).send({ + status: mentee.status, + }); + return; + } + const getPairedMentorIdPromise = getMentorId(mentee.pairingId); + const [upcomingSessionId, pastSessionId, pairedMentorId] = await Promise.all([ + getUpcomingSessionPromise, + getPastSessionPromise, + getPairedMentorIdPromise, + ]); + res.status(200).send({ + status: mentee.status, + sessionId: upcomingSessionId ?? pastSessionId, + pairedMentorId, + }); + } else if (role === "mentor") { + const mentor = await Mentor.findById(userId); + if (!mentor) { + throw ServiceError.MENTOR_WAS_NOT_FOUND; + } + + if (mentor.status !== "paired") { + res.status(200).send({ + status: mentor.status, + }); + return; + } + + const getMenteeIdsPromises = mentor.pairingIds.map(async (pairingId) => + getMenteeId(pairingId) + ); + + // For MVP, we assume there is only 1 mentee 1 mentor pairing + const getMenteeIdsPromise = getMenteeIdsPromises[0]; + + const [upcomingSessionId, pastSessionId, pairedMenteeId] = await Promise.all([ + getUpcomingSessionPromise, + getPastSessionPromise, + getMenteeIdsPromise, + ]); + + res.status(200).send({ + status: mentor.status, + sessionId: upcomingSessionId ?? pastSessionId, + pairedMenteeId, + }); + return; + } + } catch (e) { + if (e instanceof CustomError) { + next(e); + return; + } + next(InternalError.ERROR_GETTING_MENTEE); + } + } +); + +export { router as selfRouter }; diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index 8014a238..963e362a 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -4,6 +4,7 @@ import express, { NextFunction, Request, Response } from "express"; import mongoose, { ObjectId } from "mongoose"; +// import schedule from "node-schedule"; // import { boolean } from "caketype"; import { validateReqBodyWithCake } from "../middleware/validation"; import { Mentor, Mentee, Session } from "../models"; @@ -12,6 +13,7 @@ import { verifyAuthToken } from "../middleware/auth"; import { getCalendlyEventDate, deleteCalendlyEvent } from "../services/calendly"; import { createPreSessionNotes, createPostSessionNotes, deleteNotes } from "../services/note"; import { getMentorId } from "../services/user"; +import { sendNotification } from "../services/notifications"; import { InternalError, ServiceError } from "../errors"; import { formatDateTimeRange } from "../services/session"; @@ -51,6 +53,10 @@ router.post( } const accessToken = mentor.personalAccessToken; const data = await getCalendlyEventDate(req.body.calendlyURI, accessToken); + if (!data.resource.start_time || !data.resource.end_time) { + throw ServiceError.ERROR_GETTING_EVENT_DATA; + } + const session = new Session({ preSession: null, postSessionMentee: null, @@ -64,6 +70,8 @@ router.post( preSessionCompleted: false, postSessionMentorCompleted: false, postSessionMenteeCompleted: false, + upcomingSessionNotifSent: false, + postSessionNotifSent: false, }); const sessionId = session._id; const preNoteId = await createPreSessionNotes(sessionId); @@ -73,6 +81,20 @@ router.post( session.postSessionMentee = postMenteeNoteId._id; session.postSessionMentor = postMentorNoteId._id; await session.save(); + + await sendNotification( + "New session booked!", + "You have a new session with " + mentee.name + ". Check out your session details \u{1F60E}", + mentor.fcmToken + ); + await sendNotification( + "New session booked!", + "You have a new session with " + + mentor.name + + ". Fill out your pre-session notes now \u{1F60E}", + mentee.fcmToken + ); + return res.status(201).json({ sessionId: session._id, mentorId: session.mentorId, @@ -80,6 +102,7 @@ router.post( }); } catch (e) { next(); + console.log(e); return res.status(400).json({ error: e, }); @@ -135,6 +158,8 @@ router.get( preSessionCompleted, postSessionMenteeCompleted, postSessionMentorCompleted, + upcomingSessionNotifSent, + postSessionNotifSent, } = session; const [fullDateString, dateShortHandString, startTimeString, endTimeString] = formatDateTimeRange(startTime, endTime); @@ -160,6 +185,8 @@ router.get( postSessionMenteeCompleted, postSessionMentorCompleted, hasPassed, + upcomingSessionNotifSent, + postSessionNotifSent, location: mentor.location, }, }); @@ -286,6 +313,10 @@ router.patch( if (!mentor) { throw InternalError.ERROR_GETTING_MENTOR; } + const mentee = await Mentee.findById(currSession.menteeId); + if (!mentee) { + throw InternalError.ERROR_GETTING_MENTEE; + } const personalAccessToken = mentor.personalAccessToken; await deleteCalendlyEvent(oldCalendlyURI, personalAccessToken); const newEventData = await getCalendlyEventDate(newCalendlyURI, personalAccessToken); @@ -295,6 +326,20 @@ router.patch( calendlyUri: newCalendlyURI, }; await Session.findByIdAndUpdate(sessionId, { $set: updates }, { new: true }); + await sendNotification( + "A session has been rescheduled", + "Your upcoming session with " + + mentor.name + + " has been rescheduled! Check out your new session details.", + mentee.fcmToken + ); + await sendNotification( + "A session has been rescheduled", + "" + + mentee.name + + " has rescheduled your upcoming session! Check out your session details.", + mentor.fcmToken + ); return res.status(200).json({ message: "Successfuly updated the session!", }); @@ -314,6 +359,8 @@ router.delete( "/sessions/:sessionId", [verifyAuthToken], async (req: Request, res: Response, next: NextFunction) => { + console.log("Deleting a session"); + const role = req.body.role; const sessionId = req.params.sessionId; console.log(`DELETE /sessions uid - ${req.body.uid} sessionId - ${sessionId}`); @@ -324,10 +371,37 @@ router.delete( const mentor = await Mentor.findById(session.mentorId); if (!mentor) throw ServiceError.MENTOR_WAS_NOT_FOUND; const personalAccessToken = mentor.personalAccessToken; + const mentee = await Mentee.findById(session.menteeId); + if (!mentee) throw ServiceError.MENTEE_WAS_NOT_FOUND; try { deleteCalendlyEvent(uri, personalAccessToken); await deleteNotes(session.preSession, session.postSessionMentee, session.postSessionMentor); await Session.findByIdAndDelete(sessionId); + if (role === "mentee") { + await sendNotification( + "A session has been cancelled.", + "Your session with " + mentor.name + " has been cancelled.", + mentee.fcmToken + ); + await sendNotification( + "A session has been cancelled.", + "" + mentee.name + " has cancelled your upcoming session.", + mentor.fcmToken + ); + } else if (role === "mentor") { + await sendNotification( + "A session has been cancelled.", + "Your session with " + mentee.name + " has been cancelled.", + mentor.fcmToken + ); + await sendNotification( + "A session has been cancelled.", + "" + + mentor.name + + " has cancelled your upcoming session. \u{1F494} Reschedule to save your pre-session notes.", + mentee.fcmToken + ); + } return res.status(200).json({ message: "calendly successfully cancelled, notes deleted, session deleted.", }); diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts index 5fdd5127..70f4f7f1 100644 --- a/backend/src/routes/user.ts +++ b/backend/src/routes/user.ts @@ -3,11 +3,12 @@ * new users */ import express, { NextFunction, Request, Response } from "express"; +// import { Infer } from "caketype"; import mongoose from "mongoose"; import { validateReqBodyWithCake } from "../middleware/validation"; import { Mentee, Mentor, Pairing } from "../models"; import { createUser } from "../services/auth"; -import { getMenteeId, getMentorId, updateMentor, updateMentee } from "../services/user"; +import { updateMentor, updateMentee } from "../services/user"; import { CreateMenteeRequestBodyCake, CreateMentorRequestBodyCake, @@ -25,7 +26,6 @@ import { verifyAuthToken } from "../middleware/auth"; import { defaultImageID } from "../config"; import { CustomError } from "../errors"; import { AuthError } from "../errors/auth"; -import { getUpcomingSession, getLastSession } from "../services/session"; import { validateCalendlyAccessToken, validateCalendlyLink } from "../services/calendly"; const router = express.Router(); @@ -75,12 +75,14 @@ router.post( const imageId = defaultImageID; const about = "N/A"; const pairingId = "N/A"; + const fcmToken = "N/A"; const mentee = new Mentee({ name, imageId, about, status, pairingId, + fcmToken, ...args, }); await mentee.save(); @@ -138,6 +140,7 @@ router.post( const about = "N/A"; const zoomLink = "N/A"; const pairingIds: string[] = []; + const fcmToken = "N/A"; const mentor = new Mentor({ name, imageId, @@ -145,6 +148,7 @@ router.post( zoomLink, status, pairingIds, + fcmToken, personalAccessToken, calendlyLink, ...args, @@ -533,82 +537,5 @@ router.patch( * If user is mentee, * mentorId, status, upcomingSessionId */ -router.get( - "/user/me", - [verifyAuthToken], - async (req: Request, res: Response, next: NextFunction) => { - try { - const userId = req.body.uid; - const role = req.body.role; - console.log(`GET /user/me uid - ${req.body.uid}`); - - const getUpcomingSessionPromise = getUpcomingSession(userId, role); - const getPastSessionPromise = getLastSession(userId, role); - if (role === "mentee") { - // GET mentee document - const mentee = await Mentee.findById(userId); - if (!mentee) { - throw ServiceError.MENTEE_WAS_NOT_FOUND; - } - - if (mentee.status !== "paired") { - res.status(200).send({ - status: mentee.status, - }); - return; - } - const getPairedMentorIdPromise = getMentorId(mentee.pairingId); - const [upcomingSessionId, pastSessionId, pairedMentorId] = await Promise.all([ - getUpcomingSessionPromise, - getPastSessionPromise, - getPairedMentorIdPromise, - ]); - res.status(200).send({ - status: mentee.status, - sessionId: upcomingSessionId ?? pastSessionId, - pairedMentorId, - }); - } else if (role === "mentor") { - const mentor = await Mentor.findById(userId); - if (!mentor) { - throw ServiceError.MENTOR_WAS_NOT_FOUND; - } - - if (mentor.status !== "paired") { - res.status(200).send({ - status: mentor.status, - }); - return; - } - - const getMenteeIdsPromises = mentor.pairingIds.map(async (pairingId) => - getMenteeId(pairingId) - ); - - // For MVP, we assume there is only 1 mentee 1 mentor pairing - const getMenteeIdsPromise = getMenteeIdsPromises[0]; - - const [upcomingSessionId, pastSessionId, pairedMenteeId] = await Promise.all([ - getUpcomingSessionPromise, - getPastSessionPromise, - getMenteeIdsPromise, - ]); - - res.status(200).send({ - status: mentor.status, - sessionId: upcomingSessionId ?? pastSessionId, - pairedMenteeId, - }); - return; - } - } catch (e) { - if (e instanceof CustomError) { - next(e); - return; - } - next(InternalError.ERROR_GETTING_MENTEE); - } - } -); export { router as userRouter }; diff --git a/backend/src/services/note.ts b/backend/src/services/note.ts index 78fcf7fd..f9060422 100644 --- a/backend/src/services/note.ts +++ b/backend/src/services/note.ts @@ -5,7 +5,9 @@ import postSessionQuestions from "../models/postQuestionsList.json"; import { Note } from "../models/notes"; import { AnswerType, QuestionType, UpdateNoteDetailsType } from "../types/notes"; import { Session } from "../models/session"; -import { ServiceError } from "../errors"; +import { InternalError, ServiceError } from "../errors"; +import { Mentee, Mentor } from "../models"; +import { sendNotification } from "./notifications"; interface Question { question: string; @@ -136,10 +138,39 @@ async function updateNotes(updatedNotes: UpdateNoteDetailsType[], documentId: st // mongoose does not notice the change so ignores saving it unless we manually mark noteDoc.markModified("answers"); const sessionDoc = await Session.findById(noteDoc.session); + if (!sessionDoc) { + throw InternalError.ERROR_GETTING_SESSION; + } + const mentee = await Mentee.findById(sessionDoc.menteeId); + if (!mentee) { + throw InternalError.ERROR_GETTING_MENTEE; + } + const mentor = await Mentor.findById(sessionDoc.mentorId); + if (!mentor) { + throw InternalError.ERROR_GETTING_MENTOR; + } if (sessionDoc != null) { - if (noteDoc.type === "pre") sessionDoc.preSessionCompleted = true; - else if (noteDoc.type === "postMentor") sessionDoc.postSessionMentorCompleted = true; - else if (noteDoc.type === "postMentee") sessionDoc.postSessionMenteeCompleted = true; + if (noteDoc.type === "pre") { + sessionDoc.preSessionCompleted = true; + await sendNotification( + "Pre-session update!", + "Looks like " + + mentee.name + + " has some questions for you. Check out " + + mentee.name + + "'s pre-session notes.", + mentor.fcmToken + ); + } else if (noteDoc.type === "postMentor") { + sessionDoc.postSessionMentorCompleted = true; + await sendNotification( + "Post-session update!", + "" + + mentor.name + + " has updated their post-session notes. Check out what they had to say!", + mentee.fcmToken + ); + } else if (noteDoc.type === "postMentee") sessionDoc.postSessionMenteeCompleted = true; if (missedNote && sessionDoc.missedSessionReason == null) sessionDoc.missedSessionReason = missedReason; await sessionDoc.save(); diff --git a/backend/src/services/notifications.ts b/backend/src/services/notifications.ts new file mode 100644 index 00000000..5d1f873b --- /dev/null +++ b/backend/src/services/notifications.ts @@ -0,0 +1,121 @@ +import * as admin from "firebase-admin"; +import schedule from "node-schedule"; +import { Mentee, Mentor, Session } from "../models"; +import { ServiceError } from "../errors"; + +/** + * Unicode notes: + * eyes - 1F440 + * cool with glasses - 1F60E + * check mark - 2705 + * broken heart - 1F494 + * + * To find emojis and their unicode value, use https://apps.timwhitlock.info/emoji/tables/unicode + * The unicode is in format "U+1234"; in code, use "\u{1234}" + */ + +async function sendNotification(title: string, body: string, deviceToken: string) { + const message = { + notification: { + title, + body, + }, + token: deviceToken, + }; + + admin + .messaging() + .send(message) + .then((response) => { + console.log("Notification sent successfully:", response); + }) + .catch(() => { + throw ServiceError.ERROR_SENDING_NOTIFICATION; + }); +} + +async function startUpcomingSessionCronJob() { + schedule.scheduleJob("*/1 * * * *", async () => { + try { + const upcomingNotifSessions = await Session.find({ + upcomingSessionNotifSent: { $eq: false }, + }); + upcomingNotifSessions.forEach(async (session) => { + const dateNow = new Date(); + const mentee = await Mentee.findById(session.menteeId); + if (!mentee) { + // Instead of throwing an error, we skip this current iteration to avoid skipping the rest of the sessions. + return; + } + const mentor = await Mentor.findById(session.mentorId); + if (!mentor) { + return; + } + + const headerText = `You have an upcoming session.`; + let menteeNotifText = `Ready for your session with ${mentor.name} in 24 hours? \u{1F440}`; + let mentorNotifText = `Ready for your session with ${mentee.name} in 24 hours? \u{1F440}`; + + if (session.startTime.getTime() - dateNow.getTime() <= 86400000) { + if (session.preSessionCompleted) { + mentorNotifText += `Check out ${mentee.name}'s pre-session notes.`; + } else { + menteeNotifText += `Fill out your pre-session notes now!`; + } + const menteeNotif = await sendNotification(headerText, menteeNotifText, mentee.fcmToken); + console.log("Function executed successfully:", menteeNotif); + const mentorNotif = await sendNotification(headerText, mentorNotifText, mentor.fcmToken); + console.log("Function executed successfully:", mentorNotif); + session.upcomingSessionNotifSent = true; + await session.save(); + } + }); + } catch (error) { + console.error("Error executing function:", error); + } + }); +} + +async function startPostSessionCronJob() { + schedule.scheduleJob("*/1 * * * *", async () => { + try { + const postNotifSessions = await Session.find({ postSessionNotifSent: { $eq: false } }); + postNotifSessions.forEach(async (session) => { + const dateNow = new Date(); + const mentee = await Mentee.findById(session.menteeId); + if (!mentee) { + return; + } + const mentor = await Mentor.findById(session.mentorId); + if (!mentor) { + return; + } + // 600000 milliseconds = 10 minutes + if (dateNow.getTime() - session.endTime.getTime() >= 600000) { + // mentee notification + await sendNotification( + "\u{2705} Session complete!", + "How did your session with " + + mentor.name + + " go? Jot it down in your post-session notes.", + mentee.fcmToken + ); + // mentor notification + await sendNotification( + "\u{2705} Session complete!", + "How did your session with " + + mentee.name + + " go? Jot it down in your post-session notes.", + mentor.fcmToken + ); + session.postSessionNotifSent = true; + await session.save(); + } + }); + } catch (error) { + console.error("Error executing function:", error); + } + }); +} + +export { sendNotification, startUpcomingSessionCronJob, startPostSessionCronJob }; diff --git a/backend/src/services/user.ts b/backend/src/services/user.ts index 16bd80c7..3a016255 100644 --- a/backend/src/services/user.ts +++ b/backend/src/services/user.ts @@ -1,14 +1,17 @@ /** * This file will contain helper functions pertaining to user routes */ +// import { Request } from "express"; +// import mongoose from "mongoose"; +// import { Image } from "../models/image"; import { Request } from "express"; import mongoose from "mongoose"; -import { Image } from "../models/image"; import { InternalError, ServiceError } from "../errors"; +import { Mentor, Mentee } from "../models"; import { Pairing } from "../models/pairing"; +// import { User } from "../models/users"; +import { Image } from "../models/image"; import { UpdateMenteeRequestBodyType, UpdateMentorRequestBodyType } from "../types"; -import { Mentor } from "../models/mentor"; -import { Mentee } from "../models/mentee"; import { validateCalendlyAccessToken, validateCalendlyLink } from "./calendly"; async function saveImage(req: Request): Promise { @@ -85,4 +88,34 @@ async function updateMentee(updatedMentee: UpdateMenteeRequestBodyType, userID: } } -export { getMentorId, getMenteeId, updateMentor, updateMentee, saveImage }; +async function updateFCMToken(fcmToken: string, userId: string, role: string) { + console.log("FCM Token: ", fcmToken); + if (role === "mentee") { + const user = await Mentee.findById(userId); + if (!user) { + throw ServiceError.MENTEE_WAS_NOT_FOUND; + } + try { + user.fcmToken = fcmToken; + return await user.save(); + } catch (error) { + throw ServiceError.MENTEE_WAS_NOT_SAVED; + } + } else if (role === "mentor") { + const user = await Mentor.findById(userId); + if (!user) { + throw ServiceError.MENTOR_WAS_NOT_FOUND; + } + + try { + user.fcmToken = fcmToken; + return await user.save(); + } catch (error) { + throw ServiceError.MENTOR_WAS_NOT_SAVED; + } + } else { + throw ServiceError.INVALID_ROLE_WAS_FOUND; + } +} + +export { getMentorId, getMenteeId, updateMentor, updateMentee, saveImage, updateFCMToken }; diff --git a/backend/src/types/cakes.ts b/backend/src/types/cakes.ts index 5b0a409a..c7c5e099 100644 --- a/backend/src/types/cakes.ts +++ b/backend/src/types/cakes.ts @@ -17,6 +17,9 @@ export const CreateMenteeRequestBodyCake = bake({ mentorshipGoal: string, }); +/** + * POST /mentor + */ export const CreateMentorRequestBodyCake = bake({ name: string, email: string, @@ -34,6 +37,11 @@ export const CreateMentorRequestBodyCake = bake({ }); // PATCH user/id +export const UpdateUserCake = bake({ + fcmToken: string, + uid: string, + role: string, +}); export const UpdateMentorRequestBodyCake = bake({ name: string, about: string,