<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aya ait el hachmi</title>
    <description>The latest articles on DEV Community by Aya ait el hachmi (@ayapti_aya_b48682976bebf8).</description>
    <link>https://dev.to/ayapti_aya_b48682976bebf8</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3892050%2F4f796af7-b653-409d-88f9-475a3cc55275.png</url>
      <title>DEV Community: Aya ait el hachmi</title>
      <link>https://dev.to/ayapti_aya_b48682976bebf8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayapti_aya_b48682976bebf8"/>
    <language>en</language>
    <item>
      <title>Building a Secure PHP Authentication System — FashionMood Tutorial</title>
      <dc:creator>Aya ait el hachmi</dc:creator>
      <pubDate>Wed, 22 Apr 2026 08:50:35 +0000</pubDate>
      <link>https://dev.to/ayapti_aya_b48682976bebf8/building-a-secure-php-authentication-system-fashionmood-tutorial-d1k</link>
      <guid>https://dev.to/ayapti_aya_b48682976bebf8/building-a-secure-php-authentication-system-fashionmood-tutorial-d1k</guid>
      <description>&lt;p&gt;Your outfit, according to your mood&lt;br&gt;
Stack: PHP 8 + PDO | MySQL | Bootstrap 5 | JavaScript &lt;br&gt;
&lt;strong&gt;1. Introduction&lt;/strong&gt;&lt;br&gt;
FashionMood is a personalized fashion web app for women. The concept is simple: each user creates her own style universe by signing up, takes a style quiz, and receives outfit suggestions tailored to her profile and daily mood.&lt;br&gt;
This tutorial walks through the complete authentication system we built from scratch — covering database design, secure registration, login with sessions and cookies, page protection, the style quiz (JavaScript + PHP), and logout.&lt;/p&gt;

&lt;p&gt;🎯 What you will learn: Secure PHP authentication with PDO, password hashing, session management, cookie-based remember me, SQL injection prevention, and a JavaScript + PHP quiz system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Project Architecture&lt;/strong&gt;&lt;br&gt;
2.1 File Structure&lt;br&gt;
index.php      — Landing page (HTML/CSS/Bootstrap)&lt;br&gt;
register.php   — User registration with validation&lt;br&gt;
login.php      — Secure login with sessions and cookies&lt;br&gt;
quiz.php       — Style quiz (JavaScript + PHP)&lt;br&gt;
dashbord.php   — Personalized user dashboard&lt;br&gt;
logout.php     — Session and cookie destruction&lt;br&gt;
fashion.php    — PDO database connection&lt;br&gt;
2.2 Database — Table: users&lt;br&gt;
A single table stores all user information:&lt;br&gt;
ColumnTypeDescriptionidINTPrimary key, auto-incrementusernameVARCHAR(50)Unique usernameemailVARCHAR(100)Unique email addresspasswordVARCHAR(255)Bcrypt hashed passwordtelephoneVARCHAR(20)Phone number (+212 format)ageTINYINTAge between 13 and 60hijabTINYINT(1)0 = without hijab, 1 = with hijabstyle_resultVARCHAR(50)Style quiz result (NULL until quiz done)quiz_doneTINYINT(1)0 = not done, 1 = completed&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Step 1 — Database Connection (fashion.php)&lt;/strong&gt;&lt;br&gt;
We use PDO (PHP Data Objects) for a secure, object-oriented database connection. PDO supports prepared statements which prevent SQL injection attacks.&lt;br&gt;
php&amp;lt;?php&lt;br&gt;
$host   = "localhost";&lt;br&gt;
$dbname = "FashionMood";&lt;br&gt;
$user   = "root";&lt;br&gt;
$pass   = "";&lt;/p&gt;

