- Remove all authentication and org context dependencies - Simplify component to work only with invite codes from URL - Remove complex user/employee matching logic - Keep exact Figma UI components and styling - Use only submitViaInvite function for API submissions - Employees never need to log in, only use invite link
2252 lines
74 KiB
JavaScript
2252 lines
74 KiB
JavaScript
const { onRequest } = require("firebase-functions/v2/https");
|
|
const admin = require("firebase-admin");
|
|
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)
|
|
});
|
|
const db = admin.firestore();
|
|
|
|
// Auth middleware function to validate tokens and extract user context
|
|
const validateAuthAndGetContext = async (req) => {
|
|
const authHeader = req.headers.authorization;
|
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
throw new Error('Missing or invalid authorization header');
|
|
}
|
|
|
|
const token = authHeader.substring(7);
|
|
|
|
// Validate token format (should start with 'session_')
|
|
if (!token.startsWith('session_')) {
|
|
throw new Error('Invalid token format');
|
|
}
|
|
|
|
// Look up token in Firestore
|
|
const tokenDoc = await db.collection("authTokens").doc(token).get();
|
|
|
|
if (!tokenDoc.exists) {
|
|
throw new Error('Token not found');
|
|
}
|
|
|
|
const tokenData = tokenDoc.data();
|
|
|
|
if (!tokenData.isActive) {
|
|
throw new Error('Token is inactive');
|
|
}
|
|
|
|
if (Date.now() > tokenData.expiresAt) {
|
|
throw new Error('Token has expired');
|
|
}
|
|
|
|
// Update last used timestamp
|
|
await tokenDoc.ref.update({ lastUsedAt: Date.now() });
|
|
|
|
// Get user's organizations
|
|
const userOrgsSnapshot = await db.collection("users").doc(tokenData.userId).collection("organizations").get();
|
|
const orgIds = userOrgsSnapshot.docs.map(doc => doc.id);
|
|
|
|
return {
|
|
userId: tokenData.userId,
|
|
orgIds: orgIds,
|
|
// For backward compatibility, use first org as default
|
|
orgId: orgIds[0] || null,
|
|
token: token
|
|
};
|
|
};
|
|
|
|
// Helper function to verify user has access to specific organization
|
|
const verifyOrgAccess = (authContext, targetOrgId) => {
|
|
if (!targetOrgId) {
|
|
return authContext.orgId; // Use default org
|
|
}
|
|
|
|
if (!authContext.orgIds.includes(targetOrgId)) {
|
|
throw new Error('Unauthorized access to organization');
|
|
}
|
|
|
|
return targetOrgId;
|
|
};
|
|
|
|
// Initialize OpenAI if API key is available
|
|
const openai = process.env.OPENAI_API_KEY ? new OpenAI({
|
|
apiKey: process.env.OPENAI_API_KEY,
|
|
}) : null;
|
|
|
|
// Initialize Stripe if API key is available
|
|
const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, {
|
|
apiVersion: '2024-11-20.acacia',
|
|
}) : null;
|
|
|
|
const RESPONSE_FORMAT = {
|
|
type: "json_schema",
|
|
json_schema: {
|
|
name: "company_artifacts",
|
|
strict: true,
|
|
schema: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
companyPerformance: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
summary: { type: "string" },
|
|
metrics: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
name: { type: "string" },
|
|
value: { anyOf: [{ type: "string" }, { type: "number" }] },
|
|
trend: { enum: ["up", "down", "flat"] }
|
|
},
|
|
required: ["name", "value", "trend"]
|
|
}
|
|
}
|
|
},
|
|
required: ["summary", "metrics"]
|
|
},
|
|
immediateHiringNeeds: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
role: { type: "string" },
|
|
urgency: { enum: ["low", "medium", "high"] },
|
|
reason: { type: "string" }
|
|
},
|
|
required: ["role", "urgency", "reason"]
|
|
}
|
|
},
|
|
forwardOperatingPlan: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
nextQuarterObjectives: { type: "array", items: { type: "string" } },
|
|
initiatives: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
name: { type: "string" },
|
|
owner: { type: "string" },
|
|
kpis: { type: "array", items: { type: "string" } }
|
|
},
|
|
required: ["name", "owner", "kpis"]
|
|
}
|
|
},
|
|
risks: {
|
|
type: "array",
|
|
items: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
risk: { type: "string" },
|
|
mitigation: { type: "string" }
|
|
},
|
|
required: ["risk", "mitigation"]
|
|
}
|
|
}
|
|
},
|
|
required: ["nextQuarterObjectives", "initiatives", "risks"]
|
|
},
|
|
organizationalInsights: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
culture: { type: "string" },
|
|
teamDynamics: { type: "string" },
|
|
blockers: { type: "array", items: { type: "string" } }
|
|
},
|
|
required: ["culture", "teamDynamics", "blockers"]
|
|
},
|
|
strengths: { type: "array", items: { type: "string" } },
|
|
},
|
|
required: ["companyPerformance", "immediateHiringNeeds", "forwardOperatingPlan", "organizationalInsights", "strengths"]
|
|
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// Helper function to generate OTP
|
|
const generateOTP = () => {
|
|
return Math.floor(100000 + Math.random() * 900000).toString();
|
|
};
|
|
|
|
|
|
// Send OTP Function
|
|
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;
|
|
|
|
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
|
|
|
|
// 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)`);
|
|
|
|
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 = 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 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
|
|
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()}`;
|
|
|
|
// Store auth token in Firestore for validation
|
|
await db.collection("authTokens").doc(customToken).set({
|
|
userId: userId,
|
|
createdAt: Date.now(),
|
|
expiresAt: Date.now() + (30 * 24 * 60 * 60 * 1000), // 30 days
|
|
lastUsedAt: Date.now(),
|
|
isActive: true
|
|
});
|
|
|
|
// 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 = onRequest({ cors: true }, async (req, res) => {
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
const { name, email, role = "employee", department } = req.body;
|
|
|
|
if (!email || !name) {
|
|
return res.status(400).json({ error: "Name and email are required" });
|
|
}
|
|
|
|
// Use the user's default organization (first one)
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
// 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)}`;
|
|
|
|
// Create employee object for the invite
|
|
const employee = {
|
|
id: employeeId,
|
|
name: name.trim(),
|
|
email: email.trim(),
|
|
role: role?.trim() || "employee",
|
|
department: department?.trim() || "General",
|
|
status: "invited",
|
|
inviteCode: code
|
|
};
|
|
|
|
// Store invitation with employee data
|
|
const inviteRef = await db
|
|
.collection("orgs")
|
|
.doc(orgId)
|
|
.collection("invites")
|
|
.doc(code);
|
|
|
|
await inviteRef.set({
|
|
code,
|
|
employee,
|
|
email,
|
|
orgId,
|
|
status: "pending",
|
|
createdAt: Date.now(),
|
|
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days
|
|
});
|
|
|
|
// Generate invite links
|
|
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!`;
|
|
|
|
// In production, send actual invitation email
|
|
console.log(`📧 Invitation sent to ${email} (${name}) with code: ${code}`);
|
|
console.log(`📧 Invite link: ${inviteLink}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
code,
|
|
employee,
|
|
inviteLink,
|
|
emailLink,
|
|
message: "Invitation sent successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Create invitation error:", error);
|
|
res.status(500).json({ error: "Failed to create invitation" });
|
|
}
|
|
});
|
|
|
|
// Get Invitation Status Function
|
|
exports.getInvitationStatus = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { code } = req.query;
|
|
|
|
if (!code) {
|
|
return res.status(400).json({ error: "Invitation code is required" });
|
|
}
|
|
|
|
try {
|
|
const inviteDoc = await db
|
|
.collectionGroup("invites")
|
|
.where("code", "==", code)
|
|
.limit(1)
|
|
.get();
|
|
|
|
if (inviteDoc.empty) {
|
|
return res.status(404).json({ error: "Invitation not found" });
|
|
}
|
|
|
|
const invite = inviteDoc.docs[0].data();
|
|
|
|
// Check if expired
|
|
if (Date.now() > invite.expiresAt) {
|
|
return res.status(400).json({ error: "Invitation has expired" });
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
used: invite.status !== 'pending',
|
|
employee: invite.employee,
|
|
invite,
|
|
});
|
|
} catch (error) {
|
|
console.error("Get invitation status error:", error);
|
|
res.status(500).json({ error: "Failed to get invitation status" });
|
|
}
|
|
});
|
|
|
|
// Consume Invitation Function
|
|
exports.consumeInvitation = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { code, userId } = req.body;
|
|
|
|
if (!code) {
|
|
return res.status(400).json({ error: "Invitation code is required" });
|
|
}
|
|
|
|
try {
|
|
const inviteSnapshot = await db
|
|
.collectionGroup("invites")
|
|
.where("code", "==", code)
|
|
.where("status", "==", "pending")
|
|
.limit(1)
|
|
.get();
|
|
|
|
if (inviteSnapshot.empty) {
|
|
return res.status(404).json({ error: "Invitation not found or already used" });
|
|
}
|
|
|
|
const inviteDoc = inviteSnapshot.docs[0];
|
|
const invite = inviteDoc.data();
|
|
|
|
// Check if expired
|
|
if (Date.now() > invite.expiresAt) {
|
|
return res.status(400).json({ error: "Invitation has expired" });
|
|
}
|
|
|
|
// Get employee data from the invite
|
|
const employee = invite.employee;
|
|
if (!employee) {
|
|
return res.status(400).json({ error: "Invalid invitation data - missing employee information" });
|
|
}
|
|
|
|
// Mark invitation as consumed
|
|
await inviteDoc.ref.update({
|
|
status: "consumed",
|
|
consumedBy: employee.id,
|
|
consumedAt: Date.now(),
|
|
});
|
|
|
|
// Add employee to organization using data from invite
|
|
await db
|
|
.collection("orgs")
|
|
.doc(invite.orgId)
|
|
.collection("employees")
|
|
.doc(employee.id)
|
|
.set({
|
|
id: employee.id,
|
|
name: employee.name || employee.email.split("@")[0],
|
|
email: employee.email,
|
|
role: employee.role || "employee",
|
|
department: employee.department || "General",
|
|
joinedAt: Date.now(),
|
|
status: "active",
|
|
inviteCode: code,
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
orgId: invite.orgId,
|
|
message: "Invitation consumed successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Consume invitation error:", error);
|
|
res.status(500).json({ error: "Failed to consume invitation" });
|
|
}
|
|
});
|
|
|
|
// Submit Employee Answers Function
|
|
exports.submitEmployeeAnswers = onRequest({ cors: true }, async (req, res) => {
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { employeeId, answers, inviteCode } = req.body;
|
|
|
|
try {
|
|
let finalOrgId, finalEmployeeId;
|
|
|
|
if (inviteCode) {
|
|
// Invite-based submission (no auth required)
|
|
if (!inviteCode || !answers) {
|
|
return res.status(400).json({ error: "Invite code and answers are required for invite submissions" });
|
|
}
|
|
|
|
// Look up the invite to get employee and org data
|
|
const inviteSnapshot = await db
|
|
.collectionGroup("invites")
|
|
.where("code", "==", inviteCode)
|
|
.where("status", "==", "consumed")
|
|
.limit(1)
|
|
.get();
|
|
|
|
if (inviteSnapshot.empty) {
|
|
return res.status(404).json({ error: "Invitation not found or not consumed yet" });
|
|
}
|
|
|
|
const invite = inviteSnapshot.docs[0].data();
|
|
finalOrgId = invite.orgId;
|
|
finalEmployeeId = invite.employee.id;
|
|
} else {
|
|
// Authenticated submission
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
|
|
if (!employeeId || !answers) {
|
|
return res.status(400).json({ error: "Employee ID and answers are required for authenticated submissions" });
|
|
}
|
|
|
|
finalOrgId = authContext.orgId;
|
|
finalEmployeeId = employeeId;
|
|
|
|
if (!finalOrgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
}
|
|
|
|
// Store submission
|
|
const submissionRef = await db
|
|
.collection("orgs")
|
|
.doc(finalOrgId)
|
|
.collection("submissions")
|
|
.doc(finalEmployeeId);
|
|
|
|
await submissionRef.set({
|
|
employeeId: finalEmployeeId,
|
|
answers,
|
|
submittedAt: Date.now(),
|
|
status: "completed",
|
|
submissionType: inviteCode ? "invite" : "regular",
|
|
...(inviteCode && { inviteCode })
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Employee answers submitted successfully",
|
|
});
|
|
} catch (error) {
|
|
console.error("Submit employee answers error:", error);
|
|
res.status(500).json({ error: "Failed to submit answers" });
|
|
}
|
|
});
|
|
|
|
// Generate Employee Report Function
|
|
exports.generateEmployeeReport = onRequest({ cors: true }, async (req, res) => {
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { employee, submission, companyWiki } = req.body;
|
|
|
|
if (!employee || !submission) {
|
|
return res.status(400).json({ error: "Employee and submission data are required" });
|
|
}
|
|
|
|
try {
|
|
let report;
|
|
|
|
if (openai) {
|
|
// Use OpenAI to generate the report
|
|
const prompt = `
|
|
You are an expert HR analyst. Generate a comprehensive employee performance report based on the following data:
|
|
|
|
Employee Information:
|
|
- Name: ${employee.name || employee.email}
|
|
- Role: ${employee.role || "Team Member"}
|
|
- Department: ${employee.department || "General"}
|
|
|
|
Employee Submission Data:
|
|
${JSON.stringify(submission, null, 2)}
|
|
|
|
Company Context:
|
|
${companyWiki ? JSON.stringify(companyWiki, null, 2) : "No company context provided"}
|
|
|
|
Generate a detailed report with the following structure:
|
|
- roleAndOutput: Current role assessment and performance rating
|
|
- behavioralInsights: Work style, communication, and team dynamics
|
|
- strengths: List of employee strengths
|
|
- weaknesses: Areas for improvement (mark critical issues)
|
|
- opportunities: Growth and development opportunities
|
|
- risks: Potential risks or concerns
|
|
- recommendations: Specific action items
|
|
- grading: Numerical scores for different performance areas
|
|
|
|
Return ONLY valid JSON that matches this structure. Be thorough but professional.
|
|
`.trim();
|
|
|
|
const completion = await openai.chat.completions.create({
|
|
model: "gpt-4o",
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: "You are an expert HR analyst. Generate comprehensive employee performance reports in JSON format."
|
|
},
|
|
{
|
|
role: "user",
|
|
content: prompt
|
|
}
|
|
],
|
|
response_format: { type: "json_object" },
|
|
temperature: 0.7,
|
|
});
|
|
|
|
const aiResponse = completion.choices[0].message.content;
|
|
const parsedReport = JSON.parse(aiResponse);
|
|
|
|
report = {
|
|
employeeId: employee.id,
|
|
generatedAt: Date.now(),
|
|
summary: `AI-generated performance analysis for ${employee.name || employee.email}`,
|
|
...parsedReport
|
|
};
|
|
} else {
|
|
// Fallback to mock report when OpenAI is not available
|
|
report = {
|
|
employeeId: employee.id,
|
|
generatedAt: Date.now(),
|
|
summary: `Performance analysis for ${employee.name || employee.email}`,
|
|
roleAndOutput: {
|
|
currentRole: employee.role || "Team Member",
|
|
keyResponsibilities: ["Task completion", "Team collaboration", "Quality delivery"],
|
|
performanceRating: 85,
|
|
},
|
|
behavioralInsights: {
|
|
workStyle: "Collaborative and detail-oriented",
|
|
communicationSkills: "Strong verbal and written communication",
|
|
teamDynamics: "Positive team player",
|
|
},
|
|
strengths: [
|
|
"Excellent problem-solving abilities",
|
|
"Strong attention to detail",
|
|
"Reliable and consistent performance",
|
|
],
|
|
weaknesses: [
|
|
"Could improve time management",
|
|
"Needs to be more proactive in meetings",
|
|
],
|
|
opportunities: [
|
|
"Leadership development opportunities",
|
|
"Cross-functional project involvement",
|
|
"Skill enhancement in emerging technologies",
|
|
],
|
|
risks: [
|
|
"Potential burnout from heavy workload",
|
|
"Limited growth opportunities in current role",
|
|
],
|
|
recommendations: [
|
|
"Provide leadership training",
|
|
"Assign mentorship role",
|
|
"Consider promotion to senior position",
|
|
],
|
|
grading: {
|
|
overall: 85,
|
|
technical: 88,
|
|
communication: 82,
|
|
teamwork: 90,
|
|
leadership: 75,
|
|
},
|
|
};
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
report,
|
|
});
|
|
} catch (error) {
|
|
console.error("Generate employee report error:", error);
|
|
res.status(500).json({ error: "Failed to generate employee report" });
|
|
}
|
|
});
|
|
|
|
// Generate Company Wiki Function
|
|
exports.generateCompanyWiki = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { org, submissions = [] } = req.body;
|
|
|
|
if (!org) {
|
|
return res.status(400).json({ error: "Organization data is required" });
|
|
}
|
|
|
|
try {
|
|
let report, wiki;
|
|
|
|
if (openai) {
|
|
// Use OpenAI to generate the company report and wiki
|
|
db.collection("orgs").doc(orgId)
|
|
const system = "You are a cut-and-dry expert business analyst. Return ONLY JSON that conforms to the provided schema.";
|
|
const user = [
|
|
"Generate a COMPANY REPORT and COMPANY WIKI that fully leverage the input data.",
|
|
"Be thorough and professional.",
|
|
"",
|
|
"Organization Information:",
|
|
JSON.stringify(org, null, 2),
|
|
"",
|
|
"Employee Submissions:",
|
|
JSON.stringify(submissions, null, 2)
|
|
].join("\n");
|
|
|
|
const completion = await openai.chat.completions.create({
|
|
model: "gpt-4o",
|
|
temperature: 0, // consistency
|
|
response_format: RESPONSE_FORMAT,
|
|
messages: [
|
|
{ role: "system", content: system },
|
|
{ role: "user", content: user }
|
|
]
|
|
});
|
|
|
|
// 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 = {
|
|
generatedAt: Date.now(),
|
|
...parsed
|
|
};
|
|
|
|
const wiki = {
|
|
companyName: org?.name ?? parsed.wiki.companyName,
|
|
generatedAt: Date.now(),
|
|
|
|
};
|
|
|
|
const companyReport = db.collection("orgs").doc(orgId).collection("companyReport");
|
|
await companyReport.set(report);
|
|
const companyWiki = db.collection("orgs").doc(orgId).collection("companyWiki");
|
|
await companyWiki.set(wiki);
|
|
|
|
console.log(report);
|
|
console.log(wiki);
|
|
|
|
} else {
|
|
// Fallback to mock data when OpenAI is not available
|
|
report = {
|
|
generatedAt: Date.now(),
|
|
companyPerformance: {
|
|
overallScore: 82,
|
|
trend: "improving",
|
|
keyMetrics: {
|
|
productivity: 85,
|
|
satisfaction: 79,
|
|
retention: 88,
|
|
},
|
|
},
|
|
immediateHiringNeeds: [
|
|
{
|
|
role: "Frontend Developer",
|
|
priority: "high",
|
|
timeline: "2-4 weeks",
|
|
skills: ["React", "TypeScript", "CSS"],
|
|
},
|
|
],
|
|
forwardOperatingPlan: {
|
|
nextQuarter: "Focus on product development and team expansion",
|
|
challenges: ["Scaling infrastructure", "Talent acquisition"],
|
|
opportunities: ["New market segments", "Technology partnerships"],
|
|
},
|
|
organizationalInsights: {
|
|
teamDynamics: "Strong collaboration across departments",
|
|
culturalHealth: "Positive and inclusive work environment",
|
|
communicationEffectiveness: "Good but could improve cross-team coordination",
|
|
},
|
|
strengths: [
|
|
"Strong technical expertise",
|
|
"Collaborative team culture",
|
|
"Innovative problem-solving approach",
|
|
]
|
|
};
|
|
|
|
wiki = {
|
|
companyName: org.name,
|
|
industry: org.industry,
|
|
description: org.description,
|
|
mission: org.mission || "To deliver excellent products and services",
|
|
values: org.values || ["Innovation", "Teamwork", "Excellence"],
|
|
culture: "Collaborative and growth-oriented",
|
|
generatedAt: Date.now(),
|
|
};
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
...report,
|
|
...wiki,
|
|
});
|
|
} catch (error) {
|
|
console.error("Generate company wiki error:", error);
|
|
res.status(500).json({ error: "Failed to generate company wiki" });
|
|
}
|
|
});
|
|
|
|
// Chat Function
|
|
exports.chat = onRequest({ cors: true }, async (req, res) => {
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { message, employeeId, context, mentions, attachments } = req.body;
|
|
|
|
if (!message) {
|
|
return res.status(400).json({ error: "Message is required" });
|
|
}
|
|
|
|
try {
|
|
let response;
|
|
|
|
if (openai) {
|
|
// Use OpenAI for chat responses
|
|
const systemPrompt = `
|
|
You are an expert HR consultant and business analyst with access to employee performance data and company analytics.
|
|
You provide thoughtful, professional advice based on the employee context and company data provided.
|
|
|
|
${context ? `
|
|
Current Context:
|
|
${JSON.stringify(context, null, 2)}
|
|
` : ''}
|
|
|
|
${mentions && mentions.length > 0 ? `
|
|
Mentioned Employees:
|
|
${mentions.map(emp => `- ${emp.name} (${emp.role || 'Employee'})`).join('\n')}
|
|
` : ''}
|
|
|
|
Provide helpful, actionable insights while maintaining professional confidentiality and focusing on constructive feedback.
|
|
`.trim();
|
|
|
|
// Build the user message content
|
|
let userContent = [
|
|
{
|
|
type: "text",
|
|
text: message
|
|
}
|
|
];
|
|
|
|
// Add image attachments if present
|
|
if (attachments && attachments.length > 0) {
|
|
attachments.forEach(attachment => {
|
|
if (attachment.type.startsWith('image/') && attachment.data) {
|
|
userContent.push({
|
|
type: "image_url",
|
|
image_url: {
|
|
url: attachment.data,
|
|
detail: "high"
|
|
}
|
|
});
|
|
}
|
|
// For non-image files, add them as text context
|
|
else if (attachment.data) {
|
|
userContent.push({
|
|
type: "text",
|
|
text: `[Attached file: ${attachment.name} (${attachment.type})]`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
const completion = await openai.chat.completions.create({
|
|
model: "gpt-4o",
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: systemPrompt
|
|
},
|
|
{
|
|
role: "user",
|
|
content: userContent
|
|
}
|
|
],
|
|
temperature: 0.7,
|
|
max_tokens: 1000, // Increased for more detailed responses when analyzing images
|
|
});
|
|
|
|
response = completion.choices[0].message.content;
|
|
} else {
|
|
// Fallback responses when OpenAI is not available
|
|
const attachmentText = attachments && attachments.length > 0
|
|
? ` I can see you've attached ${attachments.length} file(s), but I'm currently unable to process attachments.`
|
|
: '';
|
|
|
|
const responses = [
|
|
`That's an interesting point about performance metrics.${attachmentText} Based on the data, I'd recommend focusing on...`,
|
|
`I can see from the employee report that there are opportunities for growth in...${attachmentText}`,
|
|
`The company analysis suggests that this area needs attention.${attachmentText} Here's what I would suggest...`,
|
|
`Based on the performance data, this employee shows strong potential in...${attachmentText}`,
|
|
];
|
|
|
|
response = responses[Math.floor(Math.random() * responses.length)];
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
response,
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
console.error("Chat error:", error);
|
|
res.status(500).json({ error: "Failed to process chat message" });
|
|
}
|
|
});
|
|
|
|
// Create Organization Function
|
|
exports.createOrganization = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
const { name } = req.body;
|
|
|
|
if (!name) {
|
|
return res.status(400).json({ error: "Organization name is required" });
|
|
}
|
|
// Generate unique organization ID
|
|
const orgId = `org_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
// Create comprehensive organization document
|
|
const orgData = {
|
|
name,
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now(),
|
|
onboardingCompleted: false,
|
|
ownerId: authContext.userId,
|
|
// Subscription fields (will be populated after Stripe setup)
|
|
subscription: {
|
|
status: 'trial', // trial, active, past_due, canceled
|
|
stripeCustomerId: null,
|
|
stripeSubscriptionId: null,
|
|
currentPeriodStart: null,
|
|
currentPeriodEnd: null,
|
|
trialEnd: Date.now() + (14 * 24 * 60 * 60 * 1000), // 14 day trial
|
|
},
|
|
// Usage tracking
|
|
usage: {
|
|
employeeCount: 0,
|
|
reportsGenerated: 0,
|
|
lastReportGeneration: null,
|
|
},
|
|
// Organization settings
|
|
settings: {
|
|
allowedEmployeeCount: 50, // Default limit
|
|
featuresEnabled: {
|
|
aiReports: true,
|
|
chat: true,
|
|
analytics: true,
|
|
}
|
|
}
|
|
};
|
|
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.set(orgData);
|
|
|
|
// Get user information from Firestore (since we don't use Firebase Auth)
|
|
const userRef = db.collection("users").doc(authContext.userId);
|
|
const userDoc = await userRef.get();
|
|
|
|
if (!userDoc.exists) {
|
|
console.error("User document not found:", authContext.userId);
|
|
return res.status(400).json({ error: "User not found" });
|
|
}
|
|
|
|
const userData = userDoc.data();
|
|
|
|
// Add user as owner to organization's employees collection
|
|
const employeeRef = orgRef.collection("employees").doc(authContext.userId);
|
|
await employeeRef.set({
|
|
id: authContext.userId,
|
|
role: "owner",
|
|
isOwner: true,
|
|
joinedAt: Date.now(),
|
|
status: "active",
|
|
name: userData.displayName || userData.email.split("@")[0],
|
|
email: userData.email,
|
|
department: "Management",
|
|
});
|
|
|
|
// Add organization to user's organizations (for multi-org support)
|
|
const userOrgRef = db.collection("users").doc(authContext.userId).collection("organizations").doc(orgId);
|
|
await userOrgRef.set({
|
|
orgId,
|
|
name,
|
|
role: "owner",
|
|
onboardingCompleted: false,
|
|
joinedAt: Date.now(),
|
|
});
|
|
|
|
// Update user document with latest activity
|
|
await userRef.update({
|
|
lastLoginAt: Date.now(),
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
orgId,
|
|
name,
|
|
role: "owner",
|
|
onboardingCompleted: false,
|
|
joinedAt: Date.now(),
|
|
subscription: orgData.subscription,
|
|
requiresSubscription: true, // Signal frontend to show subscription flow
|
|
});
|
|
} catch (error) {
|
|
console.error("Create organization error:", error);
|
|
res.status(500).json({ error: "Failed to create organization" });
|
|
}
|
|
});
|
|
|
|
// Get User Organizations Function
|
|
exports.getUserOrganizations = onRequest({ cors: true }, async (req, res) => {
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
|
|
// Get user's organizations
|
|
const userOrgsSnapshot = await db
|
|
.collection("users")
|
|
.doc(authContext.userId)
|
|
.collection("organizations")
|
|
.get();
|
|
|
|
const organizations = [];
|
|
userOrgsSnapshot.forEach(doc => {
|
|
organizations.push({
|
|
orgId: doc.id,
|
|
...doc.data(),
|
|
});
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
organizations,
|
|
});
|
|
} catch (error) {
|
|
console.error("Get user organizations error:", error);
|
|
if (error.message.includes('Missing or invalid authorization') ||
|
|
error.message.includes('Token')) {
|
|
return res.status(401).json({ error: error.message });
|
|
}
|
|
res.status(500).json({ error: "Failed to get user organizations" });
|
|
}
|
|
});
|
|
|
|
// Join Organization Function (via invite)
|
|
exports.joinOrganization = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
const { inviteCode } = req.body;
|
|
|
|
if (!inviteCode) {
|
|
return res.status(400).json({ error: "Invite code is required" });
|
|
}
|
|
// Find the invitation
|
|
const inviteSnapshot = await db
|
|
.collectionGroup("invites")
|
|
.where("code", "==", inviteCode)
|
|
.where("status", "==", "pending")
|
|
.limit(1)
|
|
.get();
|
|
|
|
if (inviteSnapshot.empty) {
|
|
return res.status(404).json({ error: "Invitation not found or already used" });
|
|
}
|
|
|
|
const inviteDoc = inviteSnapshot.docs[0];
|
|
const invite = inviteDoc.data();
|
|
|
|
// Check if expired
|
|
if (Date.now() > invite.expiresAt) {
|
|
return res.status(400).json({ error: "Invitation has expired" });
|
|
}
|
|
|
|
const orgId = invite.orgId;
|
|
|
|
// Get organization details
|
|
const orgDoc = await db.collection("orgs").doc(orgId).get();
|
|
if (!orgDoc.exists()) {
|
|
return res.status(404).json({ error: "Organization not found" });
|
|
}
|
|
|
|
const orgData = orgDoc.data();
|
|
|
|
// Get user information from Firestore (since we don't use Firebase Auth)
|
|
const userRef = db.collection("users").doc(authContext.userId);
|
|
const userDoc = await userRef.get();
|
|
|
|
if (!userDoc.exists) {
|
|
console.error("User document not found:", authContext.userId);
|
|
return res.status(400).json({ error: "User not found" });
|
|
}
|
|
|
|
const userData = userDoc.data();
|
|
|
|
// Mark invitation as consumed
|
|
await inviteDoc.ref.update({
|
|
status: "consumed",
|
|
consumedBy: authContext.userId,
|
|
consumedAt: Date.now(),
|
|
});
|
|
|
|
// Add user to organization employees with full information
|
|
await db
|
|
.collection("orgs")
|
|
.doc(orgId)
|
|
.collection("employees")
|
|
.doc(authContext.userId)
|
|
.set({
|
|
id: authContext.userId,
|
|
email: userData.email,
|
|
name: userData.displayName || userData.email.split("@")[0],
|
|
role: invite.role || "employee",
|
|
joinedAt: Date.now(),
|
|
status: "active",
|
|
});
|
|
|
|
// Add organization to user's organizations
|
|
await db
|
|
.collection("users")
|
|
.doc(authContext.userId)
|
|
.collection("organizations")
|
|
.doc(orgId)
|
|
.set({
|
|
orgId,
|
|
name: orgData.name,
|
|
role: invite.role || "employee",
|
|
onboardingCompleted: orgData.onboardingCompleted || false,
|
|
joinedAt: Date.now(),
|
|
});
|
|
|
|
// Update user document with latest login activity
|
|
await userRef.update({
|
|
lastLoginAt: Date.now(),
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
orgId,
|
|
name: orgData.name,
|
|
role: invite.role || "employee",
|
|
onboardingCompleted: orgData.onboardingCompleted || false,
|
|
joinedAt: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
console.error("Join organization error:", error);
|
|
res.status(500).json({ error: "Failed to join organization" });
|
|
}
|
|
});
|
|
|
|
// Create Stripe Checkout Session Function
|
|
exports.createCheckoutSession = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
const { userEmail, priceId } = req.body;
|
|
|
|
if (!userEmail) {
|
|
return res.status(400).json({ error: "User email is required" });
|
|
}
|
|
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
|
|
if (!stripe) {
|
|
return res.status(500).json({ error: "Stripe not configured" });
|
|
}
|
|
|
|
// Get or create Stripe customer
|
|
let customer;
|
|
const existingCustomers = await stripe.customers.list({
|
|
email: userEmail,
|
|
limit: 1,
|
|
});
|
|
|
|
if (existingCustomers.data.length > 0) {
|
|
customer = existingCustomers.data[0];
|
|
} else {
|
|
customer = await stripe.customers.create({
|
|
email: userEmail,
|
|
metadata: {
|
|
userId: authContext.userId,
|
|
orgId,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Default to standard plan if no priceId provided
|
|
const defaultPriceId = priceId || process.env.STRIPE_PRICE_ID || 'price_standard_monthly';
|
|
|
|
// Create checkout session
|
|
const session = await stripe.checkout.sessions.create({
|
|
customer: customer.id,
|
|
payment_method_types: ['card'],
|
|
line_items: [
|
|
{
|
|
price: defaultPriceId,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
mode: 'subscription',
|
|
success_url: `${process.env.CLIENT_URL || 'http://localhost:5174'}/#/dashboard?session_id={CHECKOUT_SESSION_ID}`,
|
|
cancel_url: `${process.env.CLIENT_URL || 'http://localhost:5174'}/#/dashboard?canceled=true`,
|
|
metadata: {
|
|
orgId,
|
|
userId: authContext.userId,
|
|
},
|
|
subscription_data: {
|
|
metadata: {
|
|
orgId,
|
|
userId: authContext.userId,
|
|
},
|
|
trial_period_days: 14, // 14-day trial
|
|
},
|
|
});
|
|
|
|
// Update organization with customer ID
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.update({
|
|
'subscription.stripeCustomerId': customer.id,
|
|
'subscription.checkoutSessionId': session.id,
|
|
updatedAt: Date.now(),
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
sessionId: session.id,
|
|
sessionUrl: session.url,
|
|
customerId: customer.id,
|
|
});
|
|
} catch (error) {
|
|
console.error("Create checkout session error:", error);
|
|
res.status(500).json({ error: "Failed to create checkout session" });
|
|
}
|
|
});
|
|
|
|
// Handle Stripe Webhook Function
|
|
exports.stripeWebhook = onRequest(async (req, res) => {
|
|
if (!stripe) {
|
|
return res.status(500).send('Stripe not configured');
|
|
}
|
|
|
|
const sig = req.headers['stripe-signature'];
|
|
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
|
|
let event;
|
|
|
|
try {
|
|
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}`);
|
|
}
|
|
|
|
// Handle the event
|
|
try {
|
|
switch (event.type) {
|
|
case 'checkout.session.completed':
|
|
await handleCheckoutCompleted(event.data.object);
|
|
break;
|
|
case 'customer.subscription.created':
|
|
await handleSubscriptionCreated(event.data.object);
|
|
break;
|
|
case 'customer.subscription.updated':
|
|
await handleSubscriptionUpdated(event.data.object);
|
|
break;
|
|
case 'customer.subscription.deleted':
|
|
await handleSubscriptionDeleted(event.data.object);
|
|
break;
|
|
case 'invoice.payment_succeeded':
|
|
await handlePaymentSucceeded(event.data.object);
|
|
break;
|
|
case 'invoice.payment_failed':
|
|
await handlePaymentFailed(event.data.object);
|
|
break;
|
|
default:
|
|
console.log(`Unhandled event type ${event.type}`);
|
|
}
|
|
|
|
res.json({ received: true });
|
|
} catch (error) {
|
|
console.error('Webhook handler error:', error);
|
|
res.status(500).json({ error: 'Webhook handler failed' });
|
|
}
|
|
});
|
|
|
|
// Get Subscription Status Function
|
|
exports.getSubscriptionStatus = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
const orgDoc = await db.collection("orgs").doc(orgId).get();
|
|
|
|
if (!orgDoc.exists()) {
|
|
return res.status(404).json({ error: "Organization not found" });
|
|
}
|
|
|
|
const orgData = orgDoc.data();
|
|
const subscription = orgData.subscription || {};
|
|
|
|
// If we have a Stripe subscription ID, get the latest status
|
|
if (stripe && subscription.stripeSubscriptionId) {
|
|
try {
|
|
const stripeSubscription = await stripe.subscriptions.retrieve(subscription.stripeSubscriptionId);
|
|
|
|
// Update local subscription data
|
|
await orgDoc.ref.update({
|
|
'subscription.status': stripeSubscription.status,
|
|
'subscription.currentPeriodStart': stripeSubscription.current_period_start * 1000,
|
|
'subscription.currentPeriodEnd': stripeSubscription.current_period_end * 1000,
|
|
updatedAt: Date.now(),
|
|
});
|
|
|
|
subscription.status = stripeSubscription.status;
|
|
subscription.currentPeriodStart = stripeSubscription.current_period_start * 1000;
|
|
subscription.currentPeriodEnd = stripeSubscription.current_period_end * 1000;
|
|
} catch (stripeError) {
|
|
console.error('Failed to fetch Stripe subscription:', stripeError);
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
subscription,
|
|
orgId,
|
|
});
|
|
} catch (error) {
|
|
console.error("Get subscription status error:", error);
|
|
res.status(500).json({ error: "Failed to get subscription status" });
|
|
}
|
|
});
|
|
|
|
// Webhook Helper Functions
|
|
async function handleCheckoutCompleted(session) {
|
|
const orgId = session.metadata?.orgId;
|
|
if (!orgId) return;
|
|
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.update({
|
|
'subscription.status': 'trialing',
|
|
'subscription.stripeSubscriptionId': session.subscription,
|
|
'subscription.checkoutSessionId': session.id,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
async function handleSubscriptionCreated(subscription) {
|
|
const orgId = subscription.metadata?.orgId;
|
|
if (!orgId) return;
|
|
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.update({
|
|
'subscription.status': subscription.status,
|
|
'subscription.stripeSubscriptionId': subscription.id,
|
|
'subscription.currentPeriodStart': subscription.current_period_start * 1000,
|
|
'subscription.currentPeriodEnd': subscription.current_period_end * 1000,
|
|
'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
async function handleSubscriptionUpdated(subscription) {
|
|
const orgId = subscription.metadata?.orgId;
|
|
if (!orgId) return;
|
|
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.update({
|
|
'subscription.status': subscription.status,
|
|
'subscription.currentPeriodStart': subscription.current_period_start * 1000,
|
|
'subscription.currentPeriodEnd': subscription.current_period_end * 1000,
|
|
'subscription.trialEnd': subscription.trial_end ? subscription.trial_end * 1000 : null,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
async function handleSubscriptionDeleted(subscription) {
|
|
const orgId = subscription.metadata?.orgId;
|
|
if (!orgId) return;
|
|
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.update({
|
|
'subscription.status': 'canceled',
|
|
'subscription.currentPeriodEnd': subscription.current_period_end * 1000,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
async function handlePaymentSucceeded(invoice) {
|
|
const subscriptionId = invoice.subscription;
|
|
if (!subscriptionId) return;
|
|
|
|
// Update subscription status to active
|
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
const orgId = subscription.metadata?.orgId;
|
|
|
|
if (orgId) {
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.update({
|
|
'subscription.status': 'active',
|
|
'subscription.currentPeriodStart': subscription.current_period_start * 1000,
|
|
'subscription.currentPeriodEnd': subscription.current_period_end * 1000,
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
}
|
|
|
|
async function handlePaymentFailed(invoice) {
|
|
const subscriptionId = invoice.subscription;
|
|
if (!subscriptionId) return;
|
|
|
|
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
const orgId = subscription.metadata?.orgId;
|
|
|
|
if (orgId) {
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.update({
|
|
'subscription.status': 'past_due',
|
|
updatedAt: Date.now(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// exports.helloWorld = onRequest((request, response) => {
|
|
// response.send("Hello from Firebase!");
|
|
// });
|
|
|
|
// exports.sendOTP = onRequest(async (request, response) => {
|
|
// // Set CORS headers
|
|
// response.set('Access-Control-Allow-Origin', '*');
|
|
// response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
// response.set('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
// if (request.method === 'OPTIONS') {
|
|
// response.status(204).send('');
|
|
// return;
|
|
// }
|
|
|
|
// if (request.method !== 'POST') {
|
|
// response.status(405).json({ error: 'Method not allowed' });
|
|
// return;
|
|
// }
|
|
|
|
// const { email } = request.body;
|
|
|
|
// if (!email) {
|
|
// response.status(400).json({ error: 'Email is required' });
|
|
// return;
|
|
// }
|
|
|
|
// // Generate a simple OTP
|
|
// const otp = Math.floor(100000 + Math.random() * 900000).toString();
|
|
|
|
// response.json({
|
|
// success: true,
|
|
// message: 'Verification code sent to your email',
|
|
// otp: otp // Always return OTP in emulator mode
|
|
// });
|
|
// });
|
|
|
|
// exports.verifyOTP = onRequest(async (request, response) => {
|
|
// // Set CORS headers
|
|
// response.set('Access-Control-Allow-Origin', '*');
|
|
// response.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
// response.set('Access-Control-Allow-Headers', 'Content-Type');
|
|
|
|
// if (request.method === 'OPTIONS') {
|
|
// response.status(204).send('');
|
|
// return;
|
|
// }
|
|
|
|
// if (request.method !== 'POST') {
|
|
// response.status(405).json({ error: 'Method not allowed' });
|
|
// return;
|
|
// }
|
|
|
|
// const { email, otp } = request.body;
|
|
|
|
// if (!email || !otp) {
|
|
// response.status(400).json({ error: 'Email and OTP are required' });
|
|
// return;
|
|
// }
|
|
|
|
// // Mock verification - accept any 6-digit code
|
|
// if (otp.length === 6) {
|
|
// response.json({
|
|
// success: true,
|
|
// user: {
|
|
// uid: 'demo-user-123',
|
|
// email: email,
|
|
// displayName: email.split('@')[0],
|
|
// emailVerified: true
|
|
// },
|
|
// token: 'demo-token-123'
|
|
// });
|
|
// } else {
|
|
// response.status(400).json({ error: 'Invalid verification code' });
|
|
// }
|
|
// });
|
|
|
|
// Save Company Report Function
|
|
exports.saveCompanyReport = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { orgId, report } = req.body;
|
|
|
|
if (!orgId || !report) {
|
|
return res.status(400).json({ error: "Organization ID and report are required" });
|
|
}
|
|
|
|
try {
|
|
// Add ID and timestamp if not present
|
|
if (!report.id) {
|
|
report.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
if (!report.createdAt) {
|
|
report.createdAt = Date.now();
|
|
}
|
|
|
|
// Save to Firestore
|
|
const reportRef = db.collection("orgs").doc(orgId).collection("fullCompanyReports").doc(report.id);
|
|
await reportRef.set(report);
|
|
|
|
console.log(`Company report saved successfully for org ${orgId}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
reportId: report.id,
|
|
message: "Company report saved successfully"
|
|
});
|
|
} catch (error) {
|
|
console.error("Save company report error:", error);
|
|
res.status(500).json({ error: "Failed to save company report" });
|
|
}
|
|
});
|
|
|
|
// Helper function to verify user authorization
|
|
const verifyUserAuthorization = async (userId, orgId) => {
|
|
if (!userId || !orgId) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Check if user exists in the organization's employees collection
|
|
const employeeDoc = await db.collection("orgs").doc(orgId).collection("employees").doc(userId).get();
|
|
return employeeDoc.exists;
|
|
} catch (error) {
|
|
console.error("Authorization check error:", error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Get Organization Data Function
|
|
exports.getOrgData = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
|
|
// Get organization data
|
|
const orgDoc = await db.collection("orgs").doc(orgId).get();
|
|
if (!orgDoc.exists) {
|
|
return res.status(404).json({ error: "Organization not found" });
|
|
}
|
|
|
|
const orgData = { id: orgId, ...orgDoc.data() };
|
|
|
|
res.json({
|
|
success: true,
|
|
org: orgData
|
|
});
|
|
} catch (error) {
|
|
console.error("Get org data error:", error);
|
|
if (error.message.includes('Missing or invalid authorization') ||
|
|
error.message.includes('Token')) {
|
|
return res.status(401).json({ error: error.message });
|
|
}
|
|
res.status(500).json({ error: "Failed to get organization data" });
|
|
}
|
|
});
|
|
|
|
// Update Organization Data Function
|
|
exports.updateOrgData = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "PUT") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
const { data } = req.body;
|
|
|
|
if (!data) {
|
|
return res.status(400).json({ error: "Data is required" });
|
|
}
|
|
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
|
|
// Update organization data
|
|
const orgRef = db.collection("orgs").doc(orgId);
|
|
await orgRef.update({
|
|
...data,
|
|
updatedAt: Date.now()
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Organization data updated successfully"
|
|
});
|
|
} catch (error) {
|
|
console.error("Update org data error:", error);
|
|
if (error.message.includes('Missing or invalid authorization') ||
|
|
error.message.includes('Token')) {
|
|
return res.status(401).json({ error: error.message });
|
|
}
|
|
res.status(500).json({ error: "Failed to update organization data" });
|
|
}
|
|
});
|
|
|
|
// Get Employees Function
|
|
exports.getEmployees = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
|
|
// Get all employees
|
|
const employeesSnapshot = await db.collection("orgs").doc(orgId).collection("employees").get();
|
|
const employees = [];
|
|
|
|
employeesSnapshot.forEach(doc => {
|
|
employees.push({ id: doc.id, ...doc.data() });
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
employees
|
|
});
|
|
} catch (error) {
|
|
console.error("Get employees error:", error);
|
|
if (error.message.includes('Missing or invalid authorization') ||
|
|
error.message.includes('Token')) {
|
|
return res.status(401).json({ error: error.message });
|
|
}
|
|
res.status(500).json({ error: "Failed to get employees" });
|
|
}
|
|
});
|
|
|
|
// Get Submissions Function
|
|
exports.getSubmissions = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
|
|
// Get all submissions
|
|
const submissionsSnapshot = await db.collection("orgs").doc(orgId).collection("submissions").get();
|
|
const submissions = {};
|
|
|
|
submissionsSnapshot.forEach(doc => {
|
|
submissions[doc.id] = { id: doc.id, ...doc.data() };
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
submissions
|
|
});
|
|
} catch (error) {
|
|
console.error("Get submissions error:", error);
|
|
if (error.message.includes('Missing or invalid authorization') ||
|
|
error.message.includes('Token')) {
|
|
return res.status(401).json({ error: error.message });
|
|
}
|
|
res.status(500).json({ error: "Failed to get submissions" });
|
|
}
|
|
});
|
|
|
|
// Get Reports Function
|
|
exports.getReports = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
// Validate auth token and get user context
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
|
|
// Get all reports
|
|
const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("reports").get();
|
|
const reports = {};
|
|
|
|
reportsSnapshot.forEach(doc => {
|
|
reports[doc.id] = { id: doc.id, ...doc.data() };
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
reports
|
|
});
|
|
} catch (error) {
|
|
console.error("Get reports error:", error);
|
|
if (error.message.includes('Missing or invalid authorization') ||
|
|
error.message.includes('Token')) {
|
|
return res.status(401).json({ error: error.message });
|
|
}
|
|
res.status(500).json({ error: "Failed to get reports" });
|
|
}
|
|
});
|
|
|
|
// Create/Update Employee Function
|
|
exports.upsertEmployee = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
|
|
const { orgId, userId, employeeData } = req.body;
|
|
|
|
if (!orgId || !userId || !employeeData) {
|
|
return res.status(400).json({ error: "Organization ID, user ID, and employee data are required" });
|
|
}
|
|
|
|
try {
|
|
// Verify user authorization
|
|
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
|
if (!isAuthorized) {
|
|
return res.status(403).json({ error: "Unauthorized access to organization" });
|
|
}
|
|
|
|
// Generate employee ID if not provided
|
|
if (!employeeData.id) {
|
|
employeeData.id = `emp_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
|
|
// Add timestamps
|
|
const currentTime = Date.now();
|
|
if (!employeeData.createdAt) {
|
|
employeeData.createdAt = currentTime;
|
|
}
|
|
employeeData.updatedAt = currentTime;
|
|
|
|
// Save employee
|
|
const employeeRef = db.collection("orgs").doc(orgId).collection("employees").doc(employeeData.id);
|
|
await employeeRef.set(employeeData);
|
|
|
|
res.json({
|
|
success: true,
|
|
employee: employeeData,
|
|
message: "Employee saved successfully"
|
|
});
|
|
} catch (error) {
|
|
console.error("Upsert employee error:", error);
|
|
res.status(500).json({ error: "Failed to save employee" });
|
|
}
|
|
});
|
|
|
|
// Save Report Function
|
|
exports.saveReport = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { orgId, userId, employeeId, reportData } = req.body;
|
|
|
|
if (!orgId || !userId || !employeeId || !reportData) {
|
|
return res.status(400).json({ error: "Organization ID, user ID, employee ID, and report data are required" });
|
|
}
|
|
|
|
try {
|
|
// Verify user authorization
|
|
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
|
if (!isAuthorized) {
|
|
return res.status(403).json({ error: "Unauthorized access to organization" });
|
|
}
|
|
|
|
// Add metadata
|
|
const currentTime = Date.now();
|
|
if (!reportData.id) {
|
|
reportData.id = `report_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
}
|
|
if (!reportData.createdAt) {
|
|
reportData.createdAt = currentTime;
|
|
}
|
|
reportData.updatedAt = currentTime;
|
|
reportData.employeeId = employeeId;
|
|
|
|
// Save report
|
|
const reportRef = db.collection("orgs").doc(orgId).collection("reports").doc(employeeId);
|
|
await reportRef.set(reportData);
|
|
|
|
res.json({
|
|
success: true,
|
|
report: reportData,
|
|
message: "Report saved successfully"
|
|
});
|
|
} catch (error) {
|
|
console.error("Save report error:", error);
|
|
res.status(500).json({ error: "Failed to save report" });
|
|
}
|
|
});
|
|
|
|
// Get Company Reports Function
|
|
exports.getCompanyReports = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
const authContext = await validateAuthAndGetContext(req);
|
|
|
|
const orgId = authContext.orgId;
|
|
if (!orgId) {
|
|
return res.status(400).json({ error: "User has no associated organizations" });
|
|
}
|
|
|
|
// Get all company reports
|
|
const reportsSnapshot = await db.collection("orgs").doc(orgId).collection("fullCompanyReports").get();
|
|
const reports = [];
|
|
|
|
reportsSnapshot.forEach(doc => {
|
|
reports.push({ id: doc.id, ...doc.data() });
|
|
});
|
|
|
|
// Sort by creation date (newest first)
|
|
reports.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
|
|
res.json({
|
|
success: true,
|
|
reports
|
|
});
|
|
} catch (error) {
|
|
console.error("Get company reports error:", error);
|
|
res.status(500).json({ error: "Failed to get company reports" });
|
|
}
|
|
});
|
|
|
|
// Upload Image Function
|
|
exports.uploadImage = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { orgId, userId, imageData } = req.body;
|
|
|
|
if (!orgId || !userId || !imageData) {
|
|
return res.status(400).json({ error: "Organization ID, user ID, and image data are required" });
|
|
}
|
|
|
|
try {
|
|
// Verify user authorization
|
|
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
|
if (!isAuthorized) {
|
|
return res.status(403).json({ error: "Unauthorized access to organization" });
|
|
}
|
|
|
|
// Validate image data
|
|
const { collectionName, documentId, dataUrl, filename, originalSize, compressedSize, width, height } = imageData;
|
|
|
|
if (!collectionName || !documentId || !dataUrl || !filename) {
|
|
return res.status(400).json({ error: "Missing required image data fields" });
|
|
}
|
|
|
|
// Create image document
|
|
const imageDoc = {
|
|
id: `${Date.now()}_${filename}`,
|
|
dataUrl,
|
|
filename,
|
|
originalSize: originalSize || 0,
|
|
compressedSize: compressedSize || 0,
|
|
uploadedAt: Date.now(),
|
|
width: width || 0,
|
|
height: height || 0,
|
|
orgId,
|
|
uploadedBy: userId
|
|
};
|
|
|
|
// Store image in organization's images collection
|
|
const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`);
|
|
await imageRef.set({
|
|
...imageDoc,
|
|
collectionName,
|
|
documentId
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
imageId: imageDoc.id,
|
|
message: "Image uploaded successfully"
|
|
});
|
|
} catch (error) {
|
|
console.error("Upload image error:", error);
|
|
res.status(500).json({ error: "Failed to upload image" });
|
|
}
|
|
});
|
|
|
|
// Get Image Function
|
|
exports.getImage = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "GET") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { orgId, userId, collectionName, documentId } = req.query;
|
|
|
|
if (!orgId || !userId || !collectionName || !documentId) {
|
|
return res.status(400).json({ error: "Organization ID, user ID, collection name, and document ID are required" });
|
|
}
|
|
|
|
try {
|
|
// Verify user authorization
|
|
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
|
if (!isAuthorized) {
|
|
return res.status(403).json({ error: "Unauthorized access to organization" });
|
|
}
|
|
|
|
// Get image document
|
|
const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`);
|
|
const imageDoc = await imageRef.get();
|
|
|
|
if (!imageDoc.exists) {
|
|
return res.status(404).json({ error: "Image not found" });
|
|
}
|
|
|
|
const imageData = imageDoc.data();
|
|
|
|
// Return image data (excluding org-specific metadata)
|
|
const responseData = {
|
|
id: imageData.id,
|
|
dataUrl: imageData.dataUrl,
|
|
filename: imageData.filename,
|
|
originalSize: imageData.originalSize,
|
|
compressedSize: imageData.compressedSize,
|
|
uploadedAt: imageData.uploadedAt,
|
|
width: imageData.width,
|
|
height: imageData.height
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
image: responseData
|
|
});
|
|
} catch (error) {
|
|
console.error("Get image error:", error);
|
|
res.status(500).json({ error: "Failed to get image" });
|
|
}
|
|
});
|
|
|
|
// Delete Image Function
|
|
exports.deleteImage = onRequest({ cors: true }, async (req, res) => {
|
|
if (req.method === 'OPTIONS') {
|
|
res.status(204).send('');
|
|
return;
|
|
}
|
|
|
|
if (req.method !== "DELETE") {
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
const { orgId, userId, collectionName, documentId } = req.body;
|
|
|
|
if (!orgId || !userId || !collectionName || !documentId) {
|
|
return res.status(400).json({ error: "Organization ID, user ID, collection name, and document ID are required" });
|
|
}
|
|
|
|
try {
|
|
// Verify user authorization
|
|
const isAuthorized = await verifyUserAuthorization(userId, orgId);
|
|
if (!isAuthorized) {
|
|
return res.status(403).json({ error: "Unauthorized access to organization" });
|
|
}
|
|
|
|
// Delete image document
|
|
const imageRef = db.collection("orgs").doc(orgId).collection("images").doc(`${collectionName}_${documentId}`);
|
|
const imageDoc = await imageRef.get();
|
|
|
|
if (!imageDoc.exists) {
|
|
return res.status(404).json({ error: "Image not found" });
|
|
}
|
|
|
|
await imageRef.delete();
|
|
|
|
res.json({
|
|
success: true,
|
|
message: "Image deleted successfully"
|
|
});
|
|
} catch (error) {
|
|
console.error("Delete image error:", error);
|
|
res.status(500).json({ error: "Failed to delete image" });
|
|
}
|
|
}); |