753 lines
16 KiB
Markdown
753 lines
16 KiB
Markdown
# 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! 🚀**
|