&lt;p&gt;try {&lt;br&gt;
    $pdo = new PDO(&lt;br&gt;
        "mysql:host=$host;dbname=$dbname;charset=utf8",&lt;br&gt;
        $user, $pass&lt;br&gt;
    );&lt;br&gt;
    $pdo-&amp;gt;setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);&lt;br&gt;
} catch (PDOException $e) {&lt;br&gt;
    die("Erreur : " . $e-&amp;gt;getMessage());&lt;br&gt;
}&lt;br&gt;
?&amp;gt;&lt;/p&gt;

&lt;p&gt;💡 Why PDO? PDO supports prepared statements that protect against SQL injection. It also supports multiple database systems (MySQL, PostgreSQL, SQLite) with the same API, making your code more portable.&lt;/p&gt;

&lt;p&gt;Best practices applied:&lt;/p&gt;

&lt;p&gt;Always set charset=utf8 to support special characters&lt;br&gt;
Use ERRMODE_EXCEPTION so errors throw exceptions instead of silently failing&lt;br&gt;
Never display raw error messages in production — use a generic message instead&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Step 2 — User Registration (register.php)&lt;/strong&gt;&lt;br&gt;
The registration page collects user information, validates everything server-side, hashes the password, and inserts securely using PDO.&lt;br&gt;
4.1 Server-Side Validation&lt;br&gt;
We validate every field before touching the database:&lt;br&gt;
phpif (empty($username))&lt;br&gt;
    $errors["username"] = "Username is required.";&lt;/p&gt;

&lt;p&gt;if (empty($email))&lt;br&gt;
    $errors["email"] = "Email is required.";&lt;br&gt;
elseif (!filter_var($email, FILTER_VALIDATE_EMAIL))&lt;br&gt;
    $errors["email"] = "Invalid email.";&lt;/p&gt;

&lt;p&gt;if (!preg_match('/^+212\s?[67]\d{8}$/', $telephone))&lt;br&gt;
    $errors["telephone"] = "Invalid phone number.";&lt;/p&gt;

&lt;p&gt;if (strlen($password) &amp;lt; 8)&lt;br&gt;
    $errors["password"] = "Min 8 characters.";&lt;/p&gt;

&lt;p&gt;if ($password !== $confirm)&lt;br&gt;
    $errors["confirm"] = "Passwords do not match.";&lt;/p&gt;

&lt;p&gt;⚠️ Lesson learned: Client-side validation (HTML required, minlength) is not enough — users can bypass it using browser DevTools or by sending direct HTTP requests. Server-side validation in PHP is always mandatory.&lt;/p&gt;

&lt;p&gt;4.2 Password Hashing&lt;br&gt;
Passwords are NEVER stored in plain text. We use PHP's built-in password_hash() function:&lt;br&gt;
php// Hash the password with bcrypt (PASSWORD_DEFAULT)&lt;br&gt;
$hash = password_hash($password, PASSWORD_DEFAULT);&lt;/p&gt;

&lt;p&gt;// password_hash() automatically:&lt;br&gt;
// - Applies bcrypt algorithm&lt;br&gt;
// - Generates a unique random salt&lt;br&gt;
// - Includes the salt in the resulting hash string&lt;br&gt;
4.3 Secure Database Insert&lt;br&gt;
We use named PDO parameters and htmlspecialchars() to prevent both SQL injection and XSS attacks:&lt;br&gt;
php$sql = "INSERT INTO users"&lt;br&gt;
     . " (username, email, password, telephone, age, hijab, style_result, quiz_done)"&lt;br&gt;
     . " VALUES (:username, :email, :password, :telephone, :age, :hijab, :style_result, :quiz_done)";&lt;/p&gt;

