diff --git a/auth-service/src/controllers/authController.js b/auth-service/src/controllers/authController.js new file mode 100644 index 0000000..67eeb88 --- /dev/null +++ b/auth-service/src/controllers/authController.js @@ -0,0 +1,38 @@ +import jwt from 'jsonwebtoken'; +import { User } from '../models/index.js'; + +function getJwtSecret(payload) { + const base = process.env.JWT_SECRET || 'secret'; + // Если rememberMe, используем только base + if (payload && payload.rememberMe) return base; + // Иначе добавляем "часовой" компонент + const hour = new Date().toISOString().slice(0, 13); // YYYY-MM-DDTHH + return base + ':' + hour; +} + +export default { + async verify(req, res) { + let token = req.body.token; + if (!token && req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) { + token = req.headers.authorization.split(' ')[1]; + } + if (!token) { + return res.status(401).json({ error: 'No token provided' }); + } + try { + // Сначала декодируем без проверки, чтобы узнать rememberMe + let payload = null; + try { + payload = jwt.decode(token); + } catch {} + const secret = getJwtSecret(payload); + payload = jwt.verify(token, secret); + // Найти пользователя по id + const user = await User.findByPk(payload.id); + if (!user) return res.status(401).json({ error: 'User not found' }); + res.json({ id: user.id, email: user.email, role_id: user.role_id }); + } catch (err) { + return res.status(401).json({ error: 'Invalid token', details: err.message }); + } + }, +}; \ No newline at end of file diff --git a/auth-service/src/controllers/userController.js b/auth-service/src/controllers/userController.js index fc70355..0bbf83c 100644 --- a/auth-service/src/controllers/userController.js +++ b/auth-service/src/controllers/userController.js @@ -65,12 +65,22 @@ export default { }, async login(req, res) { try { - const { email, password } = req.body; + const { email, password, rememberMe } = req.body; const user = await User.findOne({ where: { email } }); if (!user) return res.status(401).json({ error: 'Invalid credentials' }); const valid = await bcrypt.compare(password, user.password_hash); if (!valid) return res.status(401).json({ error: 'Invalid credentials' }); - const token = jwt.sign({ id: user.id, role_id: user.role_id }, process.env.JWT_SECRET, { expiresIn: '1d' }); + let token, secret, expiresIn; + if (rememberMe) { + secret = process.env.JWT_SECRET || 'secret'; + expiresIn = '30d'; + } else { + const base = process.env.JWT_SECRET || 'secret'; + const hour = new Date().toISOString().slice(0, 13); // YYYY-MM-DDTHH + secret = base + ':' + hour; + expiresIn = '1h'; + } + token = jwt.sign({ id: user.id, role_id: user.role_id, rememberMe: !!rememberMe }, secret, { expiresIn }); res.json({ token, user: { id: user.id, email: user.email, name: user.name, role_id: user.role_id } }); } catch (err) { res.status(500).json({ error: err.message }); diff --git a/auth-service/src/routes/index.js b/auth-service/src/routes/index.js index 9a5e3c0..355cda0 100644 --- a/auth-service/src/routes/index.js +++ b/auth-service/src/routes/index.js @@ -3,6 +3,7 @@ import userRoutes from './user.js'; import roleRoutes from './role.js'; import permissionRoutes from './permission.js'; import rolePermissionRoutes from './rolePermission.js'; +import authController from '../controllers/authController.js'; const router = Router(); @@ -10,5 +11,6 @@ router.use('/users', userRoutes); router.use('/roles', roleRoutes); router.use('/permissions', permissionRoutes); router.use('/role-permissions', rolePermissionRoutes); +router.post('/verify', authController.verify); export default router; \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 760c74a..5ce65bd 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -3,13 +3,22 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-d import Login from './pages/Login'; import Dashboard from './pages/Dashboard'; import './App.css'; +import { useUser } from './context/UserContext'; function App() { + const { user, token, isCheckingAuth } = useUser(); + + if (isCheckingAuth) { + return
Проверка авторизации...
; + } + return ( } /> - } /> + : } /> } /> diff --git a/frontend/src/context/UserContext.js b/frontend/src/context/UserContext.js index 410e6c7..73a1721 100644 --- a/frontend/src/context/UserContext.js +++ b/frontend/src/context/UserContext.js @@ -5,6 +5,7 @@ const UserContext = createContext(); export function UserProvider({ children }) { const [user, setUser] = useState(null); const [token, setToken] = useState(null); + const [isCheckingAuth, setIsCheckingAuth] = useState(true); useEffect(() => { // Инициализация из localStorage @@ -13,7 +14,27 @@ export function UserProvider({ children }) { if (storedUser && storedToken) { setUser(JSON.parse(storedUser)); setToken(storedToken); + // Проверка токена + fetch('/api/auth/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${storedToken}` }, + body: JSON.stringify({ token: storedToken }) + }) + .then(res => { + if (!res.ok) throw new Error('Invalid token'); + return res.json(); + }) + .then(() => { + setIsCheckingAuth(false); + }) + .catch(() => { + logout(); + window.location.href = '/login'; + }); + } else { + setIsCheckingAuth(false); } + // eslint-disable-next-line }, []); const login = (user, token) => { @@ -31,7 +52,7 @@ export function UserProvider({ children }) { }; return ( - + {children} ); diff --git a/frontend/src/pages/SmtpServersPage.js b/frontend/src/pages/SmtpServersPage.js index a324187..3e1c051 100644 --- a/frontend/src/pages/SmtpServersPage.js +++ b/frontend/src/pages/SmtpServersPage.js @@ -25,12 +25,18 @@ function SmtpServersPage() { }, [page]); const fetchServers = async (page) => { + if (!token) { + setError('Нет авторизации'); + setServers([]); + setTotal(0); + return; + } setLoading(true); setError(''); try { const offset = (page - 1) * PAGE_SIZE; const res = await fetch(`/api/mail/smtp-servers?limit=${PAGE_SIZE}&offset=${offset}`, { - headers: token ? { Authorization: `Bearer ${token}` } : {} + headers: { Authorization: `Bearer ${token}` } }); const data = await res.json(); if (!res.ok) { @@ -51,12 +57,16 @@ function SmtpServersPage() { }; const handleDelete = async (id) => { + if (!token) { + alert('Нет авторизации'); + return; + } if (!window.confirm('Удалить SMTP-сервер?')) return; setDeleteLoading(id); try { const res = await fetch(`/api/mail/smtp-servers/${id}`, { method: 'DELETE', - headers: token ? { Authorization: `Bearer ${token}` } : {} + headers: { Authorization: `Bearer ${token}` } }); if (!res.ok) { const data = await res.json(); @@ -77,6 +87,10 @@ function SmtpServersPage() { const handleEditSave = async (e) => { e.preventDefault(); + if (!token) { + alert('Нет авторизации'); + return; + } setEditLoading(true); try { const { group_id, ...serverData } = editServer; @@ -84,7 +98,7 @@ function SmtpServersPage() { method: 'PUT', headers: { 'Content-Type': 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}) + Authorization: `Bearer ${token}` }, body: JSON.stringify({ ...serverData, user_id: user?.id }) }); @@ -110,6 +124,10 @@ function SmtpServersPage() { const handleCreateSave = async (e) => { e.preventDefault(); + if (!token) { + alert('Нет авторизации'); + return; + } setCreateLoading(true); try { const { group_id, ...serverData } = createServer; @@ -117,7 +135,7 @@ function SmtpServersPage() { method: 'POST', headers: { 'Content-Type': 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}) + Authorization: `Bearer ${token}` }, body: JSON.stringify({ ...serverData, user_id: user?.id }) }); diff --git a/mail-service/src/index.js b/mail-service/src/index.js index 54a6ac3..a76d98a 100644 --- a/mail-service/src/index.js +++ b/mail-service/src/index.js @@ -5,6 +5,7 @@ import { sequelize } from './models/index.js'; import routes from './routes/index.js'; import { processScheduledCampaigns } from './service/queueFillerJob.js'; import { startMailSender } from './service/mailSender.js'; +import authMiddleware from './middleware/auth.js'; const app = express(); app.use(express.json()); @@ -13,7 +14,7 @@ app.get('/', (req, res) => { res.send('Mail Service is running'); }); -app.use('/api/mail', routes); +app.use('/api/mail', authMiddleware, routes); (async () => { try { diff --git a/mail-service/src/middleware/auth.js b/mail-service/src/middleware/auth.js new file mode 100644 index 0000000..5cdcaea --- /dev/null +++ b/mail-service/src/middleware/auth.js @@ -0,0 +1,23 @@ +export default async function authMiddleware(req, res, next) { + const authHeader = req.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'No token provided' }); + } + const token = authHeader.split(' ')[1]; + try { + // Проверяем токен через auth-service + const resp = await fetch('http://auth-service:3000/api/auth/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ token }), + }); + if (!resp.ok) { + return res.status(401).json({ error: 'Invalid token' }); + } + const user = await resp.json(); + req.user = user; + next(); + } catch (err) { + return res.status(401).json({ error: 'Auth service error', details: err.message }); + } +} \ No newline at end of file