Register और Login API (Token)
Token login कैसे काम करता है
App email + password एक बार भेजती है। सही होने पर server एक लंबा random token बनाता है और app उसे (DataStore में) save कर लेती है। उसके बाद हर request header में token लेकर जाती है, और server जान जाता है कि कौन call कर रहा है।
Passwords: hash करें, store नहीं
password_hash() password को random salt (bcrypt) के साथ one-way hash में बदलता है। मालिक होकर भी आप password वापस नहीं पढ़ सकते। password_verify() type किए गए password को hash से match करता है।
<?php
$hash = password_hash('Test@123', PASSWORD_DEFAULT);
echo $hash, "\n";
var_dump(password_verify('Test@123', $hash));
var_dump(password_verify('test@123', $hash));Email न हो या password गलत हो — login दोनों में "Wrong email or password." कहता है। अलग message होने पर attackers पता लगा लेंगे कि किन emails के accounts हैं।
Tokens: बनाना और check करना
ये दो helpers (helpers.php में पहले से) token का सारा काम करते हैं। जिस endpoint को logged-in user चाहिए वह शुरुआत में बस $user = require_auth(); call करता है।
<?php
// From includes/helpers.php
/** Create a new login token for a user. Only its sha256 hash is stored. */
function create_token($userId)
{
$token = bin2hex(random_bytes(32)); // 32 random bytes = 64 hex characters
$stmt = db()->prepare('INSERT INTO user_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)');
$stmt->execute(array($userId, hash('sha256', $token), date('Y-m-d H:i:s', time() + TOKEN_DAYS * 86400)));
return $token; // the app gets the real token ONCE
}
/** Returns the logged-in user row, or stops with 401. */
function require_auth()
{
$token = bearer_token(); // reads "Authorization: Bearer <token>"
if ($token === null) {
fail('Please login to continue.', 401);
}
$stmt = db()->prepare(
'SELECT u.id, u.name, u.email, u.phone, u.created_at FROM user_tokens t
JOIN users u ON u.id = t.user_id
WHERE t.token_hash = ? AND t.expires_at > NOW()'
);
$stmt->execute(array(hash('sha256', $token)));
$user = $stmt->fetch();
if (!$user) {
fail('Session expired. Please login again.', 401);
}
$user['id'] = (int) $user['id'];
return $user;
}App क्या रखती है
- token = c4a45ae77d…(64 hex)
- DataStore में saved
- Authorization header में भेजा जाता है
Database क्या रखता है
- token_hash = sha256(token)
- user_id, expires_at (30 दिन)
- हर logged-in device की एक row
Step 1: auth/register.php
सब कुछ server पर validate करें — app भी validate करती है, पर कोई भी API को सीधे call कर सकता है। फिर check करें कि email free है, hashed password के साथ user save करें और तुरंत login कर दें।
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
require_fields(array('name' => 'Name', 'email' => 'Email', 'password' => 'Password'));
$name = in_str('name');
$email = strtolower(in_str('email'));
$phone = in_str('phone');
$password = in_str('password');
if (mb_strlen($name) < 2 || mb_strlen($name) > 80) {
fail('Name must be 2 to 80 characters.', 422);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
fail('Please enter a valid email.', 422);
}
if ($phone !== '' && !preg_match('/^[6-9][0-9]{9}$/', $phone)) {
fail('Please enter a valid 10-digit mobile number.', 422);
}
if (strlen($password) < 6) {
fail('Password must be at least 6 characters.', 422);
}
$stmt = db()->prepare('SELECT id FROM users WHERE email = ?');
$stmt->execute(array($email));
if ($stmt->fetch()) {
fail('This email is already registered. Please login.', 409);
}
$stmt = db()->prepare('INSERT INTO users (name, email, phone, password_hash) VALUES (?, ?, ?, ?)');
$stmt->execute(array($name, $email, $phone === '' ? null : $phone, password_hash($password, PASSWORD_DEFAULT)));
$userId = (int) db()->lastInsertId();
ok(array(
'token' => create_token($userId),
'user' => array('id' => $userId, 'name' => $name, 'email' => $email, 'phone' => $phone === '' ? null : $phone),
), 'Account created successfully.');Step 2: auth/login.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
require_fields(array('email' => 'Email', 'password' => 'Password'));
$email = strtolower(in_str('email'));
$password = in_str('password');
$stmt = db()->prepare('SELECT id, name, email, phone, password_hash FROM users WHERE email = ?');
$stmt->execute(array($email));
$user = $stmt->fetch();
// Same message for "no such email" and "wrong password": do not reveal which emails exist.
if (!$user || !password_verify($password, $user['password_hash'])) {
fail('Wrong email or password.', 401);
}
// Remove this user's expired tokens while we are here.
db()->prepare('DELETE FROM user_tokens WHERE user_id = ? AND expires_at < NOW()')->execute(array($user['id']));
ok(array(
'token' => create_token((int) $user['id']),
'user' => array('id' => (int) $user['id'], 'name' => $user['name'], 'email' => $user['email'], 'phone' => $user['phone']),
), 'Welcome back, ' . $user['name'] . '!');Step 3: me, update, logout
me.php profile के साथ तीन counters (orders, wishlist, cart) लौटाता है जो Account screen दिखाती है — sub-selects वाली एक ही query में।
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('GET');
$user = require_auth();
$counts = db()->prepare(
'SELECT
(SELECT COUNT(*) FROM orders WHERE user_id = ?) AS orders,
(SELECT COUNT(*) FROM wishlist WHERE user_id = ?) AS wishlist,
(SELECT COALESCE(SUM(qty), 0) FROM cart_items WHERE user_id = ?) AS cart'
);
$counts->execute(array($user['id'], $user['id'], $user['id']));
$c = $counts->fetch();
$user['order_count'] = (int) $c['orders'];
$user['wishlist_count'] = (int) $c['wishlist'];
$user['cart_count'] = (int) $c['cart'];
ok($user);update.php name और phone बदलता है, और password सिर्फ तब जब current password सही हो:
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
$user = require_auth();
require_fields(array('name' => 'Name'));
$name = in_str('name');
$phone = in_str('phone');
if (mb_strlen($name) < 2 || mb_strlen($name) > 80) {
fail('Name must be 2 to 80 characters.', 422);
}
if ($phone !== '' && !preg_match('/^[6-9][0-9]{9}$/', $phone)) {
fail('Please enter a valid 10-digit mobile number.', 422);
}
db()->prepare('UPDATE users SET name = ?, phone = ? WHERE id = ?')
->execute(array($name, $phone === '' ? null : $phone, $user['id']));
// Optional password change: needs the current password.
$newPass = in_str('new_password');
if ($newPass !== '') {
$stmt = db()->prepare('SELECT password_hash FROM users WHERE id = ?');
$stmt->execute(array($user['id']));
if (!password_verify(in_str('current_password'), $stmt->fetchColumn())) {
fail('Current password is wrong.', 422);
}
if (strlen($newPass) < 6) {
fail('New password must be at least 6 characters.', 422);
}
db()->prepare('UPDATE users SET password_hash = ? WHERE id = ?')
->execute(array(password_hash($newPass, PASSWORD_DEFAULT), $user['id']));
}
ok(array('id' => $user['id'], 'name' => $name, 'email' => $user['email'], 'phone' => $phone === '' ? null : $phone), 'Profile updated.');logout.php इस device का token delete करता है। बाकी devices logged in रहते हैं।
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
require_auth();
db()->prepare('DELETE FROM user_tokens WHERE token_hash = ?')->execute(array(hash('sha256', bearer_token())));
ok(null, 'Logged out.');Step 4: curl से test
# Login (JSON body)
curl -X POST http://localhost:8000/api/auth/login.php \
-H "Content-Type: application/json" \
-d '{"email":"test@karteasy.in","password":"Test@123"}'
# Copy the token from the answer, then call a protected endpoint
TOKEN=paste-the-64-character-token-here
curl http://localhost:8000/api/auth/me.php -H "Authorization: Bearer $TOKEN"
# Logout
curl -X POST http://localhost:8000/api/auth/logout.php -H "Authorization: Bearer $TOKEN"ये KartEasy API के असली जवाब हैं:
आम गलतियाँ
- Hosting पर Authorization header गायब →
backend/.htaccessका RewriteRule रखें। - Passwords को
==से compare करना → हमेशाpassword_verify()। - JSON में
password_hashलौटाना → सिर्फ ज़रूरी columns select करें। - Tokens के लिए
rand()याuniqid()→ guess हो सकते हैं;random_bytes()use करें। - Tokens कभी expire न होना → हम 30 दिन रखते हैं और login पर पुराने साफ़ करते हैं।
