Skip to content
Merged

Dev #34

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 0 additions & 39 deletions .env.staging

This file was deleted.

2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Thumbs.db
.env.local
.env.*.local
.env.production

.env.*
# Logs
logs
*.log
Expand Down
65 changes: 58 additions & 7 deletions apps/api/api/controllers/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,11 @@ class ProjectController {
}
}

async saveProjectGeneration(req: CustomRequest, res: Response, next: NextFunction): Promise<void> {
async saveProjectGeneration(
req: CustomRequest,
res: Response,
next: NextFunction
): Promise<void> {
const userId = req.user?.uid;
const { projectId } = req.params;
const generationData = req.body;
Expand Down Expand Up @@ -268,15 +272,15 @@ class ProjectController {
return;
}
if (!req.file) {
logger.warn(
`Save project ZIP failed for projectId ${projectId}: No ZIP file provided.`
);
logger.warn(`Save project ZIP failed for projectId ${projectId}: No ZIP file provided.`);
res.status(400).json({ message: 'ZIP file is required' });
return;
}

const zipUrl = await projectService.saveProjectZip(userId, projectId as string, req.file);
logger.info(`ZIP saved successfully for project ${projectId} and user ${userId}. URL: ${zipUrl}`);
logger.info(
`ZIP saved successfully for project ${projectId} and user ${userId}. URL: ${zipUrl}`
);
res.status(200).json({ message: 'ZIP saved successfully', url: zipUrl });
} catch (error: any) {
logger.error(
Expand Down Expand Up @@ -315,8 +319,14 @@ class ProjectController {
return;
}

const repoUrl = await projectService.sendProjectToGitHub(userId, projectId as string, githubData);
logger.info(`Project ${projectId} sent to GitHub successfully for user ${userId}. Repo: ${repoUrl}`);
const repoUrl = await projectService.sendProjectToGitHub(
userId,
projectId as string,
githubData
);
logger.info(
`Project ${projectId} sent to GitHub successfully for user ${userId}. Repo: ${repoUrl}`
);
res.status(200).json({ message: 'Project sent to GitHub successfully', repoUrl });
} catch (error: any) {
logger.error(
Expand All @@ -326,6 +336,47 @@ class ProjectController {
next(error);
}
}

async getProjectCode(req: CustomRequest, res: Response, next: NextFunction): Promise<void> {
const userId = req.user?.uid;
const { projectId } = req.params;
logger.info(
`Attempting to get project code from Firebase Storage. ProjectId: ${projectId}, UserId from token: ${userId}`
);
try {
if (!userId) {
logger.warn(
`Get project code failed for projectId ${projectId}: User ID not found in token.`
);
res.status(401).json({ message: 'User not authenticated' });
return;
}
if (!projectId) {
logger.warn('Get project code failed: Project ID missing in params.');
res.status(400).json({ message: 'Project ID is required' });
return;
}

const codeFiles = await projectService.getProjectCodeFromFirebase(
userId,
projectId as string
);
if (!codeFiles) {
logger.info(`No code found for project ${projectId} and user ${userId}.`);
res.status(404).json({ message: 'No code found for this project' });
return;
}

logger.info(`Successfully retrieved code for project ${projectId} and user ${userId}.`);
res.status(200).json({ files: codeFiles });
} catch (error: any) {
logger.error(
`Error in getProjectCode controller for projectId ${projectId}, userId ${userId}: ${error.message}`,
{ stack: error.stack, details: error }
);
next(error);
}
}
}

export const projectController = new ProjectController();
46 changes: 44 additions & 2 deletions apps/api/api/routes/project.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 50 * 1024 * 1024, // 50MB limit
}
},
});

