Skip to content
Merged

Dev #112

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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,4 @@ data/
docker-compose.staging.yml

.windsurf
apps/ideploy/public/hot
113 changes: 113 additions & 0 deletions apps/api/api/controllers/appgen.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Response } from 'express';
import { CustomRequest } from '../interfaces/express.interface';
import { v4 as uuidv4 } from 'uuid';
import RedisConnection from '../config/redis.config';
import logger from '../config/logger';

const HANDOFF_TTL_SECONDS = 15 * 60; // 15 minutes
const HANDOFF_KEY_PREFIX = 'appgen:handoff:';

/**
* POST /appgen/handoff
* Stores an AppGen generation payload in Redis and returns a handoffId.
* iDeploy reads it at GET /api/ideploy/handoff/:handoffId
*/
export const createHandoffController = async (
req: CustomRequest,
res: Response
): Promise<void> => {
const userId = req.user?.uid;
logger.info('AppGen handoff requested', { userId });

try {
const { draftId, appName, description, files, metadata, messages, generatedAt, source, target, expiresAt } =
req.body;

if (!files || typeof files !== 'object') {
res.status(400).json({ success: false, message: 'files is required' });
return;
}

const handoffId = uuidv4();
const handoffData = {
handoffId,
userId: userId || null,
draftId,
appName: appName || 'Mon Application',
description: description || '',
files,
metadata: metadata || {},
messages: messages || [],
generatedAt: generatedAt || new Date().toISOString(),
createdAt: new Date().toISOString(),
source: source || 'appgen',
target: target || 'ideploy',
expiresAt: expiresAt || new Date(Date.now() + HANDOFF_TTL_SECONDS * 1000).toISOString(),
};

try {
const redis = RedisConnection.getInstance();
await redis.set(
`${HANDOFF_KEY_PREFIX}${handoffId}`,
JSON.stringify(handoffData),
'EX',
HANDOFF_TTL_SECONDS
);
logger.info('Handoff stored in Redis', { handoffId, userId });
} catch (redisError: any) {
logger.warn('Redis unavailable, handoff stored only in response', {
error: redisError.message,
});
// Still return the handoffId — iDeploy will fall back to sessionStorage on client side
}

res.status(201).json({ success: true, handoffId });
} catch (error: any) {
logger.error('Error in createHandoffController:', {
userId,
message: error.message,
stack: error.stack,
});
res.status(500).json({
success: false,
message: 'Failed to create handoff',
error: error.message,
});
}
};

/**
* GET /appgen/handoff/:handoffId
* Retrieves a handoff payload by ID (consumed by iDeploy).
*/
export const getHandoffController = async (
req: CustomRequest,
res: Response
): Promise<void> => {
const { handoffId } = req.params;
logger.info('AppGen handoff fetch', { handoffId });

try {
const redis = RedisConnection.getInstance();
const raw = await redis.get(`${HANDOFF_KEY_PREFIX}${handoffId}`);

if (!raw) {
res.status(404).json({ success: false, message: 'Handoff not found or expired' });
return;
}

const handoffData = JSON.parse(raw);
res.status(200).json({ success: true, data: handoffData });
} catch (error: any) {
logger.error('Error in getHandoffController:', {
handoffId,
message: error.message,
stack: error.stack,
});
res.status(500).json({
success: false,
message: 'Failed to retrieve handoff',
error: error.message,
});
}
};
98 changes: 98 additions & 0 deletions apps/api/api/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { userService } from '../services/user.service';
import { UserModel } from '../models/userModel';
import { refreshTokenService } from '../services/refreshToken.service';
import { CustomRequest } from '../interfaces/express.interface';
import { v4 as uuidv4 } from 'uuid';
import RedisConnection from '../config/redis.config';
export const sessionLoginController = async (req: Request, res: Response): Promise<void> => {
const token = req.body.token;
const user = req.body.user;
Expand Down Expand Up @@ -380,3 +382,99 @@ export const getRefreshTokensController = async (
});
}
};

const IDEPLOY_TOKEN_PREFIX = 'ideploy:token:';
const IDEPLOY_TOKEN_TTL = 5 * 60; // 5 minutes

/**
* POST /auth/ideploy-token
* Generates a short-lived one-time token for iDeploy SSO.
* Called by main-dashboard after Firebase login when redirect=ideploy.
*/
export const generateIdeployTokenController = async (
req: CustomRequest,
res: Response
): Promise<void> => {
const uid = req.user?.uid;
const email = req.user?.email;

if (!uid) {
res.status(401).json({ success: false, message: 'Unauthorized' });
return;
}

try {
const firebaseUser = await admin.auth().getUser(uid);
const token = uuidv4();

const payload = {
uid,
email: email || firebaseUser.email,
displayName: firebaseUser.displayName || null,
photoURL: firebaseUser.photoURL || null,
createdAt: new Date().toISOString(),
};

const redis = RedisConnection.getInstance();
await redis.set(
`${IDEPLOY_TOKEN_PREFIX}${token}`,
JSON.stringify(payload),
'EX',
IDEPLOY_TOKEN_TTL
);

logger.info('iDeploy SSO token generated', { uid });
res.status(201).json({ success: true, token });
} catch (error: any) {
logger.error('Error generating iDeploy token:', { uid, message: error.message });
res
.status(500)
.json({ success: false, message: 'Failed to generate token', error: error.message });
}
};

