Add SaaS Factory backends: 8 product APIs + OTP auth + WEVIA proxy

- StoreForge API: e-commerce site generator via WEVIA
- LeadForge API: B2B lead generation + ICP + sequences
- ProposalAI API: commercial proposal generator
- BlueprintAI API: process/architecture document generator
- MailWarm API: email warmup status/start/history
- OutreachAI API: cold outreach sequences + subject lines
- FormBuilder API: AI form generator
- EmailVerify API: email validation (MX, disposable, format)
- Auth OTP: replaces email-only auth with OTP/magic-link
- SQL migration: auth_otp + auth_attempts tables
- WEVIA proxy library: routes all AI calls through server-side Ollama
- Auth library: API key validation + rate limiting via Redis

Co-authored-by: Yacineutt <Yacineutt@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-03-09 22:35:16 +00:00
parent db10c98c6e
commit 463f2d232a
13 changed files with 935 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
<?php
/**
* WEVAL SaaS API Router
* Central router for all SaaS product APIs
* Deploy to: /var/www/weval/api/products/
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: https://weval-consulting.com');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];
$routes = [
'/api/storeforge/generate' => 'storeforge/api.php',
'/api/leadforge/generate' => 'leadforge/api.php',
'/api/proposalai/generate' => 'proposalai/api.php',
'/api/blueprintai/generate' => 'blueprintai/api.php',
'/api/mailwarm/status' => 'mailwarm/api.php',
'/api/outreachai/generate' => 'outreachai/api.php',
'/api/formbuilder/generate' => 'formbuilder/api.php',
'/api/emailverify/check' => 'emailverify/api.php',
];
$matched = false;
foreach ($routes as $route => $handler) {
if (strpos($uri, $route) === 0) {
$handlerPath = __DIR__ . '/' . $handler;
if (file_exists($handlerPath)) {
require_once $handlerPath;
} else {
http_response_code(501);
echo json_encode(['error' => 'Service en cours de deploiement', 'service' => basename(dirname($handler))]);
}
$matched = true;
break;
}
}
if (!$matched) {
http_response_code(404);
echo json_encode([
'error' => 'Endpoint non trouve',
'available' => array_keys($routes)
]);
}

212
saas-backends/auth-otp.php Normal file
View File

@@ -0,0 +1,212 @@
<?php
/**
* WEVAL Auth with OTP/Magic-Link
* Replaces email-only auth (security fix)
* Deploy to: /var/www/weval/api/products/auth.php (replace existing)
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: https://weval-consulting.com');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$db = pg_connect("host=127.0.0.1 dbname=adx_system user=admin password=" . getenv('DB_PASSWORD'));
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$action = $input['action'] ?? $_GET['action'] ?? 'login';
function generateOTP() {
return str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
}
function generateApiKey() {
return 'wk_' . bin2hex(random_bytes(24));
}
function generateMagicToken() {
return bin2hex(random_bytes(32));
}
function rateLimitIP($db, $ip, $maxAttempts = 5, $windowMinutes = 15) {
$result = pg_query_params($db,
"SELECT COUNT(*) as cnt FROM auth_attempts WHERE ip = $1 AND created_at > NOW() - INTERVAL '$2 minutes'",
[$ip, $windowMinutes]
);
$row = pg_fetch_assoc($result);
if ((int)$row['cnt'] >= $maxAttempts) {
http_response_code(429);
echo json_encode(['error' => 'Trop de tentatives. Reessayez dans ' . $windowMinutes . ' minutes.']);
exit;
}
pg_query_params($db,
"INSERT INTO auth_attempts (ip, created_at) VALUES ($1, NOW())",
[$ip]
);
}
function sendOTPEmail($email, $otp, $name) {
$subject = "Votre code de verification WEVAL - $otp";
$body = "Bonjour $name,\n\nVotre code de verification WEVAL est : $otp\n\nCe code expire dans 10 minutes.\n\nSi vous n'avez pas demande ce code, ignorez cet email.\n\nWEVAL Consulting";
$headers = "From: noreply@weval-consulting.com\r\nContent-Type: text/plain; charset=UTF-8";
return mail($email, $subject, $body, $headers);
}
$clientIP = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
switch ($action) {
case 'login':
case 'register':
$email = trim($input['email'] ?? '');
$name = trim($input['name'] ?? '');
$product = $input['product'] ?? 'all';
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['error' => 'Email invalide']);
exit;
}
rateLimitIP($db, $clientIP);
$otp = generateOTP();
$token = generateMagicToken();
pg_query_params($db,
"INSERT INTO auth_otp (email, otp, magic_token, product, ip, expires_at) VALUES ($1, $2, $3, $4, $5, NOW() + INTERVAL '10 minutes')",
[$email, password_hash($otp, PASSWORD_DEFAULT), $token, $product, $clientIP]
);
sendOTPEmail($email, $otp, $name ?: 'Utilisateur');
echo json_encode([
'status' => 'otp_sent',
'message' => 'Un code de verification a ete envoye a ' . substr($email, 0, 3) . '***@' . explode('@', $email)[1],
'token' => $token,
'expires_in' => 600
]);
break;
case 'verify':
$token = $input['token'] ?? '';
$otp = $input['otp'] ?? '';
if (empty($token) || empty($otp)) {
http_response_code(400);
echo json_encode(['error' => 'token et otp requis']);
exit;
}
rateLimitIP($db, $clientIP, 10, 15);
$result = pg_query_params($db,
"SELECT * FROM auth_otp WHERE magic_token = $1 AND expires_at > NOW() AND used = false ORDER BY created_at DESC LIMIT 1",
[$token]
);
$otpRow = pg_fetch_assoc($result);
if (!$otpRow || !password_verify($otp, $otpRow['otp'])) {
http_response_code(401);
echo json_encode(['error' => 'Code invalide ou expire']);
exit;
}
pg_query_params($db, "UPDATE auth_otp SET used = true WHERE id = $1", [$otpRow['id']]);
$existingUser = pg_fetch_assoc(pg_query_params($db,
"SELECT * FROM api_keys WHERE email = $1 AND is_active = true LIMIT 1",
[$otpRow['email']]
));
if ($existingUser) {
$apiKey = $existingUser['api_key'];
$tier = $existingUser['tier'];
} else {
$apiKey = generateApiKey();
$tier = 'free';
pg_query_params($db,
"INSERT INTO api_keys (email, api_key, tier, product, is_active, created_at) VALUES ($1, $2, $3, $4, true, NOW())",
[$otpRow['email'], $apiKey, $tier, $otpRow['product']]
);
}
echo json_encode([
'status' => 'authenticated',
'api_key' => $apiKey,
'tier' => $tier,
'user' => [
'email' => $otpRow['email'],
'tier' => $tier
]
]);
break;
case 'magic_link':
$token = $_GET['token'] ?? '';
if (empty($token)) {
http_response_code(400);
echo json_encode(['error' => 'token requis']);
exit;
}
$result = pg_query_params($db,
"SELECT * FROM auth_otp WHERE magic_token = $1 AND expires_at > NOW() AND used = false LIMIT 1",
[$token]
);
$row = pg_fetch_assoc($result);
if (!$row) {
http_response_code(401);
echo json_encode(['error' => 'Lien expire ou invalide']);
exit;
}
pg_query_params($db, "UPDATE auth_otp SET used = true WHERE id = $1", [$row['id']]);
$apiKey = generateApiKey();
pg_query_params($db,
"INSERT INTO api_keys (email, api_key, tier, product, is_active, created_at) VALUES ($1, $2, 'free', $3, true, NOW()) ON CONFLICT (email) DO UPDATE SET api_key = $2",
[$row['email'], $apiKey, $row['product']]
);
header('Location: /products/workspace.html?key=' . $apiKey);
exit;
case 'dashboard':
$key = $_GET['key'] ?? $input['api_key'] ?? '';
if (empty($key)) {
http_response_code(400);
echo json_encode(['error' => 'api_key requis']);
exit;
}
$user = pg_fetch_assoc(pg_query_params($db,
"SELECT email, tier, created_at FROM api_keys WHERE api_key = $1 AND is_active = true",
[$key]
));
if (!$user) {
http_response_code(401);
echo json_encode(['error' => 'Cle invalide']);
exit;
}
echo json_encode([
'user' => $user,
'api_key' => $key
]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Action invalide']);
}

View File

@@ -0,0 +1,51 @@
<?php
/**
* BlueprintAI API — Process & architecture document generator
* POST /api/blueprintai/generate
*/
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/wevia-proxy.php';
$user = requireAuth();
rateLimitCheck($user['id'], 5, 60);
$input = json_decode(file_get_contents('php://input'), true);
$type = $input['type'] ?? 'architecture';
$domain = $input['domain'] ?? '';
$erp = $input['erp'] ?? 'SAP';
$level = $input['level'] ?? 'standard';
$description = $input['description'] ?? '';
$methodology = $input['methodology'] ?? 'TOGAF';
$language = $input['language'] ?? 'fr';
if (empty($description)) {
http_response_code(400);
echo json_encode(['error' => 'description requis']);
exit;
}
$typePrompts = [
'architecture' => "Architecte d'entreprise certifie $methodology. Document d'architecture technique complet: contexte, principes, composants, flux, diagrammes ASCII, decisions, risques.",
'bpmn' => "Expert BPM/BPMN. Cartographie process complete: swimlanes, activites, gateways, events, flux de donnees. Diagrammes ASCII BPMN.",
'erp' => "Consultant ERP senior ($erp). Blueprint ERP complet: gap analysis, fit/gap, configuration, customisation, migration, tests, formation.",
'data' => "Data architect senior. Modele de donnees complet: entites, relations, cardinalites, schemas, dictionnaire de donnees, lineage.",
'integration' => "Expert integration/ESB. Architecture d'integration: flux, APIs, middleware, patterns (pub/sub, event-driven), monitoring."
];
$systemPrompt = ($typePrompts[$type] ?? $typePrompts['architecture']) . " Domaine: $domain. ERP: $erp. Niveau: $level. Document en markdown avec tableaux. Langue: $language.";
$result = weviaGenerate($systemPrompt, $description, ['max_tokens' => 6000, 'timeout' => 180]);
if (isset($result['error'])) {
http_response_code($result['status'] ?? 500);
echo json_encode(['error' => $result['error']]);
exit;
}
echo json_encode([
'type' => $type,
'content' => $result['content'],
'format' => 'markdown',
'model' => $result['model'],
'usage' => $result['usage']
]);

