feat: add PIN-based login with server-side cookie sessions
All checks were successful
Deploy HiHala Dashboard / deploy (push) Successful in 6s

- Server: POST /auth/login (verify PIN, set httpOnly cookie)
- Server: GET /auth/check, POST /auth/logout
- Client: Login page shown when not authenticated
- Session persists 7 days via httpOnly cookie
- PIN stored server-side only (ADMIN_PIN env var)
- Dashboard loads data only after successful auth

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-31 22:02:34 +03:00
parent c99f2abe10
commit 8cf6f9eedd
12 changed files with 331 additions and 4 deletions

View File

@@ -33,3 +33,8 @@ export const nocodb = {
export const etl = {
secret: process.env.ETL_SECRET || '',
};
export const auth = {
adminPin: process.env.ADMIN_PIN || '',
sessionSecret: process.env.SESSION_SECRET || 'hihala-dev-session-secret',
};

View File

@@ -1,15 +1,19 @@
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { server, erp, nocodb } from './config';
import authRoutes from './routes/auth';
import erpRoutes from './routes/erp';
import etlRoutes from './routes/etl';
import seasonsRoutes from './routes/seasons';
const app = express();
app.use(cors());
app.use(cors({ origin: true, credentials: true }));
app.use(cookieParser());
app.use(express.json());
// Mount routes
app.use('/auth', authRoutes);
app.use('/api/erp', erpRoutes);
app.use('/api/etl', etlRoutes);
app.use('/api/seasons', seasonsRoutes);

66
server/src/routes/auth.ts Normal file
View File

@@ -0,0 +1,66 @@
import { Router, Request, Response } from 'express';
import crypto from 'crypto';
import { auth } from '../config';
const router = Router();
// In-memory session store (simple — works for single server)
const sessions = new Map<string, { createdAt: number }>();
const SESSION_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
function generateSessionId(): string {
return crypto.randomBytes(32).toString('hex');
}
function isValidSession(sessionId: string): boolean {
const session = sessions.get(sessionId);
if (!session) return false;
if (Date.now() - session.createdAt > SESSION_MAX_AGE) {
sessions.delete(sessionId);
return false;
}
return true;
}
// POST /auth/login — verify PIN, set session cookie
router.post('/login', (req: Request, res: Response) => {
const { pin } = req.body;
if (!auth.adminPin) {
res.status(503).json({ error: 'Auth not configured' });
return;
}
if (pin !== auth.adminPin) {
res.status(401).json({ error: 'Invalid PIN' });
return;
}
const sessionId = generateSessionId();
sessions.set(sessionId, { createdAt: Date.now() });
res.cookie('hihala_session', sessionId, {
httpOnly: true,
sameSite: 'lax',
maxAge: SESSION_MAX_AGE,
path: '/',
});
res.json({ ok: true });
});
// GET /auth/check — check if session is valid
router.get('/check', (req: Request, res: Response) => {
const sessionId = req.cookies?.hihala_session;
res.json({ authenticated: !!sessionId && isValidSession(sessionId) });
});
// POST /auth/logout — destroy session
router.post('/logout', (req: Request, res: Response) => {
const sessionId = req.cookies?.hihala_session;
if (sessionId) sessions.delete(sessionId);
res.clearCookie('hihala_session', { path: '/' });
res.json({ ok: true });
});
export default router;