update 9/20/25
This commit is contained in:
@@ -1,23 +1,12 @@
|
||||
const { onRequest } = require("firebase-functions/v2/https");
|
||||
const { setGlobalOptions, logger } = require("firebase-functions/v2");
|
||||
const admin = require("firebase-admin");
|
||||
const { VertexAI } = require('@google-cloud/vertexai');
|
||||
const Stripe = require("stripe");
|
||||
const { executeQuery, executeTransaction } = require('./database');
|
||||
|
||||
// Set global options for all functions to use us-central1 region
|
||||
|
||||
setGlobalOptions({ region: "us-central1" });
|
||||
|
||||
// const serviceAccount = require("./auditly-consulting-firebase-adminsdk-fbsvc-e4b51ef5cf.json");
|
||||
const serviceAccount = require("./auditly-c0027-firebase-adminsdk-fbsvc-1db7c58141.json")
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(serviceAccount)
|
||||
});
|
||||
|
||||
//region Interface Clients
|
||||
const db = admin.firestore();
|
||||
|
||||
// Initialize Vertex AI with your project ID
|
||||
// This automatically uses IAM authentication from the service account
|
||||
const vertexAI = new VertexAI({
|
||||
@@ -441,59 +430,78 @@ const RESPONSE_FORMAT_COMPANY = {
|
||||
//endregion Constants
|
||||
|
||||
//region Helper Functions
|
||||
const validateAuthAndGetContext = async (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
async function validateAuthAndGetContext(req, res) {
|
||||
// Set CORS headers for all requests
|
||||
res.setHeader('Access-Control-Allow-Origin', '*')
|
||||
.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
||||
.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type');
|
||||
|
||||
if (req.method == "OPTIONS") {
|
||||
res.headers['Access-Control-Allow-Origin'] = '*';
|
||||
res.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS';
|
||||
res.headers['Access-Control-Allow-Headers'] = 'Authorization, Content-Type';
|
||||
if (req.method === "OPTIONS") {
|
||||
res.status(204).send('');
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('Missing or invalid authorization header');
|
||||
res.status(401).json({ error: 'Missing or invalid authorization header' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// Validate token format (should start with 'session_')
|
||||
if (!token.startsWith('session_')) {
|
||||
throw new Error('Invalid token format');
|
||||
// Validate token format more thoroughly
|
||||
if (!token.startsWith('session_') || token.length < 20) {
|
||||
res.status(401).json({ error: 'Invalid token format' });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Look up token in Firestore
|
||||
const tokenDoc = await db.collection("authTokens").doc(token).get();
|
||||
try {
|
||||
// Look up token in PostgreSQL
|
||||
const tokenRows = await executeQuery(
|
||||
'SELECT * FROM auth_tokens WHERE token = $1 AND is_active = true',
|
||||
[token]
|
||||
);
|
||||
|
||||
if (!tokenDoc.exists) {
|
||||
throw new Error('Token not found');
|
||||
if (tokenRows.length === 0) {
|
||||
res.status(401).json({ error: 'Token not found' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const tokenData = tokenRows[0];
|
||||
|
||||
if (Date.now() > tokenData.expires_at) {
|
||||
res.status(401).json({ error: 'Token has expired' });
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update last used timestamp (don't await to avoid blocking)
|
||||
executeQuery(
|
||||
'UPDATE auth_tokens SET last_used_at = $1 WHERE token = $2',
|
||||
[Date.now(), token]
|
||||
).catch(error => {
|
||||
console.warn('Failed to update token lastUsedAt:', error);
|
||||
});
|
||||
|
||||
// Get user's organizations
|
||||
const userOrgsRows = await executeQuery(
|
||||
'SELECT organization_id FROM user_organizations WHERE user_id = $1',
|
||||
[tokenData.user_id]
|
||||
);
|
||||
|
||||
const orgIds = userOrgsRows.map(row => row.organization_id);
|
||||
|
||||
return {
|
||||
userId: tokenData.user_id,
|
||||
orgIds: orgIds,
|
||||
orgId: orgIds[0] || null,
|
||||
token: token
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Auth validation error:', error);
|
||||
res.status(500).json({ error: 'Authentication validation failed' });
|
||||
return null;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
const generateOTP = () => {
|
||||
@@ -640,9 +648,18 @@ const verifyUserAuthorization = async (userId, orgId) => {
|
||||
}
|
||||
|
||||
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;
|
||||
// Check if user exists in the organization's employees or as an organization member
|
||||
const employeeRows = await executeQuery(
|
||||
'SELECT id FROM employees WHERE organization_id = $1 AND (id = $2 OR user_id = $2)',
|
||||
[orgId, userId]
|
||||
);
|
||||
|
||||
const userOrgRows = await executeQuery(
|
||||
'SELECT user_id FROM user_organizations WHERE user_id = $1 AND organization_id = $2',
|
||||
[userId, orgId]
|
||||
);
|
||||
|
||||
return employeeRows.length > 0 || userOrgRows.length > 0;
|
||||
} catch (error) {
|
||||
console.error("Authorization check error:", error);
|
||||
return false;
|
||||
@@ -667,14 +684,19 @@ exports.sendOTP = onRequest({cors: true}, async (req, res) => {
|
||||
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 PostgreSQL
|
||||
await executeQuery(
|
||||
`INSERT INTO otps (email, otp, expires_at, attempts, invite_code, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (email)
|
||||
DO UPDATE SET
|
||||
otp = EXCLUDED.otp,
|
||||
expires_at = EXCLUDED.expires_at,
|
||||
attempts = 0,
|
||||
invite_code = EXCLUDED.invite_code,
|
||||
created_at = EXCLUDED.created_at`,
|
||||
[email, otp, expiresAt, 0, inviteCode || null, Date.now()]
|
||||
);
|
||||
|
||||
// In production, send actual email
|
||||
console.log(`📧 OTP for ${email}: ${otp} (expires in 5 minutes)`);
|
||||
@@ -705,56 +727,59 @@ exports.verifyOTP = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Retrieve OTP document
|
||||
const otpDoc = await db.collection("otps").doc(email).get();
|
||||
// Retrieve OTP from PostgreSQL
|
||||
const otpRows = await executeQuery(
|
||||
'SELECT * FROM otps WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (!otpDoc.exists) {
|
||||
if (otpRows.length === 0) {
|
||||
return res.status(400).json({ error: "Invalid verification code" });
|
||||
}
|
||||
|
||||
const otpData = otpDoc.data();
|
||||
const otpData = otpRows[0];
|
||||
|
||||
// Check if OTP is expired
|
||||
if (Date.now() > otpData.expiresAt) {
|
||||
await otpDoc.ref.delete();
|
||||
if (Date.now() > otpData.expires_at) {
|
||||
await executeQuery('DELETE FROM otps WHERE email = $1', [email]);
|
||||
return res.status(400).json({ error: "Verification code has expired" });
|
||||
}
|
||||
|
||||
// Check if too many attempts
|
||||
if (otpData.attempts >= 5) {
|
||||
await otpDoc.ref.delete();
|
||||
await executeQuery('DELETE FROM otps WHERE email = $1', [email]);
|
||||
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,
|
||||
});
|
||||
await executeQuery(
|
||||
'UPDATE otps SET attempts = $1 WHERE email = $2',
|
||||
[(otpData.attempts || 0) + 1, email]
|
||||
);
|
||||
return res.status(400).json({ error: "Invalid verification code" });
|
||||
}
|
||||
|
||||
// OTP is valid - clean up and create/find user
|
||||
await otpDoc.ref.delete();
|
||||
await executeQuery('DELETE FROM otps WHERE email = $1', [email]);
|
||||
|
||||
// Generate a unique user ID for this email if it doesn't exist
|
||||
let userId;
|
||||
let userDoc;
|
||||
let userExists = false;
|
||||
|
||||
// Check if user already exists by email
|
||||
const existingUserQuery = await db.collection("users")
|
||||
.where("email", "==", email)
|
||||
.limit(1)
|
||||
.get();
|
||||
const existingUserRows = await executeQuery(
|
||||
'SELECT id FROM users WHERE email = $1 LIMIT 1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (!existingUserQuery.empty) {
|
||||
if (existingUserRows.length > 0) {
|
||||
// User exists, get their ID
|
||||
userDoc = existingUserQuery.docs[0];
|
||||
userId = userDoc.id;
|
||||
userId = existingUserRows[0].id;
|
||||
userExists = true;
|
||||
} else {
|
||||
// Create new user
|
||||
userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
userDoc = null;
|
||||
}
|
||||
|
||||
// Prepare user object for response
|
||||
@@ -765,53 +790,52 @@ exports.verifyOTP = onRequest({cors: true}, async (req, res) => {
|
||||
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 or update user document in PostgreSQL
|
||||
const currentTime = Date.now();
|
||||
|
||||
if (!userExists) {
|
||||
// Create new user document
|
||||
userData.createdAt = Date.now();
|
||||
await userRef.set(userData);
|
||||
await executeQuery(
|
||||
`INSERT INTO users (id, email, display_name, email_verified, created_at, last_login_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[userId, email, email.split("@")[0], true, currentTime, currentTime]
|
||||
);
|
||||
} else {
|
||||
// Update existing user with latest login info
|
||||
await userRef.update({
|
||||
lastLoginAt: Date.now(),
|
||||
});
|
||||
await executeQuery(
|
||||
'UPDATE users SET last_login_at = $1 WHERE id = $2',
|
||||
[currentTime, userId]
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
// Store auth token in PostgreSQL for validation
|
||||
await executeQuery(
|
||||
`INSERT INTO auth_tokens (token, user_id, created_at, expires_at, last_used_at, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
customToken,
|
||||
userId,
|
||||
currentTime,
|
||||
currentTime + (30 * 24 * 60 * 60 * 1000), // 30 days
|
||||
currentTime,
|
||||
true
|
||||
]
|
||||
);
|
||||
|
||||
// Handle invitation if present
|
||||
let inviteData = null;
|
||||
if (otpData.inviteCode) {
|
||||
if (otpData.invite_code) {
|
||||
try {
|
||||
const inviteDoc = await db
|
||||
.collectionGroup("invites")
|
||||
.where("code", "==", otpData.inviteCode)
|
||||
.where("status", "==", "pending")
|
||||
.limit(1)
|
||||
.get();
|
||||
const inviteRows = await executeQuery(
|
||||
'SELECT * FROM invites WHERE code = $1 AND status = $2 LIMIT 1',
|
||||
[otpData.invite_code, 'pending']
|
||||
);
|
||||
|
||||
if (!inviteDoc.empty) {
|
||||
inviteData = inviteDoc.docs[0].data();
|
||||
if (inviteRows.length > 0) {
|
||||
inviteData = inviteRows[0];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching invite:", error);
|
||||
@@ -833,9 +857,8 @@ exports.verifyOTP = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Create Invitation
|
||||
exports.createInvitation = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -844,8 +867,6 @@ exports.createInvitation = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
const { name, email, role = "employee", department } = req.body;
|
||||
|
||||
if (!email || !name) {
|
||||
@@ -925,7 +946,7 @@ exports.createInvitation = onRequest({cors: true}, async (req, res) => {
|
||||
},
|
||||
},
|
||||
],
|
||||
from: { email: 'no-reply@auditly.com', name: 'Auditly' },
|
||||
from: { email: 'no-reply@orbitly.com', name: 'Orbitly' },
|
||||
template_id: process.env.SENDGRID_TEMPLATE_ID,
|
||||
}),
|
||||
});
|
||||
@@ -1196,6 +1217,9 @@ exports.submitEmployeeAnswers = onRequest({cors: true}, async (req, res) => {
|
||||
} else {
|
||||
// Authenticated submission
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!employeeId || !answers) {
|
||||
return res.status(400).json({ error: "Employee ID and answers are required for authenticated submissions" });
|
||||
@@ -1391,9 +1415,8 @@ Be thorough, professional, and focus on actionable insights.
|
||||
|
||||
//region Generate Employee Report
|
||||
exports.generateEmployeeReport = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1511,8 +1534,8 @@ Be thorough, professional, and focus on actionable insights.
|
||||
|
||||
//region Generate Company Report
|
||||
exports.generateCompanyReport = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1520,8 +1543,6 @@ exports.generateCompanyReport = onRequest({cors: true}, async (req, res) => {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -1646,9 +1667,8 @@ Be thorough, professional, and focus on actionable insights.`;
|
||||
|
||||
//region Chat
|
||||
exports.chat = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1739,8 +1759,8 @@ Instructions:
|
||||
|
||||
//region Create Organization
|
||||
exports.createOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1749,90 +1769,63 @@ exports.createOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
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)}`;
|
||||
const currentTime = Date.now();
|
||||
|
||||
// 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,
|
||||
}
|
||||
await executeTransaction(async (client) => {
|
||||
// Create comprehensive organization document
|
||||
if (process.env.USE_NEON_SERVERLESS === 'true') {
|
||||
await client(
|
||||
`INSERT INTO organizations (
|
||||
id, name, owner_id, onboarding_completed,
|
||||
subscription_status, trial_end, employee_count, reports_generated,
|
||||
allowed_employee_count, features_enabled, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
||||
[
|
||||
orgId, name, authContext.userId, false,
|
||||
'trial', currentTime + (14 * 24 * 60 * 60 * 1000), // 14 day trial
|
||||
0, 0, 50,
|
||||
JSON.stringify({"aiReports": true, "chat": true, "analytics": true}),
|
||||
currentTime, currentTime
|
||||
]
|
||||
);
|
||||
|
||||
// Add organization to user's organizations (for multi-org support)
|
||||
await client(
|
||||
`INSERT INTO user_organizations (user_id, organization_id, role, onboarding_completed, joined_at)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[authContext.userId, orgId, 'owner', false, currentTime]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO organizations (
|
||||
id, name, owner_id, onboarding_completed,
|
||||
subscription_status, trial_end, employee_count, reports_generated,
|
||||
allowed_employee_count, features_enabled, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
|
||||
[
|
||||
orgId, name, authContext.userId, false,
|
||||
'trial', currentTime + (14 * 24 * 60 * 60 * 1000), // 14 day trial
|
||||
0, 0, 50,
|
||||
JSON.stringify({"aiReports": true, "chat": true, "analytics": true}),
|
||||
currentTime, currentTime
|
||||
]
|
||||
);
|
||||
|
||||
// Add organization to user's organizations (for multi-org support)
|
||||
await client.query(
|
||||
`INSERT INTO user_organizations (user_id, organization_id, role, onboarding_completed, joined_at)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
[authContext.userId, orgId, 'owner', false, currentTime]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 owner info to organization document (owners are NOT employees)
|
||||
const ownerInfo = {
|
||||
id: authContext.userId,
|
||||
name: userData.displayName || userData.email.split("@")[0],
|
||||
email: userData.email,
|
||||
joinedAt: Date.now()
|
||||
};
|
||||
|
||||
// Update org document with owner info
|
||||
await orgRef.update({
|
||||
ownerInfo: ownerInfo,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// 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({
|
||||
@@ -1841,9 +1834,12 @@ exports.createOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
name,
|
||||
role: "owner",
|
||||
onboardingCompleted: false,
|
||||
joinedAt: Date.now(),
|
||||
subscription: orgData.subscription,
|
||||
requiresSubscription: true, // Signal frontend to show subscription flow
|
||||
joinedAt: currentTime,
|
||||
subscription: {
|
||||
status: 'trial',
|
||||
trialEnd: currentTime + (14 * 24 * 60 * 60 * 1000)
|
||||
},
|
||||
requiresSubscription: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Create organization error:", error);
|
||||
@@ -1853,17 +1849,9 @@ exports.createOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
//endregion Create Organization
|
||||
|
||||
//region Get Organizations
|
||||
exports.getUserOrganizations = onRequest(async (req, res) => {
|
||||
let authContext;
|
||||
try {
|
||||
authContext = await validateAuthAndGetContext(req, res);
|
||||
} catch (error) {
|
||||
logger.debug("Auth validation failed:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
exports.getUserOrganizations = onRequest({cors: true}, async (req, res) => {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1872,8 +1860,6 @@ exports.getUserOrganizations = onRequest(async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
|
||||
// Get user's organizations
|
||||
const userOrgsSnapshot = await db
|
||||
.collection("users")
|
||||
@@ -1906,8 +1892,8 @@ exports.getUserOrganizations = onRequest(async (req, res) => {
|
||||
|
||||
//region Join Organization
|
||||
exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1916,8 +1902,6 @@ exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
const { inviteCode } = req.body;
|
||||
|
||||
if (!inviteCode) {
|
||||
@@ -2167,7 +2151,7 @@ exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: 'Webhook handler failed' });
|
||||
// }
|
||||
// });
|
||||
//endregion Stripe Webhook
|
||||
//#endregion Stripe Webhook
|
||||
|
||||
//region Get Sub Status
|
||||
// exports.getSubscriptionStatus = onRequest(async (req, res) => {
|
||||
@@ -2228,7 +2212,7 @@ exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: "Failed to get subscription status" });
|
||||
// }
|
||||
// });
|
||||
//endregion Get Sub Status
|
||||
//#endregion Get Sub Status
|
||||
|
||||
//region Save Company Report
|
||||
// exports.saveCompanyReport = onRequest(async (req, res) => {
|
||||
@@ -2272,12 +2256,12 @@ exports.joinOrganization = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: "Failed to save company report" });
|
||||
// }
|
||||
// });
|
||||
//endregion Save Company Report
|
||||
//#endregion Save Company Report
|
||||
|
||||
//region Get Org Data
|
||||
exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2286,21 +2270,34 @@ exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
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) {
|
||||
const orgRows = await executeQuery(
|
||||
'SELECT * FROM organizations WHERE id = $1',
|
||||
[orgId]
|
||||
);
|
||||
|
||||
if (orgRows.length === 0) {
|
||||
return res.status(404).json({ error: "Organization not found" });
|
||||
}
|
||||
|
||||
const orgData = { id: orgId, ...orgDoc.data() };
|
||||
const orgData = {
|
||||
id: orgRows[0].id,
|
||||
name: orgRows[0].name,
|
||||
industry: orgRows[0].industry,
|
||||
description: orgRows[0].description,
|
||||
mission: orgRows[0].mission,
|
||||
vision: orgRows[0].vision,
|
||||
values: orgRows[0].values,
|
||||
onboardingCompleted: orgRows[0].onboarding_completed,
|
||||
onboardingData: orgRows[0].onboarding_data,
|
||||
createdAt: orgRows[0].created_at,
|
||||
updatedAt: orgRows[0].updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -2308,10 +2305,6 @@ exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
});
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
@@ -2319,8 +2312,8 @@ exports.getOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Update Organization Data
|
||||
exports.updateOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2329,8 +2322,6 @@ exports.updateOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
const { data } = req.body;
|
||||
|
||||
if (!data) {
|
||||
@@ -2366,8 +2357,8 @@ exports.updateOrgData = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Get Employees
|
||||
exports.getEmployees = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2376,9 +2367,6 @@ exports.getEmployees = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -2413,8 +2401,8 @@ exports.getEmployees = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Get Submissions
|
||||
exports.getSubmissions = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2423,9 +2411,6 @@ exports.getSubmissions = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -2456,8 +2441,8 @@ exports.getSubmissions = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Get Reports
|
||||
exports.getReports = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2466,9 +2451,6 @@ exports.getReports = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate auth token and get user context
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -2552,8 +2534,8 @@ exports.getReports = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Save Report
|
||||
exports.saveReport = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2568,12 +2550,6 @@ exports.saveReport = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -2603,8 +2579,8 @@ exports.saveReport = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Get Company Reports
|
||||
exports.getCompanyReports = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2613,8 +2589,6 @@ exports.getCompanyReports = onRequest({cors: true}, async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
|
||||
const orgId = authContext.orgId;
|
||||
if (!orgId) {
|
||||
return res.status(400).json({ error: "User has no associated organizations" });
|
||||
@@ -2646,8 +2620,8 @@ exports.getCompanyReports = onRequest({cors: true}, async (req, res) => {
|
||||
|
||||
//region Upload Image
|
||||
exports.uploadImage = onRequest({cors: true}, async (req, res) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.status(204).send('');
|
||||
const authContext = await validateAuthAndGetContext(req, res);
|
||||
if (!authContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2655,19 +2629,14 @@ exports.uploadImage = onRequest({cors: true}, async (req, res) => {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { orgId, userId, imageData } = req.body;
|
||||
const { orgId, userId } = authContext;
|
||||
const { 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;
|
||||
|
||||
@@ -2764,7 +2733,7 @@ exports.uploadImage = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: "Failed to get image" });
|
||||
// }
|
||||
// });
|
||||
//endregion Get Image
|
||||
//#endregion Get Image
|
||||
|
||||
//region Delete Image
|
||||
// exports.deleteImage = onRequest(async (req, res) => {
|
||||
@@ -2809,4 +2778,4 @@ exports.uploadImage = onRequest({cors: true}, async (req, res) => {
|
||||
// res.status(500).json({ error: "Failed to delete image" });
|
||||
// }
|
||||
// });
|
||||
//endregion Delete Image
|
||||
//#endregion Delete Image
|
||||
|
||||
Reference in New Issue
Block a user