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