export const projectRoutes = Router();
Expand Down Expand Up @@ -323,7 +323,12 @@ projectRoutes.post('/:projectId/generation', authenticate, projectController.sav
* '500':
* description: Internal server error.
*/
projectRoutes.post('/:projectId/zip', authenticate, upload.single('zip'), projectController.saveProjectZip);
projectRoutes.post(
'/:projectId/zip',
authenticate,
upload.single('zip'),
projectController.saveProjectZip
);

// Send project to GitHub
/**
Expand Down Expand Up @@ -384,3 +389,40 @@ projectRoutes.post('/:projectId/zip', authenticate, upload.single('zip'), projec
* description: Internal server error.
*/
projectRoutes.post('/:projectId/github', authenticate, projectController.sendProjectToGitHub);

// Get project code from Firebase Storage
/**
* @openapi
* /projects/{projectId}/code:
* get:
* tags:
* - Project Generation
* summary: Get project code from Firebase Storage
* security:
* - bearerAuth: []
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* description: The ID of the project.
* responses:
* '200':
* description: Project code retrieved successfully.
* content:
* application/json:
* schema:
* type: object
* properties:
* files:
* type: object
* description: Project files as key-value pairs (filename -> content).
* '401':
* description: Unauthorized.
* '404':
* description: No code found for this project.
* '500':
* description: Internal server error.
*/
projectRoutes.get('/:projectId/code', authenticate, projectController.getProjectCode);
35 changes: 35 additions & 0 deletions apps/api/api/services/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,41 @@ class ProjectService {
throw error;
}
}

async getProjectCodeFromFirebase(
userId: string,
projectId: string
): Promise<Record<string, string> | null> {
if (!userId || !projectId) {
logger.error('User ID and Project ID are required to get project code from Firebase.');
return null;
}

try {
logger.info(
`Attempting to retrieve project code from Firebase Storage for project ${projectId} and user ${userId}`
);

// Use storage service to download and extract the project code ZIP
const codeFiles = await storageService.downloadProjectCodeZip(projectId, userId);

if (!codeFiles || Object.keys(codeFiles).length === 0) {
logger.info(`No code files found for project ${projectId} and user ${userId}`);
return null;
}

logger.info(
`Successfully retrieved ${Object.keys(codeFiles).length} code files for project ${projectId} and user ${userId}`
);
return codeFiles;
} catch (error: any) {
logger.error(
`Error retrieving project code from Firebase for project ${projectId} and user ${userId}: ${error.message}`,
{ stack: error.stack, details: error }
);
return null;
}
}
}

export const projectService = new ProjectService();
85 changes: 85 additions & 0 deletions apps/api/api/services/storage.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,91 @@ export class StorageService {
}
}

/**
* Download and extract project code ZIP from Firebase Storage
* @param projectId - Project ID for folder structure
* @param userId - User ID for folder structure (optional)
* @returns Extracted files as Record<string, string> or null if not found
*/
async downloadProjectCodeZip(
projectId: string,
userId?: string
): Promise<Record<string, string> | null> {
try {
const folderPath = userId
? `users/${userId}/projects/${projectId}/code`
: `projects/${projectId}/code`;

logger.info(`Downloading project code ZIP from Firebase Storage`, {
projectId,
userId,
folderPath,
});

// List files in the folder to find the latest ZIP
const [files] = await this.bucket.getFiles({
prefix: folderPath,
});

if (!files || files.length === 0) {
logger.info(`No code ZIP files found for project ${projectId}`);
return null;
}

// Find the most recent ZIP file
const zipFiles = files.filter((file: any) => file.name.endsWith('.zip'));
if (zipFiles.length === 0) {
logger.info(`No ZIP files found for project ${projectId}`);
return null;
}

// Sort by creation time and get the latest
zipFiles.sort((a: any, b: any) => {
const aTime = a.metadata.timeCreated || '0';
const bTime = b.metadata.timeCreated || '0';
return new Date(bTime).getTime() - new Date(aTime).getTime();
});

const latestZipFile = zipFiles[0];
logger.info(`Found latest ZIP file: ${latestZipFile.name}`);

// Download the ZIP file
const [zipBuffer] = await latestZipFile.download();

// Extract the ZIP file using JSZip
const JSZip = require('jszip');
const zip = new JSZip();
const zipContent = await zip.loadAsync(zipBuffer);

const extractedFiles: Record<string, string> = {};

// Extract all files from the ZIP
for (const [filePath, file] of Object.entries(zipContent.files)) {
const zipFile = file as any;
if (!zipFile.dir) {
const content = await zipFile.async('string');
extractedFiles[filePath] = content;
}
}

logger.info(`Successfully extracted ${Object.keys(extractedFiles).length} files from ZIP`, {
projectId,
userId,
zipFileName: latestZipFile.name,
});

return extractedFiles;
} catch (error: any) {
logger.error(`Error downloading project code ZIP`, {
projectId,
userId,
error: error.message,
stack: error.stack,
});
return null;
}
}

/**
* Generate a unique project ID for storage purposes
* @returns A unique project ID
Expand Down
Loading