Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file added .vs/SAE/v17/.wsuo
Binary file not shown.
Binary file added .vs/slnx.sqlite
Binary file not shown.
48 changes: 24 additions & 24 deletions App/Controller/DashboardApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function __construct()
// -------------------------------------------------------------------------

/**
* Return a paginated list of unique student identifiers (from attempts.user).
* Return a paginated list of unique student identifiers (from attempts.user_id).
* Optionally filtered by resource_id (via exercice → ressource join).
*
* @return void
Expand All @@ -55,16 +55,16 @@ public function students(): void

if ($resourceId !== null) {
$stmt = $pdo->prepare(
"SELECT DISTINCT a.user
"SELECT DISTINCT a.user_id
FROM attempts a
INNER JOIN exercices e ON a.exercice_id = e.exercice_id
WHERE e.ressource_id = :rid
ORDER BY a.user ASC"
ORDER BY a.user_id ASC"
);
$stmt->execute(['rid' => $resourceId]);
} else {
$stmt = $pdo->query(
"SELECT DISTINCT user FROM attempts ORDER BY user ASC"
"SELECT DISTINCT user_id FROM attempts ORDER BY user_id ASC"
);
}

Expand Down Expand Up @@ -96,7 +96,7 @@ public function students(): void
/**
* Return details, attempts and stats for a given student identifier.
*
* @param string $identifier Student identifier (value of attempts.user)
* @param string $identifier Student identifier (value of attempts.user_id)
* @return void
*/
public function student(string $identifier): void
Expand All @@ -115,19 +115,19 @@ public function student(string $identifier): void
"SELECT a.*, e.exercice_name, e.ressource_id
FROM attempts a
INNER JOIN exercices e ON a.exercice_id = e.exercice_id
WHERE a.user = :user AND e.ressource_id = :rid
WHERE a.user_id = :user_id AND e.ressource_id = :rid
ORDER BY a.attempt_id DESC"
);
$stmt->execute(['user' => $identifier, 'rid' => $resourceId]);
$stmt->execute(['user_id' => $identifier, 'rid' => $resourceId]);
} else {
$stmt = $pdo->prepare(
"SELECT a.*, e.exercice_name
FROM attempts a
LEFT JOIN exercices e ON a.exercice_id = e.exercice_id
WHERE a.user = :user
WHERE a.user_id = :user_id
ORDER BY a.attempt_id DESC"
);
$stmt->execute(['user' => $identifier]);
$stmt->execute(['user_id' => $identifier]);
}

$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
Expand Down Expand Up @@ -195,7 +195,7 @@ public function exercises(): void
"SELECT e.exercice_id, e.ressource_id, e.exercice_name, e.extention, e.`date`,
COALESCE(c.funcname, e.exercice_name) AS display_name
FROM exercices e
LEFT JOIN corrections c ON e.exercice_name = c.exo_name
LEFT JOIN corrections c ON e.exercice_name = c.exercice_name
WHERE e.exercice_id = :eid"
);
$stmt->execute(['eid' => $exerciseId]);
Expand All @@ -208,17 +208,17 @@ public function exercises(): void

// Fetch all attempts for this exercise grouped by student
$stmt2 = $pdo->prepare(
"SELECT user,
"SELECT user_id,
COUNT(*) AS total,
SUM(CASE WHEN correct=1 THEN 1 ELSE 0 END) AS correct_count
FROM attempts WHERE exercice_id = :eid GROUP BY user ORDER BY user ASC"
FROM attempts WHERE exercice_id = :eid GROUP BY user_id ORDER BY user_id ASC"
);
$stmt2->execute(['eid' => $exerciseId]);
$studentRows = $stmt2->fetchAll(\PDO::FETCH_ASSOC);

