harmadik gyakorlat
This commit is contained in:
@@ -0,0 +1,752 @@
|
||||
# Implementation Hints & Code Snippets
|
||||
|
||||
Ez a fájl segítséget nyújt az implementációhoz konkrét kód példákkal.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 JWT Token Kezelés
|
||||
|
||||
### Token Generálás
|
||||
|
||||
```javascript
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
function generateAccessToken(user) {
|
||||
return jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
|
||||
);
|
||||
}
|
||||
|
||||
function generateRefreshToken(user) {
|
||||
return jwt.sign(
|
||||
{
|
||||
userId: user.id
|
||||
},
|
||||
process.env.JWT_REFRESH_SECRET,
|
||||
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '30d' }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Token Validálás
|
||||
|
||||
```javascript
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
function verifyAccessToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, process.env.JWT_SECRET);
|
||||
} catch (error) {
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
}
|
||||
|
||||
function verifyRefreshToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, process.env.JWT_REFRESH_SECRET);
|
||||
} catch (error) {
|
||||
throw new Error('Invalid refresh token');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Token Kiolvasása Request-ből
|
||||
|
||||
```javascript
|
||||
function extractToken(req) {
|
||||
// 1. Először cookie-ból próbálkozunk
|
||||
if (req.cookies && req.cookies.accessToken) {
|
||||
return req.cookies.accessToken;
|
||||
}
|
||||
|
||||
// 2. Ha nincs cookie, akkor Authorization header
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
return authHeader.substring(7); // "Bearer " eltávolítása
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Jelszó Hash-elés
|
||||
|
||||
### Regisztrációnál
|
||||
|
||||
```javascript
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
async function hashPassword(plainPassword) {
|
||||
const saltRounds = 10;
|
||||
return await bcrypt.hash(plainPassword, saltRounds);
|
||||
}
|
||||
|
||||
// Használat:
|
||||
const hashedPassword = await hashPassword(req.body.password);
|
||||
const user = await userRepository.create({
|
||||
email: req.body.email,
|
||||
username: req.body.username,
|
||||
password: hashedPassword, // Hash-elt jelszó!
|
||||
role: 'USER'
|
||||
});
|
||||
```
|
||||
|
||||
### Bejelentkezésnél
|
||||
|
||||
```javascript
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
async function verifyPassword(plainPassword, hashedPassword) {
|
||||
return await bcrypt.compare(plainPassword, hashedPassword);
|
||||
}
|
||||
|
||||
// Használat:
|
||||
const user = await userRepository.findByEmail(req.body.email);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const isPasswordValid = await verifyPassword(req.body.password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🍪 Cookie Kezelés
|
||||
|
||||
### Cookie Beállítás
|
||||
|
||||
```javascript
|
||||
function setAuthCookies(res, accessToken, refreshToken) {
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict'
|
||||
};
|
||||
|
||||
// Access token - 7 nap
|
||||
res.cookie('accessToken', accessToken, {
|
||||
...cookieOptions,
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000
|
||||
});
|
||||
|
||||
// Refresh token - 30 nap
|
||||
res.cookie('refreshToken', refreshToken, {
|
||||
...cookieOptions,
|
||||
maxAge: 30 * 24 * 60 * 60 * 1000
|
||||
});
|
||||
}
|
||||
|
||||
// Használat a login()-ban:
|
||||
const accessToken = generateAccessToken(user);
|
||||
const refreshToken = generateRefreshToken(user);
|
||||
setAuthCookies(res, accessToken, refreshToken);
|
||||
```
|
||||
|
||||
### Cookie Törlés
|
||||
|
||||
```javascript
|
||||
function clearAuthCookies(res) {
|
||||
res.clearCookie('accessToken');
|
||||
res.clearCookie('refreshToken');
|
||||
}
|
||||
|
||||
// Használat a logout()-ban:
|
||||
clearAuthCookies(res);
|
||||
res.json({ success: true, message: 'Logged out successfully' });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Input Validáció
|
||||
|
||||
### Email Validáció
|
||||
|
||||
```javascript
|
||||
function isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
```
|
||||
|
||||
### Password Validáció
|
||||
|
||||
```javascript
|
||||
function isValidPassword(password) {
|
||||
// Minimum 8 karakter, legalább egy szám és egy betű
|
||||
return password && password.length >= 8;
|
||||
}
|
||||
```
|
||||
|
||||
### Register Input Validáció
|
||||
|
||||
```javascript
|
||||
function validateRegisterInput(data) {
|
||||
const errors = [];
|
||||
|
||||
if (!data.email || !isValidEmail(data.email)) {
|
||||
errors.push('Valid email is required');
|
||||
}
|
||||
|
||||
if (!data.username || data.username.length < 3) {
|
||||
errors.push('Username must be at least 3 characters');
|
||||
}
|
||||
|
||||
if (!data.password || !isValidPassword(data.password)) {
|
||||
errors.push('Password must be at least 8 characters');
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
// Használat:
|
||||
const validation = validateRegisterInput(req.body);
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({ errors: validation.errors });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Redis Session Management
|
||||
|
||||
### Session Tárolás
|
||||
|
||||
```javascript
|
||||
import { redis } from '../../infrastructure/database/redis.js';
|
||||
|
||||
async function createSession(userId, sessionData) {
|
||||
const sessionKey = `session:${userId}`;
|
||||
const ttl = 7 * 24 * 60 * 60; // 7 nap másodpercben
|
||||
|
||||
await redis.set(
|
||||
sessionKey,
|
||||
JSON.stringify({
|
||||
userId,
|
||||
loginTime: new Date().toISOString(),
|
||||
...sessionData
|
||||
}),
|
||||
'EX',
|
||||
ttl
|
||||
);
|
||||
}
|
||||
|
||||
// Használat a login()-ban:
|
||||
await createSession(user.id, {
|
||||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
```
|
||||
|
||||
### Session Lekérés
|
||||
|
||||
```javascript
|
||||
async function getSession(userId) {
|
||||
const sessionKey = `session:${userId}`;
|
||||
const session = await redis.get(sessionKey);
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(session);
|
||||
}
|
||||
```
|
||||
|
||||
### Session Törlés
|
||||
|
||||
```javascript
|
||||
async function deleteSession(userId) {
|
||||
const sessionKey = `session:${userId}`;
|
||||
await redis.del(sessionKey);
|
||||
}
|
||||
|
||||
// Használat a logout()-ban:
|
||||
await deleteSession(req.user.id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ AuthController Implementáció
|
||||
|
||||
### Register
|
||||
|
||||
```javascript
|
||||
async register(req, res) {
|
||||
try {
|
||||
// 1. Input validáció
|
||||
const validation = validateRegisterInput(req.body);
|
||||
if (!validation.isValid) {
|
||||
return res.status(400).json({ errors: validation.errors });
|
||||
}
|
||||
|
||||
const { email, username, password } = req.body;
|
||||
|
||||
// 2. Uniqueness ellenőrzés
|
||||
const existingUser = await this.userRepository.findByEmail(email);
|
||||
if (existingUser) {
|
||||
return res.status(409).json({ error: 'Email already exists' });
|
||||
}
|
||||
|
||||
const existingUsername = await this.userRepository.findByUsername(username);
|
||||
if (existingUsername) {
|
||||
return res.status(409).json({ error: 'Username already exists' });
|
||||
}
|
||||
|
||||
// 3. Jelszó hash-elés
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// 4. User létrehozás
|
||||
const user = await this.userRepository.create({
|
||||
email,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
role: 'USER'
|
||||
});
|
||||
|
||||
// 5. Token generálás
|
||||
const accessToken = generateAccessToken(user);
|
||||
const refreshToken = generateRefreshToken(user);
|
||||
|
||||
// 6. Cookie beállítás
|
||||
setAuthCookies(res, accessToken, refreshToken);
|
||||
|
||||
// 7. Válasz
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
user: user.toPublicObject(),
|
||||
accessToken,
|
||||
refreshToken
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Login
|
||||
|
||||
```javascript
|
||||
async login(req, res) {
|
||||
try {
|
||||
const { email, username, password } = req.body;
|
||||
|
||||
// Input validáció
|
||||
if (!password || (!email && !username)) {
|
||||
return res.status(400).json({
|
||||
error: 'Email/username and password required'
|
||||
});
|
||||
}
|
||||
|
||||
// User keresés
|
||||
let user;
|
||||
if (email) {
|
||||
user = await this.userRepository.findByEmail(email);
|
||||
} else {
|
||||
user = await this.userRepository.findByUsername(username);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Jelszó ellenőrzés
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||
if (!isPasswordValid) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Token generálás
|
||||
const accessToken = generateAccessToken(user);
|
||||
const refreshToken = generateRefreshToken(user);
|
||||
|
||||
// Cookie beállítás
|
||||
setAuthCookies(res, accessToken, refreshToken);
|
||||
|
||||
// Redis session (opcionális)
|
||||
await createSession(user.id, {
|
||||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: user.toPublicObject(),
|
||||
accessToken,
|
||||
refreshToken
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
|
||||
```javascript
|
||||
async logout(req, res) {
|
||||
try {
|
||||
// Redis session törlés
|
||||
if (req.user && req.user.id) {
|
||||
await deleteSession(req.user.id);
|
||||
}
|
||||
|
||||
// Cookie törlés
|
||||
clearAuthCookies(res);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Logged out successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh Token
|
||||
|
||||
```javascript
|
||||
async refreshToken(req, res) {
|
||||
try {
|
||||
// Refresh token kiolvasása
|
||||
const refreshToken = req.cookies.refreshToken;
|
||||
|
||||
if (!refreshToken) {
|
||||
return res.status(401).json({ error: 'Refresh token required' });
|
||||
}
|
||||
|
||||
// Token validálás
|
||||
const decoded = verifyRefreshToken(refreshToken);
|
||||
|
||||
// User lekérés
|
||||
const user = await this.userRepository.findById(decoded.userId);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
// Új access token generálás
|
||||
const newAccessToken = generateAccessToken(user);
|
||||
|
||||
// Cookie frissítés
|
||||
res.cookie('accessToken', newAccessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
accessToken: newAccessToken
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid refresh token' });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Get Current User
|
||||
|
||||
```javascript
|
||||
async getCurrentUser(req, res) {
|
||||
try {
|
||||
// req.user az authenticateToken middleware állítja be
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
user: req.user.toPublicObject()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ AuthMiddleware Implementáció
|
||||
|
||||
### authenticateToken
|
||||
|
||||
```javascript
|
||||
export async function authenticateToken(req, res, next) {
|
||||
try {
|
||||
// 1. Token kiolvasása
|
||||
const token = extractToken(req);
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
error: 'Access token required'
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Token validálás
|
||||
const decoded = verifyAccessToken(token);
|
||||
|
||||
// 3. User betöltése (FONTOS: ezt implementálni kell!)
|
||||
// Ehhez kell egy userRepository instance
|
||||
// Lehetőség 1: req.app.locals.userRepository
|
||||
// Lehetőség 2: Singleton pattern
|
||||
// Lehetőség 3: Middleware factory function
|
||||
|
||||
const userRepository = req.app.locals.userRepository;
|
||||
const user = await userRepository.findById(decoded.userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
// 4. req.user beállítása
|
||||
req.user = user;
|
||||
|
||||
// 5. Következő middleware
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid or expired token'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**FONTOS:** A userRepository-t be kell injektálni a middleware-be!
|
||||
|
||||
Megoldás az index.js-ben:
|
||||
|
||||
```javascript
|
||||
// index.js-ben
|
||||
app.locals.userRepository = userRepository;
|
||||
```
|
||||
|
||||
### requireRole
|
||||
|
||||
```javascript
|
||||
export function requireRole(allowedRoles) {
|
||||
return (req, res, next) => {
|
||||
// Az authenticateToken után fut, tehát req.user létezik
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
if (!allowedRoles.includes(req.user.role)) {
|
||||
return res.status(403).json({
|
||||
error: 'Insufficient permissions',
|
||||
required: allowedRoles,
|
||||
current: req.user.role
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Használat:
|
||||
// router.delete('/users/:id', authenticateToken, requireRole(['ADMIN']), ...)
|
||||
```
|
||||
|
||||
### checkOwnership
|
||||
|
||||
```javascript
|
||||
export function checkOwnership(getResourceOwnerId) {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
// Resource owner ID megszerzése
|
||||
const ownerId = await getResourceOwnerId(req);
|
||||
|
||||
// Admin mindent módosíthat
|
||||
if (req.user.role === 'ADMIN') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Ownership ellenőrzés
|
||||
if (req.user.id !== ownerId) {
|
||||
return res.status(403).json({
|
||||
error: 'You can only modify your own resources'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(403).json({ error: 'Access denied' });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Használat blogRoutes.js-ben:
|
||||
router.put('/:id',
|
||||
authenticateToken,
|
||||
checkOwnership(async (req) => {
|
||||
const blogRepository = req.app.locals.blogRepository;
|
||||
const blog = await blogRepository.findById(req.params.id);
|
||||
if (!blog) {
|
||||
throw new Error('Blog not found');
|
||||
}
|
||||
return blog.authorId;
|
||||
}),
|
||||
(req, res) => blogController.updateBlog(req, res)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Route Protection Példák
|
||||
|
||||
### authRoutes.js
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import { authenticateToken } from '../middlewares/authMiddleware.js';
|
||||
|
||||
export function createAuthRoutes(authController) {
|
||||
const router = express.Router();
|
||||
|
||||
// Publikus route-ok
|
||||
router.post('/register', (req, res) => authController.register(req, res));
|
||||
router.post('/login', (req, res) => authController.login(req, res));
|
||||
router.post('/refresh', (req, res) => authController.refreshToken(req, res));
|
||||
|
||||
// Védett route-ok
|
||||
router.post('/logout', authenticateToken, (req, res) =>
|
||||
authController.logout(req, res)
|
||||
);
|
||||
router.get('/me', authenticateToken, (req, res) =>
|
||||
authController.getCurrentUser(req, res)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
### blogRoutes.js
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import { authenticateToken, checkOwnership } from '../middlewares/authMiddleware.js';
|
||||
|
||||
export function createBlogRoutes(blogController) {
|
||||
const router = express.Router();
|
||||
|
||||
// Publikus route-ok
|
||||
router.get('/', (req, res) => blogController.getAllBlogs(req, res));
|
||||
router.get('/:id', (req, res) => blogController.getBlog(req, res));
|
||||
|
||||
// Védett route-ok
|
||||
router.post('/',
|
||||
authenticateToken,
|
||||
(req, res) => blogController.createBlog(req, res)
|
||||
);
|
||||
|
||||
router.put('/:id',
|
||||
authenticateToken,
|
||||
checkOwnership(async (req) => {
|
||||
const blogRepository = req.app.locals.blogRepository;
|
||||
const blog = await blogRepository.findById(req.params.id);
|
||||
return blog?.authorId;
|
||||
}),
|
||||
(req, res) => blogController.updateBlog(req, res)
|
||||
);
|
||||
|
||||
router.delete('/:id',
|
||||
authenticateToken,
|
||||
checkOwnership(async (req) => {
|
||||
const blogRepository = req.app.locals.blogRepository;
|
||||
const blog = await blogRepository.findById(req.params.id);
|
||||
return blog?.authorId;
|
||||
}),
|
||||
(req, res) => blogController.deleteBlog(req, res)
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 index.js Módosítások
|
||||
|
||||
```javascript
|
||||
// Repository-k injektálása app.locals-ba
|
||||
app.locals.userRepository = userRepository;
|
||||
app.locals.blogRepository = blogRepository;
|
||||
|
||||
// Így a middleware-ek hozzáférhetnek:
|
||||
// const userRepository = req.app.locals.userRepository;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Tesztelési Példák
|
||||
|
||||
### Regisztráció
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "alice@example.com",
|
||||
"username": "alice",
|
||||
"password": "Alice1234"
|
||||
}'
|
||||
```
|
||||
|
||||
### Bejelentkezés
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-c cookies.txt \
|
||||
-d '{
|
||||
"email": "alice@example.com",
|
||||
"password": "Alice1234"
|
||||
}'
|
||||
```
|
||||
|
||||
### Védett endpoint (cookie-val)
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/auth/me \
|
||||
-b cookies.txt
|
||||
```
|
||||
|
||||
### Védett endpoint (Bearer token-nel)
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/auth/me \
|
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ez a hints fájl minden szükséges kódrészletet tartalmaz a sikeres implementációhoz! 🚀**
|
||||
Reference in New Issue
Block a user