mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
feat: mandatory auto-generated passwords for share links + admin password copy/regeneration + password gate rate limiting
This commit is contained in:
@@ -41,18 +41,29 @@ class ShareLink
|
||||
|
||||
// ── CRUD ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate a cryptographically secure random password.
|
||||
* Returns a 16-byte hex string (32 characters).
|
||||
*/
|
||||
public static function generatePassword(): string
|
||||
{
|
||||
return bin2hex(random_bytes(16));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new share link.
|
||||
*
|
||||
* Password is always auto-generated — no custom passwords allowed.
|
||||
*
|
||||
* @param int $createdBy Admin user ID
|
||||
* @param string|null $password Plain-text password (will be hashed), null = no password
|
||||
* @param string|null $expiresAt ISO-8601 expiration date, null = never expires
|
||||
* @return array|null The created link row
|
||||
* @return array|null The created link row with _plain_password attached
|
||||
*/
|
||||
public function create(int $createdBy, ?string $password = null, ?string $expiresAt = null, ?string $objetRestriction = null, ?string $name = null): ?array
|
||||
public function create(int $createdBy, ?string $expiresAt = null, ?string $objetRestriction = null, ?string $name = null): ?array
|
||||
{
|
||||
$slug = self::generateSlug();
|
||||
$passwordHash = $password !== null ? password_hash($password, PASSWORD_BCRYPT) : null;
|
||||
$plainPassword = self::generatePassword();
|
||||
$passwordHash = password_hash($plainPassword, PASSWORD_BCRYPT);
|
||||
$validObjet = ['tfe', 'thèse', 'frart'];
|
||||
if ($objetRestriction !== null && $objetRestriction !== '') {
|
||||
$parts = array_intersect(explode(',', $objetRestriction), $validObjet);
|
||||
@@ -62,12 +73,17 @@ class ShareLink
|
||||
}
|
||||
|
||||
$stmt = $this->db->getConnection()->prepare(
|
||||
'INSERT INTO share_links (slug, name, objet_restriction, password_hash, is_active, created_by, expires_at)
|
||||
VALUES (?, ?, ?, ?, 1, ?, ?)'
|
||||
'INSERT INTO share_links (slug, name, objet_restriction, password_hash, encrypted_password, is_active, created_by, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?, 1, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$slug, $name, $objetRestriction, $passwordHash, $createdBy, $expiresAt]);
|
||||
$stmt->execute([$slug, $name, $objetRestriction, $passwordHash, Crypto::encrypt($plainPassword), $createdBy, $expiresAt]);
|
||||
|
||||
return $this->findBySlug($slug);
|
||||
$link = $this->findBySlug($slug);
|
||||
if ($link) {
|
||||
// Attach plain-text password temporarily so the caller can display it
|
||||
$link['_plain_password'] = $plainPassword;
|
||||
}
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,15 +116,44 @@ class ShareLink
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt and return the plain-text password for a link.
|
||||
* Returns empty string if not set or decryption fails.
|
||||
*/
|
||||
public function getDecryptedPassword(int $id): string
|
||||
{
|
||||
$stmt = $this->db->getConnection()->prepare(
|
||||
'SELECT encrypted_password FROM share_links WHERE id = ?'
|
||||
);
|
||||
$stmt->execute([$id]);
|
||||
$enc = $stmt->fetchColumn();
|
||||
return $enc ? Crypto::decrypt($enc) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* List active (non-archived) share links, ordered by creation date descending.
|
||||
* Decorates each row with its decrypted password.
|
||||
*/
|
||||
public function listActive(): array
|
||||
{
|
||||
$stmt = $this->db->getConnection()->query(
|
||||
'SELECT * FROM share_links WHERE is_archived = 0 ORDER BY created_at DESC'
|
||||
);
|
||||
return $stmt->fetchAll();
|
||||
$rows = $stmt->fetchAll();
|
||||
return array_map(fn($row) => $this->decorateWithPassword($row), $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorate a link row with its decrypted password.
|
||||
*/
|
||||
private function decorateWithPassword(array $row): array
|
||||
{
|
||||
if (!empty($row['encrypted_password'])) {
|
||||
$row['_decrypted_password'] = Crypto::decrypt($row['encrypted_password']);
|
||||
} else {
|
||||
$row['_decrypted_password'] = '';
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,9 +231,9 @@ class ShareLink
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a share link (name, password, expiration).
|
||||
* Update a share link (name, expiration).
|
||||
*/
|
||||
public function update(int $id, ?string $name = null, ?string $password = null, ?string $expiresAt = null): void
|
||||
public function update(int $id, ?string $name = null, ?string $expiresAt = null): void
|
||||
{
|
||||
$pdo = $this->db->getConnection();
|
||||
$fields = [];
|
||||
@@ -198,10 +243,6 @@ class ShareLink
|
||||
$fields[] = 'name = ?';
|
||||
$params[] = $name;
|
||||
}
|
||||
if ($password !== null) {
|
||||
$fields[] = 'password_hash = ?';
|
||||
$params[] = $password !== '' ? password_hash($password, PASSWORD_BCRYPT) : null;
|
||||
}
|
||||
if ($expiresAt !== null) {
|
||||
$expiresAtVal = $expiresAt !== '' ? date('Y-m-d H:i:s', strtotime($expiresAt)) : null;
|
||||
$fields[] = 'expires_at = ?';
|
||||
|
||||
Reference in New Issue
Block a user