Add JWT authentication with animated login page
- Backend: JWT auth with /api/auth/login and /api/auth/verify endpoints - Middleware: requireAuth protects all /api routes except /api/auth - Frontend: Animated characters login page with eye-tracking effects - Auth state persisted in localStorage with token verification on mount - Sidebar logout button, 401 auto-logout, 30-day token expiry - shadcn button, input, checkbox, label components added Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,16 +8,18 @@
|
||||
"start": "tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
"ssh2": "^1.16.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.0"
|
||||
"dotenv": "^16.4.0",
|
||||
"express": "^4.21.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"ssh2": "^1.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/ssh2": "^1.15.0",
|
||||
"@types/cors": "^2.8.17",
|
||||
"typescript": "^5.8.0",
|
||||
"tsx": "^4.19.0"
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/ssh2": "^1.15.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,9 @@ export const config = {
|
||||
),
|
||||
},
|
||||
port: parseInt(process.env.PORT ?? '3002', 10),
|
||||
auth: {
|
||||
email: process.env.AUTH_EMAIL ?? 'admin@eventifyplus.com',
|
||||
password: process.env.AUTH_PASSWORD ?? 'eventify2026',
|
||||
jwtSecret: process.env.JWT_SECRET ?? 'eventify-server-monitor-secret-key-change-in-production',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -5,6 +5,8 @@ import { dirname, resolve } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { config } from './config.js';
|
||||
import { healthRouter } from './routes/health.js';
|
||||
import { authRouter } from './routes/auth.js';
|
||||
import { requireAuth } from './middleware/auth.js';
|
||||
import { sshManager } from './ssh/client.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -22,7 +24,8 @@ app.use(
|
||||
app.use(express.json());
|
||||
|
||||
// ── API Routes ──
|
||||
app.use('/api', healthRouter);
|
||||
app.use('/api/auth', authRouter);
|
||||
app.use('/api', requireAuth, healthRouter);
|
||||
|
||||
// ── Static file serving for production SPA ──
|
||||
const clientDistPath = resolve(__dirname, '../../client/dist');
|
||||
|
||||
19
server/src/middleware/auth.ts
Normal file
19
server/src/middleware/auth.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { config } from '../config.js';
|
||||
|
||||
export function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
res.status(401).json({ error: 'Authentication required' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
jwt.verify(authHeader.slice(7), config.auth.jwtSecret);
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
}
|
||||
42
server/src/routes/auth.ts
Normal file
42
server/src/routes/auth.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Router } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { config } from '../config.js';
|
||||
|
||||
export const authRouter = Router();
|
||||
|
||||
// POST /api/auth/login
|
||||
authRouter.post('/login', (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
res.status(400).json({ error: 'Email and password are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (email === config.auth.email && password === config.auth.password) {
|
||||
const token = jwt.sign(
|
||||
{ email, role: 'admin' },
|
||||
config.auth.jwtSecret,
|
||||
{ expiresIn: '30d' }
|
||||
);
|
||||
res.json({ token });
|
||||
} else {
|
||||
res.status(401).json({ error: 'Invalid email or password' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/auth/verify - check if token is still valid
|
||||
authRouter.get('/verify', (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
res.status(401).json({ valid: false });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(authHeader.slice(7), config.auth.jwtSecret);
|
||||
res.json({ valid: true, user: payload });
|
||||
} catch {
|
||||
res.status(401).json({ valid: false });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user