/**
* POST /auth/ideploy-token/validate
* Validates a one-time iDeploy SSO token and returns user data.
* Called by iDeploy Laravel backend to verify the token.
* Protected by IDEPLOY_SHARED_SECRET header.
*/
export const validateIdeployTokenController = async (
req: Request,
res: Response
): Promise<void> => {
const sharedSecret = process.env.IDEPLOY_SHARED_SECRET;
const providedSecret = req.headers['x-ideploy-secret'];

if (sharedSecret && providedSecret !== sharedSecret) {
res.status(403).json({ success: false, message: 'Invalid secret' });
return;
}

const { token } = req.body;
if (!token) {
res.status(400).json({ success: false, message: 'Token is required' });
return;
}

try {
const redis = RedisConnection.getInstance();
const raw = await redis.get(`${IDEPLOY_TOKEN_PREFIX}${token}`);

if (!raw) {
res.status(401).json({ success: false, message: 'Token not found or expired' });
return;
}

await redis.del(`${IDEPLOY_TOKEN_PREFIX}${token}`);
const userData = JSON.parse(raw);

logger.info('iDeploy SSO token validated', { uid: userData.uid });
res.status(200).json({ success: true, user: userData });
} catch (error: any) {
logger.error('Error validating iDeploy token:', { message: error.message });
res
.status(500)
.json({ success: false, message: 'Failed to validate token', error: error.message });
}
};
4 changes: 4 additions & 0 deletions apps/api/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import { teamsRoutes } from './routes/teams.routes';
import contactRoutes from './routes/contactRoutes';
import logoImportRoutes from './routes/logo-import.routes';
import ideployRoutes from './routes/ideploy.routes';
import appgenRoutes from './routes/appgen.routes';

const app: Express = express();

Expand Down Expand Up @@ -155,6 +156,9 @@ app.use('/api/logo', logoImportRoutes);
// iDeploy routes
app.use('/api/ideploy', ideployRoutes);

// AppGen routes
app.use('/appgen', appgenRoutes);

// Swagger setup
const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
Expand Down
79 changes: 79 additions & 0 deletions apps/api/api/routes/appgen.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Router } from 'express';
import { authenticate } from '../services/auth.service';
import { createHandoffController, getHandoffController } from '../controllers/appgen.controller';

const router = Router();

/**
* @swagger
* /appgen/handoff:
* post:
* summary: Crée un handoff AppGen vers iDeploy
* tags: [AppGen]
* security:
* - bearerAuth: []
* - cookieAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [files]
* properties:
* draftId:
* type: string
* appName:
* type: string
* description:
* type: string
* files:
* type: object
* metadata:
* type: object
* messages:
* type: array
* responses:
* 201:
* description: Handoff créé avec succès
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* handoffId:
* type: string
* 400:
* description: Données invalides
* 401:
* description: Non authentifié
* 500:
* description: Erreur serveur
*/
router.post('/handoff', authenticate, createHandoffController);

/**
* @swagger
* /appgen/handoff/{handoffId}:
* get:
* summary: Récupère un handoff AppGen par son ID
* tags: [AppGen]
* parameters:
* - in: path
* name: handoffId
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Handoff trouvé
* 404:
* description: Handoff non trouvé ou expiré
* 500:
* description: Erreur serveur
*/
router.get('/handoff/:handoffId', getHandoffController);

export default router;
7 changes: 7 additions & 0 deletions apps/api/api/routes/auth.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
logoutAllController,
getRefreshTokensController,
verifySessionController,
generateIdeployTokenController,
validateIdeployTokenController,
} from '../controllers/auth.controller';
import { authenticate } from '../services/auth.service';

Expand Down Expand Up @@ -259,3 +261,8 @@ authRoutes.get('/refresh-tokens', authenticate, getRefreshTokensController);
* description: Invalid API key
*/
authRoutes.post('/verify-session', verifySessionController);

// iDeploy SSO — generate a one-time token (requires authenticated session)
authRoutes.post('/ideploy-token', authenticate, generateIdeployTokenController);
// iDeploy SSO — validate a one-time token (called by iDeploy Laravel backend)
authRoutes.post('/ideploy-token/validate', validateIdeployTokenController);
4 changes: 2 additions & 2 deletions apps/appgen/apps/we-dev-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"email": "your.email@example.com"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"dev": "vite --port 5174",
"build": "vite build ",
"tsc": "tsc",
"start": "vite preview --host 0.0.0.0"
},
Expand Down
Loading
Loading