Skip to content

Commit

Permalink
fcm notify service fb function
Browse files Browse the repository at this point in the history
  • Loading branch information
Resul Avan authored and Resul Avan committed Jul 16, 2020
1 parent 985f656 commit 84efaeb
Show file tree
Hide file tree
Showing 16 changed files with 415 additions and 188 deletions.
6 changes: 4 additions & 2 deletions functions/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { json } from 'body-parser'
import cors from 'cors'
import { RuntimeOptions, runWith } from 'firebase-functions'
import { ApiConfig } from '../types';
import { claimsHandler, verifyHandler } from './auth-handler';
import { claimsHandler, verifyHandler } from './auth-handler'
import { healthyHandler, tokenHandler } from './api-handler'
import { notifyHandler } from './notification-handler'

const router = Router()
router.get(ApiConfig.auth.healthy, healthyHandler)
router.get(ApiConfig.healthy, healthyHandler)
router.get(ApiConfig.auth.verify, tokenHandler, verifyHandler)
router.post(ApiConfig.auth.claims, tokenHandler, claimsHandler)
router.post(ApiConfig.notification.notify.context, tokenHandler, notifyHandler)

const app = express()
app.use(cookieParser())
Expand Down
62 changes: 62 additions & 0 deletions functions/src/api/notification-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { RequestHandler } from 'express'
import { NO_CONTENT } from 'http-status-codes'
import admin from 'firebase-admin'
import { getDecodedIdTokenFromRequest } from './api-handler'
import { ApiErrorCode, UserDevice } from '../types'
import { deleteUserDevice, getPushNotification, getUserDevices } from '../service/firebase-admin-service'
import { handleApiErrors } from '../service/api-error-service'
import DecodedIdToken = admin.auth.DecodedIdToken

export const notifyHandler: RequestHandler = async (req, res) => {
console.log(`${req.originalUrl} - notifyHandler called`)
await getDecodedIdTokenFromRequest(req)
.then(async (decodedIdToken: DecodedIdToken) => {
const notificationId = req.params.notificationId
console.log(`notify request from ${decodedIdToken.uid} with notificationId ${notificationId}`)
if (!notificationId) {
throw new Error(ApiErrorCode.BAD_REQUEST)
}
console.log(`getting notification by id ${notificationId}`)

const notification = await getPushNotification(notificationId)
if (!notification) {
throw new Error(ApiErrorCode.BAD_REQUEST)
}
console.log(`notification goes from ${notification.from} to ${notification.to}`)

const userDevices: UserDevice[] = await getUserDevices(notification.to)
if (userDevices.length <= 0) {
console.log(`No userDevice for ${notification.to}`)
return res.status(NO_CONTENT).send()
}
console.log(`${userDevices.length} userDevice(s) found for ${notification.to}`)

const deviceTokens = userDevices.map(value => value.deviceToken)
const payload = {
data: {
type: notification.notificationType
}
}
const removeUserDevicePromises: Promise<any>[] = []

await admin.messaging().sendToDevice(deviceTokens, payload)
.then((messagingDevicesResponse) => {
messagingDevicesResponse.results.forEach((result, index) => {
const error = result.error
if (error) {
console.error('Failure sending notification to', userDevices[index], error)
// Cleanup the tokens who are not registered anymore.
if (error.code === 'messaging/invalid-registration-token' ||
error.code === 'messaging/registration-token-not-registered') {
removeUserDevicePromises.push(deleteUserDevice(userDevices[index]))
}
}
})
Promise.all(removeUserDevicePromises).catch(error => console.log(error))
}).catch(error => console.log(error))

console.log(`notification sent from ${notification.from} to ${notification.to}`)

return res.status(NO_CONTENT).send()
}).catch((error: Error) => handleApiErrors(res, error))
}
2 changes: 1 addition & 1 deletion functions/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const config = {
WEBSITE_URL: 'https://nuxt-ts-firebase-auth-ssr.web.app/'
WEBSITE_URL: 'https://nuxt-ts-firebase-auth-ssr.web.app/'
}
52 changes: 26 additions & 26 deletions functions/src/nuxt-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,35 @@ const { Nuxt } = require('nuxt');
console.log('Creating nuxtOnFunction function.')

