התנתקות
חלק מהיחידה אימות ואבטחה במסלול ה-Node.js של Coddy. שיעור 7 מתוך 17.
מחיקת העוגייה רק מבקשת מהדפדפן לשכוח את המזהה. כל מי שהעתיק אותו, ממכשיר אחר או מעוגייה שנגנבה, עדיין מחזיק במפתח שעובד. כדי להתנתק, צריך למחוק את ההפעלה בשרת.
res.clearCookie(name) מורה לדפדפן למחוק עוגייה. הסטטוס 204 ללא תוכן פירושו שהפעולה הצליחה ושאין מה לשלוח בחזרה: res.status(204).end().
הנה נתיב קשור, שמנתק משתמש מכל המכשירים בבת אחת:
app.post('/logout-everywhere', requireUser, async (req, res) => {
await sessions.deleteMany({ userId: new ObjectId(req.user.id) });
res.clearCookie('sid');
res.status(204).end();
});אתגר
קלPOST /logoutמוחק את ההפעלה של קובץ ה-cookiesid, מנקה את קובץ ה-cookie ומשיב 204.- ללא הפעלה, הוא עדיין משיב 204.
התחבר, שלח POST /logout, ואז שלח GET /me.
נסו בעצמכם
import express from 'express';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const app = express();
app.use(express.json());
app.post('/signup', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!username || password.length < 8) {
return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
}
if (await users.findOne({ username })) {
return res.status(409).json({ error: 'That username is taken' });
}
await users.insertOne({ username, passwordHash: await hashPassword(password) });
res.status(201).json({ username });
});
app.post('/login', async (req, res) => {
const username = String(req.body.username ?? '').trim().toLowerCase();
const user = await users.findOne({ username });
if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
return res.status(401).json({ error: 'Wrong username or password' });
}
const sid = randomBytes(24).toString('base64url');
await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
res.cookie('sid', sid, { httpOnly: true });
res.json({ username: user.username });
});
// מאתר את המשתמש המשויך לעוגיית הסשן, או מחזיר 401.
async function requireUser(req, res, next) {
const { sid } = parseCookies(req.get('cookie'));
const session = sid && (await sessions.findOne({ _id: sid }));
const user = session && (await users.findOne({ _id: session.userId }));
if (!user) {
return res.status(401).json({ error: 'Log in first' });
}
req.user = { id: user._id.toHexString(), username: user.username };
next();
}
app.get('/me', requireUser, (req, res) => {
res.json({ username: req.user.username });
});
// סופר כמה פעמים הדפדפן הזה שלח בקשת GET /visit.
app.get('/visit', (req, res) => {
const cookies = parseCookies(req.get('cookie'));
const visits = Number(cookies.visits ?? 0) + 1;
res.cookie('visits', String(visits));
res.json({ visits });
});
// פתקים: כל משתמש קורא וכותב רק את הפתקים שלו.
app.get('/notes', requireUser, async (req, res) => {
res.json(await notes.find({ owner: req.user.id }).toArray());
});
app.post('/notes', requireUser, async (req, res) => {
const note = { owner: req.user.id, text: String(req.body.text ?? '') };
await notes.insertOne(note);
res.status(201).json(note);
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});