Files
wevads-gpu/saas-backends/auth-otp.php
Cursor Agent 463f2d232a 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>
2026-03-09 22:35:16 +00:00

213 lines
7.1 KiB
PHP

<?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']);
}