Express: ro'yxatdan o'tish va parol hash

Sessiya darsida server foydalanuvchini eslab qolishni o’rgandi. Lekin hali eslab qoladigan odam yo’q: hisob yaratish imkoniyati yo’q.

Bu darsda foydalanuvchi ro’yxatdan o’tadi. Darsning markazida bitta qoida turadi va u boshqa hammasidan muhim: parolni hech qachon ochiq saqlamang.

Nega ochiq parol falokat

Tasavvur qiling, users jadvalida parollar shundoq yozilgan. Bazaga kirish huquqi bo’lgan har kim ularni o’qiydi: siz, hamkasbingiz, zaxira nusxani ko’rgan har kim va bazani sizib chiqargan hujumchi.

Zarar bitta sayt bilan tugamaydi. Ko’p odam bir xil parolni pochta, bank va ijtimoiy tarmoqda ishlatadi. Sizning bazangiz sizib chiqsa, ularning butun raqamli hayoti ochiladi.

Hash va salt

bcrypt ikkalasini ham o’zi bajaradi — salt yaratadi, hash qiladi va ikkalasini bitta qatorga jamlaydi:

npm install bcrypt
const bcrypt = require("bcrypt");

const first = bcrypt.hashSync("parol12345", 10);
const second = bcrypt.hashSync("parol12345", 10);

console.log(first);
console.log(second);
console.log("Teng?", first === second);
console.log("To'g'ri parol:", bcrypt.compareSync("parol12345", first));
console.log("Xato parol:", bcrypt.compareSync("parol123", first));
node bcrypt-demo.js
$2b$10$B1IVEZQy6PW7xidn7SHdxu9EiugddTdZtKYPm46Wn5uE9WdNVVPeu
$2b$10$K4MIXOrKx.DviS9y7MkH9.slWzjmeMYPvrdXPytVNFU9vpCmsG/xe
Teng? false
To'g'ri parol: true
Xato parol: false

Ikki natija butunlay har xil — salt tasodifiy edi. Shunga qaramay compareSync ikkalasida ham to’g’ri javob beradi, chunki salt hash qatorining ichida saqlangan.

Qatorni qismlarga bo’lib qarang:

Qism Ma’nosi
$2b$ Algoritm belgisi (bcrypt)
10$ Murakkablik darajasi
Keyingi 22 belgi Salt
Qolgani Hash natijasi

Butun qator 60 belgi — jadval ustunini shunga mo’ljallab olasiz.

users jadvali

Migratsiya darsida qurgan tizimga yangi fayl qo’shamiz. migrations/003-create-users.js:

// migrations/003-create-users.js
module.exports = {
  name: "003-create-users",
  up(database) {
    database.exec(`
      CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        email TEXT NOT NULL UNIQUE,
        password_hash TEXT NOT NULL,
        created_at TEXT NOT NULL DEFAULT (datetime('now'))
      )
    `);
  },
};
npm run migrate
O'tkazib yuborildi: 001-create-tasks
O'tkazib yuborildi: 002-add-priority
Bajarildi: 003-create-users
Jami yangi migratsiya: 1

Ikki tafsilot ataylab tanlangan. Ustun nomi password emas, password_hash — kod o’qigan har kim u yerda ochiq parol emasligini darhol tushunadi. email ustunida esa UNIQUE cheklovi bor: baza darajasida bir email ikki marta yozilmaydi.

Foydalanuvchi modeli

models/user.js:

// models/user.js
const bcrypt = require("bcrypt");
const database = require("../db");

const SALT_ROUNDS = 10;

// Javobga password_hash tushmasligi uchun alohida shakl
function toUser(row) {
  return {
    id: row.id,
    email: row.email,
    createdAt: row.created_at,
  };
}

function findByEmail(email) {
  const row = database
    .prepare("SELECT * FROM users WHERE email = ?")
    .get(email.toLowerCase());

  return row || null;
}

