mirror of
https://github.com/urosran/cally.git
synced 2025-07-10 07:07:16 +00:00
284 lines
10 KiB
JavaScript
284 lines
10 KiB
JavaScript
const {onRequest} = require("firebase-functions/v2/https");
|
|
const {getAuth} = require("firebase-admin/auth");
|
|
const {getFirestore} = require("firebase-admin/firestore");
|
|
const logger = require("firebase-functions/logger");
|
|
const functions = require('firebase-functions');
|
|
const admin = require('firebase-admin');
|
|
const {Expo} = require('expo-server-sdk');
|
|
|
|
admin.initializeApp();
|
|
const db = admin.firestore();
|
|
|
|
let expo = new Expo({ accessToken: process.env.EXPO_ACCESS_TOKEN });
|
|
let notificationTimeout = null;
|
|
let eventCount = 0;
|
|
let pushTokens = [];
|
|
|
|
exports.sendNotificationOnEventCreation = functions.firestore
|
|
.document('Events/{eventId}')
|
|
.onCreate(async (snapshot, context) => {
|
|
const eventData = snapshot.data();
|
|
const {familyId, creatorId} = eventData;
|
|
|
|
if (!familyId || !creatorId) {
|
|
console.error('Missing familyId or creatorId in event data');
|
|
return;
|
|
}
|
|
|
|
if (!pushTokens.length) {
|
|
pushTokens = await getPushTokensForFamilyExcludingCreator(familyId, creatorId);
|
|
if (!pushTokens.length) {
|
|
console.log('No push tokens available for the event.');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Increment event count for debouncing
|
|
eventCount++;
|
|
|
|
if (notificationTimeout) {
|
|
clearTimeout(notificationTimeout); // Reset the timer if events keep coming
|
|
}
|
|
|
|
// Set a debounce time (e.g., 5 seconds)
|
|
notificationTimeout = setTimeout(async () => {
|
|
const eventMessage = eventCount === 1
|
|
? `An event "${eventData.title}" has been added. Check it out!`
|
|
: `${eventCount} new events have been added.`;
|
|
|
|
let messages = [];
|
|
for (let pushToken of pushTokens) {
|
|
if (!Expo.isExpoPushToken(pushToken)) {
|
|
console.error(`Push token ${pushToken} is not a valid Expo push token`);
|
|
continue;
|
|
}
|
|
|
|
messages.push({
|
|
to: pushToken,
|
|
sound: 'default',
|
|
title: 'New Events Added!',
|
|
body: eventMessage,
|
|
data: {eventId: context.params.eventId},
|
|
});
|
|
}
|
|
|
|
let chunks = expo.chunkPushNotifications(messages);
|
|
let tickets = [];
|
|
|
|
for (let chunk of chunks) {
|
|
try {
|
|
let ticketChunk = await expo.sendPushNotificationsAsync(chunk);
|
|
tickets.push(...ticketChunk);
|
|
|
|
for (let ticket of ticketChunk) {
|
|
if (ticket.status === 'ok') {
|
|
console.log('Notification successfully sent:', ticket.id);
|
|
} else if (ticket.status === 'error') {
|
|
console.error(`Notification error: ${ticket.message}`);
|
|
if (ticket.details && ticket.details.error === 'DeviceNotRegistered') {
|
|
await removeInvalidPushToken(ticket.to);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error sending notification:', error);
|
|
}
|
|
}
|
|
|
|
eventCount = 0; // Reset the event count after sending notification
|
|
pushTokens = []; // Reset push tokens for the next round
|
|
|
|
}, 5000); // Debounce time (5 seconds)
|
|
});
|
|
|
|
|
|
exports.createSubUser = onRequest(async (request, response) => {
|
|
const authHeader = request.get('Authorization');
|
|
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
logger.warn("Missing or incorrect Authorization header", {authHeader});
|
|
response.status(401).json({error: 'Unauthorized'});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = authHeader.split('Bearer ')[1];
|
|
logger.info("Verifying ID token", {token});
|
|
|
|
let decodedToken;
|
|
try {
|
|
decodedToken = await getAuth().verifyIdToken(token);
|
|
logger.info("ID token verified successfully", {uid: decodedToken.uid});
|
|
} catch (verifyError) {
|
|
logger.error("ID token verification failed", {error: verifyError.message});
|
|
response.status(401).json({error: 'Unauthorized: Invalid token'});
|
|
return;
|
|
}
|
|
|
|
logger.info("Processing user creation", {requestBody: request.body.data});
|
|
|
|
const {userType, firstName, lastName, email, password, familyId} = request.body.data;
|
|
|
|
if (!email || !password || !firstName || !userType || !familyId) {
|
|
logger.warn("Missing required fields in request body", {requestBody: request.body.data});
|
|
response.status(400).json({error: "Missing required fields"});
|
|
return;
|
|
}
|
|
|
|
let userRecord;
|
|
try {
|
|
userRecord = await getAuth().createUser({
|
|
email, password, displayName: `${firstName} ${lastName}`,
|
|
});
|
|
logger.info("User record created", {userId: userRecord.uid});
|
|
} catch (createUserError) {
|
|
logger.error("User creation failed", {error: createUserError.message});
|
|
response.status(500).json({error: "Failed to create user"});
|
|
return;
|
|
}
|
|
|
|
const userProfile = {
|
|
userType, firstName, lastName, familyId, email, uid: userRecord.uid
|
|
};
|
|
|
|
try {
|
|
await getFirestore().collection("Profiles").doc(userRecord.uid).set(userProfile);
|
|
logger.info("User profile saved to Firestore", {userId: userRecord.uid});
|
|
} catch (firestoreError) {
|
|
logger.error("Failed to save user profile to Firestore", {error: firestoreError.message});
|
|
response.status(500).json({error: "Failed to save user profile"});
|
|
return;
|
|
}
|
|
|
|
response.status(200).json({
|
|
data: {
|
|
message: "User created successfully", userId: userRecord.uid,
|
|
}
|
|
});
|
|
} catch (error) {
|
|
logger.error("Error in createSubUser function", {error: error.message});
|
|
response.status(500).json({data: {error: error.message}});
|
|
}
|
|
});
|
|
|
|
exports.generateCustomToken = onRequest(async (request, response) => {
|
|
try {
|
|
const {userId} = request.body.data;
|
|
|
|
console.log("Generating custom token for userId", {userId});
|
|
|
|
if (!userId) {
|
|
response.status(400).json({error: 'Missing userId'});
|
|
return;
|
|
}
|
|
|
|
const customToken = await getAuth().createCustomToken(userId);
|
|
response.status(200).json({data: {token: customToken}});
|
|
} catch (error) {
|
|
console.error("Error generating custom token", {error: error.message});
|
|
response.status(500).json({error: "Failed to generate custom token"});
|
|
}
|
|
});
|
|
|
|
exports.refreshTokens = functions.pubsub.schedule('every 12 hours').onRun(async (context) => {
|
|
console.log('Running token refresh job...');
|
|
|
|
const profilesSnapshot = await db.collection('Profiles').get();
|
|
|
|
profilesSnapshot.forEach(async (profileDoc) => {
|
|
const profileData = profileDoc.data();
|
|
|
|
if (profileData.googleToken) {
|
|
try {
|
|
const refreshedGoogleToken = await refreshGoogleToken(profileData.googleToken);
|
|
await profileDoc.ref.update({googleToken: refreshedGoogleToken});
|
|
console.log(`Google token updated for user ${profileDoc.id}`);
|
|
} catch (error) {
|
|
console.error(`Error refreshing Google token for user ${profileDoc.id}:`, error.message);
|
|
}
|
|
}
|
|
|
|
if (profileData.microsoftToken) {
|
|
try {
|
|
const refreshedMicrosoftToken = await refreshMicrosoftToken(profileData.microsoftToken);
|
|
await profileDoc.ref.update({microsoftToken: refreshedMicrosoftToken});
|
|
console.log(`Microsoft token updated for user ${profileDoc.id}`);
|
|
} catch (error) {
|
|
console.error(`Error refreshing Microsoft token for user ${profileDoc.id}:`, error.message);
|
|
}
|
|
}
|
|
|
|
if (profileData.appleToken) {
|
|
try {
|
|
const refreshedAppleToken = await refreshAppleToken(profileData.appleToken);
|
|
await profileDoc.ref.update({appleToken: refreshedAppleToken});
|
|
console.log(`Apple token updated for user ${profileDoc.id}`);
|
|
} catch (error) {
|
|
console.error(`Error refreshing Apple token for user ${profileDoc.id}:`, error.message);
|
|
}
|
|
}
|
|
});
|
|
|
|
return null;
|
|
});
|
|
|
|
// Function to refresh Google token
|
|
async function refreshGoogleToken(token) {
|
|
// Assuming you use OAuth2 token refresh flow
|
|
const response = await axios.post('https://oauth2.googleapis.com/token', {
|
|
grant_type: 'refresh_token',
|
|
refresh_token: token, // Add refresh token stored previously
|
|
client_id: 'YOUR_GOOGLE_CLIENT_ID',
|
|
client_secret: 'YOUR_GOOGLE_CLIENT_SECRET',
|
|
});
|
|
|
|
return response.data.access_token; // Return new access token
|
|
}
|
|
|
|
async function refreshMicrosoftToken(token) {
|
|
const response = await axios.post('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
|
|
grant_type: 'refresh_token',
|
|
refresh_token: token, // Add refresh token stored previously
|
|
client_id: 'YOUR_MICROSOFT_CLIENT_ID',
|
|
client_secret: 'YOUR_MICROSOFT_CLIENT_SECRET',
|
|
scope: 'https://graph.microsoft.com/Calendars.ReadWrite offline_access',
|
|
});
|
|
|
|
return response.data.access_token; // Return new access token
|
|
}
|
|
|
|
async function getPushTokensForEvent() {
|
|
const usersRef = db.collection('Profiles');
|
|
const snapshot = await usersRef.get();
|
|
let pushTokens = [];
|
|
|
|
snapshot.forEach(doc => {
|
|
const data = doc.data();
|
|
if (data.pushToken) {
|
|
pushTokens.push(data.pushToken);
|
|
}
|
|
});
|
|
|
|
console.log('Push Tokens:', pushTokens);
|
|
return pushTokens;
|
|
}
|
|
|
|
async function getPushTokensForFamilyExcludingCreator(familyId, creatorId) {
|
|
const usersRef = db.collection('Profiles');
|
|
const snapshot = await usersRef.where('familyId', '==', familyId).get();
|
|
let pushTokens = [];
|
|
|
|
snapshot.forEach(doc => {
|
|
const data = doc.data();
|
|
// Exclude the creator
|
|
if (data.uid !== creatorId && data.pushToken) {
|
|
pushTokens.push(data.pushToken);
|
|
}
|
|
});
|
|
|
|
return pushTokens;
|
|
}
|
|
|
|
async function removeInvalidPushToken(pushToken) {
|
|
// TODO
|
|
} |