fitlien-service-881 Removed the moved functions and the sendEmailSMS, sendSMSMessage #125
| @ -1,6 +1,3 @@ | ||||
| TWILIO_ACCOUNT_SID=#{TWILIO_ACCOUNT_SID}# | ||||
| TWILIO_AUTH_TOKEN=#{TWILIO_AUTH_TOKEN}# | ||||
| TWILIO_PHONE_NUMBER=#{TWILIO_PHONE_NUMBER}# | ||||
| SERVICES_RGN=#{SERVICES_RGN}# | ||||
| CASHFREE_CLIENT_ID=#{CASHFREE_CLIENT_ID}# | ||||
| CASHFREE_CLIENT_SECRET=#{CASHFREE_CLIENT_SECRET}# | ||||
|  | ||||
| @ -1 +0,0 @@ | ||||
| export { sendEmailSES } from './sendEmailSES'; | ||||
| @ -1,209 +0,0 @@ | ||||
| import { getLogger } from "../shared/config"; | ||||
| import { getCorsHandler } from "../shared/middleware"; | ||||
| import { onRequest } from "firebase-functions/v2/https"; | ||||
| import { Request } from "firebase-functions/v2/https"; | ||||
| import { Response } from "express"; | ||||
| import { SESClient } from "@aws-sdk/client-ses"; | ||||
| import { SendEmailCommand, SendRawEmailCommand } from "@aws-sdk/client-ses"; | ||||
| import { HttpsError } from "firebase-functions/v2/https"; | ||||
| import * as mime from 'mime-types'; | ||||
| import axios from 'axios'; | ||||
| 
 | ||||
| const logger = getLogger(); | ||||
| const corsHandler = getCorsHandler(); | ||||
| 
 | ||||
| interface EmailRequest { | ||||
|     to: string | string[]; | ||||
|     subject: string; | ||||
|     html: string; | ||||
|     text?: string; | ||||
|     from: string; | ||||
|     replyTo?: string; | ||||
|     attachments?: Attachment[]; | ||||
|     fileUrl?: string; | ||||
|     fileName?: string; | ||||
| } | ||||
| 
 | ||||
| interface Attachment { | ||||
|     filename: string; | ||||
|     content: string | Buffer; | ||||
|     contentType?: string; | ||||
| } | ||||
| 
 | ||||
| const stripHtml = (html: string): string => { | ||||
|     if (!html) return ''; | ||||
|     return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); | ||||
| } | ||||
| 
 | ||||