const config = {
// Don't start in dev mode.
dev: false,
// Set the path to the .nuxt folder.
buildDir: '.nuxt',
// // Enable debug when in the develop environment.
// debug: process.env.GCP_PROJECT === 'nuxt2-example-dev',
// Path to the assets.
build: {
publicPath: '/assets/',
},
// Don't start in dev mode.
dev: false,
// Set the path to the .nuxt folder.
buildDir: '.nuxt',
// // Enable debug when in the develop environment.
// debug: process.env.GCP_PROJECT === 'nuxt2-example-dev',
// Path to the assets.
build: {
publicPath: '/assets/',
},
};
const nuxt = new Nuxt(config);

let isReady = false
const readyPromise = nuxt
.ready()
.then(() => {
isReady = true
})
.catch(() => {
process.exit(1)
})
.ready()
.then(() => {
isReady = true
})
.catch(() => {
process.exit(1)
})

const handleRequest = async (req: Request, res: Response) => {
if (!isReady) {
await readyPromise
}
res.set('Cache-Control', 'public, max-age=31536000, s-maxage=1000')
await nuxt.render(req, res)
if (!isReady) {
await readyPromise
}
res.set('Cache-Control', 'public, max-age=31536000, s-maxage=1000')
await nuxt.render(req, res)
}

// Init express.
Expand All @@ -46,10 +46,10 @@ app.get('*', handleRequest)
app.use(handleRequest)

const runtimeOpts: RuntimeOptions = {
timeoutSeconds: 300,
memory: '2GB'
timeoutSeconds: 300,
memory: '2GB'
}

export const nuxtOnFunction = runWith(runtimeOpts)
.https
.onRequest(app);
.https
.onRequest(app);
50 changes: 25 additions & 25 deletions functions/src/service/api-error-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,40 @@ import { ApiErrorCode } from '../types';
import { FirebaseError } from 'firebase-admin';

const handleFirebaseError = (response: Response, error: FirebaseError) => {
console.error('Firebase error', error);
switch (error?.code) {
case 'auth/id-token-expired':
response.status(UNAUTHORIZED).send('re-authentication required')
break

default:
response.status(INTERNAL_SERVER_ERROR).send(ApiErrorCode.INTERNAL_ERROR)
}
console.error('Firebase error', error);
switch (error?.code) {
case 'auth/id-token-expired':
response.status(UNAUTHORIZED).send('re-authentication required')
break

default:
response.status(INTERNAL_SERVER_ERROR).send(ApiErrorCode.INTERNAL_ERROR)
}
}

export const handleGenericError = (response: Response, error: Error) => {
console.error('Error occurred', error)
console.error('Error occurred', error)

switch (error.message) {
case ApiErrorCode.FORBIDDEN:
response.status(FORBIDDEN).send(error.message)
break
switch (error.message) {
case ApiErrorCode.FORBIDDEN:
response.status(FORBIDDEN).send(error.message)
break

case ApiErrorCode.BAD_REQUEST:
response.status(BAD_REQUEST).send(error.message)
break
case ApiErrorCode.BAD_REQUEST:
response.status(BAD_REQUEST).send(error.message)
break

default:
response.status(INTERNAL_SERVER_ERROR).send(ApiErrorCode.INTERNAL_ERROR)
}
default:
response.status(INTERNAL_SERVER_ERROR).send(ApiErrorCode.INTERNAL_ERROR)
}
}

