Page publique /upload.php (mobile-first, sans auth) : le client scanne un QR code au comptoir, choisit le poste de destination, et envoie ses documents. Les fichiers sont déposés dans le share SMB du compte, enregistrés en BDD (mêmes tables que les mails) et déclenchent le webhook si configuré. Page admin /admin/qrcode.php pour générer/imprimer les QR codes par poste. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017SChGCatJdbmZKCyYvTJMf
298 lines
15 KiB
PHP
298 lines
15 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../includes/db.php';
|
|
require_once __DIR__ . '/../includes/auth.php';
|
|
require_once __DIR__ . '/../includes/layout.php';
|
|
|
|
admin_check();
|
|
|
|
$error = '';
|
|
|
|
// Création d'un compte
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
|
|
|
|
if ($_POST['action'] === 'create') {
|
|
$username = preg_replace('/[^a-z0-9._-]/', '', strtolower(trim($_POST['username'] ?? '')));
|
|
$password = trim($_POST['password'] ?? '');
|
|
$display = trim($_POST['display_name'] ?? '');
|
|
$domain = cfg('domain');
|
|
|
|
if (strlen($username) < 2) {
|
|
$error = 'Nom d\'utilisateur invalide (min 2 caractères, a-z0-9._-)';
|
|
} elseif (strlen($password) < 6) {
|
|
$error = 'Mot de passe trop court (min 6 caractères).';
|
|
} else {
|
|
$email = "{$username}@{$domain}";
|
|
$smbPath = "/srv/copymail/{$username}";
|
|
|
|
$st = db()->prepare('SELECT id FROM accounts WHERE username = ? OR email = ?');
|
|
$st->execute([$username, $email]);
|
|
if ($st->fetch()) {
|
|
$error = "Le compte « {$username} » existe déjà.";
|
|
} else {
|
|
$webhook = trim($_POST['webhook_url'] ?? '');
|
|
db()->prepare(
|
|
'INSERT INTO accounts (username, email, display_name, smb_path, webhook_url) VALUES (?, ?, ?, ?, ?)'
|
|
)->execute([$username, $email, $display, $smbPath, $webhook ?: null]);
|
|
|
|
// Crée le dossier, l'utilisateur Dovecot et le share Samba
|
|
$cmd = sprintf(
|
|
'/opt/copymail/scripts/create_account.sh %s %s %s 2>&1',
|
|
escapeshellarg($username),
|
|
escapeshellarg($password),
|
|
escapeshellarg($domain)
|
|
);
|
|
shell_exec($cmd);
|
|
|
|
flash('success', "Compte <strong>{$email}</strong> créé. Share SMB : <code>\\\\serveur\\{$username}</code>");
|
|
header('Location: /admin/accounts.php');
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($_POST['action'] === 'delete') {
|
|
$id = (int)$_POST['account_id'];
|
|
$st = db()->prepare('SELECT username FROM accounts WHERE id = ?');
|
|
$st->execute([$id]);
|
|
$row = $st->fetch();
|
|
if ($row) {
|
|
$cmd = sprintf('/opt/copymail/scripts/delete_account.sh %s 2>&1', escapeshellarg($row['username']));
|
|
shell_exec($cmd);
|
|
db()->prepare('DELETE FROM accounts WHERE id = ?')->execute([$id]);
|
|
flash('success', "Compte « {$row['username']} » supprimé.");
|
|
}
|
|
header('Location: /admin/accounts.php');
|
|
exit;
|
|
}
|
|
|
|
if ($_POST['action'] === 'toggle') {
|
|
$id = (int)$_POST['account_id'];
|
|
db()->prepare('UPDATE accounts SET active = 1 - active WHERE id = ?')->execute([$id]);
|
|
header('Location: /admin/accounts.php');
|
|
exit;
|
|
}
|
|
|
|
if ($_POST['action'] === 'edit') {
|
|
$id = (int)$_POST['account_id'];
|
|
$display = trim($_POST['display_name'] ?? '');
|
|
$webhook = trim($_POST['webhook_url'] ?? '');
|
|
$newPassword = trim($_POST['new_password'] ?? '');
|
|
|
|
db()->prepare('UPDATE accounts SET display_name = ?, webhook_url = ? WHERE id = ?')
|
|
->execute([$display ?: null, $webhook ?: null, $id]);
|
|
|
|
if (strlen($newPassword) >= 6) {
|
|
$st = db()->prepare('SELECT username FROM accounts WHERE id = ?');
|
|
$st->execute([$id]);
|
|
$row = $st->fetch();
|
|
if ($row) {
|
|
$domain = cfg('domain');
|
|
$hash = shell_exec(sprintf('doveadm pw -s SHA512-CRYPT -p %s', escapeshellarg($newPassword)));
|
|
$hash = trim($hash ?? '');
|
|
if ($hash) {
|
|
shell_exec(sprintf(
|
|
"sed -i 's|^%s@%s:.*|%s@%s:%s:::|' /etc/dovecot/users",
|
|
$row['username'], $domain,
|
|
$row['username'], $domain, $hash
|
|
));
|
|
$smbUser = $row['username'];
|
|
shell_exec(sprintf('(echo %s; echo %s) | smbpasswd -s %s 2>/dev/null',
|
|
escapeshellarg($newPassword), escapeshellarg($newPassword), escapeshellarg($smbUser)
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
flash('success', "Compte mis à jour.");
|
|
header('Location: /admin/accounts.php');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
$accounts = db()->query(
|
|
'SELECT a.*, COUNT(sf.id) as mail_count FROM accounts a
|
|
LEFT JOIN source_file sf ON sf.account_id = a.id
|
|
GROUP BY a.id ORDER BY a.created_at DESC'
|
|
)->fetchAll();
|
|
|
|
$domain = cfg('domain');
|
|
|
|
html_head('Comptes');
|
|
html_navbar('accounts');
|
|
?>
|
|
<div class="container-fluid">
|
|
<?php flash_render(); ?>
|
|
|
|
<div class="d-flex justify-content-end mb-3">
|
|
<a href="/admin/qrcode.php" class="btn btn-outline-primary btn-sm">
|
|
<i class="bi bi-qr-code me-1"></i>QR Codes téléversement
|
|
</a>
|
|
</div>
|
|
|
|
<div class="row g-4">
|
|
|
|
<!-- Formulaire création -->
|
|
<div class="col-lg-4">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header fw-semibold"><i class="bi bi-person-plus-fill me-2"></i>Nouveau compte</div>
|
|
<div class="card-body">
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-danger py-2 small"><?= htmlspecialchars($error) ?></div>
|
|
<?php endif; ?>
|
|
<form method="post">
|
|
<input type="hidden" name="action" value="create">
|
|
<div class="mb-3">
|
|
<label class="form-label">Nom d'utilisateur</label>
|
|
<div class="input-group">
|
|
<input type="text" name="username" class="form-control" placeholder="copycaisse" pattern="[a-zA-Z0-9._-]+" required>
|
|
<span class="input-group-text text-muted small">@<?= htmlspecialchars($domain) ?></span>
|
|
</div>
|
|
<div class="form-text">Lettres, chiffres, . _ -</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Mot de passe IMAP/SMB</label>
|
|
<input type="password" name="password" class="form-control" required minlength="6">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Nom affiché <span class="text-muted">(optionnel)</span></label>
|
|
<input type="text" name="display_name" class="form-control" placeholder="Copy Caisse">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Webhook URL <span class="text-muted">(optionnel)</span></label>
|
|
<input type="url" name="webhook_url" class="form-control" placeholder="https://…/webhook">
|
|
<div class="form-text">POST JSON à chaque réception de mail.</div>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="bi bi-plus-circle me-1"></i>Créer le compte
|
|
</button>
|
|
</form>
|
|
<hr class="my-3">
|
|
<div class="small text-muted">
|
|
<i class="bi bi-info-circle me-1"></i>La création génère automatiquement :
|
|
<ul class="mt-1 mb-0">
|
|
<li>Boîte mail <code>username@<?= htmlspecialchars($domain) ?></code></li>
|
|
<li>Dossier <code>/srv/copymail/username/</code></li>
|
|
<li>Share SMB <code>\\serveur\username</code></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Liste des comptes -->
|
|
<div class="col-lg-8">
|
|
<div class="card shadow-sm">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<span><i class="bi bi-people-fill me-2"></i>Comptes existants</span>
|
|
<span class="badge bg-secondary"><?= count($accounts) ?></span>
|
|
</div>
|
|
<?php if (empty($accounts)): ?>
|
|
<div class="card-body text-muted">Aucun compte créé.</div>
|
|
<?php else: ?>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Compte</th>
|
|
<th>Email</th>
|
|
<th>Share SMB</th>
|
|
<th class="text-center">Mails</th>
|
|
<th class="text-center">Statut</th>
|
|
<th class="text-center">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($accounts as $acc): ?>
|
|
<tr>
|
|
<td>
|
|
<strong><?= htmlspecialchars($acc['username']) ?></strong>
|
|
<?php if ($acc['display_name']): ?>
|
|
<br><small class="text-muted"><?= htmlspecialchars($acc['display_name']) ?></small>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="small"><?= htmlspecialchars($acc['email']) ?></td>
|
|
<td class="small font-monospace">\\serveur\<?= htmlspecialchars($acc['username']) ?></td>
|
|
<td class="text-center">
|
|
<span class="badge bg-light text-dark"><?= $acc['mail_count'] ?></span>
|
|
</td>
|
|
<td class="text-center">
|
|
<form method="post" class="d-inline">
|
|
<input type="hidden" name="action" value="toggle">
|
|
<input type="hidden" name="account_id" value="<?= $acc['id'] ?>">
|
|
<button class="badge border-0 <?= $acc['active'] ? 'bg-success' : 'bg-secondary' ?>" title="Cliquer pour basculer">
|
|
<?= $acc['active'] ? 'actif' : 'inactif' ?>
|
|
</button>
|
|
</form>
|
|
</td>
|
|
<td class="text-center">
|
|
<div class="d-flex gap-1 justify-content-center">
|
|
<button class="btn btn-sm btn-outline-secondary" title="Modifier"
|
|
onclick="openEdit(<?= $acc['id'] ?>, '<?= htmlspecialchars(addslashes($acc['display_name'] ?? ''), ENT_QUOTES) ?>', '<?= htmlspecialchars(addslashes($acc['webhook_url'] ?? ''), ENT_QUOTES) ?>', '<?= htmlspecialchars($acc['username']) ?>')">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
<form method="post" onsubmit="return confirm('Supprimer le compte <?= htmlspecialchars($acc['username']) ?> et tous ses fichiers ?')">
|
|
<input type="hidden" name="action" value="delete">
|
|
<input type="hidden" name="account_id" value="<?= $acc['id'] ?>">
|
|
<button class="btn btn-sm btn-outline-danger" title="Supprimer">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Modal édition -->
|
|
<div class="modal fade" id="editModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<form method="post">
|
|
<input type="hidden" name="action" value="edit">
|
|
<input type="hidden" name="account_id" id="edit-id">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="bi bi-pencil me-2"></i>Modifier le compte <strong id="edit-username"></strong></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Nom affiché</label>
|
|
<input type="text" name="display_name" id="edit-display" class="form-control" placeholder="Nom affiché">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Webhook URL</label>
|
|
<input type="url" name="webhook_url" id="edit-webhook" class="form-control" placeholder="https://…/webhook">
|
|
<div class="form-text">POST JSON à chaque réception. Laisser vide pour désactiver.</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Nouveau mot de passe <span class="text-muted">(laisser vide pour ne pas changer)</span></label>
|
|
<input type="password" name="new_password" id="edit-password" class="form-control" minlength="6" placeholder="••••••">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Annuler</button>
|
|
<button type="submit" class="btn btn-primary"><i class="bi bi-floppy-fill me-1"></i>Enregistrer</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function openEdit(id, display, webhook, username) {
|
|
document.getElementById('edit-id').value = id;
|
|
document.getElementById('edit-username').textContent = username;
|
|
document.getElementById('edit-display').value = display;
|
|
document.getElementById('edit-webhook').value = webhook;
|
|
document.getElementById('edit-password').value = '';
|
|
new bootstrap.Modal(document.getElementById('editModal')).show();
|
|
}
|
|
</script>
|
|
<?php html_foot(); ?>
|