auth service

This commit is contained in:
romantarkin 2025-07-23 17:45:31 +05:00
parent 790411a2d7
commit 26973a5126
8 changed files with 131 additions and 9 deletions

View File

@ -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 });
}
},
};

View File

@ -65,12 +65,22 @@ export default {
}, },
async login(req, res) { async login(req, res) {
try { try {
const { email, password } = req.body; const { email, password, rememberMe } = req.body;
const user = await User.findOne({ where: { email } }); const user = await User.findOne({ where: { email } });
if (!user) return res.status(401).json({ error: 'Invalid credentials' }); if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const valid = await bcrypt.compare(password, user.password_hash); const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) return res.status(401).json({ error: 'Invalid credentials' }); 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 } }); res.json({ token, user: { id: user.id, email: user.email, name: user.name, role_id: user.role_id } });
} catch (err) { } catch (err) {
res.status(500).json({ error: err.message }); res.status(500).json({ error: err.message });

View File

@ -3,6 +3,7 @@ import userRoutes from './user.js';
import roleRoutes from './role.js'; import roleRoutes from './role.js';
import permissionRoutes from './permission.js'; import permissionRoutes from './permission.js';
import rolePermissionRoutes from './rolePermission.js'; import rolePermissionRoutes from './rolePermission.js';
import authController from '../controllers/authController.js';
const router = Router(); const router = Router();
@ -10,5 +11,6 @@ router.use('/users', userRoutes);
router.use('/roles', roleRoutes); router.use('/roles', roleRoutes);
router.use('/permissions', permissionRoutes); router.use('/permissions', permissionRoutes);
router.use('/role-permissions', rolePermissionRoutes); router.use('/role-permissions', rolePermissionRoutes);
router.post('/verify', authController.verify);
export default router; export default router;

View File

@ -3,13 +3,22 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-d
import Login from './pages/Login'; import Login from './pages/Login';
import Dashboard from './pages/Dashboard'; import Dashboard from './pages/Dashboard';
import './App.css'; import './App.css';
import { useUser } from './context/UserContext';
function App() { function App() {
const { user, token, isCheckingAuth } = useUser();
if (isCheckingAuth) {
return <div style={{ textAlign: 'center', marginTop: 80 }}>Проверка авторизации...</div>;
}
return ( return (
<Router> <Router>
<Routes> <Routes>
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} /> <Route
path="/dashboard"
element={user && token ? <Dashboard /> : <Navigate to="/login" replace />} />
<Route path="*" element={<Navigate to="/login" replace />} /> <Route path="*" element={<Navigate to="/login" replace />} />
</Routes> </Routes>
</Router> </Router>

View File

@ -5,6 +5,7 @@ const UserContext = createContext();
export function UserProvider({ children }) { export function UserProvider({ children }) {
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [token, setToken] = useState(null); const [token, setToken] = useState(null);
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
useEffect(() => { useEffect(() => {
// Инициализация из localStorage // Инициализация из localStorage
@ -13,7 +14,27 @@ export function UserProvider({ children }) {
if (storedUser && storedToken) { if (storedUser && storedToken) {
setUser(JSON.parse(storedUser)); setUser(JSON.parse(storedUser));
setToken(storedToken); 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) => { const login = (user, token) => {
@ -31,7 +52,7 @@ export function UserProvider({ children }) {
}; };
return ( return (
<UserContext.Provider value={{ user, token, login, logout }}> <UserContext.Provider value={{ user, token, login, logout, isCheckingAuth }}>
{children} {children}
</UserContext.Provider> </UserContext.Provider>
); );

View File

@ -25,12 +25,18 @@ function SmtpServersPage() {
}, [page]); }, [page]);
const fetchServers = async (page) => { const fetchServers = async (page) => {
if (!token) {
setError('Нет авторизации');
setServers([]);
setTotal(0);
return;
}
setLoading(true); setLoading(true);
setError(''); setError('');
try { try {
const offset = (page - 1) * PAGE_SIZE; const offset = (page - 1) * PAGE_SIZE;
const res = await fetch(`/api/mail/smtp-servers?limit=${PAGE_SIZE}&offset=${offset}`, { 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(); const data = await res.json();
if (!res.ok) { if (!res.ok) {
@ -51,12 +57,16 @@ function SmtpServersPage() {
}; };
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (!token) {
alert('Нет авторизации');
return;
}
if (!window.confirm('Удалить SMTP-сервер?')) return; if (!window.confirm('Удалить SMTP-сервер?')) return;
setDeleteLoading(id); setDeleteLoading(id);
try { try {
const res = await fetch(`/api/mail/smtp-servers/${id}`, { const res = await fetch(`/api/mail/smtp-servers/${id}`, {
method: 'DELETE', method: 'DELETE',
headers: token ? { Authorization: `Bearer ${token}` } : {} headers: { Authorization: `Bearer ${token}` }
}); });
if (!res.ok) { if (!res.ok) {
const data = await res.json(); const data = await res.json();
@ -77,6 +87,10 @@ function SmtpServersPage() {
const handleEditSave = async (e) => { const handleEditSave = async (e) => {
e.preventDefault(); e.preventDefault();
if (!token) {
alert('Нет авторизации');
return;
}
setEditLoading(true); setEditLoading(true);
try { try {
const { group_id, ...serverData } = editServer; const { group_id, ...serverData } = editServer;
@ -84,7 +98,7 @@ function SmtpServersPage() {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}) Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ ...serverData, user_id: user?.id }) body: JSON.stringify({ ...serverData, user_id: user?.id })
}); });
@ -110,6 +124,10 @@ function SmtpServersPage() {
const handleCreateSave = async (e) => { const handleCreateSave = async (e) => {
e.preventDefault(); e.preventDefault();
if (!token) {
alert('Нет авторизации');
return;
}
setCreateLoading(true); setCreateLoading(true);
try { try {
const { group_id, ...serverData } = createServer; const { group_id, ...serverData } = createServer;
@ -117,7 +135,7 @@ function SmtpServersPage() {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}) Authorization: `Bearer ${token}`
}, },
body: JSON.stringify({ ...serverData, user_id: user?.id }) body: JSON.stringify({ ...serverData, user_id: user?.id })
}); });

View File

@ -5,6 +5,7 @@ import { sequelize } from './models/index.js';
import routes from './routes/index.js'; import routes from './routes/index.js';
import { processScheduledCampaigns } from './service/queueFillerJob.js'; import { processScheduledCampaigns } from './service/queueFillerJob.js';
import { startMailSender } from './service/mailSender.js'; import { startMailSender } from './service/mailSender.js';
import authMiddleware from './middleware/auth.js';
const app = express(); const app = express();
app.use(express.json()); app.use(express.json());
@ -13,7 +14,7 @@ app.get('/', (req, res) => {
res.send('Mail Service is running'); res.send('Mail Service is running');
}); });
app.use('/api/mail', routes); app.use('/api/mail', authMiddleware, routes);
(async () => { (async () => {
try { try {

View File

@ -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 });
}
}