code invite avec 30 minutes de sécurité
This commit is contained in:
parent
a763e6e5b1
commit
b6b25ed90d
27
api/refresh_invite_code.php
Normal file
27
api/refresh_invite_code.php
Normal 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']);
|
||||||
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'auth/session.php';
|
require_once 'auth/session.php';
|
||||||
|
require_once 'includes/utils.php';
|
||||||
requireLogin();
|
requireLogin();
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
@ -8,11 +9,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
|
|
||||||
if ($action === 'join') {
|
if ($action === 'join') {
|
||||||
$invite_code = $_POST['invite_code'] ?? '';
|
$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]);
|
$stmt->execute([$invite_code]);
|
||||||
$server = $stmt->fetch();
|
$server = $stmt->fetch();
|
||||||
|
|
||||||
if ($server) {
|
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 = db()->prepare("INSERT IGNORE INTO server_members (server_id, user_id) VALUES (?, ?)");
|
||||||
$stmt->execute([$server['id'], $user_id]);
|
$stmt->execute([$server['id'], $user_id]);
|
||||||
header('Location: index.php?server_id=' . $server['id']);
|
header('Location: index.php?server_id=' . $server['id']);
|
||||||
@ -56,9 +60,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$db->beginTransaction();
|
$db->beginTransaction();
|
||||||
|
|
||||||
// Create server
|
// Create server
|
||||||
$invite_code = substr(strtoupper(md5(uniqid())), 0, 8);
|
$invite_code = generateInviteCode();
|
||||||
$stmt = $db->prepare("INSERT INTO servers (name, owner_id, invite_code, icon_url) VALUES (?, ?, ?, ?)");
|
$expires_at = date('Y-m-d H:i:s', time() + 1800); // 30 minutes
|
||||||
$stmt->execute([$name, $user_id, $invite_code, $icon_url]);
|
$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();
|
$server_id = $db->lastInsertId();
|
||||||
|
|
||||||
// Add owner as member
|
// Add owner as member
|
||||||
|
|||||||
@ -2796,4 +2796,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
restoreCollapsedStates();
|
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();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,11 +14,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (empty($invite_code)) {
|
if (empty($invite_code)) {
|
||||||
$error = "An invitation code is required.";
|
$error = "An invitation code is required.";
|
||||||
} else {
|
} 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]);
|
$stmt->execute([$invite_code]);
|
||||||
$server = $stmt->fetch();
|
$server = $stmt->fetch();
|
||||||
if (!$server) {
|
if (!$server) {
|
||||||
$error = "Invalid invitation code.";
|
$error = "Invalid invitation code.";
|
||||||
|
} elseif ($server['invite_code_expires_at'] && strtotime($server['invite_code_expires_at']) < time()) {
|
||||||
|
$error = "This invitation code has expired.";
|
||||||
} else {
|
} else {
|
||||||
$server_id = $server['id'];
|
$server_id = $server['id'];
|
||||||
}
|
}
|
||||||
@ -89,7 +91,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<?php if (PRIVATE_REGISTRATION): ?>
|
<?php if (PRIVATE_REGISTRATION): ?>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">Invite Code</label>
|
<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>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button type="submit" class="btn btn-blurple">Continue</button>
|
<button type="submit" class="btn btn-blurple">Continue</button>
|
||||||
|
|||||||
2
db/migrations/20260218_make_invite_code_binary.sql
Normal file
2
db/migrations/20260218_make_invite_code_binary.sql
Normal 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
44
includes/utils.php
Normal 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);
|
||||||
|
}
|
||||||
24
index.php
24
index.php
@ -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>
|
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Invite Code</label>
|
||||||
<?php
|
<?php
|
||||||
$invite = '';
|
$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">
|
<div class="input-group mb-2">
|
||||||
<input type="text" class="form-control bg-dark text-white border-0" value="<?php echo $invite; ?>" readonly>
|
<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('<?php echo $invite; ?>')">Copy</button>
|
<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>
|
||||||
</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>
|
<p style="color: var(--text-muted); font-size: 0.9em;">Enter an invite code to join an existing server.</p>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label text-uppercase fw-bold" style="font-size: 0.7em; color: var(--text-muted);">Invite Code</label>
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-success w-100" style="background-color: #23a559; border: none;">Join Server</button>
|
<button type="submit" class="btn btn-success w-100" style="background-color: #23a559; border: none;">Join Server</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
12
requests.log
12
requests.log
@ -642,3 +642,15 @@
|
|||||||
2026-02-18 16:17:10 - GET / - POST: []
|
2026-02-18 16:17:10 - GET / - POST: []
|
||||||
2026-02-18 16:20:19 - GET /?fl_project=38443 - 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: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: []
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user