export const handleApiErrors = (response: Response, error: Error | FirebaseError) => {
isFirebaseError(error)
? handleFirebaseError(response, error as FirebaseError)
: handleGenericError(response, error as Error)
export const handleApiErrors = (response: Response, error: Error|FirebaseError) => {
isFirebaseError(error)
? handleFirebaseError(response, error as FirebaseError)
: handleGenericError(response, error as Error)
}

const isFirebaseError = (error: any) => {
return !!error.code
return !!error.code
}
6 changes: 3 additions & 3 deletions functions/src/service/firebase-admin-init.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import admin from 'firebase-admin';

if (admin.apps.length === 0) {
admin.initializeApp({
credential: admin.credential.applicationDefault()
});
admin.initializeApp({
credential: admin.credential.applicationDefault()
});
}

export default admin;
Expand Down
107 changes: 66 additions & 41 deletions functions/src/service/firebase-admin-service.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,83 @@
import admin from './firebase-admin-init'
import { AuthUser, collection, FirebaseClaimKey, FirebaseClaims, ProviderType, User } from '../types'
import DecodedIdToken = admin.auth.DecodedIdToken;
import {
AuthUser,
collection,
CollectionField,
FirebaseClaimKey,
FirebaseClaims,
FirebaseQueryOperator,
ProviderType,
PushNotification,
User,
UserDevice,
WhereClause
} from '../types'
import { deleteModel, getModelById, getModelsByWhereClauses } from './firestore-admin-collection-service'
import DecodedIdToken = admin.auth.DecodedIdToken

export const getDecodedIdToken = (idToken: string): Promise<DecodedIdToken> => {
return admin.auth()
.verifyIdToken(idToken)
.then((decodedIdToken: DecodedIdToken) => decodedIdToken)
return admin.auth()
.verifyIdToken(idToken)
.then((decodedIdToken: DecodedIdToken) => decodedIdToken)
}

export const setCustomClaims = async (uid: string, firebaseClaims: FirebaseClaims): Promise<void> => {
console.log('setCustomClaims', firebaseClaims)
await admin.auth().setCustomUserClaims(uid, firebaseClaims);
console.log('setCustomClaims', firebaseClaims)
await admin.auth().setCustomUserClaims(uid, firebaseClaims);
}

export const validateClaimsAndGet = async (decodedIdToken: DecodedIdToken) => {
let username = decodedIdToken[FirebaseClaimKey.USERNAME]
if (username) {
return { username }
}
let username = decodedIdToken[FirebaseClaimKey.USERNAME]
if (username) {
return { username }
}

const user = await getUser(decodedIdToken.sub)
if (!user) {
throw new Error('User not found by id: ' + decodedIdToken.sub)
}
const user = await getUser(decodedIdToken.sub)
if (!user) {
throw new Error('User not found by id: ' + decodedIdToken.sub)
}

username = user.username || user.id
const firebaseClaims = { username }
username = user.username || user.id
const firebaseClaims = { username }

await setCustomClaims(decodedIdToken.sub, firebaseClaims)
await setCustomClaims(decodedIdToken.sub, firebaseClaims)

return firebaseClaims
return firebaseClaims
}

export const toAuthUser = (decodedIdToken: DecodedIdToken, firebaseClaims: FirebaseClaims): AuthUser => {
return {
name: decodedIdToken.name,
verified: decodedIdToken.email_verified as boolean,
email: decodedIdToken.email as string,
profilePhoto: {
src: decodedIdToken.picture as string,
alt: `Profile photo of ${firebaseClaims[FirebaseClaimKey.USERNAME]}`
},
userId: decodedIdToken.sub,
username: firebaseClaims[FirebaseClaimKey.USERNAME],
providers: [{ providerType: decodedIdToken.firebase.sign_in_provider as ProviderType }]
};
}

export const getUser = async (uid: string): Promise<User> => {
return await admin.firestore()
.collection(collection.USER)
.doc(uid)
.get()
.then((document) => {
return document.data() as User
})
return {
name: decodedIdToken.name,
verified: decodedIdToken.email_verified as boolean,
email: decodedIdToken.email as string,
profilePhoto: {
src: decodedIdToken.picture as string,
alt: `Profile photo of ${firebaseClaims[FirebaseClaimKey.USERNAME]}`
},
userId: decodedIdToken.sub,
username: firebaseClaims[FirebaseClaimKey.USERNAME],
providers: [{ providerType: decodedIdToken.firebase.sign_in_provider as ProviderType }]
};
}

export const getUser = async (userId: string): Promise<User> => {
return await getModelById(collection.USER, userId)
}

export const getPushNotification = async (notificationId: string): Promise<PushNotification> => {
return await getModelById(collection.NOTIFICATION, notificationId)
}

export const getUserDevices = async (userId: string): Promise<UserDevice[]> => {
const userWhereClause: WhereClause = {
field: CollectionField.USER_DEVICE.userId,
operator: FirebaseQueryOperator.EQ,
value: userId
}

return await getModelsByWhereClauses(collection.USER_DEVICE, userWhereClause)
}

export const deleteUserDevice = (userDevice: UserDevice) => {
return deleteModel(collection.USER_DEVICE, userDevice)
}
Loading

0 comments on commit 84efaeb

Please sign in to comment.