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:
2026-03-23 16:35:30 +05:30
parent eb6c097664
commit ed13991515
15 changed files with 1015 additions and 16 deletions

View File

@@ -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"
}
}

View File

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

View File

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

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