mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7f46d6c68 |
@@ -20,7 +20,7 @@ import { getDirName } from '../utils.js';
|
|||||||
import { demoRouter } from './routes/demoRouter.js';
|
import { demoRouter } from './routes/demoRouter.js';
|
||||||
import logger from '../services/logger.js';
|
import logger from '../services/logger.js';
|
||||||
import { listingsRouter } from './routes/listingsRouter.js';
|
import { listingsRouter } from './routes/listingsRouter.js';
|
||||||
import { getSettings } from '../services/storage/settingsStorage.js';
|
import { getSettings, getOrCreateSessionSecret } from '../services/storage/settingsStorage.js';
|
||||||
import { dashboardRouter } from './routes/dashboardRouter.js';
|
import { dashboardRouter } from './routes/dashboardRouter.js';
|
||||||
import { backupRouter } from './routes/backupRouter.js';
|
import { backupRouter } from './routes/backupRouter.js';
|
||||||
import { trackingRouter } from './routes/trackingRoute.js';
|
import { trackingRouter } from './routes/trackingRoute.js';
|
||||||
@@ -28,9 +28,10 @@ import { registerMcpRoutes } from '../mcp/mcpHttpRoute.js';
|
|||||||
const service = restana();
|
const service = restana();
|
||||||
const staticService = files(path.join(getDirName(), '../ui/public'));
|
const staticService = files(path.join(getDirName(), '../ui/public'));
|
||||||
const PORT = (await getSettings()).port || 9998;
|
const PORT = (await getSettings()).port || 9998;
|
||||||
|
const sessionSecret = await getOrCreateSessionSecret();
|
||||||
|
|
||||||
service.use(bodyParser.json());
|
service.use(bodyParser.json());
|
||||||
service.use(cookieSession());
|
service.use(cookieSession(sessionSecret));
|
||||||
service.use(staticService);
|
service.use(staticService);
|
||||||
service.use('/api/admin', authInterceptor());
|
service.use('/api/admin', authInterceptor());
|
||||||
service.use('/api/jobs', authInterceptor());
|
service.use('/api/jobs', authInterceptor());
|
||||||
|
|||||||
@@ -9,6 +9,27 @@ import * as hasher from '../../services/security/hash.js';
|
|||||||
import { trackDemoAccessed } from '../../services/tracking/Tracker.js';
|
import { trackDemoAccessed } from '../../services/tracking/Tracker.js';
|
||||||
import logger from '../../services/logger.js';
|
import logger from '../../services/logger.js';
|
||||||
import { getSettings } from '../../services/storage/settingsStorage.js';
|
import { getSettings } from '../../services/storage/settingsStorage.js';
|
||||||
|
|
||||||
|
const MAX_LOGIN_ATTEMPTS = 10;
|
||||||
|
const LOGIN_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
||||||
|
const loginAttempts = new Map(); // ip -> { count, firstAttempt }
|
||||||
|
|
||||||
|
function getClientIp(req) {
|
||||||
|
const forwarded = req.headers['x-forwarded-for'];
|
||||||
|
return (forwarded ? forwarded.split(',')[0] : req.socket?.remoteAddress) || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRateLimited(ip) {
|
||||||
|
const now = Date.now();
|
||||||
|
const record = loginAttempts.get(ip);
|
||||||
|
if (!record || now - record.firstAttempt > LOGIN_WINDOW_MS) {
|
||||||
|
loginAttempts.set(ip, { count: 1, firstAttempt: now });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
record.count++;
|
||||||
|
return record.count > MAX_LOGIN_ATTEMPTS;
|
||||||
|
}
|
||||||
|
|
||||||
const service = restana();
|
const service = restana();
|
||||||
const loginRouter = service.newRouter();
|
const loginRouter = service.newRouter();
|
||||||
loginRouter.get('/user', async (req, res) => {
|
loginRouter.get('/user', async (req, res) => {
|
||||||
@@ -25,6 +46,12 @@ loginRouter.get('/user', async (req, res) => {
|
|||||||
res.send();
|
res.send();
|
||||||
});
|
});
|
||||||
loginRouter.post('/', async (req, res) => {
|
loginRouter.post('/', async (req, res) => {
|
||||||
|
const ip = getClientIp(req);
|
||||||
|
if (isRateLimited(ip)) {
|
||||||
|
logger.error(`Login rate limit exceeded for IP ${ip}`);
|
||||||
|
res.send(429);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const settings = await getSettings();
|
const settings = await getSettings();
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
const user = userStorage.getUsers(true).find((user) => user.username === username);
|
const user = userStorage.getUsers(true).find((user) => user.username === username);
|
||||||
@@ -38,6 +65,8 @@ loginRouter.post('/', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.session.currentUser = user.id;
|
req.session.currentUser = user.id;
|
||||||
|
req.session.createdAt = Date.now();
|
||||||
|
loginAttempts.delete(ip);
|
||||||
userStorage.setLastLoginToNow({ userId: user.id });
|
userStorage.setLastLoginToNow({ userId: user.id });
|
||||||
res.send(200);
|
res.send(200);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,12 +5,17 @@
|
|||||||
|
|
||||||
import * as userStorage from '../services/storage/userStorage.js';
|
import * as userStorage from '../services/storage/userStorage.js';
|
||||||
import cookieSession from 'cookie-session';
|
import cookieSession from 'cookie-session';
|
||||||
import { nanoid } from 'nanoid';
|
const SESSION_MAX_AGE = 2 * 60 * 60 * 1000; // 2 hours
|
||||||
const unauthorized = (res) => {
|
const unauthorized = (res) => {
|
||||||
return res.send(401);
|
return res.send(401);
|
||||||
};
|
};
|
||||||
const isUnauthorized = (req) => {
|
const isUnauthorized = (req) => {
|
||||||
return req.session.currentUser == null;
|
if (req.session.currentUser == null) return true;
|
||||||
|
if (Date.now() - req.session.createdAt > SESSION_MAX_AGE) {
|
||||||
|
req.session = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
const isAdmin = (req) => {
|
const isAdmin = (req) => {
|
||||||
if (!isUnauthorized(req)) {
|
if (!isUnauthorized(req)) {
|
||||||
@@ -37,12 +42,11 @@ const adminInterceptor = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const cookieSession$0 = (userId) => {
|
const cookieSession$0 = (secret) => {
|
||||||
return cookieSession({
|
return cookieSession({
|
||||||
name: 'fredy-admin-session',
|
name: 'fredy-admin-session',
|
||||||
keys: ['fredy', 'super', 'fancy', 'key', nanoid()],
|
keys: [secret],
|
||||||
userId,
|
maxAge: SESSION_MAX_AGE,
|
||||||
maxAge: 2 * 60 * 60 * 1000, // 2 hours
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export { cookieSession$0 as cookieSession };
|
export { cookieSession$0 as cookieSession };
|
||||||
|
|||||||
@@ -67,6 +67,19 @@ export async function getSettings() {
|
|||||||
return cachedSettingsConfig;
|
return cachedSettingsConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create a persistent session signing secret.
|
||||||
|
* Generated once and stored in the settings table under the key 'session_secret'.
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
export async function getOrCreateSessionSecret() {
|
||||||
|
const settings = await getSettings();
|
||||||
|
if (settings.session_secret) return settings.session_secret;
|
||||||
|
const secret = nanoid(64);
|
||||||
|
upsertSettings({ session_secret: secret });
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upsert settings rows.
|
* Upsert settings rows.
|
||||||
* - Accepts an object map of name -> value, or an entry {name, value}.
|
* - Accepts an object map of name -> value, or an entry {name, value}.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "20.1.1",
|
"version": "20.1.2",
|
||||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
|||||||
Reference in New Issue
Block a user