feat: major UI overhaul with new components and enhanced UX
- Add comprehensive Company Wiki feature with complete state management - CompanyWikiManager, empty states, invite modals - Implement new Chat system with enhanced layout and components - ChatLayout, ChatSidebar, MessageThread, FileUploadInput - Create modern Login and OTP verification flows - LoginNew page, OTPVerification component - Add new Employee Forms system with enhanced controller - Introduce Figma-based design components and multiple choice inputs - Add new font assets (NeueMontreal) and robot images for onboarding - Enhance existing components with improved styling and functionality - Update build configuration and dependencies - Remove deprecated ModernLogin component
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
const { onRequest } = require("firebase-functions/v2/https");
|
||||
const admin = require("firebase-admin");
|
||||
const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json");
|
||||
const functions = require("firebase-functions");
|
||||
const OpenAI = require("openai");
|
||||
const Stripe = require("stripe");
|
||||
|
||||
const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json");
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount)
|
||||
});
|
||||
@@ -177,206 +177,179 @@ const generateOTP = () => {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
};
|
||||
|
||||
// CORS middleware
|
||||
const cors = (req, res, next) => {
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');
|
||||
res.set('Access-Control-Max-Age', '3600');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Helper function to set CORS headers
|
||||
const setCorsHeaders = (res) => {
|
||||
res.set('Access-Control-Allow-Origin', '*');
|
||||
res.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
res.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');
|
||||
res.set('Access-Control-Max-Age', '3600');
|
||||
};
|
||||
|
||||
// Send OTP Function
|
||||
exports.sendOTP = functions.https.onRequest((req, res) => {
|
||||
cors(req, res, async () => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
exports.sendOTP = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { email, inviteCode } = req.body;
|
||||
const { email, inviteCode } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: "Email is required" });
|
||||
}
|
||||
if (!email) {
|
||||
return res.status(400).json({ error: "Email is required" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate OTP
|
||||
const otp = generateOTP();
|
||||
const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes expiry
|
||||
try {
|
||||
// Generate OTP
|
||||
const otp = generateOTP();
|
||||
const expiresAt = Date.now() + (5 * 60 * 1000); // 5 minutes expiry
|
||||
|
||||
// Store OTP in Firestore
|
||||
await db.collection("otps").doc(email).set({
|
||||
otp,
|
||||
expiresAt,
|
||||
attempts: 0,
|
||||
inviteCode: inviteCode || null,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
// Store OTP in Firestore
|
||||
await db.collection("otps").doc(email).set({
|
||||
otp,
|
||||
expiresAt,
|
||||
attempts: 0,
|
||||
inviteCode: inviteCode || null,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
// In production, send actual email
|
||||
console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`);
|
||||
// In production, send actual email
|
||||
console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Verification code sent to your email",
|
||||
// Always include OTP in emulator mode for testing
|
||||
otp,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Send OTP error:", error);
|
||||
res.status(500).json({ error: "Failed to send verification code" });
|
||||
}
|
||||
});
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Verification code sent to your email",
|
||||
// Always include OTP in emulator mode for testing
|
||||
otp,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Send OTP error:", error);
|
||||
res.status(500).json({ error: "Failed to send verification code" });
|
||||
}
|
||||
});
|
||||
|
||||
// Verify OTP Function
|
||||
exports.verifyOTP = functions.https.onRequest((req, res) => {
|
||||
cors(req, res, async () => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
exports.verifyOTP = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { email, otp } = req.body;
|
||||
|
||||
if (!email || !otp) {
|
||||
return res.status(400).json({ error: "Email and OTP are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve OTP document
|
||||
const otpDoc = await db.collection("otps").doc(email).get();
|
||||
|
||||
if (!otpDoc.exists) {
|
||||
return res.status(400).json({ error: "Invalid verification code" });
|
||||
}
|
||||
|
||||
const { email, otp } = req.body;
|
||||
const otpData = otpDoc.data();
|
||||
|
||||
if (!email || !otp) {
|
||||
return res.status(400).json({ error: "Email and OTP are required" });
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve OTP document
|
||||
const otpDoc = await db.collection("otps").doc(email).get();
|
||||
|
||||
if (!otpDoc.exists) {
|
||||
return res.status(400).json({ error: "Invalid verification code" });
|
||||
}
|
||||
|
||||
const otpData = otpDoc.data();
|
||||
|
||||
// Check if OTP is expired
|
||||
if (Date.now() > otpData.expiresAt) {
|
||||
await otpDoc.ref.delete();
|
||||
return res.status(400).json({ error: "Verification code has expired" });
|
||||
}
|
||||
|
||||
// Check if too many attempts
|
||||
if (otpData.attempts >= 5) {
|
||||
await otpDoc.ref.delete();
|
||||
return res.status(400).json({ error: "Too many failed attempts" });
|
||||
}
|
||||
|
||||
// Verify OTP
|
||||
if (otpData.otp !== otp) {
|
||||
await otpDoc.ref.update({
|
||||
attempts: (otpData.attempts || 0) + 1,
|
||||
});
|
||||
return res.status(400).json({ error: "Invalid verification code" });
|
||||
}
|
||||
|
||||
// OTP is valid - clean up and create/find user
|
||||
// Check if OTP is expired
|
||||
if (Date.now() > otpData.expiresAt) {
|
||||
await otpDoc.ref.delete();
|
||||
|
||||
// Generate a unique user ID for this email if it doesn't exist
|
||||
let userId;
|
||||
let userDoc;
|
||||
|
||||
// Check if user already exists by email
|
||||
const existingUserQuery = await db.collection("users")
|
||||
.where("email", "==", email)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!existingUserQuery.empty) {
|
||||
// User exists, get their ID
|
||||
userDoc = existingUserQuery.docs[0];
|
||||
userId = userDoc.id;
|
||||
} else {
|
||||
// Create new user
|
||||
userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
userDoc = null;
|
||||
}
|
||||
|
||||
// Prepare user object for response
|
||||
const user = {
|
||||
uid: userId,
|
||||
email: email,
|
||||
displayName: email.split("@")[0],
|
||||
emailVerified: true,
|
||||
};
|
||||
|
||||
// Create or update user document in Firestore
|
||||
const userRef = db.collection("users").doc(userId);
|
||||
|
||||
const userData = {
|
||||
id: userId,
|
||||
email: email,
|
||||
displayName: email.split("@")[0],
|
||||
emailVerified: true,
|
||||
lastLoginAt: Date.now(),
|
||||
};
|
||||
|
||||
if (!userDoc) {
|
||||
// Create new user document
|
||||
userData.createdAt = Date.now();
|
||||
await userRef.set(userData);
|
||||
} else {
|
||||
// Update existing user with latest login info
|
||||
await userRef.update({
|
||||
lastLoginAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a simple session token (in production, use proper JWT)
|
||||
const customToken = `session_${userId}_${Date.now()}`;
|
||||
|
||||
// Handle invitation if present
|
||||
let inviteData = null;
|
||||
if (otpData.inviteCode) {
|
||||
try {
|
||||
const inviteDoc = await db
|
||||
.collectionGroup("invites")
|
||||
.where("code", "==", otpData.inviteCode)
|
||||
.where("status", "==", "pending")
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!inviteDoc.empty) {
|
||||
inviteData = inviteDoc.docs[0].data();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching invite:", error);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user,
|
||||
token: customToken,
|
||||
invite: inviteData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Verify OTP error:", error);
|
||||
res.status(500).json({ error: "Failed to verify code" });
|
||||
return res.status(400).json({ error: "Verification code has expired" });
|
||||
}
|
||||
});
|
||||
|
||||
// Check if too many attempts
|
||||
if (otpData.attempts >= 5) {
|
||||
await otpDoc.ref.delete();
|
||||
return res.status(400).json({ error: "Too many failed attempts" });
|
||||
}
|
||||
|
||||
// Verify OTP
|
||||
if (otpData.otp !== otp) {
|
||||
await otpDoc.ref.update({
|
||||
attempts: (otpData.attempts || 0) + 1,
|
||||
});
|
||||
return res.status(400).json({ error: "Invalid verification code" });
|
||||
}
|
||||
|
||||
// OTP is valid - clean up and create/find user
|
||||
await otpDoc.ref.delete();
|
||||
|
||||
// Generate a unique user ID for this email if it doesn't exist
|
||||
let userId;
|
||||
let userDoc;
|
||||
|
||||
// Check if user already exists by email
|
||||
const existingUserQuery = await db.collection("users")
|
||||
.where("email", "==", email)
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!existingUserQuery.empty) {
|
||||
// User exists, get their ID
|
||||
userDoc = existingUserQuery.docs[0];
|
||||
userId = userDoc.id;
|
||||
} else {
|
||||
// Create new user
|
||||
userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
userDoc = null;
|
||||
}
|
||||
|
||||
// Prepare user object for response
|
||||
const user = {
|
||||
uid: userId,
|
||||
email: email,
|
||||
displayName: email.split("@")[0],
|
||||
emailVerified: true,
|
||||
};
|
||||
|
||||
// Create or update user document in Firestore
|
||||
const userRef = db.collection("users").doc(userId);
|
||||
|
||||
const userData = {
|
||||
id: userId,
|
||||
email: email,
|
||||
displayName: email.split("@")[0],
|
||||
emailVerified: true,
|
||||
lastLoginAt: Date.now(),
|
||||
};
|
||||
|
||||
if (!userDoc) {
|
||||
// Create new user document
|
||||
userData.createdAt = Date.now();
|
||||
await userRef.set(userData);
|
||||
} else {
|
||||
// Update existing user with latest login info
|
||||
await userRef.update({
|
||||
lastLoginAt: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
// Generate a simple session token (in production, use proper JWT)
|
||||
const customToken = `session_${userId}_${Date.now()}`;
|
||||
|
||||
// Handle invitation if present
|
||||
let inviteData = null;
|
||||
if (otpData.inviteCode) {
|
||||
try {
|
||||
const inviteDoc = await db
|
||||
.collectionGroup("invites")
|
||||
.where("code", "==", otpData.inviteCode)
|
||||
.where("status", "==", "pending")
|
||||
.limit(1)
|
||||
.get();
|
||||
|
||||
if (!inviteDoc.empty) {
|
||||
inviteData = inviteDoc.docs[0].data();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching invite:", error);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user,
|
||||
token: customToken,
|
||||
invite: inviteData,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Verify OTP error:", error);
|
||||
res.status(500).json({ error: "Failed to verify code" });
|
||||
}
|
||||
});
|
||||
|
||||
// Create Invitation Function
|
||||
exports.createInvitation = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
exports.createInvitation = onRequest({ cors: true }, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
@@ -396,7 +369,7 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => {
|
||||
try {
|
||||
// Generate invite code
|
||||
const code = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
|
||||
// Generate employee ID
|
||||
const employeeId = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
@@ -429,7 +402,7 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Generate invite links
|
||||
const baseUrl = process.env.CLIENT_URL || 'http://localhost:5174';
|
||||
const baseUrl = process.env.CLIENT_URL || 'http://localhost:5173';
|
||||
const inviteLink = `${baseUrl}/#/employee-form/${code}`;
|
||||
const emailLink = `mailto:${email}?subject=You're invited to join our organization&body=Hi ${name},%0A%0AYou've been invited to complete a questionnaire for our organization. Please click the link below to get started:%0A%0A${inviteLink}%0A%0AThis link will expire in 7 days.%0A%0AThank you!`;
|
||||
|
||||
@@ -452,9 +425,7 @@ exports.createInvitation = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Get Invitation Status Function
|
||||
exports.getInvitationStatus = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
|
||||
exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
@@ -501,9 +472,7 @@ exports.getInvitationStatus = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Consume Invitation Function
|
||||
exports.consumeInvitation = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
|
||||
exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
@@ -581,8 +550,7 @@ exports.consumeInvitation = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Submit Employee Answers Function
|
||||
exports.submitEmployeeAnswers = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
@@ -659,8 +627,7 @@ exports.submitEmployeeAnswers = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Generate Employee Report Function
|
||||
exports.generateEmployeeReport = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
@@ -794,9 +761,7 @@ Return ONLY valid JSON that matches this structure. Be thorough but professional
|
||||
});
|
||||
|
||||
// Generate Company Wiki Function
|
||||
exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
|
||||
exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
@@ -840,6 +805,8 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// content is guaranteed to be schema-conformant JSON
|
||||
console.log(completion.choices[0].message);
|
||||
console.log(completion.choices[0].message.content);
|
||||
const parsed = JSON.parse(completion.choices[0].message.content);
|
||||
|
||||
const report = {
|
||||
@@ -852,6 +819,9 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
|
||||
generatedAt: Date.now(),
|
||||
...parsed.wiki,
|
||||
};
|
||||
console.log(report);
|
||||
console.log(wiki);
|
||||
|
||||
} else {
|
||||
// Fallback to mock data when OpenAI is not available
|
||||
report = {
|
||||
@@ -921,8 +891,8 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
report,
|
||||
wiki,
|
||||
...report,
|
||||
...wiki,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Generate company wiki error:", error);
|
||||
@@ -931,8 +901,7 @@ exports.generateCompanyWiki = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Chat Function
|
||||
exports.chat = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
exports.chat = onRequest({ cors: true }, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
@@ -1007,9 +976,7 @@ Provide helpful, actionable insights while maintaining professional confidential
|
||||
});
|
||||
|
||||
// Create Organization Function
|
||||
exports.createOrganization = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
|
||||
exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
@@ -1121,8 +1088,7 @@ exports.createOrganization = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Get User Organizations Function
|
||||
exports.getUserOrganizations = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
@@ -1166,9 +1132,7 @@ exports.getUserOrganizations = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Join Organization Function (via invite)
|
||||
exports.joinOrganization = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
|
||||
exports.joinOrganization = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
@@ -1282,9 +1246,7 @@ exports.joinOrganization = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Create Stripe Checkout Session Function
|
||||
exports.createCheckoutSession = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
|
||||
exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
@@ -1374,7 +1336,7 @@ exports.createCheckoutSession = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Handle Stripe Webhook Function
|
||||
exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
|
||||
exports.stripeWebhook = onRequest(async (req, res) => {
|
||||
if (!stripe) {
|
||||
return res.status(500).send('Stripe not configured');
|
||||
}
|
||||
@@ -1385,7 +1347,7 @@ exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
|
||||
let event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
|
||||
event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
|
||||
} catch (err) {
|
||||
console.error('Webhook signature verification failed:', err.message);
|
||||
return res.status(400).send(`Webhook Error: ${err.message}`);
|
||||
@@ -1424,9 +1386,7 @@ exports.stripeWebhook = functions.https.onRequest(async (req, res) => {
|
||||
});
|
||||
|
||||
// Get Subscription Status Function
|
||||
exports.getSubscriptionStatus = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
|
||||
exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
@@ -1652,9 +1612,7 @@ async function handlePaymentFailed(invoice) {
|
||||
// });
|
||||
|
||||
// Save Company Report Function
|
||||
exports.saveCompanyReport = functions.https.onRequest(async (req, res) => {
|
||||
setCorsHeaders(res);
|
||||
|
||||
exports.saveCompanyReport = onRequest({ cors: true }, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user