$students = array_map(fn($r) => [
'id' => $r['user'],
'identifier' => $r['user'],
'id' => $r['user_id'],
'identifier' => $r['user_id'],
'total_attempts' => (int)$r['total'],
'correct_count' => (int)$r['correct_count'],
'success_rate' => (int)$r['total'] > 0
Expand Down Expand Up @@ -247,7 +247,7 @@ public function exercises(): void
COUNT(a.attempt_id) AS total_attempts,
SUM(CASE WHEN a.correct = 1 THEN 1 ELSE 0 END) AS successful_attempts
FROM exercices e
LEFT JOIN corrections c ON e.exercice_name = c.exo_name
LEFT JOIN corrections c ON e.exercice_name = c.exercice_name
LEFT JOIN attempts a ON e.exercice_id = a.exercice_id
WHERE e.ressource_id = :rid
GROUP BY e.exercice_id
Expand All @@ -261,7 +261,7 @@ public function exercises(): void
COUNT(a.attempt_id) AS total_attempts,
SUM(CASE WHEN a.correct = 1 THEN 1 ELSE 0 END) AS successful_attempts
FROM exercices e
LEFT JOIN corrections c ON e.exercice_name = c.exo_name
LEFT JOIN corrections c ON e.exercice_name = c.exercice_name
LEFT JOIN attempts a ON e.exercice_id = a.exercice_id
GROUP BY e.exercice_id
ORDER BY display_name ASC"
Expand Down Expand Up @@ -312,31 +312,31 @@ public function studentsStats(): void

if ($resourceId !== null) {
$stmt = $pdo->prepare(
"SELECT a.user,
"SELECT a.user_id,
COUNT(a.attempt_id) AS total_attempts,
SUM(CASE WHEN a.correct = 1 THEN 1 ELSE 0 END) AS correct_attempts
FROM attempts a
INNER JOIN exercices e ON a.exercice_id = e.exercice_id
WHERE e.ressource_id = :rid
GROUP BY a.user
ORDER BY a.user ASC"
GROUP BY a.user_id
ORDER BY a.user_id ASC"
);
$stmt->execute(['rid' => $resourceId]);
} else {
$stmt = $pdo->query(
"SELECT a.user,
"SELECT a.user_id,
COUNT(a.attempt_id) AS total_attempts,
SUM(CASE WHEN a.correct = 1 THEN 1 ELSE 0 END) AS correct_attempts
FROM attempts a
GROUP BY a.user
ORDER BY a.user ASC"
GROUP BY a.user_id
ORDER BY a.user_id ASC"
);
}

$rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$stats = array_map(fn($r) => [
'student_id' => $r['user'],
'identifier' => $r['user'],
'student_id' => $r['user_id'],
'identifier' => $r['user_id'],
'total_attempts' => (int) $r['total_attempts'],
'correct_attempts' => (int) $r['correct_attempts'],
'success_rate' => (int)$r['total_attempts'] > 0
Expand Down
43 changes: 39 additions & 4 deletions App/Controller/ForgotPasswordController.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function resetForm(): void

$user = $this->userRepository->findByResetToken($token);

if (!$user || $user->getResetTokenExpiration() < date('Y-m-d H:i:s')) {
if (!$user || !$user->isResetTokenValid()) {
$this->renderView('auth/forgot-password', [
'error_message' => 'Ce lien est invalide ou a expiré.',
]);
Expand Down Expand Up @@ -133,17 +133,52 @@ public function reset(): void
return;
}

if (strlen($newPassword) < 6) {
if (!$this->isPasswordValid($newPassword)) {
$this->renderView('auth/reset-password', [
'token' => $token,
'error_message' => 'Le mot de passe doit contenir au moins 6 caractères.',
'error_message' => 'Le mot de passe doit contenir au moins 12 caractères, une majuscule, une minuscule et un caractère spécial.',
]);
return;
}

$user->changePassword($newPassword);
$this->userRepository->save($user);

$this->redirect('/auth/login');
$this->renderView('auth/login', [
'success_message' => 'Votre mot de passe a été réinitialisé avec succès. Vous pouvez vous connecter.',
]);
Comment on lines +147 to +149

Copilot AI Mar 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Après un POST de réinitialisation, le contrôleur rend directement la vue de login au lieu de rediriger. Cela casse le pattern PRG et expose à une resoumission du POST au refresh (et potentiellement à des messages incohérents). Préférer un redirect vers /auth/login avec un mécanisme de message flash (session) ou un query param, puis afficher le message sur la page de login.

Suggested change
$this->renderView('auth/login', [
'success_message' => 'Votre mot de passe a été réinitialisé avec succès. Vous pouvez vous connecter.',
]);
$message = urlencode('Votre mot de passe a été réinitialisé avec succès. Vous pouvez vous connecter.');
$this->redirect('/auth/login?success_message=' . $message);
return;

Copilot uses AI. Check for mistakes.
}

/**
* Validate password strength.
*
* A valid password must:
* - Be at least 12 characters long
* - Contain at least one uppercase letter
* - Contain at least one lowercase letter
* - Contain at least one special character
*
* @param string $password Password to validate.
* @return bool True if the password meets all requirements.
*/
private function isPasswordValid(string $password): bool
{
if (strlen($password) < 12) {
return false;
}

if (!preg_match('/[A-Z]/', $password)) {
return false;
}

if (!preg_match('/[a-z]/', $password)) {
return false;
}

if (!preg_match('/[\W_]/', $password)) {
return false;
}

return true;
}
}
2 changes: 2 additions & 0 deletions App/Controller/ImportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function __construct()
public function exercises(): void
{
ob_start();
header('Content-Type: application/json; charset=utf-8');

set_time_limit(300);
ini_set('memory_limit', '512M');
Expand Down Expand Up @@ -149,6 +150,7 @@ public function exercises(): void
public function attempts(): void
{
ob_start();
header('Content-Type: application/json; charset=utf-8');

set_time_limit(300);
ini_set('memory_limit', '512M');
Expand Down
6 changes: 4 additions & 2 deletions App/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
use Core\Service\SessionService;

/**
* Login Controller
* Handles user login
* Login Controller.
*
* Handles user login. Wires the LoginUserUseCase with a concrete
* UserRepository (which implements UserAuthFinderPort).
*/
class LoginController extends AbstractController
{
Expand Down
Loading