- 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>
213 lines
7.1 KiB
PHP
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']);
|
|
}
|