mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
admin: replace header 'Ajouter un TFE' nav link with toolbar button
This commit is contained in:
197
src/ShareLink.php
Normal file
197
src/ShareLink.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
/**
|
||||
* ShareLink — model for student-access share links.
|
||||
*
|
||||
* Share links enable students to submit TFEs via unique URLs without
|
||||
* requiring admin authentication. Each link has a unique slug, optional
|
||||
* password, activity flag, optional expiration, and usage count.
|
||||
*/
|
||||
class ShareLink
|
||||
{
|
||||
private Database $db;
|
||||
|
||||
public function __construct(Database $db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
public static function make(): self
|
||||
{
|
||||
require_once APP_ROOT . '/src/Database.php';
|
||||
return new self(new Database());
|
||||
}
|
||||
|
||||
// ── Slug generation ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generate a unique slug in the format YYYYMMDD-<random>.
|
||||
* The random portion uses 8 base32 chars (~40 bits of entropy).
|
||||
*/
|
||||
public static function generateSlug(): string
|
||||
{
|
||||
$date = date('Ymd');
|
||||
$random = substr(strtoupper(rtrim(base64_encode(random_bytes(7)), '=')), 0, 8);
|
||||
return $date . '-' . $random;
|
||||
}
|
||||
|
||||
// ── CRUD ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Create a new share link.
|
||||
*
|
||||
* @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 The created link row
|
||||
*/
|
||||
public function create(int $createdBy, ?string $password = null, ?string $expiresAt = null): array
|
||||
{
|
||||
$slug = self::generateSlug();
|
||||
$passwordHash = $password !== null ? password_hash($password, PASSWORD_BCRYPT) : null;
|
||||
|
||||
$stmt = $this->db->getConnection()->prepare(
|
||||
"INSERT INTO share_links (slug, password_hash, is_active, created_by, expires_at)
|
||||
VALUES (?, ?, 1, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$slug, $passwordHash, $createdBy, $expiresAt]);
|
||||
|
||||
return $this->findBySlug($slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a share link by its slug.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function findBySlug(string $slug): ?array
|
||||
{
|
||||
$stmt = $this->db->getConnection()->prepare(
|
||||
"SELECT * FROM share_links WHERE slug = ?"
|
||||
);
|
||||
$stmt->execute([$slug]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a share link by its ID.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
$stmt = $this->db->getConnection()->prepare(
|
||||
"SELECT * FROM share_links WHERE id = ?"
|
||||
);
|
||||
$stmt->execute([$id]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all share links, ordered by creation date descending.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function listAll(): array
|
||||
{
|
||||
$stmt = $this->db->getConnection()->query(
|
||||
"SELECT * FROM share_links ORDER BY created_at DESC"
|
||||
);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the active status of a share link.
|
||||
*/
|
||||
public function toggleActive(int $id): void
|
||||
{
|
||||
$this->db->getConnection()->prepare(
|
||||
"UPDATE share_links SET is_active = NOT is_active WHERE id = ?"
|
||||
)->execute([$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or clear the password for a share link.
|
||||
*
|
||||
* @param string|null $password Plain-text password, or null to clear
|
||||
*/
|
||||
public function setPassword(int $id, ?string $password): void
|
||||
{
|
||||
$hash = $password !== null ? password_hash($password, PASSWORD_BCRYPT) : null;
|
||||
$this->db->getConnection()->prepare(
|
||||
"UPDATE share_links SET password_hash = ? WHERE id = ?"
|
||||
)->execute([$hash, $id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a share link.
|
||||
*/
|
||||
public function delete(int $id): void
|
||||
{
|
||||
$this->db->getConnection()->prepare(
|
||||
"DELETE FROM share_links WHERE id = ?"
|
||||
)->execute([$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the usage count for a share link.
|
||||
*/
|
||||
public function incrementUsage(int $id): void
|
||||
{
|
||||
$this->db->getConnection()->prepare(
|
||||
"UPDATE share_links SET usage_count = usage_count + 1 WHERE id = ?"
|
||||
)->execute([$id]);
|
||||
}
|
||||
|
||||
// ── Validation ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Validate whether a share link is usable.
|
||||
*
|
||||
* Returns an array:
|
||||
* ['valid' => true] if the link is active and not expired
|
||||
* ['valid' => false, 'reason' => 'disabled'] if deactivated
|
||||
* ['valid' => false, 'reason' => 'expired'] if past expiration
|
||||
* ['valid' => false, 'reason' => 'not_found'] if slug doesn't exist
|
||||
* ['valid' => false, 'reason' => 'needs_password', 'link' => array] if password required
|
||||
*/
|
||||
public function validateLink(?string $slug): array
|
||||
{
|
||||
if ($slug === null || $slug === '') {
|
||||
return ['valid' => false, 'reason' => 'not_found'];
|
||||
}
|
||||
|
||||
$link = $this->findBySlug($slug);
|
||||
if ($link === null) {
|
||||
return ['valid' => false, 'reason' => 'not_found'];
|
||||
}
|
||||
|
||||
if (!$link['is_active']) {
|
||||
return ['valid' => false, 'reason' => 'disabled', 'link' => $link];
|
||||
}
|
||||
|
||||
if ($link['expires_at'] !== null && strtotime($link['expires_at']) < time()) {
|
||||
return ['valid' => false, 'reason' => 'expired', 'link' => $link];
|
||||
}
|
||||
|
||||
if ($link['password_hash'] !== null) {
|
||||
return ['valid' => false, 'reason' => 'needs_password', 'link' => $link];
|
||||
}
|
||||
|
||||
return ['valid' => true, 'link' => $link];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the password against a share link.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function verifyPassword(array $link, string $password): bool
|
||||
{
|
||||
if ($link['password_hash'] === null) {
|
||||
return true; // No password set
|
||||
}
|
||||
return password_verify($password, $link['password_hash']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user