| async function sendSimpleEmail(data: EmailRequest, recipients: string[]) { | ||||
|     const ses = new SESClient({ | ||||
|         region: process.env.AWS_REGION, | ||||
|         credentials: { | ||||
|             accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', | ||||
|             secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     const command = new SendEmailCommand({ | ||||
|         Source: data.from, | ||||
|         Destination: { ToAddresses: recipients }, | ||||
|         Message: { | ||||
|             Subject: { Data: data.subject }, | ||||
|             Body: { | ||||
|                 Html: { Data: data.html }, | ||||
|                 Text: { Data: data.text || stripHtml(data.html) } | ||||
|             } | ||||
|         }, | ||||
|         ReplyToAddresses: data.replyTo ? [data.replyTo] : undefined, | ||||
|     }); | ||||
| 
 | ||||
|     const result = await ses.send(command); | ||||
|     return { messageId: result.MessageId }; | ||||
| } | ||||
| 
 | ||||
| async function sendEmailWithAttachments(data: EmailRequest, recipients: string[]) { | ||||
|     const ses = new SESClient({ | ||||
|         region: process.env.AWS_REGION, | ||||
|         credentials: { | ||||
|             accessKeyId: process.env.AWS_ACCESS_KEY_ID || '', | ||||
|             secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || '' | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     const boundary = `boundary_${Math.random().toString(16).substr(2)}`; | ||||
|     let rawMessage = `From: ${data.from}\n`; | ||||
|     rawMessage += `To: ${recipients.join(', ')}\n`; | ||||
|     rawMessage += `Subject: ${data.subject}\n`; | ||||
|     rawMessage += `MIME-Version: 1.0\n`; | ||||
|     rawMessage += `Content-Type: multipart/mixed; boundary="${boundary}"\n\n`; | ||||
| 
 | ||||
|     // Add email body (multipart/alternative)
 | ||||
|     rawMessage += `--${boundary}\n`; | ||||
|     rawMessage += `Content-Type: multipart/alternative; boundary="alt_${boundary}"\n\n`; | ||||
| 
 | ||||
|     // Text part
 | ||||
|     if (data.text) { | ||||
|         rawMessage += `--alt_${boundary}\n`; | ||||
|         rawMessage += `Content-Type: text/plain; charset=UTF-8\n\n`; | ||||
|         rawMessage += `${data.text}\n\n`; | ||||
|     } | ||||
| 
 | ||||
|     // HTML part
 | ||||
|     rawMessage += `--alt_${boundary}\n`; | ||||
|     rawMessage += `Content-Type: text/html; charset=UTF-8\n\n`; | ||||
|     rawMessage += `${data.html}\n\n`; | ||||
| 
 | ||||
|     // Close alternative part
 | ||||
|     rawMessage += `--alt_${boundary}--\n\n`; | ||||
| 
 | ||||
|     // Add attachments
 | ||||
|     for (const attachment of data.attachments || []) { | ||||
|         const contentType = attachment.contentType || | ||||
|             mime.lookup(attachment.filename) || | ||||
|             'application/octet-stream'; | ||||
| 
 | ||||
|         rawMessage += `--${boundary}\n`; | ||||
|         rawMessage += `Content-Type: ${contentType}; name="${attachment.filename}"\n`; | ||||
|         rawMessage += `Content-Disposition: attachment; filename="${attachment.filename}"\n`; | ||||
|         rawMessage += `Content-Transfer-Encoding: base64\n\n`; | ||||
| 
 | ||||
|         const contentBuffer = typeof attachment.content === 'string' | ||||
|             ? Buffer.from(attachment.content, 'base64') | ||||
|             : attachment.content; | ||||
| 
 | ||||
|         rawMessage += contentBuffer.toString('base64') + '\n\n'; | ||||
|     } | ||||
| 
 | ||||
|     // Close message
 | ||||
|     rawMessage += `--${boundary}--`; | ||||
| 
 | ||||
|     const command = new SendRawEmailCommand({ | ||||
|         RawMessage: { Data: Buffer.from(rawMessage) } | ||||
|     }); | ||||
| 
 | ||||
|     const result = await ses.send(command); | ||||
|     return { messageId: result.MessageId }; | ||||
| } | ||||
| 
 | ||||
| async function downloadFileFromUrl(url: string): Promise<Buffer> { | ||||
|     try { | ||||
|         const response = await axios.get(url, { responseType: 'arraybuffer' }); | ||||
|         return Buffer.from(response.data); | ||||
|     } catch (error) { | ||||
|         logger.error(`Error downloading file from URL: ${error}`); | ||||
|         throw new Error(`Failed to download file: ${error}`); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export const sendEmailSES = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, (request: Request, response: Response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             const toAddress = request.body.toAddress; | ||||
|             const subject = request.body.subject; | ||||
|             const message = request.body.message; | ||||
| 
 | ||||
|             // Initialize data with basic fields
 | ||||
|             const data: EmailRequest = { | ||||
|                 to: toAddress, | ||||
|                 html: message, | ||||
|                 subject: subject, | ||||
|                 text: stripHtml(message), | ||||
|                 from: process.env.SES_FROM_EMAIL || 'support@fitlien.com', | ||||
|                 replyTo: process.env.SES_REPLY_TO_EMAIL || 'support@fitlien.com', | ||||
|                 attachments: request.body.attachments as Attachment[] || [] | ||||
|             }; | ||||
| 
 | ||||
|             // Handle file URL if provided
 | ||||
|             if (request.body.fileUrl && request.body.fileName) { | ||||
|                 logger.info(`Downloading attachment from URL: ${request.body.fileUrl}`); | ||||
|                 try { | ||||
|                     const fileContent = await downloadFileFromUrl(request.body.fileUrl); | ||||
| 
 | ||||
|                     // If attachments array doesn't exist, create it
 | ||||
|                     if (!data.attachments) { | ||||
|                         data.attachments = []; | ||||
|                     } | ||||
| 
 | ||||
|                     // Add the downloaded file as an attachment
 | ||||
|                     data.attachments.push({ | ||||
|                         filename: request.body.fileName, | ||||
|                         content: fileContent, | ||||
|                         contentType: mime.lookup(request.body.fileName) || 'application/octet-stream' | ||||
|                     }); | ||||
| 
 | ||||
|                     logger.info(`Successfully downloaded attachment: ${request.body.fileName}`); | ||||
|                 } catch (downloadError) { | ||||
|                     logger.error(`Failed to download attachment: ${downloadError}`); | ||||
|                     throw new Error(`Failed to process attachment: ${downloadError}`); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (!data.to || !data.subject || !data.html || !data.from) { | ||||
|                 throw new HttpsError( | ||||
|                     'invalid-argument', | ||||
|                     'Missing required email fields' | ||||
|                 ); | ||||
|             } | ||||
| 
 | ||||
|             logger.info(`Sending Email '${data.subject}' to '${data.to}' from '${data.from}'`); | ||||
|             const recipients = Array.isArray(data.to) ? data.to : [data.to]; | ||||
| 
 | ||||
|             if (data.attachments && data.attachments.length > 0) { | ||||
|                 const messageResult = await sendEmailWithAttachments(data, recipients); | ||||
|                 response.status(200).json(messageResult); | ||||
|             } else { | ||||
|                 const messageResult = await sendSimpleEmail(data, recipients); | ||||
|                 response.status(200).json(messageResult); | ||||
|             } | ||||
|         } catch (e) { | ||||
|             logger.error(`Error while sending E-mail. Error: ${e}`); | ||||
|             console.error(`Error while sending E-mail. Error: ${e}`); | ||||
|             response.status(500).json({ | ||||
|                 success: false, | ||||
|                 error: 'Error while sending E-mail' | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| @ -10,16 +10,12 @@ setGlobalOptions({ | ||||
| }); | ||||
| 
 | ||||
| export * from "./shared/config"; | ||||
| export { sendEmailSES } from "./email"; | ||||
| export { sendSMSMessage } from "./sms"; | ||||
| export { accessFile } from "./storage"; | ||||
| export { | ||||
|   processNotificationOnCreate, | ||||
|   checkExpiredMemberships, | ||||
| } from "./notifications"; | ||||
| export * from "./payments"; | ||||
| export { getPlaceDetails, getPlacesAutocomplete } from "./places"; | ||||
| export { registerClient } from "./users"; | ||||
| export { | ||||
|   esslGetUserDetails, | ||||
|   esslUpdateUser, | ||||
|  | ||||
| @ -1,67 +0,0 @@ | ||||
| import { onRequest } from "firebase-functions/v2/https"; | ||||
| import { Request } from "firebase-functions/v2/https"; | ||||
| import * as express from "express"; | ||||
| import { getLogger } from "../shared/config"; | ||||
| import { getCorsHandler } from "../shared/middleware"; | ||||
| import axios from "axios"; | ||||
| 
 | ||||
| const logger = getLogger(); | ||||
| const corsHandler = getCorsHandler(); | ||||
| 
 | ||||
| export const getPlacesAutocomplete = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (request: Request, response: express.Response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             const { input, location, radius, types, components, sessiontoken } = request.query; | ||||
| 
 | ||||
|             if (!input) { | ||||
|                 response.status(400).json({ | ||||
|                     error: 'Input parameter is required for autocomplete' | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const apiKey = process.env.GOOGLE_MAPS_API_KEY; | ||||
|             if (!apiKey) { | ||||
|                 logger.error('Google Places API key is not configured'); | ||||
|                 response.status(500).json({ error: 'Server configuration error' }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const url = 'https://maps.googleapis.com/maps/api/place/autocomplete/json'; | ||||
|             const params: any = { | ||||
|                 key: apiKey, | ||||
|                 input: input | ||||
|             }; | ||||
| 
 | ||||
|             if (location && radius) { | ||||
|                 params.location = location; | ||||
|                 params.radius = radius; | ||||
|             } | ||||
| 
 | ||||
|             if (types) { | ||||
|                 params.types = types; | ||||
|             } | ||||
| 
 | ||||
|             if (components) { | ||||
|                 params.components = components; | ||||
|             } | ||||
| 
 | ||||
|             if (sessiontoken) { | ||||
|                 params.sessiontoken = sessiontoken; | ||||
|             } | ||||
| 
 | ||||
|             const result = await axios.get(url, { params }); | ||||
| 
 | ||||
|             logger.info('Google Places Autocomplete API request completed successfully'); | ||||
|             response.json(result.data); | ||||
|         } catch (error) { | ||||
|             logger.error('Error fetching place autocomplete suggestions:', error); | ||||
|             response.status(500).json({ | ||||
|                 success: false, | ||||
|                 error: error instanceof Error ? error.message : String(error) | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| @ -1,51 +0,0 @@ | ||||
| import { onRequest } from "firebase-functions/v2/https"; | ||||
| import { Request } from "firebase-functions/v2/https"; | ||||
| import * as express from "express"; | ||||
| import axios from "axios"; | ||||
| 
 | ||||
| const { getCorsHandler } = require('../shared/middleware'); | ||||
| const corsHandler = getCorsHandler(); | ||||
| const { getLogger } = require('../shared/config'); | ||||
| const logger = getLogger(); | ||||
| 
 | ||||
| 
 | ||||
| export const getPlaceDetails = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (request: Request, response: express.Response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             const { place_id, fields } = request.query; | ||||
| 
 | ||||
|             if (!place_id) { | ||||
|                 response.status(400).json({ | ||||
|                     error: 'place_id parameter is required' | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const apiKey = process.env.GOOGLE_MAPS_API_KEY; | ||||
|             if (!apiKey) { | ||||
|                 logger.error('Google Places API key is not configured'); | ||||
|                 response.status(500).json({ error: 'Server configuration error' }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const url = 'https://maps.googleapis.com/maps/api/place/details/json'; | ||||
|             const params: any = { | ||||
|                 key: apiKey, | ||||
|                 place_id: place_id, | ||||
|                 fields: fields || 'geometry' | ||||
|             }; | ||||
| 
 | ||||
|             const result = await axios.get(url, { params }); | ||||
|             logger.info('Google Places Details API request completed successfully'); | ||||
|             response.json(result.data); | ||||
|         } catch (error) { | ||||
|             logger.error('Error fetching place details:', error); | ||||
|             response.status(500).json({ | ||||
|                 success: false, | ||||
|                 error: error instanceof Error ? error.message : String(error) | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| @ -1,2 +0,0 @@ | ||||
| export { getPlaceDetails } from './details'; | ||||
| export { getPlacesAutocomplete } from './autocomplete'; | ||||
| @ -1 +0,0 @@ | ||||
| export { sendSMSMessage } from './sendSMS'; | ||||
| @ -1,77 +0,0 @@ | ||||
| import { onRequest } from "firebase-functions/v2/https"; | ||||
| import { Request } from "firebase-functions/v2/https"; | ||||
| import { getCorsHandler } from "../shared/middleware"; | ||||
| import { getLogger } from "../shared/config"; | ||||
| import twilio from 'twilio'; | ||||
| 
 | ||||
| const corsHandler = getCorsHandler(); | ||||
| const logger = getLogger(); | ||||
| 
 | ||||
| // Initialize Twilio client
 | ||||
| const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN); | ||||
| 
 | ||||
| interface SMSRequest { | ||||
|     to: string; | ||||
|     body: string; | ||||
| } | ||||
| 
 | ||||
| export const sendSMSMessage = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (request: Request, response) => { | ||||
|     return corsHandler(request, response, async () => { | ||||
|         try { | ||||
|             const { to, body } = request.body as SMSRequest; | ||||
| 
 | ||||
|             // Input validation
 | ||||
|             if (!to || !body) { | ||||
|                 logger.error('Missing required SMS parameters'); | ||||
|                 response.status(400).json({ | ||||
|                     success: false, | ||||
|                     error: 'Both "to" and "body" parameters are required' | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Validate phone number format (basic check)
 | ||||
|             if (!/^\+?[1-9]\d{1,14}$/.test(to)) { | ||||
|                 logger.error('Invalid phone number format', { to }); | ||||
|                 response.status(400).json({ | ||||
|                     success: false, | ||||
|                     error: 'Invalid phone number format' | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             // Send SMS
 | ||||
|             const message = await twilioClient.messages.create({ | ||||
|                 body: body, | ||||
|                 from: process.env.TWILIO_PHONE_NUMBER, | ||||
|                 to: to | ||||
|             }); | ||||
| 
 | ||||
|             logger.info('SMS sent successfully', { | ||||
|                 messageId: message.sid, | ||||
|                 to: to, | ||||
|                 length: body.length | ||||
|             }); | ||||
| 
 | ||||
|             response.json({ | ||||
|                 success: true, | ||||
|                 messageId: message.sid, | ||||
|                 timestamp: message.dateCreated | ||||
|             }); | ||||
| 
 | ||||
|         } catch (error: any) { | ||||
|             logger.error('Error sending SMS:', error); | ||||
| 
 | ||||
|             const statusCode = error.status === 401 ? 401 : 500; | ||||
| 
 | ||||
|             response.status(statusCode).json({ | ||||
|                 success: false, | ||||
|                 error: error.message, | ||||
|                 code: error.code, | ||||
|                 moreInfo: error.moreInfo | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| @ -1,117 +0,0 @@ | ||||
| import { onRequest } from "firebase-functions/v2/https"; | ||||
| import { getCorsHandler } from "../shared/middleware"; | ||||
| import { getAdmin, getLogger } from "../shared/config"; | ||||
| import { Request } from "firebase-functions/v2/https"; | ||||
| import { Response } from "express"; | ||||
| 
 | ||||
| 
 | ||||
| const corsHandler = getCorsHandler(); | ||||
| const admin = getAdmin(); | ||||
| const logger = getLogger(); | ||||
| 
 | ||||
| export const registerClient = onRequest({ | ||||
|     region: '#{SERVICES_RGN}#' | ||||
| }, async (req: Request, res: Response) => { | ||||
|     return corsHandler(req, res, async () => { | ||||
|         try { | ||||
|             if (req.method !== 'POST') { | ||||
|                 return res.status(405).json({ error: 'Method not allowed. Please use POST.' }); | ||||
|             } | ||||
|             const authHeader = req.headers.authorization; | ||||
|             if (!authHeader || !authHeader.startsWith('Bearer ')) { | ||||
|                 return res.status(401).json({ error: 'Unauthorized. Missing or invalid authorization header.' }); | ||||
|             } | ||||
|             const idToken = authHeader.split('Bearer ')[1]; | ||||
|             try { | ||||
|                 const decodedToken = await admin.auth().verifyIdToken(idToken); | ||||
|                 const uid = decodedToken.uid; | ||||
|                 const userDoc = await admin.firestore().collection('users').doc(uid).get(); | ||||
| 
 | ||||
|                 if (!userDoc.exists) { | ||||
|                     return res.status(403).json({ error: 'Forbidden. User not found.' }); | ||||
|                 } | ||||
| 
 | ||||
|                 const userData = userDoc.data(); | ||||
|                 if (!userData || !userData.roles || !userData.roles.includes('gym_owner')) { | ||||
|                     return res.status(403).json({ error: 'Forbidden. Only gym owners can register clients.' }); | ||||
|                 } | ||||
|                 const gymUser = req.body; | ||||
|                 if (!gymUser.fields["phone-number"]) { | ||||
|                     return res.status(400).json({ error: 'Phone number is required' }); | ||||
|                 } | ||||
| 
 | ||||
|                 let clientUid; | ||||
|                 try { | ||||
|                     const userRecord = await admin.auth().getUserByPhoneNumber(gymUser.fields["phone-number"]) | ||||
|                         .catch(() => null); | ||||
| 
 | ||||
|                     if (userRecord) { | ||||
|                         clientUid = userRecord.uid; | ||||
|                     } else { | ||||
|                         const newUser = await admin.auth().createUser({ | ||||
|                             phoneNumber: gymUser.fields["phone-number"], | ||||
|                             displayName: gymUser.fields["first-name"] || '', | ||||
|                         }); | ||||
|                         clientUid = newUser.uid; | ||||
|                     } | ||||
|                 } catch (error) { | ||||
|                     logger.error('Error creating authentication user:', error); | ||||
|                     return res.status(500).json({ | ||||
|                         error: 'Failed to create authentication user', | ||||
|                         details: error | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|                 try { | ||||
|                     gymUser.uid = clientUid; | ||||
|                     gymUser.registeredBy = uid; | ||||
| 
 | ||||
|                     if (gymUser.name) { | ||||
|                         gymUser.normalizedName = gymUser.name.toLowerCase(); | ||||
|                     } | ||||
| 
 | ||||
|                     if (gymUser.dateOfBirth && !(typeof gymUser.dateOfBirth === 'string')) { | ||||
|                         gymUser.dateOfBirth = new Date(gymUser.dateOfBirth).toISOString(); | ||||
|                     } | ||||
| 
 | ||||
|                     const clientData = { | ||||
|                         ...gymUser, | ||||
|                     }; | ||||
| 
 | ||||
|                     await admin.firestore().collection('client_profiles').doc(clientUid).set(clientData); | ||||
| 
 | ||||
|                     return res.status(201).json({ | ||||
|                         success: true, | ||||
|                         message: 'Client registered successfully', | ||||
|                         clientId: clientUid | ||||
|                     }); | ||||
|                 } catch (error) { | ||||
|                     logger.error('Error creating client profile:', error); | ||||
| 
 | ||||
|                     try { | ||||
|                         if (!gymUser.uid) { | ||||
|                             await admin.auth().deleteUser(clientUid); | ||||
|                         } | ||||
|                     } catch (deleteError) { | ||||
|                         logger.error('Error deleting auth user after failed profile creation:', deleteError); | ||||
|                     } | ||||
| 
 | ||||
|                     return res.status(500).json({ | ||||
|                         error: 'Failed to create client profile', | ||||
|                         details: error | ||||
|                     }); | ||||
|                 } | ||||
| 
 | ||||
|             } catch (authError) { | ||||
|                 logger.error('Authentication error:', authError); | ||||
|                 return res.status(401).json({ error: 'Unauthorized. Invalid token.' }); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             logger.error('Unexpected error in client registration:', error); | ||||
|             return res.status(500).json({ | ||||
|                 error: 'Internal server error', | ||||
|                 details: error | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| @ -1 +0,0 @@ | ||||
| export { registerClient } from './clientRegistration'; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user