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:
Ra
2025-08-20 03:30:04 -07:00
parent 1a9e92d7bd
commit cf565df13e
47 changed files with 6654 additions and 2007 deletions

View File

@@ -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;