&lt;p&gt;$stmt = $pdo-&amp;gt;prepare($sql);&lt;br&gt;
$stmt-&amp;gt;execute([&lt;br&gt;
    ":username"     =&amp;gt; htmlspecialchars($username),&lt;br&gt;
    ":email"        =&amp;gt; htmlspecialchars($email),&lt;br&gt;
    ":password"     =&amp;gt; $hash,&lt;br&gt;
    ":telephone"    =&amp;gt; htmlspecialchars($telephone),&lt;br&gt;
    ":age"          =&amp;gt; (int)$age,&lt;br&gt;
    ":hijab"        =&amp;gt; $hijab,&lt;br&gt;
    ":style_result" =&amp;gt; null,  // Will be set after quiz&lt;br&gt;
    ":quiz_done"    =&amp;gt; 0,     // 0 = quiz not done yet&lt;br&gt;
]);&lt;/p&gt;

&lt;p&gt;🔒 Security note: Never concatenate variables directly into SQL strings. Always use prepared statements with named parameters (:param) or question marks (?). This is the primary defense against SQL injection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Step 3 — Login System (login.php)&lt;/strong&gt;&lt;br&gt;
5.1 Credential Verification&lt;br&gt;
We fetch the user by email, then verify the password against its stored hash:&lt;br&gt;
php$stmt = $pdo-&amp;gt;prepare("SELECT * FROM users WHERE email = ?");&lt;br&gt;
$stmt-&amp;gt;execute([$email]);&lt;br&gt;
$user = $stmt-&amp;gt;fetch();&lt;/p&gt;