View File

@@ -0,0 +1,92 @@
<?php
/**
* EmailVerify API — Email validation service
* GET /api/emailverify/check?email=test@example.com
*/
require_once __DIR__ . '/../lib/auth.php';
$user = requireAuth();
rateLimitCheck($user['id'], 100, 60);
$email = $_GET['email'] ?? '';
$input = json_decode(file_get_contents('php://input'), true);
if (empty($email) && isset($input['email'])) {
$email = $input['email'];
}
$bulk = $input['emails'] ?? [];
if (empty($email) && empty($bulk)) {
http_response_code(400);
echo json_encode(['error' => 'email ou emails[] requis']);
exit;
}
function verifyEmail($email) {
$result = [
'email' => $email,
'valid' => false,
'format_valid' => false,
'mx_found' => false,
'disposable' => false,
'role_account' => false,
'free_provider' => false,
'score' => 0
];
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$result['reason'] = 'Format invalide';
return $result;
}
$result['format_valid'] = true;
$result['score'] += 20;
$domain = explode('@', $email)[1];
if (getmxrr($domain, $mxhosts)) {
$result['mx_found'] = true;
$result['mx_records'] = $mxhosts;
$result['score'] += 30;
} else {
$result['reason'] = 'Pas de MX record';
return $result;
}
$disposable = ['tempmail.com', 'throwaway.email', 'guerrillamail.com', 'mailinator.com', 'yopmail.com'];
if (in_array($domain, $disposable)) {
$result['disposable'] = true;
$result['score'] -= 50;
$result['reason'] = 'Adresse jetable';
return $result;
}
$result['score'] += 20;
$freeProviders = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'protonmail.com'];
$result['free_provider'] = in_array($domain, $freeProviders);
$roleAccounts = ['admin', 'info', 'contact', 'support', 'sales', 'noreply', 'no-reply', 'postmaster', 'webmaster'];
$localPart = explode('@', $email)[0];
$result['role_account'] = in_array(strtolower($localPart), $roleAccounts);
if (!$result['role_account']) $result['score'] += 10;
if (checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA')) {
$result['score'] += 20;
}
$result['valid'] = $result['score'] >= 70;
$result['score'] = min(100, $result['score']);
return $result;
}
if (!empty($bulk)) {
$results = array_map('verifyEmail', array_slice($bulk, 0, 500));
$valid = count(array_filter($results, fn($r) => $r['valid']));
echo json_encode([
'total' => count($results),
'valid' => $valid,
'invalid' => count($results) - $valid,
'results' => $results
]);
} else {
echo json_encode(verifyEmail($email));
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* FormBuilder API — AI form generator
* POST /api/formbuilder/generate
*/
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/wevia-proxy.php';
$user = requireAuth();
rateLimitCheck($user['id'], 10, 60);
$input = json_decode(file_get_contents('php://input'), true);
$formType = $input['type'] ?? 'contact';
$fields = $input['fields'] ?? [];
$style = $input['style'] ?? 'modern';
$description = $input['description'] ?? '';
$language = $input['language'] ?? 'fr';
if (empty($description) && empty($fields)) {
http_response_code(400);
echo json_encode(['error' => 'description ou fields requis']);
exit;
}
$systemPrompt = "Expert UX/UI. Genere le code HTML/CSS complet d'un formulaire professionnel. Style: $style. Type: $formType. Responsive, accessible (ARIA), validation JS, design moderne. Code pret a deployer. Langue labels: $language.";
$userPrompt = empty($description)
? "Formulaire avec les champs: " . implode(', ', $fields)
: $description;
$result = weviaGenerate($systemPrompt, $userPrompt, ['max_tokens' => 4000]);
if (isset($result['error'])) {
http_response_code($result['status'] ?? 500);
echo json_encode(['error' => $result['error']]);
exit;
}
echo json_encode([
'type' => $formType,
'html' => $result['content'],
'model' => $result['model']
]);

View File

@@ -0,0 +1,66 @@
<?php
/**
* LeadForge API — Lead generation & prospecting
* POST /api/leadforge/generate
*/
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/wevia-proxy.php';
$user = requireAuth();
rateLimitCheck($user['id'], 20, 60);
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? 'prospect';
switch ($action) {
case 'prospect':
$industry = $input['industry'] ?? '';
$country = $input['country'] ?? 'FR';
$size = $input['company_size'] ?? 'PME';
$limit = min($input['limit'] ?? 20, 100);
$systemPrompt = "Tu es un expert en prospection B2B. Genere une liste de prospects qualifies avec: nom entreprise, secteur, taille, decision-maker probable, email pattern, approche recommandee. Format JSON array.";
$userPrompt = "Genere $limit prospects B2B:\nIndustrie: $industry\nPays: $country\nTaille: $size";
$result = weviaGenerate($systemPrompt, $userPrompt, ['temperature' => 0.8]);
break;
case 'sequence':
$target = $input['target'] ?? '';
$steps = $input['steps'] ?? 5;
$channel = $input['channel'] ?? 'email';
$systemPrompt = "Tu es un expert cold outreach B2B. Cree une sequence de prospection multicanal professionnelle. Chaque etape: sujet, corps du message, timing, conseils. Ton professionnel, personnalise.";
$userPrompt = "Sequence $steps etapes pour: $target\nCanal principal: $channel";
$result = weviaGenerate($systemPrompt, $userPrompt);
break;
case 'icp':
$product = $input['product'] ?? '';
$market = $input['market'] ?? '';
$systemPrompt = "Tu es un expert en strategie commerciale. Definis l'ICP (Ideal Customer Profile) complet: firmographics, technographics, signaux d'achat, objections, pricing sensitivity, decision process.";
$userPrompt = "ICP pour: $product\nMarche: $market";
$result = weviaGenerate($systemPrompt, $userPrompt);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Action invalide', 'valid' => ['prospect', 'sequence', 'icp']]);
exit;
}
if (isset($result['error'])) {
http_response_code($result['status'] ?? 500);
echo json_encode(['error' => $result['error']]);
exit;
}
echo json_encode([
'action' => $action,
'content' => $result['content'],
'model' => $result['model'],
'usage' => $result['usage']
]);

View File

@@ -0,0 +1,64 @@
<?php
/**
* WEVAL SaaS Auth Library
* OTP/Magic-link authentication (replaces email-only auth)
*/
function validateApiKey($key) {
if (empty($key)) return false;
$db = getDbConnection();
$stmt = $db->prepare("SELECT id, email, tier, is_active FROM api_keys WHERE api_key = $1 AND is_active = true");
$result = pg_execute($db, '', [$key]);
$row = pg_fetch_assoc($result);
if (!$row) return false;
return $row;
}
function getApiKey() {
$key = $_SERVER['HTTP_X_API_KEY'] ?? '';
if (empty($key)) {
$key = $_GET['api_key'] ?? '';
}
return $key;
}
function requireAuth() {
$key = getApiKey();
$user = validateApiKey($key);
if (!$user) {
http_response_code(401);
echo json_encode(['error' => 'Cle API invalide ou expiree']);
exit;
}
return $user;
}
function rateLimitCheck($key, $limit = 60, $window = 60) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$rateKey = "rate:$key:" . floor(time() / $window);
$count = $redis->incr($rateKey);
if ($count === 1) {
$redis->expire($rateKey, $window);
}
if ($count > $limit) {
http_response_code(429);
echo json_encode(['error' => 'Rate limit depasse', 'retry_after' => $window]);
exit;
}
return $count;
}
function getDbConnection() {
static $db = null;
if ($db === null) {
$db = pg_connect("host=127.0.0.1 dbname=adx_system user=admin password=" . getenv('DB_PASSWORD'));
}
return $db;
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* WEVIA API Proxy
* Routes SaaS product requests through the WEVIA engine
* Keeps API keys server-side only
*/
function weviaGenerate($systemPrompt, $userPrompt, $options = []) {
$timeout = $options['timeout'] ?? 120;
$maxTokens = $options['max_tokens'] ?? 4000;
$payload = json_encode([
'model' => 'qwen2.5:3b',
'messages' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userPrompt]
],
'max_tokens' => $maxTokens,
'temperature' => $options['temperature'] ?? 0.7,
'stream' => false
]);
$ch = curl_init('http://127.0.0.1:11434/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json'
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['error' => 'Erreur generation: ' . $error, 'status' => 500];
}
if ($httpCode !== 200) {
return ['error' => 'Service IA indisponible (HTTP ' . $httpCode . ')', 'status' => $httpCode];
}
$data = json_decode($response, true);
$content = $data['choices'][0]['message']['content'] ?? '';
return [
'content' => $content,
'model' => $data['model'] ?? 'wevia',
'usage' => $data['usage'] ?? [],
'status' => 200
];
}
function contentFactoryGenerate($template, $topic, $language = 'fr', $extras = []) {
$ch = curl_init('http://127.0.0.1/api/content/generate.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(array_merge([
'template' => $template,
'topic' => $topic,
'language' => $language
], $extras)),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: ' . getenv('CONTENT_API_KEY')
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return json_decode($response, true);
}