async function create(email, password) {
  const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);

  const result = database
    .prepare("INSERT INTO users (email, password_hash) VALUES (?, ?)")
    .run(email.toLowerCase(), passwordHash);

  const row = database
    .prepare("SELECT * FROM users WHERE id = ?")
    .get(result.lastInsertRowid);

  return toUser(row);
}

module.exports = { findByEmail, create, toUser };

Uch qaror izoh talab qiladi.

toUser password_hash ni tashlab ketadi. Hash — ichki ma’lumot, JSON javobda unga o’rin yo’q. Tasodifan chiqib ketmasligi uchun uni model darajasida kesib tashlash eng ishonchli yo’l.

email.toLowerCase() — email katta-kichik harfga sezgir emas. Aziz@example.com va aziz@example.com bir xil odam. Saqlashda ham, qidirishda ham kichik harfga keltiriladi.

findByEmail to’liq qatorni qaytaradi, toUser dan o’tkazmaydi — keyingi darsda parolni solishtirish uchun password_hash kerak bo’ladi.

Ro’yxatdan o’tish marshruti

routes/auth.js:

const express = require("express");
const User = require("../models/user");

const router = express.Router();

// Email va parol shaklini tekshiradi
function validateCredentials(body) {
  const errors = [];
  const email = typeof body.email === "string" ? body.email.trim() : "";
  const password = typeof body.password === "string" ? body.password : "";

  if (!email.includes("@") || email.length < 5) {
    errors.push({ field: "email", message: "Email manzili noto'g'ri" });
  }

  if (password.length < 8) {
    errors.push({ field: "password", message: "Parol kamida 8 belgi bo'lsin" });
  }

  return errors;
}

router.post("/register", async (request, response, next) => {
  try {
    const errors = validateCredentials(request.body);

    if (errors.length > 0) {
      return response.status(400).json({ errors: errors });
    }

    const email = request.body.email.trim();

    // Bu email bilan hisob bormi
    if (User.findByEmail(email)) {
      return response.status(409).json({
        errors: [
          { field: "email", message: "Bu email allaqachon ro'yxatdan o'tgan" },
        ],
      });
    }

    const user = await User.create(email, request.body.password);
    response.status(201).json(user);
  } catch (error) {
    next(error);
  }
});

module.exports = router;

Tekshiruv usuli forma va validatsiya darsidagi qolipda: xatolar massivga yig’iladi, har biri field va message bilan.

try/catch va next(error) bu yerda majburiy — xatolarni boshqarish darsida ko’rilganidek, Express 4 da async marshrutdagi xato o’z-o’zidan ushlanmaydi.

app.js ga ulaymiz:

const authRouter = require("./routes/auth");

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/api/tasks", tasksRouter);
app.use("/", authRouter);

Sinash

curl -i -X POST http://localhost:3000/register -H "Content-Type: application/json" -d '{"email":"nodira@example.com","password":"kuchli-parol-123"}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8

{"id":2,"email":"nodira@example.com","createdAt":"2026-09-12 14:09:21"}

Javobda password_hash yo’q — toUser uni kesib tashladi.

Xuddi shu so’rovni qayta yuboring:

HTTP/1.1 409 Conflict
Content-Type: application/json; charset=utf-8

{"errors":[{"field":"email","message":"Bu email allaqachon ro'yxatdan o'tgan"}]}

Endi katta harflar bilan:

curl -X POST http://localhost:3000/register -H "Content-Type: application/json" -d '{"email":"NODIRA@Example.com","password":"kuchli-parol-123"}'
{"errors":[{"field":"email","message":"Bu email allaqachon ro'yxatdan o'tgan"}]}

toLowerCase ishladi: katta harf bilan yozilgan email yangi hisob yaratmadi.

Noto’g’ri ma’lumot bilan:

curl -i -X POST http://localhost:3000/register -H "Content-Type: application/json" -d '{"email":"salom","password":"123"}'
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8