&lt;p&gt;if ($user &amp;amp;&amp;amp; password_verify($password, $user["password"])) {&lt;br&gt;
    // Login successful&lt;br&gt;
} else {&lt;br&gt;
    $errors["global"] = "Email or password incorrect.";&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;💡 Why password_verify()? This function securely compares a plain text password with a bcrypt hash. It automatically extracts and uses the salt embedded in the hash string — you never need to handle the salt manually.&lt;/p&gt;

&lt;p&gt;5.2 Session Management&lt;br&gt;
After successful login, we store the user's data in PHP session variables:&lt;br&gt;
phpsession_start();&lt;br&gt;
$_SESSION["id"]           = $user["id"];&lt;br&gt;
$_SESSION["username"]     = $user["username"];&lt;br&gt;
$_SESSION["email"]        = $user["email"];&lt;br&gt;
$_SESSION["telephone"]    = $user["telephone"];&lt;br&gt;
$_SESSION["age"]          = $user["age"];&lt;br&gt;
$_SESSION["hijab"]        = $user["hijab"];&lt;br&gt;
$_SESSION["style_result"] = $user["style_result"]; // Critical!&lt;/p&gt;

&lt;p&gt;⚠️ Lesson learned: We initially forgot to store style_result in the session during login. This caused the dashboard to show NULL even after the quiz was completed. The fix: always load all relevant user data into session at login time.&lt;/p&gt;

&lt;p&gt;5.3 Remember Me Feature&lt;br&gt;
We use a cookie to remember the user's email for 30 days:&lt;br&gt;
phpif (isset($_POST["remember"])) {&lt;br&gt;
    // Set cookie for 30 days&lt;br&gt;
    setcookie("remember_email", $email, time() + (30 * 24 * 60 * 60), "/");&lt;br&gt;
} else {&lt;br&gt;
    // Remove cookie if not checked&lt;br&gt;
    setcookie("remember_email", "", time() - 3600, "/");&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;⚠️ Bug we encountered: setcookie() was placed before the $email variable was defined, causing an "undefined variable" error. The fix: always place setcookie() inside the POST block, after $email has been assigned.&lt;/p&gt;

&lt;p&gt;5.4 Smart Redirect Based on quiz_done&lt;br&gt;
After login, we check whether the user has already completed the quiz:&lt;br&gt;
phpif ($user["quiz_done"] == 1) {&lt;br&gt;
    header("Location: dashbord.php"); // Already done — go to dashboard&lt;br&gt;
} else {&lt;br&gt;
    header("Location: quiz.php");     // First time — take the quiz&lt;br&gt;
}&lt;br&gt;
exit();&lt;/p&gt;

&lt;p&gt;💡 Why exit() after header()? PHP continues executing code after header() unless you explicitly stop it. Always call exit() immediately after a redirect to prevent unintended code execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Step 4 — Protecting Pages&lt;/strong&gt;&lt;br&gt;
Every protected page starts with the same security check:&lt;br&gt;
php&amp;lt;?php&lt;br&gt;
session_start();&lt;br&gt;
if (!isset($_SESSION["id"])) {&lt;br&gt;
    header("Location: login.php");&lt;br&gt;
    exit();&lt;br&gt;
}&lt;br&gt;
?&amp;gt;&lt;/p&gt;

&lt;p&gt;⚠️ Lesson learned: We initially used different session variable names across files — $_SESSION["id"] in some places and $_SESSION["user_id"] in others. This mismatch caused pages to incorrectly redirect logged-in users back to login. Fix: use the exact same session variable names across ALL files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Step 5 — Style Quiz (quiz.php)&lt;/strong&gt;&lt;br&gt;
The quiz combines JavaScript (client-side interactivity) and PHP (server-side processing).&lt;br&gt;
7.1 JavaScript — Interactive Questions&lt;br&gt;
JavaScript displays questions one at a time, collects answers, and calculates the winning style:&lt;br&gt;
javascriptlet current = 0;      // Current question index (0-3)&lt;br&gt;
const answers = [];   // Stores user answers&lt;/p&gt;

&lt;p&gt;function selectOpt(val, el) {&lt;br&gt;
    answers[current] = val;&lt;br&gt;
    document.querySelectorAll(".opt").forEach(o =&amp;gt; o.classList.remove("sel"));&lt;br&gt;
    el.classList.add("sel");&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// Calculate winner in JavaScript&lt;br&gt;
function showResult() {&lt;br&gt;
    const count = {};&lt;br&gt;
    answers.forEach(v =&amp;gt; count[v] = (count[v] || 0) + 1);&lt;br&gt;
    const winner = Object.entries(count).sort((a, b) =&amp;gt; b[1] - a[1])[0][0];&lt;br&gt;
    // Display result + hidden form to send to PHP&lt;br&gt;
}&lt;br&gt;
7.2 Hidden Inputs — The Bridge Between JS and PHP&lt;br&gt;
JavaScript stores answers in the browser. To send them to PHP, we use hidden form inputs:&lt;br&gt;
html&lt;/p&gt;
&lt;br&gt;
    &lt;br&gt;
    &lt;br&gt;
    &lt;br&gt;
    &lt;br&gt;
    Découvrir mes tenues →&lt;br&gt;
&lt;br&gt;
7.3 PHP — Validation and Saving&lt;br&gt;
PHP receives the POST data, validates it, calculates the winner, and saves to the database:&lt;br&gt;
php// Reset old result each time quiz is taken&lt;br&gt;
unset($_SESSION["style_result"]);

&lt;p&gt;// Validate — only accept known values&lt;br&gt;
$valeurs_autorisees = ["romantique", "elegante", "naturelle", "casual"];&lt;br&gt;
if (!in_array($Q1, $valeurs_autorisees) || ...) {&lt;br&gt;
    $erreur = "Réponse invalide";&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// Calculate winning style&lt;br&gt;
$votes  = [$Q1, $Q2, $Q3, $Q4];&lt;br&gt;
$count  = array_count_values($votes); // Count votes per style&lt;br&gt;
arsort($count);                       // Sort descending&lt;br&gt;
$winner = array_key_first($count);    // Get the top style&lt;/p&gt;

&lt;p&gt;// Save to session and database&lt;br&gt;
$_SESSION["style_result"] = $winner;&lt;br&gt;
$stmt = $pdo-&amp;gt;prepare(&lt;br&gt;
    "UPDATE users SET style_result = :style, quiz_done = 1 WHERE id = :id"&lt;br&gt;
);&lt;br&gt;
$stmt-&amp;gt;execute([":style" =&amp;gt; $winner, ":id" =&amp;gt; $_SESSION["id"]]);&lt;br&gt;
header("Location: dashbord.php");&lt;br&gt;
exit;&lt;/p&gt;

&lt;p&gt;⚠️ Bug we encountered: Clicking "Découvrir mes tenues" returned HTTP 405 error. The cause: quiz.php had no PHP POST handler. The fix: add the complete PHP POST processing block at the top of the file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Step 6 — Logout (logout.php)&lt;/strong&gt;&lt;br&gt;
Logout must destroy both the session AND the remember-me cookie:&lt;br&gt;
php&amp;lt;?php&lt;br&gt;
session_start();&lt;/p&gt;

&lt;p&gt;// Delete the remember me cookie&lt;br&gt;
setcookie("remember_email", "", time() - 3600, "/");&lt;/p&gt;

&lt;p&gt;// Destroy the session&lt;br&gt;
session_unset();&lt;br&gt;
session_destroy();&lt;/p&gt;

&lt;p&gt;header("Location: login.php");&lt;br&gt;
exit();&lt;br&gt;
?&amp;gt;&lt;/p&gt;

&lt;p&gt;💡 Why set the cookie time to the past? Setting a cookie's expiry time to time() - 3600 (one hour in the past) tells the browser to immediately delete it. This is the standard way to remove cookies in PHP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Best Practices &amp;amp; Lessons Learned&lt;/strong&gt;&lt;br&gt;
9.1 Security Checklist&lt;/p&gt;

&lt;p&gt;Never store passwords in plain text — always use password_hash()&lt;br&gt;
Always use PDO prepared statements — never concatenate SQL strings&lt;br&gt;
Validate ALL data server-side (PHP), not just client-side (HTML/JS)&lt;br&gt;
Always call exit() immediately after header("Location: ...")&lt;br&gt;
Use htmlspecialchars() when displaying user data to prevent XSS&lt;br&gt;
Use in_array() to validate form values against a whitelist&lt;/p&gt;

&lt;p&gt;9.2 Session Best Practices&lt;/p&gt;

&lt;p&gt;Call session_start() before any HTML output&lt;br&gt;
Use consistent session variable names across ALL files — mismatches cause silent bugs&lt;br&gt;
Always destroy both the session AND the cookie on logout&lt;br&gt;
Load all necessary user data into session at login time&lt;/p&gt;

&lt;p&gt;9.3 The 4 Real Bugs We Fixed&lt;/p&gt;

&lt;h1&gt;
  
  
  BugSolution1HTTP 405 on quiz submit buttonAdded complete PHP POST handler to quiz.php2style_result always NULL in databaseAdded $_SESSION["style_result"] to login.php after successful auth3Session variable mismatch (id vs user_id)Standardized to $_SESSION["id"] across all files4Remember Me — $email undefined errorMoved setcookie() inside the POST block, after $email is defined
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;10. Conclusion&lt;/strong&gt;&lt;br&gt;
FashionMood implements a complete, production-ready authentication system using PHP and MySQL. The features covered in this tutorial include:&lt;/p&gt;

&lt;p&gt;✅ Secure registration with server-side validation&lt;br&gt;
✅ Bcrypt password hashing&lt;br&gt;
✅ PDO prepared statements&lt;br&gt;
✅ PHP sessions&lt;br&gt;
✅ Cookie-based remember me&lt;br&gt;
✅ Smart redirects&lt;br&gt;
✅ Page protection&lt;br&gt;
✅ JavaScript + PHP quiz system&lt;/p&gt;

&lt;p&gt;The bugs we encountered — and fixed — taught us more than the code itself. Real debugging experience is irreplaceable.&lt;/p&gt;

&lt;p&gt;🔒 Security: password_hash, PDO | 📦 Sessions: $_SESSION, cookies | 🗄️ Database: PDO, MySQL | ⚡ JavaScript: Quiz + hidden inputs&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