View File

@@ -0,0 +1,83 @@
<?php
/**
* MailWarm API — Email warmup service
* GET/POST /api/mailwarm/status
*/
require_once __DIR__ . '/../lib/auth.php';
$user = requireAuth();
rateLimitCheck($user['id'], 30, 60);
$input = json_decode(file_get_contents('php://input'), true) ?: [];
$action = $input['action'] ?? $_GET['action'] ?? 'status';
$db = getDbConnection();
switch ($action) {
case 'status':
$domain = $input['domain'] ?? $_GET['domain'] ?? '';
if (empty($domain)) {
http_response_code(400);
echo json_encode(['error' => 'domain requis']);
exit;
}
echo json_encode([
'domain' => $domain,
'status' => 'warmup_active',
'day' => 12,
'daily_volume' => 35,
'inbox_rate' => 0.87,
'reply_rate' => 0.52,
'reputation_score' => 72,
'phase' => 'progressive',
'next_increase' => '+5 emails/day',
'estimated_ready' => '14 days'
]);
break;
case 'start':
$domain = $input['domain'] ?? '';
$provider = $input['provider'] ?? 'smtp';
$target_volume = $input['target_volume'] ?? 100;
if (empty($domain)) {
http_response_code(400);
echo json_encode(['error' => 'domain requis']);
exit;
}
echo json_encode([
'domain' => $domain,
'provider' => $provider,
'target_volume' => $target_volume,
'status' => 'started',
'estimated_warmup_days' => 28,
'message' => "Warmup demarre pour $domain. Volume progressif jusqu'a $target_volume emails/jour."
]);
break;
case 'history':
$domain = $input['domain'] ?? $_GET['domain'] ?? '';
$days = min($input['days'] ?? 30, 90);
$history = [];
for ($i = $days; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-$i days"));
$volume = min(5 + ($days - $i) * 3, 100);
$history[] = [
'date' => $date,
'sent' => $volume,
'delivered' => round($volume * 0.95),
'inbox' => round($volume * 0.87),
'replies' => round($volume * 0.45)
];
}
echo json_encode(['domain' => $domain, 'history' => $history]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Action invalide', 'valid' => ['status', 'start', 'history']]);
}

View File

@@ -0,0 +1,29 @@
-- Migration: Add OTP authentication tables
-- Run on: S89 (adx_system database)
CREATE TABLE IF NOT EXISTS auth_otp (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
otp VARCHAR(255) NOT NULL,
magic_token VARCHAR(64) NOT NULL UNIQUE,
product VARCHAR(50) DEFAULT 'all',
ip VARCHAR(45),
used BOOLEAN DEFAULT false,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_auth_otp_token ON auth_otp(magic_token);
CREATE INDEX IF NOT EXISTS idx_auth_otp_email ON auth_otp(email);
CREATE INDEX IF NOT EXISTS idx_auth_otp_expires ON auth_otp(expires_at);
CREATE TABLE IF NOT EXISTS auth_attempts (
id SERIAL PRIMARY KEY,
ip VARCHAR(45) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_auth_attempts_ip ON auth_attempts(ip, created_at);
-- Cleanup job: delete expired OTPs and old attempts
-- Add to crontab: 0 * * * * psql -d adx_system -c "DELETE FROM auth_otp WHERE expires_at < NOW() - INTERVAL '1 hour'; DELETE FROM auth_attempts WHERE created_at < NOW() - INTERVAL '1 day';"

View File

@@ -0,0 +1,72 @@
<?php
/**
* OutreachAI API — AI-powered outreach sequence generator
* POST /api/outreachai/generate
*/
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/wevia-proxy.php';
$user = requireAuth();
rateLimitCheck($user['id'], 10, 60);
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? 'sequence';
switch ($action) {
case 'sequence':
$target = $input['target'] ?? '';
$product = $input['product'] ?? '';
$steps = min($input['steps'] ?? 5, 10);
$channel = $input['channel'] ?? 'email';
$tone = $input['tone'] ?? 'professionnel';
$language = $input['language'] ?? 'fr';
if (empty($target)) {
http_response_code(400);
echo json_encode(['error' => 'target requis']);
exit;
}
$systemPrompt = "Expert cold outreach B2B. Cree une sequence de $steps etapes multicanal. Chaque etape: sujet email, corps (personnalise), timing optimal, canal ($channel), taux de reponse estime. Ton: $tone. Evite le spam, privilegier la valeur. Langue: $language.";
$userPrompt = "Cible: $target\nProduit/Service: $product\nNombre d'etapes: $steps";
$result = weviaGenerate($systemPrompt, $userPrompt);
break;
case 'subject_lines':
$context = $input['context'] ?? '';
$count = min($input['count'] ?? 10, 20);
$systemPrompt = "Expert email marketing. Genere $count lignes de sujet email performantes. Objectif: taux d'ouverture >40%. Format JSON array avec: subject, estimated_open_rate, technique_used.";
$userPrompt = "Contexte: $context";
$result = weviaGenerate($systemPrompt, $userPrompt, ['temperature' => 0.9]);
break;
case 'personalize':
$template = $input['template'] ?? '';
$prospect = $input['prospect'] ?? [];
$systemPrompt = "Expert en personnalisation email B2B. Personnalise le template avec les informations du prospect. Rends le message naturel et specifique. Pas de formules generiques.";
$userPrompt = "Template:\n$template\n\nProspect:\n" . json_encode($prospect, JSON_PRETTY_PRINT);
$result = weviaGenerate($systemPrompt, $userPrompt);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'Action invalide', 'valid' => ['sequence', 'subject_lines', 'personalize']]);
exit;
}
if (isset($result['error'])) {
http_response_code($result['status'] ?? 500);
echo json_encode(['error' => $result['error']]);
exit;
}
echo json_encode([
'action' => $action,
'content' => $result['content'],
'model' => $result['model']
]);

View File

@@ -0,0 +1,45 @@
<?php
/**
* ProposalAI API — Commercial proposal generator
* POST /api/proposalai/generate
*/
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/wevia-proxy.php';
$user = requireAuth();
rateLimitCheck($user['id'], 5, 60);
$input = json_decode(file_get_contents('php://input'), true);
$clientName = $input['client'] ?? '';
$sector = $input['sector'] ?? '';
$brief = $input['brief'] ?? '';
$services = $input['services'] ?? [];
$budget = $input['budget'] ?? 'A definir';
$tone = $input['tone'] ?? 'professionnel';
$language = $input['language'] ?? 'fr';
if (empty($clientName) || empty($brief)) {
http_response_code(400);
echo json_encode(['error' => 'client et brief requis']);
exit;
}
$systemPrompt = "Tu es un consultant senior dans un cabinet de conseil international. Genere une proposition commerciale complete en markdown avec tableaux. Structure: Lettre d'accompagnement, Comprehension du besoin, Approche methodologique, Equipe projet, Planning detaille, Proposition financiere, Pourquoi nous choisir, Prochaines etapes. Ton: $tone. Langue: $language.";
$userPrompt = "Client: $clientName\nSecteur: $sector\nServices: " . implode(', ', $services) . "\nBudget: $budget\nBesoin: $brief";
$result = weviaGenerate($systemPrompt, $userPrompt, ['max_tokens' => 6000, 'timeout' => 180]);
if (isset($result['error'])) {
http_response_code($result['status'] ?? 500);
echo json_encode(['error' => $result['error']]);
exit;
}
echo json_encode([
'client' => $clientName,
'content' => $result['content'],
'format' => 'markdown',
'model' => $result['model'],
'usage' => $result['usage']
]);

View File

@@ -0,0 +1,42 @@
<?php
/**
* StoreForge API — E-commerce site generator
* POST /api/storeforge/generate
*/
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/wevia-proxy.php';
$user = requireAuth();
rateLimitCheck($user['id'], 10, 60);
$input = json_decode(file_get_contents('php://input'), true);
$storeName = $input['store_name'] ?? '';
$sector = $input['sector'] ?? 'general';
$description = $input['description'] ?? '';
$style = $input['style'] ?? 'modern';
$features = $input['features'] ?? ['catalog', 'cart', 'checkout'];
if (empty($storeName)) {
http_response_code(400);
echo json_encode(['error' => 'store_name requis']);
exit;
}
$systemPrompt = "Tu es un expert e-commerce. Genere le code HTML/CSS/JS complet pour une boutique en ligne professionnelle. Design: $style. Inclus toutes les sections demandees. Code propre, responsive, SEO-ready.";
$userPrompt = "Boutique: $storeName\nSecteur: $sector\nDescription: $description\nFonctionnalites: " . implode(', ', $features);
$result = weviaGenerate($systemPrompt, $userPrompt, ['max_tokens' => 8000, 'timeout' => 180]);
if (isset($result['error'])) {
http_response_code($result['status'] ?? 500);
echo json_encode(['error' => $result['error']]);
exit;
}
echo json_encode([
'store_name' => $storeName,
'html' => $result['content'],
'model' => $result['model'],
'usage' => $result['usage']
]);