feat: add PIN-based login with server-side cookie sessions
All checks were successful
Deploy HiHala Dashboard / deploy (push) Successful in 6s
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:
@@ -14,3 +14,7 @@ NOCODB_BASE_ID=your-base-id
|
||||
|
||||
# ETL sync secret (for cron auth)
|
||||
ETL_SECRET=your-secret-here
|
||||
|
||||
# Auth
|
||||
ADMIN_PIN=your-pin-code
|
||||
SESSION_SECRET=your-random-session-secret
|
||||
|
||||
31
server/package-lock.json
generated
31
server/package-lock.json
generated
@@ -9,11 +9,13 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"tsx": "^4.19.0",
|
||||
@@ -483,6 +485,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookie-parser": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
|
||||
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
@@ -730,6 +742,25 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser": {
|
||||
"version": "1.4.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
|
||||
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "0.7.2",
|
||||
"cookie-signature": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-parser/node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"tsx": "^4.19.0",
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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
66
server/src/routes/auth.ts
Normal 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;
|
||||
Reference in New Issue
Block a user