code invite avec 30 minutes de sécurité

This commit is contained in:
Flatlogic Bot 2026-02-18 16:47:44 +00:00
parent a763e6e5b1
commit b6b25ed90d
8 changed files with 174 additions and 11 deletions

View File

@ -0,0 +1,27 @@
<?php
require_once __DIR__ . '/../auth/session.php';
require_once __DIR__ . '/../includes/utils.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$server_id = $_POST['server_id'] ?? 0;
$user_id = $_SESSION['user_id'];
require_once __DIR__ . '/../includes/permissions.php';
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_SERVER) || Permissions::hasPermission($user_id, $server_id, Permissions::ADMINISTRATOR)) {
$new_invite_code = generateInviteCode();
$expires_at = date('c', time() + 1800); // ISO 8601 format
$stmt = db()->prepare("UPDATE servers SET invite_code = ?, invite_code_expires_at = ? WHERE id = ?");
$stmt->execute([$new_invite_code, date('Y-m-d H:i:s', time() + 1800), $server_id]);
echo json_encode([
'success' => true,
'invite_code' => $new_invite_code,
'expires_at' => $expires_at
]);
exit;
}
}
echo json_encode(['success' => false, 'error' => 'Permission denied']);

View File

@ -1,5 +1,6 @@
<?php
require_once 'auth/session.php';
require_once 'includes/utils.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@ -8,11 +9,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($action === 'join') {
$invite_code = $_POST['invite_code'] ?? '';
$stmt = db()->prepare("SELECT id FROM servers WHERE invite_code = ?");
$stmt = db()->prepare("SELECT id, invite_code_expires_at FROM servers WHERE invite_code = ?");
$stmt->execute([$invite_code]);
$server = $stmt->fetch();
if ($server) {
if ($server['invite_code_expires_at'] && strtotime($server['invite_code_expires_at']) < time()) {
die("This invite code has expired.");
}
$stmt = db()->prepare("INSERT IGNORE INTO server_members (server_id, user_id) VALUES (?, ?)");
$stmt->execute([$server['id'], $user_id]);
header('Location: index.php?server_id=' . $server['id']);
@ -56,9 +60,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$db->beginTransaction();
// Create server
$invite_code = substr(strtoupper(md5(uniqid())), 0, 8);
$stmt = $db->prepare("INSERT INTO servers (name, owner_id, invite_code, icon_url) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $user_id, $invite_code, $icon_url]);
$invite_code = generateInviteCode();
$expires_at = date('Y-m-d H:i:s', time() + 1800); // 30 minutes
$stmt = $db->prepare("INSERT INTO servers (name, owner_id, invite_code, icon_url, invite_code_expires_at) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$name, $user_id, $invite_code, $icon_url, $expires_at]);
$server_id = $db->lastInsertId();
// Add owner as member

View File

@ -2796,4 +2796,61 @@ document.addEventListener('DOMContentLoaded', () => {
};
restoreCollapsedStates();
// Invite code refresh and timer
const refreshBtn = document.getElementById('refresh-invite-code-btn');
const inviteInput = document.getElementById('server-invite-code');
const timerContainer = document.getElementById('invite-code-timer');
if (refreshBtn) {
refreshBtn.addEventListener('click', async () => {
const formData = new FormData();
formData.append('server_id', window.activeServerId);
try {
const resp = await fetch('api/refresh_invite_code.php', {
method: 'POST',
body: formData
});
const data = await resp.json();
if (data.success) {
if (inviteInput) inviteInput.value = data.invite_code;
if (timerContainer) {
timerContainer.dataset.expires = data.expires_at;
timerContainer.innerHTML = 'Expires in: <span id="invite-timer-display">30:00</span>';
}
} else {
alert('Error: ' + data.error);
}
} catch (e) {
console.error(e);
alert('Failed to refresh invite code.');
}
});
}
function updateInviteTimer() {
const display = document.getElementById('invite-timer-display');
const container = document.getElementById('invite-code-timer');
if (!display || !container || !container.dataset.expires) return;
const expiresAt = new Date(container.dataset.expires).getTime();
const now = new Date().getTime();
const diff = expiresAt - now;
if (diff <= 0) {
container.innerHTML = '<span class="text-danger">Expired</span>';
return;
}
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
display.innerText = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
if (timerContainer) {
setInterval(updateInviteTimer, 1000);
updateInviteTimer();
}
});

View File

@ -14,11 +14,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($invite_code)) {
$error = "An invitation code is required.";
} else {
$stmt = db()->prepare("SELECT id FROM servers WHERE invite_code = ?");
$stmt = db()->prepare("SELECT id, invite_code_expires_at FROM servers WHERE invite_code = ?");
$stmt->execute([$invite_code]);
$server = $stmt->fetch();
if (!$server) {
$error = "Invalid invitation code.";
} elseif ($server['invite_code_expires_at'] && strtotime($server['invite_code_expires_at']) < time()) {
$error = "This invitation code has expired.";
} else {
$server_id = $server['id'];
}
@ -89,7 +91,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<?php if (PRIVATE_REGISTRATION): ?>
<div class="mb-3">
<label class="form-label">Invite Code</label>
<input type="text" name="invite_code" class="form-control" placeholder="Enter invitation code" required>
<input type="text" name="invite_code" class="form-control" placeholder="Ex: aB1!c2D3@4" required>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-blurple">Continue</button>

View File

@ -0,0 +1,2 @@
-- Migration to make invite_code case-sensitive
ALTER TABLE servers MODIFY invite_code VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

44
includes/utils.php Normal file
View File

@ -0,0 +1,44 @@
<?php
/**
* Generates a secure random invite code.
* Requirements: lowercase, uppercase, digits, special characters.
* Length: 10 to 12 characters.
*/
function generateInviteCode($length = null) {
if ($length === null) {
$length = rand(10, 12);
}
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+';
$code = '';
$max = strlen($chars) - 1;
// Ensure at least one of each required type if possible,
// but a simple random selection from the full set is usually sufficient
// if the set is diverse enough and the length is 10-12.
// However, to be strictly compliant with "must have...", let's ensure it.
$sets = [
'abcdefghijklmnopqrstuvwxyz',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
'0123456789',
'!@#$%^&*()-_=+'
];
// Pick one from each set
foreach ($sets as $set) {
$code .= $set[random_int(0, strlen($set) - 1)];
}
// Fill the rest
while (strlen($code) < $length) {
$code .= $chars[random_int(0, $max)];
}
// Shuffle to avoid predictable pattern
$codeArray = str_split($code);
shuffle($codeArray);
return implode('', $codeArray);
}

View File

@ -1518,11 +1518,25 @@ async function handleSaveUserSettings(btn) {
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Invite Code</label>
<?php
$invite = '';
foreach($servers as $s) if($s['id'] == $active_server_id) $invite = $s['invite_code'];
$expires_at = '';
foreach($servers as $s) {
if($s['id'] == $active_server_id) {
$invite = $s['invite_code'];
$expires_at = $s['invite_code_expires_at'] ? date('c', strtotime($s['invite_code_expires_at'])) : '';
}
}
?>
<div class="input-group">
<input type="text" class="form-control bg-dark text-white border-0" value="<?php echo $invite; ?>" readonly>
<button class="btn btn-secondary" type="button" onclick="navigator.clipboard.writeText('<?php echo $invite; ?>')">Copy</button>
<div class="input-group mb-2">
<input type="text" id="server-invite-code" class="form-control bg-dark text-white border-0" value="<?php echo $invite; ?>" readonly>
<button class="btn btn-secondary" type="button" onclick="navigator.clipboard.writeText(document.getElementById('server-invite-code').value)">Copy</button>
<button class="btn btn-primary" type="button" id="refresh-invite-code-btn">Refresh</button>
</div>
<div id="invite-code-timer" class="small text-muted" data-expires="<?php echo $expires_at; ?>">
<?php if ($expires_at): ?>
Expires in: <span id="invite-timer-display">--:--</span>
<?php else: ?>
No expiration set.
<?php endif; ?>
</div>
</div>
@ -1648,7 +1662,7 @@ async function handleSaveUserSettings(btn) {
<p style="color: var(--text-muted); font-size: 0.9em;">Enter an invite code to join an existing server.</p>
<div class="mb-3">
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Invite Code</label>
<input type="text" name="invite_code" class="form-control" placeholder="GEN-123" required>
<input type="text" name="invite_code" class="form-control" placeholder="Ex: aB1!c2D3@4" required>
</div>
<button type="submit" class="btn btn-success w-100" style="background-color: #23a559; border: none;">Join Server</button>
</form>

View File

@ -642,3 +642,15 @@
2026-02-18 16:17:10 - GET / - POST: []
2026-02-18 16:20:19 - GET /?fl_project=38443 - POST: []
2026-02-18 16:21:39 - GET /index.php - POST: []
2026-02-18 16:27:32 - GET /?fl_project=38443 - POST: []
2026-02-18 16:32:55 - GET / - POST: []
2026-02-18 16:33:31 - GET /?fl_project=38443 - POST: []
2026-02-18 16:33:40 - GET /index.php - POST: []
2026-02-18 16:37:19 - GET / - POST: []
2026-02-18 16:37:47 - GET /?fl_project=38443 - POST: []
2026-02-18 16:39:21 - GET /index.php - POST: []
2026-02-18 16:40:32 - GET /index.php - POST: []
2026-02-18 16:43:32 - GET /?fl_project=38443 - POST: []
2026-02-18 16:45:36 - GET / - POST: []
2026-02-18 16:45:55 - GET /?fl_project=38443 - POST: []
2026-02-18 16:47:17 - GET /index.php - POST: []