{"errors":[{"field":"email","message":"Email manzili noto'g'ri"},{"field":"password","message":"Parol kamida 8 belgi bo'lsin"}]}

Ikkala xato bir yo’la qaytdi — foydalanuvchi ikki marta urinmaydi.

Ikki tekshiruv: kodda va bazada

Diqqat qilgan bo’lsangiz, takroriy email ikki joyda to’siladi: marshrutdagi findByEmail va jadvaldagi UNIQUE cheklovi.

Takrorlanish emas, bu. Ikkisi har xil ish qiladi:

To’siq Nima beradi
findByEmail tekshiruvi Foydalanuvchiga tushunarli xabar
UNIQUE cheklovi Kafolat: baza hech qachon ikki xil yozuvga ruxsat bermaydi

Kodli tekshiruv yolg’iz yetarli emas: ikki so’rov bir vaqtda kelsa, ikkalasi ham “email bo’sh” deb topib, ikkalasi ham yozishga urinadi. Baza cheklovi shu paytda ikkinchisini to’xtatadi — va u catch orqali xato middleware’iga boradi.

Ro’yxatdan o’tish formasi

Brauzerdan sinash uchun sahifa. views/register.ejs:

<%- include("partials/header") %>

<h1>Ro'yxatdan o'tish</h1>

<form action="/register" method="POST">
  <p>
    <label for="email">Email:</label>
    <input type="email" id="email" name="email" required />
  </p>
  <p>
    <label for="password">Parol:</label>
    <input type="password" id="password" name="password" minlength="8" required />
  </p>
  <button type="submit">Ro'yxatdan o'tish</button>
</form>

<%- include("partials/footer") %>
app.get("/register", (request, response) => {
  response.render("register", { pageTitle: "Ro'yxatdan o'tish" });
});

type="password" matnni yulduzchalar bilan yashiradi. required va minlength esa foydalanuvchiga qulaylik beradi — lekin validatsiya darsidagi qoida kuchida qoladi: brauzerdagi tekshiruv himoya emas, server tekshiruvi majburiy.

Amaliy mashq

task-app loyihasida davom eting:

  1. bcrypt ni o’rnatib, bir xil parolni ikki marta hash qiling va natijalar har xil chiqishini ko’ring;
  2. 003-create-users.js migratsiyasini yozib, npm run migrate bilan jadval yarating;
  3. models/user.js da create va findByEmail ni yozing;
  4. POST /register marshrutini ulab, bitta hisob yarating va javobda password_hash yo’qligini tasdiqlang;
  5. O’sha emailni qayta yuborib 409, so’ng katta harflar bilan yuborib ham 409 olishingizni tekshiring;
  6. Bazaga node -e "..." bilan qarang va password_hash ustunida $2b$10$ bilan boshlanadigan qator turganini ko’ring.

6-qadam eng muhimi: bazaga o’z ko’zingiz bilan qarab, parol ochiq emasligini tasdiqlash — bu odatni har loyihada takrorlang.

Qisqacha xulosa

  • Parol hech qachon ochiq saqlanmaydi; md5 va sha1 ham yaramaydi, chunki juda tez.
  • bcrypt ataylab sekin ishlaydi va salt’ni o’zi yaratadi.
  • Salt tufayli bir xil paroldan har safar boshqa hash chiqadi, lekin compare baribir to’g’ri javob beradi.
  • Ustun nomi password_hash deb ataladi va 60 belgilik qatorni saqlaydi.
  • bcrypt.hashasync; await unutilsa bazaga [object Promise] tushadi.
  • Email toLowerCase() bilan saqlanadi va qidiriladi.
  • Takroriy email ikki qavat to’siladi: kodda tushunarli 409 xabari, bazada UNIQUE kafolati.

Keyingi darsda kirish va marshrutni himoyalash — parolni solishtirish, sessiya ochish va himoyalangan sahifalar.

Bu dars foydali bo'ldimi?

marta ko'rildi