mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
moving from restana to fastify
This commit is contained in:
26
CLAUDE.md
26
CLAUDE.md
@@ -8,7 +8,7 @@ Fredy is a self-hosted real estate finder for Germany. It scrapes German real es
|
||||
|
||||
- Node.js >= 22, ESM-only (`"type": "module"`)
|
||||
- Default port: 9998, default login: admin / admin
|
||||
- SQLite via `better-sqlite3` (synchronous — all DB ops are sync; only network I/O is async)
|
||||
- SQLite via `better-sqlite3` (synchronous - all DB ops are sync; only network I/O is async)
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -66,13 +66,13 @@ scheduler (every N minutes) or manual trigger via POST /api/jobs/:id/run
|
||||
|
||||
### Plugin systems
|
||||
|
||||
**Providers** (`lib/provider/*.js`) — each module exports:
|
||||
- `metaInformation` — `{ id, name, baseUrl }`
|
||||
- `config` — `ProviderConfig` with `requiredFieldNames`, `crawlContainer`, `crawlFields`, `sortByDateParam`, `normalize()`, `filter()`, optional `getListings()`, `fetchDetails()`, `activeTester()`
|
||||
- `init(sourceConfig, blacklist)` — called before each job run; providers are **stateful modules** holding mutable `url` and `appliedBlackList` at module scope
|
||||
**Providers** (`lib/provider/*.js`) - each module exports:
|
||||
- `metaInformation` - `{ id, name, baseUrl }`
|
||||
- `config` - `ProviderConfig` with `requiredFieldNames`, `crawlContainer`, `crawlFields`, `sortByDateParam`, `normalize()`, `filter()`, optional `getListings()`, `fetchDetails()`, `activeTester()`
|
||||
- `init(sourceConfig, blacklist)` - called before each job run; providers are **stateful modules** holding mutable `url` and `appliedBlackList` at module scope
|
||||
|
||||
**Notification adapters** (`lib/notification/adapter/*.js`) — each exports:
|
||||
- `config` — `{ id, name, description, fields }` (drives the UI form)
|
||||
**Notification adapters** (`lib/notification/adapter/*.js`) - each exports:
|
||||
- `config` - `{ id, name, description, fields }` (drives the UI form)
|
||||
- `send({ serviceName, newListings, notificationConfig, jobKey, baseUrl })`
|
||||
- Loaded dynamically at startup via `fs.readdirSync`
|
||||
|
||||
@@ -98,18 +98,18 @@ scheduler (every N minutes) or manual trigger via POST /api/jobs/:id/run
|
||||
### MCP server
|
||||
|
||||
Two transports:
|
||||
1. **stdio** (`lib/mcp/stdio.js`) — for Claude Desktop/LM Studio; opens its own DB connection (main process need not be running)
|
||||
2. **HTTP** (`/api/mcp`) — authenticated via Bearer token (`mcp_token` column in `users` table)
|
||||
1. **stdio** (`lib/mcp/stdio.js`) - for Claude Desktop/LM Studio; opens its own DB connection (main process need not be running)
|
||||
2. **HTTP** (`/api/mcp`) - authenticated via Bearer token (`mcp_token` column in `users` table)
|
||||
|
||||
Tools: `list_jobs`, `get_job`, `list_listings`, `get_listing`, `get_current_date_time`. Responses are Markdown via `lib/mcp/mcpNormalizer.js`.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- **ESM only** — `import`/`export` everywhere, no CommonJS
|
||||
- **JSDoc typedefs** (no TypeScript) in `lib/types/` — `listing.js`, `job.js`, `filter.js`, `providerConfig.js`
|
||||
- **Copyright header** required on all `.js` files — enforced by `lint-staged` pre-commit hook via `copyright.js`
|
||||
- **ESM only** - `import`/`export` everywhere, no CommonJS
|
||||
- **JSDoc typedefs** (no TypeScript) in `lib/types/` - `listing.js`, `job.js`, `filter.js`, `providerConfig.js`
|
||||
- **Copyright header** required on all `.js` files - enforced by `lint-staged` pre-commit hook via `copyright.js`
|
||||
- **`NoNewListingsWarning`** (`lib/errors.js`) is used as control flow to short-circuit the pipeline (not an error)
|
||||
- **Test fixtures** in `test/testFixtures/` — HTML/JSON snapshots per provider; `TEST_MODE=offline` mocks `puppeteerExtractor` and global `fetch` via `test/offlineFixtures.js`
|
||||
- **Test fixtures** in `test/testFixtures/` - HTML/JSON snapshots per provider; `TEST_MODE=offline` mocks `puppeteerExtractor` and global `fetch` via `test/offlineFixtures.js`
|
||||
- **`conf/config.json`** is the only runtime config file; created with defaults if missing
|
||||
|
||||
## Coding
|
||||
|
||||
@@ -43,13 +43,13 @@ for i in $(seq 1 30); do
|
||||
done
|
||||
|
||||
# Verify the DB is readable/writable via the API.
|
||||
# /api/demo is unauthenticated and reads the settings table — if SQLite is broken this returns an error.
|
||||
# /api/demo is unauthenticated and reads the settings table - if SQLite is broken this returns an error.
|
||||
echo "Testing DB via API (/api/demo)..."
|
||||
DEMO_RESPONSE=$(docker exec fredy curl -sf http://localhost:9998/api/demo 2>&1)
|
||||
if echo "$DEMO_RESPONSE" | grep -q "demoMode"; then
|
||||
echo "DB is readable (got demoMode from /api/demo)"
|
||||
else
|
||||
echo "DB check failed — unexpected response from /api/demo: $DEMO_RESPONSE"
|
||||
echo "DB check failed - unexpected response from /api/demo: $DEMO_RESPONSE"
|
||||
docker logs fredy
|
||||
exit 1
|
||||
fi
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
142
lib/api/api.js
142
lib/api/api.js
@@ -3,64 +3,100 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import { notificationAdapterRouter } from './routes/notificationAdapterRouter.js';
|
||||
import { authInterceptor, cookieSession, adminInterceptor } from './security.js';
|
||||
import { generalSettingsRouter } from './routes/generalSettingsRoute.js';
|
||||
import { providerRouter } from './routes/providerRouter.js';
|
||||
import { versionRouter } from './routes/versionRouter.js';
|
||||
import { loginRouter } from './routes/loginRoute.js';
|
||||
import { userRouter } from './routes/userRoute.js';
|
||||
import { userSettingsRouter } from './routes/userSettingsRoute.js';
|
||||
import { jobRouter } from './routes/jobRouter.js';
|
||||
import bodyParser from 'body-parser';
|
||||
import restana from 'restana';
|
||||
import files from 'serve-static';
|
||||
import Fastify from 'fastify';
|
||||
import fastifyHelmet from '@fastify/helmet';
|
||||
import fastifyCookie from '@fastify/cookie';
|
||||
import fastifySession from '@fastify/session';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import path from 'path';
|
||||
import { getDirName } from '../utils.js';
|
||||
import { demoRouter } from './routes/demoRouter.js';
|
||||
import logger from '../services/logger.js';
|
||||
import { listingsRouter } from './routes/listingsRouter.js';
|
||||
import { getSettings, getOrCreateSessionSecret } from '../services/storage/settingsStorage.js';
|
||||
import { dashboardRouter } from './routes/dashboardRouter.js';
|
||||
import { backupRouter } from './routes/backupRouter.js';
|
||||
import { trackingRouter } from './routes/trackingRoute.js';
|
||||
import logger from '../services/logger.js';
|
||||
import { authHook, adminHook } from './security.js';
|
||||
|
||||
import loginPlugin from './routes/loginRoute.js';
|
||||
import demoPlugin from './routes/demoRouter.js';
|
||||
import jobPlugin from './routes/jobRouter.js';
|
||||
import versionPlugin from './routes/versionRouter.js';
|
||||
import listingsPlugin from './routes/listingsRouter.js';
|
||||
import dashboardPlugin from './routes/dashboardRouter.js';
|
||||
import userSettingsPlugin from './routes/userSettingsRoute.js';
|
||||
import trackingPlugin from './routes/trackingRoute.js';
|
||||
import generalSettingsPlugin from './routes/generalSettingsRoute.js';
|
||||
import backupPlugin from './routes/backupRouter.js';
|
||||
import userPlugin from './routes/userRoute.js';
|
||||
import notificationAdapterPlugin from './routes/notificationAdapterRouter.js';
|
||||
import providerPlugin from './routes/providerRouter.js';
|
||||
import { registerMcpRoutes } from '../mcp/mcpHttpRoute.js';
|
||||
const service = restana();
|
||||
const staticService = files(path.join(getDirName(), '../ui/public'));
|
||||
|
||||
const PORT = (await getSettings()).port || 9998;
|
||||
const sessionSecret = await getOrCreateSessionSecret();
|
||||
const SESSION_MAX_AGE = 2 * 60 * 60 * 1000;
|
||||
|
||||
service.use(bodyParser.json());
|
||||
service.use(cookieSession(sessionSecret));
|
||||
service.use(staticService);
|
||||
service.use('/api/admin', authInterceptor());
|
||||
service.use('/api/jobs', authInterceptor());
|
||||
service.use('/api/version', authInterceptor());
|
||||
service.use('/api/listings', authInterceptor());
|
||||
service.use('/api/dashboard', authInterceptor());
|
||||
service.use('/api/user/settings', authInterceptor());
|
||||
service.use('/api/tracking', authInterceptor());
|
||||
|
||||
// /admin can only be accessed when user is having admin permissions
|
||||
service.use('/api/admin', adminInterceptor());
|
||||
service.use('/api/jobs/notificationAdapter', notificationAdapterRouter);
|
||||
service.use('/api/admin/generalSettings', generalSettingsRouter);
|
||||
service.use('/api/admin/backup', backupRouter);
|
||||
service.use('/api/jobs/provider', providerRouter);
|
||||
service.use('/api/admin/users', userRouter);
|
||||
service.use('/api/user/settings', userSettingsRouter);
|
||||
service.use('/api/version', versionRouter);
|
||||
service.use('/api/jobs', jobRouter);
|
||||
service.use('/api/login', loginRouter);
|
||||
service.use('/api/listings', listingsRouter);
|
||||
service.use('/api/dashboard', dashboardRouter);
|
||||
service.use('/api/tracking', trackingRouter);
|
||||
//this route is unsecured intentionally as it is being queried from the login page
|
||||
service.use('/api/demo', demoRouter);
|
||||
|
||||
// MCP Streamable HTTP endpoint (secured via Bearer token, not cookie-session)
|
||||
registerMcpRoutes(service);
|
||||
|
||||
service.start(PORT).then(() => {
|
||||
logger.debug(`Started API service on port ${PORT}`);
|
||||
const fastify = Fastify({
|
||||
logger: false,
|
||||
bodyLimit: 50 * 1024 * 1024, // 50 MB for backup uploads
|
||||
});
|
||||
|
||||
// Security headers (CSP disabled to avoid breaking the SPA)
|
||||
await fastify.register(fastifyHelmet, { contentSecurityPolicy: false });
|
||||
|
||||
// Cookie + session (in-memory store, signed cookie)
|
||||
await fastify.register(fastifyCookie);
|
||||
await fastify.register(fastifySession, {
|
||||
secret: sessionSecret,
|
||||
cookieName: 'fredy-admin-session',
|
||||
cookie: {
|
||||
maxAge: SESSION_MAX_AGE,
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
},
|
||||
saveUninitialized: false,
|
||||
});
|
||||
|
||||
// Serve the React SPA from ui/public/
|
||||
await fastify.register(fastifyStatic, {
|
||||
root: path.join(getDirName(), '../ui/public'),
|
||||
wildcard: false,
|
||||
});
|
||||
|
||||
// Public routes - no auth required
|
||||
fastify.register(loginPlugin, { prefix: '/api/login' });
|
||||
fastify.register(demoPlugin, { prefix: '/api/demo' });
|
||||
|
||||
// User-authenticated routes
|
||||
fastify.register(async (app) => {
|
||||
app.addHook('preHandler', authHook);
|
||||
app.register(jobPlugin, { prefix: '/api/jobs' });
|
||||
app.register(notificationAdapterPlugin, { prefix: '/api/jobs/notificationAdapter' });
|
||||
app.register(providerPlugin, { prefix: '/api/jobs/provider' });
|
||||
app.register(versionPlugin, { prefix: '/api/version' });
|
||||
app.register(listingsPlugin, { prefix: '/api/listings' });
|
||||
app.register(dashboardPlugin, { prefix: '/api/dashboard' });
|
||||
app.register(userSettingsPlugin, { prefix: '/api/user/settings' });
|
||||
app.register(trackingPlugin, { prefix: '/api/tracking' });
|
||||
});
|
||||
|
||||
// Admin-only routes
|
||||
fastify.register(async (app) => {
|
||||
app.addHook('preHandler', authHook);
|
||||
app.addHook('preHandler', adminHook);
|
||||
app.register(generalSettingsPlugin, { prefix: '/api/admin/generalSettings' });
|
||||
app.register(backupPlugin, { prefix: '/api/admin/backup' });
|
||||
app.register(userPlugin, { prefix: '/api/admin/users' });
|
||||
});
|
||||
|
||||
// MCP Streamable HTTP (Bearer token auth - no session)
|
||||
registerMcpRoutes(fastify);
|
||||
|
||||
// SPA fallback - serve index.html for all non-API GET requests
|
||||
fastify.setNotFoundHandler((request, reply) => {
|
||||
if (!request.url.startsWith('/api/')) {
|
||||
return reply.sendFile('index.html');
|
||||
}
|
||||
return reply.code(404).send({ error: 'Not found' });
|
||||
});
|
||||
|
||||
await fastify.listen({ port: PORT, host: '0.0.0.0' });
|
||||
logger.debug(`Started API service on port ${PORT}`);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import {
|
||||
buildBackupFileName,
|
||||
createBackupZip,
|
||||
@@ -12,64 +11,41 @@ import {
|
||||
} from '../../services/storage/backupRestoreService.js';
|
||||
|
||||
/**
|
||||
* Backup & Restore Admin Router
|
||||
*
|
||||
* Endpoints:
|
||||
* - GET /api/admin/backup
|
||||
* Returns the current database as a zip download. Content-Type: application/zip
|
||||
* - POST /api/admin/backup/restore?dryRun=true
|
||||
* Accepts a zip file (raw body). Returns a compatibility report, does not restore.
|
||||
* - POST /api/admin/backup/restore?force=true|false
|
||||
* Accepts a zip file (raw body). Restores the database; when incompatible and force=false, returns 400.
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
const service = restana();
|
||||
const backupRouter = service.newRouter();
|
||||
export default async function backupPlugin(fastify) {
|
||||
// Parse raw binary uploads as Buffer
|
||||
fastify.addContentTypeParser(
|
||||
['application/zip', 'application/octet-stream'],
|
||||
{ parseAs: 'buffer' },
|
||||
(req, body, done) => done(null, body),
|
||||
);
|
||||
|
||||
backupRouter.get('/', async (req, res) => {
|
||||
const zipBuffer = await createBackupZip();
|
||||
const fileName = await buildBackupFileName();
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
||||
res.send(zipBuffer);
|
||||
});
|
||||
fastify.get('/', async (_request, reply) => {
|
||||
const zipBuffer = await createBackupZip();
|
||||
const fileName = await buildBackupFileName();
|
||||
reply.header('Content-Type', 'application/zip');
|
||||
reply.header('Content-Disposition', `attachment; filename="${fileName}"`);
|
||||
return reply.send(zipBuffer);
|
||||
});
|
||||
|
||||
/**
|
||||
* Read the full request body as a Buffer. Used for raw zip uploads.
|
||||
* @param {import('http').IncomingMessage} req
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
req.on('data', (c) => chunks.push(c));
|
||||
req.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
req.on('error', (e) => reject(e));
|
||||
fastify.post('/restore', async (request, reply) => {
|
||||
const { dryRun = 'false', force = 'false' } = request.query || {};
|
||||
const doDryRun = String(dryRun) === 'true';
|
||||
const doForce = String(force) === 'true';
|
||||
const body = request.body; // Buffer from addContentTypeParser
|
||||
|
||||
if (doDryRun) {
|
||||
return precheckRestore(body);
|
||||
}
|
||||
|
||||
try {
|
||||
return restoreFromZip(body, { force: doForce });
|
||||
} catch (e) {
|
||||
return reply.code(400).send({
|
||||
message: e?.message || 'Restore failed',
|
||||
details: e?.payload || null,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Upload endpoint. Accepts raw zip (Content-Type: application/zip or application/octet-stream)
|
||||
// Query parameters:
|
||||
// - dryRun=true => only validate and return compatibility info
|
||||
// - force=true => proceed even if incompatible
|
||||
backupRouter.post('/restore', async (req, res) => {
|
||||
const { dryRun = 'false', force = 'false' } = req.query || {};
|
||||
const doDryRun = String(dryRun) === 'true';
|
||||
const doForce = String(force) === 'true';
|
||||
const body = await readBody(req);
|
||||
|
||||
if (doDryRun) {
|
||||
res.body = await precheckRestore(body);
|
||||
return res.send();
|
||||
}
|
||||
|
||||
try {
|
||||
res.body = await restoreFromZip(body, { force: doForce });
|
||||
return res.send();
|
||||
} catch (e) {
|
||||
res.statusCode = 400;
|
||||
res.body = { message: e?.message || 'Restore failed', details: e?.payload || null };
|
||||
return res.send();
|
||||
}
|
||||
});
|
||||
|
||||
export { backupRouter };
|
||||
|
||||
@@ -3,23 +3,14 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import * as jobStorage from '../../services/storage/jobStorage.js';
|
||||
import * as userStorage from '../../services/storage/userStorage.js';
|
||||
import { getListingsKpisForJobIds, getProviderDistributionForJobIds } from '../../services/storage/listingsStorage.js';
|
||||
import { getSettings } from '../../services/storage/settingsStorage.js';
|
||||
import { isAdmin } from '../security.js';
|
||||
|
||||
const service = restana();
|
||||
export const dashboardRouter = service.newRouter();
|
||||
|
||||
function isAdmin(req) {
|
||||
const user = req.session?.currentUser ? userStorage.getUser(req.session.currentUser) : null;
|
||||
return !!user?.isAdmin;
|
||||
}
|
||||
|
||||
function getAccessibleJobs(req) {
|
||||
const currentUser = req.session.currentUser;
|
||||
const admin = isAdmin(req);
|
||||
function getAccessibleJobs(request) {
|
||||
const currentUser = request.session.currentUser;
|
||||
const admin = isAdmin(request);
|
||||
return jobStorage
|
||||
.getJobs()
|
||||
.filter((job) => admin || job.userId === currentUser || job.shared_with_user.includes(currentUser));
|
||||
@@ -29,43 +20,45 @@ function cap(val) {
|
||||
return String(val).charAt(0).toUpperCase() + String(val).slice(1);
|
||||
}
|
||||
|
||||
dashboardRouter.get('/', async (req, res) => {
|
||||
const jobs = getAccessibleJobs(req);
|
||||
const settings = await getSettings();
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function dashboardPlugin(fastify) {
|
||||
fastify.get('/', async (request) => {
|
||||
const jobs = getAccessibleJobs(request);
|
||||
const settings = await getSettings();
|
||||
|
||||
// KPIs
|
||||
const totalJobs = jobs.length;
|
||||
const totalListings = jobs.reduce((sum, j) => sum + (j.numberOfFoundListings || 0), 0);
|
||||
const jobIds = jobs.map((j) => j.id);
|
||||
const { numberOfActiveListings, medianPriceOfListings } = getListingsKpisForJobIds(jobIds);
|
||||
// Build Pie data in a simple shape the frontend can consume directly
|
||||
// Shape: { labels: string[], values: number[] } with values as percentages
|
||||
const providerPieRaw = getProviderDistributionForJobIds(jobIds);
|
||||
const providerPie = Array.isArray(providerPieRaw)
|
||||
? {
|
||||
labels: providerPieRaw.map((p) => cap(p.type)),
|
||||
values: providerPieRaw.map((p) => Number(p.value) || 0),
|
||||
}
|
||||
: providerPieRaw && typeof providerPieRaw === 'object'
|
||||
const totalJobs = jobs.length;
|
||||
const totalListings = jobs.reduce((sum, j) => sum + (j.numberOfFoundListings || 0), 0);
|
||||
const jobIds = jobs.map((j) => j.id);
|
||||
const { numberOfActiveListings, medianPriceOfListings } = getListingsKpisForJobIds(jobIds);
|
||||
|
||||
const providerPieRaw = getProviderDistributionForJobIds(jobIds);
|
||||
const providerPie = Array.isArray(providerPieRaw)
|
||||
? {
|
||||
labels: Array.isArray(providerPieRaw.labels) ? providerPieRaw.labels : [],
|
||||
values: Array.isArray(providerPieRaw.values) ? providerPieRaw.values : [],
|
||||
labels: providerPieRaw.map((p) => cap(p.type)),
|
||||
values: providerPieRaw.map((p) => Number(p.value) || 0),
|
||||
}
|
||||
: { labels: [], values: [] };
|
||||
: providerPieRaw && typeof providerPieRaw === 'object'
|
||||
? {
|
||||
labels: Array.isArray(providerPieRaw.labels) ? providerPieRaw.labels : [],
|
||||
values: Array.isArray(providerPieRaw.values) ? providerPieRaw.values : [],
|
||||
}
|
||||
: { labels: [], values: [] };
|
||||
|
||||
res.body = {
|
||||
general: {
|
||||
interval: settings.interval,
|
||||
lastRun: settings.lastRun || null,
|
||||
nextRun: settings.lastRun == null ? 0 : settings.lastRun + settings.interval * 60000,
|
||||
},
|
||||
kpis: {
|
||||
totalJobs,
|
||||
totalListings,
|
||||
numberOfActiveListings,
|
||||
medianPriceOfListings,
|
||||
},
|
||||
pie: providerPie,
|
||||
};
|
||||
res.send();
|
||||
});
|
||||
return {
|
||||
general: {
|
||||
interval: settings.interval,
|
||||
lastRun: settings.lastRun || null,
|
||||
nextRun: settings.lastRun == null ? 0 : settings.lastRun + settings.interval * 60000,
|
||||
},
|
||||
kpis: {
|
||||
totalJobs,
|
||||
totalListings,
|
||||
numberOfActiveListings,
|
||||
medianPriceOfListings,
|
||||
},
|
||||
pie: providerPie,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import { getSettings } from '../../services/storage/settingsStorage.js';
|
||||
const service = restana();
|
||||
const demoRouter = service.newRouter();
|
||||
|
||||
demoRouter.get('/', async (req, res) => {
|
||||
const settings = await getSettings();
|
||||
res.body = Object.assign({}, { demoMode: settings.demoMode });
|
||||
res.send();
|
||||
});
|
||||
|
||||
export { demoRouter };
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function demoPlugin(fastify) {
|
||||
fastify.get('/', async () => {
|
||||
const settings = await getSettings();
|
||||
return { demoMode: settings.demoMode };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,43 +3,42 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import { getDirName } from '../../utils.js';
|
||||
import fs from 'fs';
|
||||
import { ensureDemoUserExists } from '../../services/storage/userStorage.js';
|
||||
import logger from '../../services/logger.js';
|
||||
import { getSettings, upsertSettings } from '../../services/storage/settingsStorage.js';
|
||||
import { isAdmin } from '../security.js';
|
||||
const service = restana();
|
||||
const generalSettingsRouter = service.newRouter();
|
||||
|
||||
generalSettingsRouter.get('/', async (req, res) => {
|
||||
res.body = Object.assign({}, await getSettings());
|
||||
res.send();
|
||||
});
|
||||
generalSettingsRouter.post('/', async (req, res) => {
|
||||
const { sqlitepath, ...appSettings } = req.body || {};
|
||||
if (typeof appSettings.baseUrl === 'string') {
|
||||
appSettings.baseUrl = appSettings.baseUrl.trim().replace(/\/$/, '');
|
||||
}
|
||||
const localSettings = await getSettings();
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function generalSettingsPlugin(fastify) {
|
||||
fastify.get('/', async () => {
|
||||
return Object.assign({}, await getSettings());
|
||||
});
|
||||
|
||||
if (localSettings.demoMode && !isAdmin(req)) {
|
||||
res.send(new Error('In demo mode, it is not allowed to change these settings.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof sqlitepath !== 'undefined') {
|
||||
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({ sqlitepath }));
|
||||
fastify.post('/', async (request, reply) => {
|
||||
const { sqlitepath, ...appSettings } = request.body || {};
|
||||
if (typeof appSettings.baseUrl === 'string') {
|
||||
appSettings.baseUrl = appSettings.baseUrl.trim().replace(/\/$/, '');
|
||||
}
|
||||
upsertSettings(appSettings);
|
||||
ensureDemoUserExists();
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
res.send(new Error('Error while trying to write settings.'));
|
||||
return;
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
export { generalSettingsRouter };
|
||||
const localSettings = await getSettings();
|
||||
|
||||
if (localSettings.demoMode && !isAdmin(request)) {
|
||||
return reply.code(403).send({ error: 'In demo mode, it is not allowed to change these settings.' });
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof sqlitepath !== 'undefined') {
|
||||
fs.writeFileSync(`${getDirName()}/../conf/config.json`, JSON.stringify({ sqlitepath }));
|
||||
}
|
||||
upsertSettings(appSettings);
|
||||
ensureDemoUserExists();
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
return reply.code(500).send({ error: 'Error while trying to write settings.' });
|
||||
}
|
||||
return reply.send();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import * as jobStorage from '../../services/storage/jobStorage.js';
|
||||
import * as userStorage from '../../services/storage/userStorage.js';
|
||||
import { isAdmin } from '../security.js';
|
||||
@@ -13,257 +12,234 @@ import { isRunning as isJobRunning } from '../../services/jobs/run-state.js';
|
||||
import { addClient as addSseClient, removeClient } from '../../services/sse/sse-broker.js';
|
||||
import { getSettings } from '../../services/storage/settingsStorage.js';
|
||||
|
||||
const service = restana();
|
||||
const jobRouter = service.newRouter();
|
||||
|
||||
const DEMO_JOB_NAME = 'Demo-Job';
|
||||
|
||||
function doesJobBelongsToUser(job, req) {
|
||||
const userId = req.session.currentUser;
|
||||
if (userId == null) {
|
||||
return false;
|
||||
}
|
||||
function doesJobBelongsToUser(job, request) {
|
||||
const userId = request.session.currentUser;
|
||||
if (userId == null) return false;
|
||||
const user = userStorage.getUser(userId);
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
if (user == null) return false;
|
||||
return user.isAdmin || job.userId === user.id;
|
||||
}
|
||||
|
||||
jobRouter.get('/', async (req, res) => {
|
||||
const isUserAdmin = isAdmin(req);
|
||||
//show only the jobs which belongs to the user (or all of the user is an admin)
|
||||
res.body = jobStorage
|
||||
.getJobs()
|
||||
.filter(
|
||||
(job) =>
|
||||
isUserAdmin || job.userId === req.session.currentUser || job.shared_with_user.includes(req.session.currentUser),
|
||||
)
|
||||
.map((job) => {
|
||||
return {
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function jobPlugin(fastify) {
|
||||
fastify.get('/', async (request) => {
|
||||
const isUserAdmin = isAdmin(request);
|
||||
return jobStorage
|
||||
.getJobs()
|
||||
.filter(
|
||||
(job) =>
|
||||
isUserAdmin ||
|
||||
job.userId === request.session.currentUser ||
|
||||
job.shared_with_user.includes(request.session.currentUser),
|
||||
)
|
||||
.map((job) => ({
|
||||
...job,
|
||||
running: isJobRunning(job.id),
|
||||
isOnlyShared:
|
||||
!isUserAdmin &&
|
||||
job.userId !== req.session.currentUser &&
|
||||
job.shared_with_user.includes(req.session.currentUser),
|
||||
};
|
||||
});
|
||||
|
||||
res.send();
|
||||
});
|
||||
|
||||
jobRouter.get('/data', async (req, res) => {
|
||||
const { page, pageSize = 50, activityFilter, sortfield = null, sortdir = 'asc', freeTextFilter } = req.query || {};
|
||||
|
||||
// normalize booleans
|
||||
const toBool = (v) => {
|
||||
if (v === true || v === 'true' || v === 1 || v === '1') return true;
|
||||
if (v === false || v === 'false' || v === 0 || v === '0') return false;
|
||||
return null;
|
||||
};
|
||||
const normalizedActivity = toBool(activityFilter);
|
||||
|
||||
const queryResult = jobStorage.queryJobs({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 50,
|
||||
freeTextFilter: freeTextFilter || null,
|
||||
activityFilter: normalizedActivity,
|
||||
sortField: sortfield || null,
|
||||
sortDir: sortdir === 'desc' ? 'desc' : 'asc',
|
||||
userId: req.session.currentUser,
|
||||
isAdmin: isAdmin(req),
|
||||
job.userId !== request.session.currentUser &&
|
||||
job.shared_with_user.includes(request.session.currentUser),
|
||||
}));
|
||||
});
|
||||
|
||||
const isUserAdmin = isAdmin(req);
|
||||
fastify.get('/data', async (request) => {
|
||||
const {
|
||||
page,
|
||||
pageSize = 50,
|
||||
activityFilter,
|
||||
sortfield = null,
|
||||
sortdir = 'asc',
|
||||
freeTextFilter,
|
||||
} = request.query || {};
|
||||
|
||||
// Map result to include runtime status
|
||||
queryResult.result = queryResult.result.map((job) => {
|
||||
return {
|
||||
const toBool = (v) => {
|
||||
if (v === true || v === 'true' || v === 1 || v === '1') return true;
|
||||
if (v === false || v === 'false' || v === 0 || v === '0') return false;
|
||||
return null;
|
||||
};
|
||||
const normalizedActivity = toBool(activityFilter);
|
||||
|
||||
const queryResult = jobStorage.queryJobs({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 50,
|
||||
freeTextFilter: freeTextFilter || null,
|
||||
activityFilter: normalizedActivity,
|
||||
sortField: sortfield || null,
|
||||
sortDir: sortdir === 'desc' ? 'desc' : 'asc',
|
||||
userId: request.session.currentUser,
|
||||
isAdmin: isAdmin(request),
|
||||
});
|
||||
|
||||
const isUserAdmin = isAdmin(request);
|
||||
queryResult.result = queryResult.result.map((job) => ({
|
||||
...job,
|
||||
running: isJobRunning(job.id),
|
||||
isOnlyShared:
|
||||
!isUserAdmin &&
|
||||
job.userId !== req.session.currentUser &&
|
||||
job.shared_with_user.includes(req.session.currentUser),
|
||||
};
|
||||
job.userId !== request.session.currentUser &&
|
||||
job.shared_with_user.includes(request.session.currentUser),
|
||||
}));
|
||||
|
||||
return queryResult;
|
||||
});
|
||||
|
||||
res.body = queryResult;
|
||||
res.send();
|
||||
});
|
||||
// Server-Sent Events for real-time job status updates
|
||||
fastify.get('/events', async (request, reply) => {
|
||||
const userId = request.session?.currentUser;
|
||||
if (userId == null) {
|
||||
return reply.code(401).send({ message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
reply.hijack();
|
||||
const raw = reply.raw;
|
||||
raw.setHeader('Content-Type', 'text/event-stream');
|
||||
raw.setHeader('Cache-Control', 'no-cache');
|
||||
raw.setHeader('Connection', 'keep-alive');
|
||||
|
||||
// Server-Sent Events for job status updates
|
||||
jobRouter.get('/events', async (req, res) => {
|
||||
const userId = req.session.currentUser;
|
||||
if (userId == null) {
|
||||
res.send({ message: 'Unauthorized' }, 401);
|
||||
return;
|
||||
}
|
||||
// SSE headers
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
try {
|
||||
// Initial comment to establish stream
|
||||
res.write(': connected\n\n');
|
||||
addSseClient(userId, res);
|
||||
// Cleanup on close/aborted
|
||||
const onClose = () => removeClient(userId, res);
|
||||
// restana exposes original req/res; use both close and finish
|
||||
req.on('close', onClose);
|
||||
req.on('aborted', onClose);
|
||||
res.on('close', onClose);
|
||||
} catch (e) {
|
||||
logger.error('Error establishing SSE connection', e);
|
||||
try {
|
||||
res.end();
|
||||
} catch {
|
||||
//noop
|
||||
raw.write(': connected\n\n');
|
||||
addSseClient(userId, raw);
|
||||
const onClose = () => removeClient(userId, raw);
|
||||
request.raw.on('close', onClose);
|
||||
} catch (e) {
|
||||
logger.error('Error establishing SSE connection', e);
|
||||
try {
|
||||
raw.end();
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
jobRouter.post('/startAll', async (req, res) => {
|
||||
try {
|
||||
const userId = req.session.currentUser;
|
||||
// Emit only the userId; handler will decide based on admin/ownership
|
||||
bus.emit('jobs:runAll', { userId });
|
||||
res.send({ message: 'Run all accepted' }, 202);
|
||||
} catch (err) {
|
||||
logger.error('Failed to trigger startAll', err);
|
||||
res.send({ message: 'Unexpected error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger a single job run
|
||||
jobRouter.post('/:jobId/run', async (req, res) => {
|
||||
const { jobId } = req.params;
|
||||
try {
|
||||
const job = jobStorage.getJob(jobId);
|
||||
if (!job) {
|
||||
res.send({ message: 'Job not found' }, 404);
|
||||
return;
|
||||
fastify.post('/startAll', async (request, reply) => {
|
||||
try {
|
||||
const userId = request.session.currentUser;
|
||||
bus.emit('jobs:runAll', { userId });
|
||||
return reply.code(202).send({ message: 'Run all accepted' });
|
||||
} catch (err) {
|
||||
logger.error('Failed to trigger startAll', err);
|
||||
return reply.code(500).send({ message: 'Unexpected error' });
|
||||
}
|
||||
if (!doesJobBelongsToUser(job, req)) {
|
||||
res.send({ message: 'You are trying to run a job that is not associated to your user' }, 403);
|
||||
return;
|
||||
}
|
||||
if (isJobRunning(jobId)) {
|
||||
res.send({ message: 'Job is already running' }, 409);
|
||||
return;
|
||||
}
|
||||
// fire and forget; actual execution handled by index.js listener
|
||||
bus.emit('jobs:runOne', { jobId });
|
||||
res.send({ message: 'Job run accepted' }, 202);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
res.send({ message: 'Unexpected error triggering job' }, 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
jobRouter.post('/', async (req, res) => {
|
||||
const {
|
||||
provider,
|
||||
notificationAdapter,
|
||||
name,
|
||||
blacklist = [],
|
||||
jobId,
|
||||
enabled,
|
||||
shareWithUsers = [],
|
||||
spatialFilter = null,
|
||||
specFilter = null,
|
||||
} = req.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
let jobFromDb = jobStorage.getJob(jobId);
|
||||
|
||||
if (jobFromDb && !doesJobBelongsToUser(jobFromDb, req)) {
|
||||
res.send(new Error('You are trying to change a job that is not associated to your user.'));
|
||||
return;
|
||||
fastify.post('/:jobId/run', async (request, reply) => {
|
||||
const { jobId } = request.params;
|
||||
try {
|
||||
const job = jobStorage.getJob(jobId);
|
||||
if (!job) {
|
||||
return reply.code(404).send({ message: 'Job not found' });
|
||||
}
|
||||
if (!doesJobBelongsToUser(job, request)) {
|
||||
return reply.code(403).send({ message: 'You are trying to run a job that is not associated to your user' });
|
||||
}
|
||||
if (isJobRunning(jobId)) {
|
||||
return reply.code(409).send({ message: 'Job is already running' });
|
||||
}
|
||||
bus.emit('jobs:runOne', { jobId });
|
||||
return reply.code(202).send({ message: 'Job run accepted' });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return reply.code(500).send({ message: 'Unexpected error triggering job' });
|
||||
}
|
||||
});
|
||||
|
||||
if (settings.demoMode && !isAdmin(req) && jobFromDb && jobFromDb.name === DEMO_JOB_NAME) {
|
||||
res.send(new Error('Sorry, but you cannot change the Status of our Demo Job ;)'));
|
||||
return;
|
||||
}
|
||||
|
||||
jobStorage.upsertJob({
|
||||
userId: req.session.currentUser,
|
||||
jobId,
|
||||
enabled,
|
||||
name,
|
||||
blacklist,
|
||||
fastify.post('/', async (request, reply) => {
|
||||
const {
|
||||
provider,
|
||||
notificationAdapter,
|
||||
shareWithUsers,
|
||||
spatialFilter,
|
||||
specFilter,
|
||||
});
|
||||
} catch (error) {
|
||||
res.send(new Error(error));
|
||||
logger.error(error);
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
name,
|
||||
blacklist = [],
|
||||
jobId,
|
||||
enabled,
|
||||
shareWithUsers = [],
|
||||
spatialFilter = null,
|
||||
specFilter = null,
|
||||
} = request.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
const jobFromDb = jobStorage.getJob(jobId);
|
||||
|
||||
jobRouter.delete('', async (req, res) => {
|
||||
const { jobId } = req.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
const job = jobStorage.getJob(jobId);
|
||||
if (settings.demoMode && !isAdmin(req) && job.name === DEMO_JOB_NAME) {
|
||||
res.send(new Error('Sorry, but you cannot remove the Demo Job ;)'));
|
||||
return;
|
||||
}
|
||||
if (jobFromDb && !doesJobBelongsToUser(jobFromDb, request)) {
|
||||
return reply.code(403).send({ error: 'You are trying to change a job that is not associated to your user.' });
|
||||
}
|
||||
|
||||
if (!doesJobBelongsToUser(job, req)) {
|
||||
res.send(new Error('You are trying to remove a job that is not associated to your user'));
|
||||
} else {
|
||||
jobStorage.removeJob(jobId);
|
||||
}
|
||||
} catch (error) {
|
||||
res.send(new Error(error));
|
||||
logger.error(error);
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
jobRouter.put('/:jobId/status', async (req, res) => {
|
||||
const { status } = req.body;
|
||||
const { jobId } = req.params;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
const job = jobStorage.getJob(jobId);
|
||||
if (settings.demoMode && !isAdmin(request) && jobFromDb && jobFromDb.name === DEMO_JOB_NAME) {
|
||||
return reply.code(403).send({ error: 'Sorry, but you cannot change the Status of our Demo Job ;)' });
|
||||
}
|
||||
|
||||
if (settings.demoMode && !isAdmin(req) && job.name === DEMO_JOB_NAME) {
|
||||
res.send(new Error('Sorry, but you cannot change the Status of our Demo Job ;)'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doesJobBelongsToUser(job, req)) {
|
||||
res.send(new Error('You are trying change a job that is not associated to your user'));
|
||||
} else {
|
||||
jobStorage.setJobStatus({
|
||||
jobStorage.upsertJob({
|
||||
userId: request.session.currentUser,
|
||||
jobId,
|
||||
status,
|
||||
enabled,
|
||||
name,
|
||||
blacklist,
|
||||
provider,
|
||||
notificationAdapter,
|
||||
shareWithUsers,
|
||||
spatialFilter,
|
||||
specFilter,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
} catch (error) {
|
||||
res.send(new Error(error));
|
||||
logger.error(error);
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
return reply.send();
|
||||
});
|
||||
|
||||
jobRouter.get('/shareableUserList', async (req, res) => {
|
||||
const currentUser = req.session.currentUser;
|
||||
const users = userStorage.getUsers(false);
|
||||
res.body = users
|
||||
.filter((user) => !user.isAdmin && user.id !== currentUser)
|
||||
.map((user) => ({
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
}));
|
||||
res.send();
|
||||
});
|
||||
export { jobRouter };
|
||||
fastify.delete('/', async (request, reply) => {
|
||||
const { jobId } = request.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
const job = jobStorage.getJob(jobId);
|
||||
if (settings.demoMode && !isAdmin(request) && job.name === DEMO_JOB_NAME) {
|
||||
return reply.code(403).send({ error: 'Sorry, but you cannot remove the Demo Job ;)' });
|
||||
}
|
||||
|
||||
if (!doesJobBelongsToUser(job, request)) {
|
||||
return reply.code(403).send({ error: 'You are trying to remove a job that is not associated to your user' });
|
||||
}
|
||||
jobStorage.removeJob(jobId);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
return reply.send();
|
||||
});
|
||||
|
||||
fastify.put('/:jobId/status', async (request, reply) => {
|
||||
const { status } = request.body;
|
||||
const { jobId } = request.params;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
const job = jobStorage.getJob(jobId);
|
||||
|
||||
if (settings.demoMode && !isAdmin(request) && job.name === DEMO_JOB_NAME) {
|
||||
return reply.code(403).send({ error: 'Sorry, but you cannot change the Status of our Demo Job ;)' });
|
||||
}
|
||||
|
||||
if (!doesJobBelongsToUser(job, request)) {
|
||||
return reply.code(403).send({ error: 'You are trying change a job that is not associated to your user' });
|
||||
}
|
||||
jobStorage.setJobStatus({ jobId, status });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
return reply.send();
|
||||
});
|
||||
|
||||
fastify.get('/shareableUserList', async (request) => {
|
||||
const currentUser = request.session.currentUser;
|
||||
const users = userStorage.getUsers(false);
|
||||
return users
|
||||
.filter((user) => !user.isAdmin && user.id !== currentUser)
|
||||
.map((user) => ({
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import * as listingStorage from '../../services/storage/listingsStorage.js';
|
||||
import * as watchListStorage from '../../services/storage/watchListStorage.js';
|
||||
import { isAdmin as isAdminFn } from '../security.js';
|
||||
@@ -12,128 +11,114 @@ import { nullOrEmpty } from '../../utils.js';
|
||||
import { getJobs } from '../../services/storage/jobStorage.js';
|
||||
import { getSettings } from '../../services/storage/settingsStorage.js';
|
||||
|
||||
const service = restana();
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function listingsPlugin(fastify) {
|
||||
fastify.get('/table', async (request) => {
|
||||
const {
|
||||
page,
|
||||
pageSize = 50,
|
||||
activityFilter,
|
||||
jobNameFilter,
|
||||
providerFilter,
|
||||
watchListFilter,
|
||||
sortfield = null,
|
||||
sortdir = 'asc',
|
||||
freeTextFilter,
|
||||
} = request.query || {};
|
||||
|
||||
const listingsRouter = service.newRouter();
|
||||
const toBool = (v) => {
|
||||
if (v === true || v === 'true' || v === 1 || v === '1') return true;
|
||||
if (v === false || v === 'false' || v === 0 || v === '0') return false;
|
||||
return null;
|
||||
};
|
||||
const normalizedActivity = toBool(activityFilter);
|
||||
const normalizedWatch = toBool(watchListFilter);
|
||||
|
||||
listingsRouter.get('/table', async (req, res) => {
|
||||
const {
|
||||
page,
|
||||
pageSize = 50,
|
||||
activityFilter,
|
||||
jobNameFilter,
|
||||
providerFilter,
|
||||
watchListFilter,
|
||||
sortfield = null,
|
||||
sortdir = 'asc',
|
||||
freeTextFilter,
|
||||
} = req.query || {};
|
||||
let jobFilter = null;
|
||||
let jobIdFilter = null;
|
||||
const jobs = getJobs();
|
||||
if (!nullOrEmpty(jobNameFilter)) {
|
||||
const job = jobs.find((j) => j.id === jobNameFilter);
|
||||
jobFilter = job != null ? job.name : null;
|
||||
jobIdFilter = job != null ? job.id : null;
|
||||
}
|
||||
|
||||
// normalize booleans (accept true, 'true', 1, '1' for true; false, 'false', 0, '0' for false)
|
||||
const toBool = (v) => {
|
||||
if (v === true || v === 'true' || v === 1 || v === '1') return true;
|
||||
if (v === false || v === 'false' || v === 0 || v === '0') return false;
|
||||
return null;
|
||||
};
|
||||
const normalizedActivity = toBool(activityFilter);
|
||||
const normalizedWatch = toBool(watchListFilter);
|
||||
|
||||
let jobFilter = null;
|
||||
let jobIdFilter = null;
|
||||
const jobs = getJobs();
|
||||
if (!nullOrEmpty(jobNameFilter)) {
|
||||
const job = jobs.find((j) => j.id === jobNameFilter);
|
||||
jobFilter = job != null ? job.name : null;
|
||||
jobIdFilter = job != null ? job.id : null;
|
||||
}
|
||||
|
||||
res.body = listingStorage.queryListings({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 50,
|
||||
freeTextFilter: freeTextFilter || null,
|
||||
activityFilter: normalizedActivity,
|
||||
jobNameFilter: jobFilter,
|
||||
jobIdFilter: jobIdFilter,
|
||||
providerFilter,
|
||||
watchListFilter: normalizedWatch,
|
||||
sortField: sortfield || null,
|
||||
sortDir: sortdir === 'desc' ? 'desc' : 'asc',
|
||||
userId: req.session.currentUser,
|
||||
isAdmin: isAdminFn(req),
|
||||
return listingStorage.queryListings({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 50,
|
||||
freeTextFilter: freeTextFilter || null,
|
||||
activityFilter: normalizedActivity,
|
||||
jobNameFilter: jobFilter,
|
||||
jobIdFilter: jobIdFilter,
|
||||
providerFilter,
|
||||
watchListFilter: normalizedWatch,
|
||||
sortField: sortfield || null,
|
||||
sortDir: sortdir === 'desc' ? 'desc' : 'asc',
|
||||
userId: request.session.currentUser,
|
||||
isAdmin: isAdminFn(request),
|
||||
});
|
||||
});
|
||||
res.send();
|
||||
});
|
||||
|
||||
listingsRouter.get('/map', async (req, res) => {
|
||||
const { jobId } = req.query || {};
|
||||
|
||||
res.body = listingStorage.getListingsForMap({
|
||||
jobId: nullOrEmpty(jobId) ? null : jobId,
|
||||
userId: req.session.currentUser,
|
||||
isAdmin: isAdminFn(req),
|
||||
fastify.get('/map', async (request) => {
|
||||
const { jobId } = request.query || {};
|
||||
return listingStorage.getListingsForMap({
|
||||
jobId: nullOrEmpty(jobId) ? null : jobId,
|
||||
userId: request.session.currentUser,
|
||||
isAdmin: isAdminFn(request),
|
||||
});
|
||||
});
|
||||
res.send();
|
||||
});
|
||||
|
||||
listingsRouter.get('/:listingId', async (req, res) => {
|
||||
const { listingId } = req.params;
|
||||
const listing = listingStorage.getListingById(listingId, req.session.currentUser, isAdminFn(req));
|
||||
if (!listing) {
|
||||
res.statusCode = 404;
|
||||
res.body = { message: 'Listing not found' };
|
||||
return res.send();
|
||||
}
|
||||
res.body = listing;
|
||||
res.send();
|
||||
});
|
||||
|
||||
// Toggle watch state for the current user on a listing
|
||||
listingsRouter.post('/watch', async (req, res) => {
|
||||
try {
|
||||
const { listingId } = req.body || {};
|
||||
const userId = req.session?.currentUser;
|
||||
if (!listingId || !userId) {
|
||||
res.statusCode = 400;
|
||||
res.body = { message: 'listingId or user not provided' };
|
||||
return res.send();
|
||||
fastify.get('/:listingId', async (request, reply) => {
|
||||
const { listingId } = request.params;
|
||||
const listing = listingStorage.getListingById(listingId, request.session.currentUser, isAdminFn(request));
|
||||
if (!listing) {
|
||||
return reply.code(404).send({ message: 'Listing not found' });
|
||||
}
|
||||
watchListStorage.toggleWatch(listingId, userId);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
res.statusCode = 500;
|
||||
res.body = { message: 'Failed to toggle watch' };
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
return listing;
|
||||
});
|
||||
|
||||
listingsRouter.delete('/job', async (req, res) => {
|
||||
const { jobId, hardDelete = false } = req.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
if (settings.demoMode && !isAdminFn(req)) {
|
||||
res.send(new Error('Sorry, but you cannot remove listings in demo mode ;)'));
|
||||
return;
|
||||
fastify.post('/watch', async (request, reply) => {
|
||||
try {
|
||||
const { listingId } = request.body || {};
|
||||
const userId = request.session?.currentUser;
|
||||
if (!listingId || !userId) {
|
||||
return reply.code(400).send({ message: 'listingId or user not provided' });
|
||||
}
|
||||
watchListStorage.toggleWatch(listingId, userId);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return reply.code(500).send({ message: 'Failed to toggle watch' });
|
||||
}
|
||||
return reply.send();
|
||||
});
|
||||
|
||||
listingStorage.deleteListingsByJobId(jobId, hardDelete);
|
||||
} catch (error) {
|
||||
res.send(new Error(error));
|
||||
logger.error(error);
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
|
||||
listingsRouter.delete('/', async (req, res) => {
|
||||
const { ids, hardDelete = false } = req.body;
|
||||
try {
|
||||
if (Array.isArray(ids) && ids.length > 0) {
|
||||
listingStorage.deleteListingsById(ids, hardDelete);
|
||||
fastify.delete('/job', async (request, reply) => {
|
||||
const { jobId, hardDelete = false } = request.body;
|
||||
const settings = await getSettings();
|
||||
try {
|
||||
if (settings.demoMode && !isAdminFn(request)) {
|
||||
return reply.code(403).send({ error: 'Sorry, but you cannot remove listings in demo mode ;)' });
|
||||
}
|
||||
listingStorage.deleteListingsByJobId(jobId, hardDelete);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
} catch (error) {
|
||||
res.send(new Error(error));
|
||||
logger.error(error);
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
return reply.send();
|
||||
});
|
||||
|
||||
export { listingsRouter };
|
||||
fastify.delete('/', async (request, reply) => {
|
||||
const { ids, hardDelete = false } = request.body;
|
||||
try {
|
||||
if (Array.isArray(ids) && ids.length > 0) {
|
||||
listingStorage.deleteListingsById(ids, hardDelete);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
return reply.send();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import * as userStorage from '../../services/storage/userStorage.js';
|
||||
import * as hasher from '../../services/security/hash.js';
|
||||
import { trackDemoAccessed } from '../../services/tracking/Tracker.js';
|
||||
@@ -11,12 +10,12 @@ import logger from '../../services/logger.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 }
|
||||
const LOGIN_WINDOW_MS = 15 * 60 * 1000;
|
||||
const loginAttempts = new Map();
|
||||
|
||||
function getClientIp(req) {
|
||||
const forwarded = req.headers['x-forwarded-for'];
|
||||
return (forwarded ? forwarded.split(',')[0] : req.socket?.remoteAddress) || 'unknown';
|
||||
function getClientIp(request) {
|
||||
const forwarded = request.headers['x-forwarded-for'];
|
||||
return (forwarded ? forwarded.split(',')[0] : request.socket?.remoteAddress) || 'unknown';
|
||||
}
|
||||
|
||||
function isRateLimited(ip) {
|
||||
@@ -30,53 +29,51 @@ function isRateLimited(ip) {
|
||||
return record.count > MAX_LOGIN_ATTEMPTS;
|
||||
}
|
||||
|
||||
const service = restana();
|
||||
const loginRouter = service.newRouter();
|
||||
loginRouter.get('/user', async (req, res) => {
|
||||
const currentUserId = req.session.currentUser;
|
||||
const currentUser = currentUserId == null ? null : userStorage.getUser(currentUserId);
|
||||
if (currentUser == null) {
|
||||
res.body = {};
|
||||
} else {
|
||||
res.body = {
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function loginPlugin(fastify) {
|
||||
fastify.get('/user', async (request) => {
|
||||
const currentUserId = request.session?.currentUser;
|
||||
const currentUser = currentUserId == null ? null : userStorage.getUser(currentUserId);
|
||||
if (currentUser == null) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
userId: currentUser.id,
|
||||
isAdmin: currentUser.isAdmin,
|
||||
};
|
||||
}
|
||||
res.send();
|
||||
});
|
||||
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 { username, password } = req.body;
|
||||
const user = userStorage.getUsers(true).find((user) => user.username === username);
|
||||
if (user == null) {
|
||||
res.send(401);
|
||||
return;
|
||||
}
|
||||
if (user.password === hasher.hash(password)) {
|
||||
if (settings.demoMode) {
|
||||
await trackDemoAccessed();
|
||||
}
|
||||
});
|
||||
|
||||
req.session.currentUser = user.id;
|
||||
req.session.createdAt = Date.now();
|
||||
loginAttempts.delete(ip);
|
||||
userStorage.setLastLoginToNow({ userId: user.id });
|
||||
res.send(200);
|
||||
return;
|
||||
} else {
|
||||
logger.error(`User ${username} tried to login, but password was wrong.`);
|
||||
}
|
||||
res.send(401);
|
||||
});
|
||||
loginRouter.post('/logout', async (req, res) => {
|
||||
req.session = null;
|
||||
res.send(200);
|
||||
});
|
||||
export { loginRouter };
|
||||
fastify.post('/', async (request, reply) => {
|
||||
const ip = getClientIp(request);
|
||||
if (isRateLimited(ip)) {
|
||||
logger.error(`Login rate limit exceeded for IP ${ip}`);
|
||||
return reply.code(429).send();
|
||||
}
|
||||
const settings = await getSettings();
|
||||
const { username, password } = request.body;
|
||||
const user = userStorage.getUsers(true).find((u) => u.username === username);
|
||||
if (user == null) {
|
||||
return reply.code(401).send();
|
||||
}
|
||||
if (user.password === hasher.hash(password)) {
|
||||
if (settings.demoMode) {
|
||||
await trackDemoAccessed();
|
||||
}
|
||||
request.session.currentUser = user.id;
|
||||
request.session.createdAt = Date.now();
|
||||
loginAttempts.delete(ip);
|
||||
userStorage.setLastLoginToNow({ userId: user.id });
|
||||
return reply.code(200).send();
|
||||
} else {
|
||||
logger.error(`User ${username} tried to login, but password was wrong.`);
|
||||
}
|
||||
return reply.code(401).send();
|
||||
});
|
||||
|
||||
fastify.post('/logout', async (request, reply) => {
|
||||
await request.session.destroy();
|
||||
return reply.code(200).send();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,62 +4,64 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import restana from 'restana';
|
||||
import logger from '../../services/logger.js';
|
||||
|
||||
const service = restana();
|
||||
const notificationAdapterRouter = service.newRouter();
|
||||
const notificationAdapterList = fs.readdirSync('./lib//notification/adapter').filter((file) => file.endsWith('.js'));
|
||||
const notificationAdapter = await Promise.all(
|
||||
notificationAdapterList.map(async (pro) => {
|
||||
return await import(`../../notification/adapter/${pro}`);
|
||||
}),
|
||||
);
|
||||
notificationAdapterRouter.post('/try', async (req, res) => {
|
||||
const { id, fields } = req.body;
|
||||
const adapter = notificationAdapter.find((adapter) => adapter.config.id === id);
|
||||
if (adapter == null) {
|
||||
res.send(404);
|
||||
}
|
||||
const notificationConfig = [];
|
||||
const notificationObject = {};
|
||||
Object.keys(fields).forEach((key) => {
|
||||
notificationObject[key] = fields[key].value;
|
||||
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function notificationAdapterPlugin(fastify) {
|
||||
fastify.get('/', async () => {
|
||||
return notificationAdapter.map((adapter) => adapter.config);
|
||||
});
|
||||
notificationConfig.push({
|
||||
fields: { ...notificationObject },
|
||||
enabled: true,
|
||||
id,
|
||||
});
|
||||
try {
|
||||
await adapter.send({
|
||||
serviceName: 'TestCall',
|
||||
newListings: [
|
||||
{
|
||||
address: 'Heidestrasse 17, 51147 Köln',
|
||||
description: exampleDescription,
|
||||
id: '1',
|
||||
imageUrl: 'https://placehold.co/600x400/png',
|
||||
price: '1.000 €',
|
||||
size: '76 m²',
|
||||
title: 'Stilvolle gepflegte 3-Raum-Wohnung mit gehobener Innenausstattung',
|
||||
url: 'https://www.orange-coding.net',
|
||||
},
|
||||
],
|
||||
notificationConfig,
|
||||
jobKey: 'TestJob',
|
||||
|
||||
fastify.post('/try', async (request, reply) => {
|
||||
const { id, fields } = request.body;
|
||||
const adapter = notificationAdapter.find((adapter) => adapter.config.id === id);
|
||||
if (adapter == null) {
|
||||
return reply.code(404).send();
|
||||
}
|
||||
const notificationConfig = [];
|
||||
const notificationObject = {};
|
||||
Object.keys(fields).forEach((key) => {
|
||||
notificationObject[key] = fields[key].value;
|
||||
});
|
||||
res.send();
|
||||
} catch (Exception) {
|
||||
logger.error('Error during notification adapter test:', Exception);
|
||||
res.send(new Error(Exception));
|
||||
}
|
||||
});
|
||||
notificationAdapterRouter.get('/', async (req, res) => {
|
||||
res.body = notificationAdapter.map((adapter) => adapter.config);
|
||||
res.send();
|
||||
});
|
||||
export { notificationAdapterRouter };
|
||||
notificationConfig.push({
|
||||
fields: { ...notificationObject },
|
||||
enabled: true,
|
||||
id,
|
||||
});
|
||||
try {
|
||||
await adapter.send({
|
||||
serviceName: 'TestCall',
|
||||
newListings: [
|
||||
{
|
||||
address: 'Heidestrasse 17, 51147 Köln',
|
||||
description: exampleDescription,
|
||||
id: '1',
|
||||
imageUrl: 'https://placehold.co/600x400/png',
|
||||
price: '1.000 €',
|
||||
size: '76 m²',
|
||||
title: 'Stilvolle gepflegte 3-Raum-Wohnung mit gehobener Innenausstattung',
|
||||
url: 'https://www.orange-coding.net',
|
||||
},
|
||||
],
|
||||
notificationConfig,
|
||||
jobKey: 'TestJob',
|
||||
});
|
||||
return reply.send();
|
||||
} catch (Exception) {
|
||||
logger.error('Error during notification adapter test:', Exception);
|
||||
return reply.code(500).send({ error: String(Exception) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const exampleDescription = `
|
||||
Wohnungstyp: Etagenwohnung
|
||||
@@ -94,7 +96,7 @@ Die Wohnung ist ideal für Paare oder kleine Familien geeignet.
|
||||
Ausstattung:
|
||||
- neuer hochwertiger Bodenbelag (Holzoptik) in allen Räumen außer Bad/Küche
|
||||
- sonniger Balkon (Süd)
|
||||
- Tiefgaragenstellplatz
|
||||
- Tiefgaragenstellplatz
|
||||
- Kellerabteil
|
||||
- gepflegtes Mehrfamilienhaus
|
||||
|
||||
@@ -104,7 +106,7 @@ Vermietung direkt vom Eigentümer - provisionsfrei!
|
||||
|
||||
Lage:
|
||||
• Park: 1 Minute zu Fuß
|
||||
• S-Bahn Station: 2 Minuten zu Fuß
|
||||
• S-Bahn Station: 2 Minuten zu Fuß
|
||||
• Supermärkte, Restaurants, täglicher Bedarf in der Nähe
|
||||
• Gute Anbindung Richtung Großstadt und Flughafen
|
||||
`;
|
||||
|
||||
@@ -4,17 +4,15 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import restana from 'restana';
|
||||
const service = restana();
|
||||
const providerRouter = service.newRouter();
|
||||
|
||||
const providerList = fs.readdirSync('./lib/provider').filter((file) => file.endsWith('.js'));
|
||||
const provider = await Promise.all(
|
||||
providerList.map(async (pro) => {
|
||||
return await import(`../../provider/${pro}`);
|
||||
}),
|
||||
);
|
||||
providerRouter.get('/', async (req, res) => {
|
||||
res.body = provider.map((p) => p.metaInformation);
|
||||
res.send();
|
||||
});
|
||||
export { providerRouter };
|
||||
const providers = await Promise.all(providerList.map(async (pro) => import(`../../provider/${pro}`)));
|
||||
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function providerPlugin(fastify) {
|
||||
fastify.get('/', async () => {
|
||||
return providers.map((p) => p.metaInformation);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,35 +3,29 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import { trackPoi } from '../../services/tracking/Tracker.js';
|
||||
import { TRACKING_POIS } from '../../TRACKING_POIS.js';
|
||||
import logger from '../../services/logger.js';
|
||||
|
||||
const service = restana();
|
||||
const trackingRouter = service.newRouter();
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function trackingPlugin(fastify) {
|
||||
fastify.get('/trackingPois', async () => {
|
||||
return TRACKING_POIS;
|
||||
});
|
||||
|
||||
trackingRouter.get('/trackingPois', async (req, res) => {
|
||||
res.body = TRACKING_POIS;
|
||||
res.send();
|
||||
});
|
||||
|
||||
trackingRouter.post('/poi', async (req, res) => {
|
||||
const { poi } = req.body;
|
||||
if (!poi) {
|
||||
res.statusCode = 400;
|
||||
res.send({ error: 'Feature name is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await trackPoi(poi);
|
||||
res.send({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Error tracking feature', error);
|
||||
res.statusCode = 500;
|
||||
res.send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export { trackingRouter };
|
||||
fastify.post('/poi', async (request, reply) => {
|
||||
const { poi } = request.body;
|
||||
if (!poi) {
|
||||
return reply.code(400).send({ error: 'Feature name is required' });
|
||||
}
|
||||
try {
|
||||
await trackPoi(poi);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('Error tracking feature', error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,82 +3,73 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import * as userStorage from '../../services/storage/userStorage.js';
|
||||
import * as jobStorage from '../../services/storage/jobStorage.js';
|
||||
import { getSettings } from '../../services/storage/settingsStorage.js';
|
||||
import { isAdmin as isAdminUser } from '../security.js';
|
||||
const service = restana();
|
||||
const userRouter = service.newRouter();
|
||||
|
||||
function checkIfAnyAdminAfterRemovingUser(userIdToBeRemoved, allUser) {
|
||||
return allUser.filter((user) => user.id !== userIdToBeRemoved && user.isAdmin).length > 0;
|
||||
}
|
||||
function checkIfUserToBeRemovedIsLoggedIn(userIdToBeRemoved, req) {
|
||||
return req.session.currentUser === userIdToBeRemoved;
|
||||
|
||||
function checkIfUserToBeRemovedIsLoggedIn(userIdToBeRemoved, request) {
|
||||
return request.session.currentUser === userIdToBeRemoved;
|
||||
}
|
||||
|
||||
const nullOrEmpty = (str) => str == null || str.length === 0;
|
||||
|
||||
userRouter.get('/', async (req, res) => {
|
||||
res.body = userStorage.getUsers(false);
|
||||
res.send();
|
||||
});
|
||||
|
||||
userRouter.get('/:userId', async (req, res) => {
|
||||
const { userId } = req.params;
|
||||
res.body = userStorage.getUser(userId);
|
||||
res.send();
|
||||
});
|
||||
userRouter.delete('/', async (req, res) => {
|
||||
const settings = await getSettings();
|
||||
if (settings.demoMode && !isAdminUser(req)) {
|
||||
res.send(new Error('In demo mode, it is not allowed to remove user.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { userId } = req.body;
|
||||
const allUser = userStorage.getUsers(false);
|
||||
if (!checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
|
||||
res.send(new Error('You are trying to remove the last admin user. This is prohibited.'));
|
||||
return;
|
||||
}
|
||||
if (checkIfUserToBeRemovedIsLoggedIn(userId, req)) {
|
||||
res.send(new Error('You are trying to remove yourself. This is prohibited.'));
|
||||
return;
|
||||
}
|
||||
//TODO: Remove also analytics
|
||||
jobStorage.removeJobsByUserId(userId);
|
||||
userStorage.removeUser(userId);
|
||||
res.send();
|
||||
});
|
||||
userRouter.post('/', async (req, res) => {
|
||||
const settings = await getSettings();
|
||||
if (settings.demoMode && !isAdminUser(req)) {
|
||||
res.send(new Error('In demo mode, it is not allowed to change or add user.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const { username, password, password2, isAdmin, userId } = req.body;
|
||||
if (password !== password2) {
|
||||
res.send(new Error('Passwords does not match'));
|
||||
return;
|
||||
}
|
||||
if (nullOrEmpty(username) || nullOrEmpty(password) || nullOrEmpty(password2)) {
|
||||
res.send(new Error('Username and password are mandatory.'));
|
||||
return;
|
||||
}
|
||||
const allUser = userStorage.getUsers(false);
|
||||
if (!isAdmin && !checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
|
||||
res.send(
|
||||
new Error('You cannot change the admin flag for this user as otherwise, there is no other user in the system'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
userStorage.upsertUser({
|
||||
userId,
|
||||
username,
|
||||
password,
|
||||
isAdmin,
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function userPlugin(fastify) {
|
||||
fastify.get('/', async () => {
|
||||
return userStorage.getUsers(false);
|
||||
});
|
||||
res.send();
|
||||
});
|
||||
export { userRouter };
|
||||
|
||||
fastify.get('/:userId', async (request) => {
|
||||
const { userId } = request.params;
|
||||
return userStorage.getUser(userId);
|
||||
});
|
||||
|
||||
fastify.delete('/', async (request, reply) => {
|
||||
const settings = await getSettings();
|
||||
if (settings.demoMode && !isAdminUser(request)) {
|
||||
return reply.code(403).send({ error: 'In demo mode, it is not allowed to remove user.' });
|
||||
}
|
||||
|
||||
const { userId } = request.body;
|
||||
const allUser = userStorage.getUsers(false);
|
||||
if (!checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
|
||||
return reply.code(400).send({ error: 'You are trying to remove the last admin user. This is prohibited.' });
|
||||
}
|
||||
if (checkIfUserToBeRemovedIsLoggedIn(userId, request)) {
|
||||
return reply.code(400).send({ error: 'You are trying to remove yourself. This is prohibited.' });
|
||||
}
|
||||
jobStorage.removeJobsByUserId(userId);
|
||||
userStorage.removeUser(userId);
|
||||
return reply.send();
|
||||
});
|
||||
|
||||
fastify.post('/', async (request, reply) => {
|
||||
const settings = await getSettings();
|
||||
if (settings.demoMode && !isAdminUser(request)) {
|
||||
return reply.code(403).send({ error: 'In demo mode, it is not allowed to change or add user.' });
|
||||
}
|
||||
|
||||
const { username, password, password2, isAdmin, userId } = request.body;
|
||||
if (password !== password2) {
|
||||
return reply.code(400).send({ error: 'Passwords does not match' });
|
||||
}
|
||||
if (nullOrEmpty(username) || nullOrEmpty(password) || nullOrEmpty(password2)) {
|
||||
return reply.code(400).send({ error: 'Username and password are mandatory.' });
|
||||
}
|
||||
const allUser = userStorage.getUsers(false);
|
||||
if (!isAdmin && !checkIfAnyAdminAfterRemovingUser(userId, allUser)) {
|
||||
return reply.code(400).send({
|
||||
error: 'You cannot change the admin flag for this user as otherwise, there is no other user in the system',
|
||||
});
|
||||
}
|
||||
userStorage.upsertUser({ userId, username, password, isAdmin });
|
||||
return reply.send();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import SqliteConnection from '../../services/storage/SqliteConnection.js';
|
||||
import { getSettings, upsertSettings } from '../../services/storage/settingsStorage.js';
|
||||
import { isAdmin } from '../security.js';
|
||||
@@ -16,113 +15,98 @@ import { TRACKING_POIS } from '../../TRACKING_POIS.js';
|
||||
import logger from '../../services/logger.js';
|
||||
import { runGeoCordTask } from '../../services/crons/geocoding-cron.js';
|
||||
|
||||
const service = restana();
|
||||
const userSettingsRouter = service.newRouter();
|
||||
|
||||
userSettingsRouter.get('/', async (req, res) => {
|
||||
const userId = req.session.currentUser;
|
||||
const rows = SqliteConnection.query('SELECT name, value FROM settings WHERE user_id = @userId', { userId });
|
||||
const settings = {};
|
||||
for (const r of rows) {
|
||||
settings[r.name] = fromJson(r.value, null);
|
||||
}
|
||||
res.body = settings;
|
||||
res.send();
|
||||
});
|
||||
|
||||
userSettingsRouter.get('/autocomplete', async (req, res) => {
|
||||
const { q } = req.query;
|
||||
try {
|
||||
const results = await autocompleteAddress(q);
|
||||
res.body = results;
|
||||
res.send();
|
||||
} catch (error) {
|
||||
res.statusCode = 500;
|
||||
res.send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
userSettingsRouter.post('/home-address', async (req, res) => {
|
||||
const userId = req.session.currentUser;
|
||||
const { home_address } = req.body;
|
||||
const settings = await getSettings();
|
||||
|
||||
if (settings.demoMode && !isAdmin(req)) {
|
||||
res.send(new Error('In demo mode, it is not allowed to change the home address.'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (home_address) {
|
||||
await trackPoi(TRACKING_POIS.DISTANCE_ADDRESS_ENTERED);
|
||||
const coords = await geocodeAddress(home_address);
|
||||
if (coords && coords.lat !== -1) {
|
||||
upsertSettings({ home_address: { address: home_address, coords } }, userId);
|
||||
resetGeocoordinatesAndDistanceForUser(userId);
|
||||
//we do NOT wait for this to finish, as we don't want to block the response
|
||||
runGeoCordTask();
|
||||
res.send({ success: true, coords });
|
||||
} else {
|
||||
res.statusCode = 400;
|
||||
res.send({ error: 'Could not geocode address' });
|
||||
}
|
||||
} else {
|
||||
upsertSettings({ home_address: null }, userId);
|
||||
res.send({ success: true });
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function userSettingsPlugin(fastify) {
|
||||
fastify.get('/', async (request) => {
|
||||
const userId = request.session.currentUser;
|
||||
const rows = SqliteConnection.query('SELECT name, value FROM settings WHERE user_id = @userId', { userId });
|
||||
const settings = {};
|
||||
for (const r of rows) {
|
||||
settings[r.name] = fromJson(r.value, null);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error updating home address settings', error);
|
||||
res.statusCode = 500;
|
||||
res.send({ error: error.message });
|
||||
}
|
||||
});
|
||||
return settings;
|
||||
});
|
||||
|
||||
userSettingsRouter.post('/news-hash', async (req, res) => {
|
||||
const userId = req.session.currentUser;
|
||||
const { news_hash } = req.body;
|
||||
fastify.get('/autocomplete', async (request, reply) => {
|
||||
const { q } = request.query;
|
||||
try {
|
||||
const results = await autocompleteAddress(q);
|
||||
return results;
|
||||
} catch (error) {
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
const globalSettings = await getSettings();
|
||||
if (globalSettings.demoMode && !isAdmin(req)) {
|
||||
res.statusCode = 403;
|
||||
res.send({ error: 'In demo mode, it is not allowed to change settings.' });
|
||||
return;
|
||||
}
|
||||
fastify.post('/home-address', async (request, reply) => {
|
||||
const userId = request.session.currentUser;
|
||||
const { home_address } = request.body;
|
||||
const settings = await getSettings();
|
||||
|
||||
try {
|
||||
upsertSettings({ news_hash }, userId);
|
||||
res.send({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Error updating news hash', error);
|
||||
res.statusCode = 500;
|
||||
res.send({ error: error.message });
|
||||
}
|
||||
});
|
||||
if (settings.demoMode && !isAdmin(request)) {
|
||||
return reply.code(403).send({ error: 'In demo mode, it is not allowed to change the home address.' });
|
||||
}
|
||||
|
||||
userSettingsRouter.post('/provider-details', async (req, res) => {
|
||||
const userId = req.session.currentUser;
|
||||
const { provider_details } = req.body;
|
||||
try {
|
||||
if (home_address) {
|
||||
await trackPoi(TRACKING_POIS.DISTANCE_ADDRESS_ENTERED);
|
||||
const coords = await geocodeAddress(home_address);
|
||||
if (coords && coords.lat !== -1) {
|
||||
upsertSettings({ home_address: { address: home_address, coords } }, userId);
|
||||
resetGeocoordinatesAndDistanceForUser(userId);
|
||||
runGeoCordTask();
|
||||
return { success: true, coords };
|
||||
} else {
|
||||
return reply.code(400).send({ error: 'Could not geocode address' });
|
||||
}
|
||||
} else {
|
||||
upsertSettings({ home_address: null }, userId);
|
||||
return { success: true };
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error updating home address settings', error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
const globalSettings = await getSettings();
|
||||
if (globalSettings.demoMode && !isAdmin(req)) {
|
||||
res.statusCode = 403;
|
||||
res.send({ error: 'In demo mode, it is not allowed to change settings.' });
|
||||
return;
|
||||
}
|
||||
fastify.post('/news-hash', async (request, reply) => {
|
||||
const userId = request.session.currentUser;
|
||||
const { news_hash } = request.body;
|
||||
|
||||
if (!Array.isArray(provider_details)) {
|
||||
res.statusCode = 400;
|
||||
res.send({ error: 'provider_details must be an array of provider ids.' });
|
||||
return;
|
||||
}
|
||||
const globalSettings = await getSettings();
|
||||
if (globalSettings.demoMode && !isAdmin(request)) {
|
||||
return reply.code(403).send({ error: 'In demo mode, it is not allowed to change settings.' });
|
||||
}
|
||||
|
||||
try {
|
||||
upsertSettings({ provider_details }, userId);
|
||||
res.send({ success: true });
|
||||
} catch (error) {
|
||||
logger.error('Error updating provider details setting', error);
|
||||
res.statusCode = 500;
|
||||
res.send({ error: error.message });
|
||||
}
|
||||
});
|
||||
try {
|
||||
upsertSettings({ news_hash }, userId);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('Error updating news hash', error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export { userSettingsRouter };
|
||||
fastify.post('/provider-details', async (request, reply) => {
|
||||
const userId = request.session.currentUser;
|
||||
const { provider_details } = request.body;
|
||||
|
||||
const globalSettings = await getSettings();
|
||||
if (globalSettings.demoMode && !isAdmin(request)) {
|
||||
return reply.code(403).send({ error: 'In demo mode, it is not allowed to change settings.' });
|
||||
}
|
||||
|
||||
if (!Array.isArray(provider_details)) {
|
||||
return reply.code(400).send({ error: 'provider_details must be an array of provider ids.' });
|
||||
}
|
||||
|
||||
try {
|
||||
upsertSettings({ provider_details }, userId);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('Error updating provider details setting', error);
|
||||
return reply.code(500).send({ error: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,27 +3,10 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
import restana from 'restana';
|
||||
import fetch from 'node-fetch';
|
||||
import { getPackageVersion } from '../../utils.js';
|
||||
import semver from 'semver';
|
||||
|
||||
const service = restana();
|
||||
const versionRouter = service.newRouter();
|
||||
|
||||
versionRouter.get('/', async (req, res) => {
|
||||
const versionPayload = await getCurrentVersionFromGithub();
|
||||
const localFredyVersion = await getPackageVersion();
|
||||
res.body =
|
||||
versionPayload == null
|
||||
? {
|
||||
newVersion: false,
|
||||
localFredyVersion,
|
||||
}
|
||||
: versionPayload;
|
||||
res.send();
|
||||
});
|
||||
|
||||
async function getCurrentVersionFromGithub() {
|
||||
const raw = await fetch('https://api.github.com/repos/orangecoding/fredy/releases/latest');
|
||||
const data = await raw.json();
|
||||
@@ -40,4 +23,13 @@ async function getCurrentVersionFromGithub() {
|
||||
};
|
||||
}
|
||||
|
||||
export { versionRouter };
|
||||
/**
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export default async function versionPlugin(fastify) {
|
||||
fastify.get('/', async () => {
|
||||
const versionPayload = await getCurrentVersionFromGithub();
|
||||
const localFredyVersion = await getPackageVersion();
|
||||
return versionPayload ?? { newVersion: false, localFredyVersion };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,53 +4,50 @@
|
||||
*/
|
||||
|
||||
import * as userStorage from '../services/storage/userStorage.js';
|
||||
import cookieSession from 'cookie-session';
|
||||
|
||||
const SESSION_MAX_AGE = 2 * 60 * 60 * 1000; // 2 hours
|
||||
const unauthorized = (res) => {
|
||||
return res.send(401);
|
||||
};
|
||||
const isUnauthorized = (req) => {
|
||||
if (req.session.currentUser == null) return true;
|
||||
if (Date.now() - req.session.createdAt > SESSION_MAX_AGE) {
|
||||
req.session = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the request has no valid, non-expired session.
|
||||
* @param {import('fastify').FastifyRequest} request
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isUnauthorized(request) {
|
||||
if (!request.session?.currentUser) return true;
|
||||
if (Date.now() - (request.session.createdAt || 0) > SESSION_MAX_AGE) return true;
|
||||
return false;
|
||||
};
|
||||
const isAdmin = (req) => {
|
||||
if (!isUnauthorized(req)) {
|
||||
const user = userStorage.getUser(req.session.currentUser);
|
||||
return user != null && user.isAdmin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the session belongs to an admin user.
|
||||
* @param {import('fastify').FastifyRequest} request
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isAdmin(request) {
|
||||
if (isUnauthorized(request)) return false;
|
||||
const user = userStorage.getUser(request.session.currentUser);
|
||||
return user != null && user.isAdmin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fastify preHandler hook - rejects unauthenticated requests with 401.
|
||||
* @param {import('fastify').FastifyRequest} request
|
||||
* @param {import('fastify').FastifyReply} reply
|
||||
*/
|
||||
export async function authHook(request, reply) {
|
||||
if (isUnauthorized(request)) {
|
||||
reply.code(401).send();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const authInterceptor = () => {
|
||||
return (req, res, next) => {
|
||||
if (isUnauthorized(req)) {
|
||||
return unauthorized(res);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
};
|
||||
const adminInterceptor = () => {
|
||||
return (req, res, next) => {
|
||||
if (!isAdmin(req)) {
|
||||
return unauthorized(res);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
};
|
||||
const cookieSession$0 = (secret) => {
|
||||
return cookieSession({
|
||||
name: 'fredy-admin-session',
|
||||
keys: [secret],
|
||||
maxAge: SESSION_MAX_AGE,
|
||||
});
|
||||
};
|
||||
export { cookieSession$0 as cookieSession };
|
||||
export { adminInterceptor };
|
||||
export { authInterceptor };
|
||||
export { isUnauthorized };
|
||||
export { isAdmin };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fastify preHandler hook - rejects non-admin requests with 401.
|
||||
* Apply after authHook.
|
||||
* @param {import('fastify').FastifyRequest} request
|
||||
* @param {import('fastify').FastifyReply} reply
|
||||
*/
|
||||
export async function adminHook(request, reply) {
|
||||
if (!isAdmin(request)) {
|
||||
reply.code(401).send();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ The LLM will automatically call the appropriate Fredy MCP tools and present the
|
||||
#### Setup
|
||||
|
||||
1. Open **Claude Desktop**
|
||||
2. Go to **Settings → Developer → Edit Config** — this opens the `claude_desktop_config.json` file
|
||||
2. Go to **Settings → Developer → Edit Config** - this opens the `claude_desktop_config.json` file
|
||||
3. Add the `fredy` server to the `mcpServers` object:
|
||||
|
||||
```json
|
||||
@@ -158,7 +158,7 @@ The LLM will automatically call the appropriate Fredy MCP tools and present the
|
||||
> - nvm: `/Users/<you>/.nvm/versions/node/<version>/bin/node`
|
||||
|
||||
4. Save the file and **restart Claude Desktop**
|
||||
5. You should see a hammer icon (🔨) in the chat input — click it to confirm the Fredy tools are listed
|
||||
5. You should see a hammer icon (🔨) in the chat input - click it to confirm the Fredy tools are listed
|
||||
|
||||
#### Usage
|
||||
|
||||
@@ -170,7 +170,7 @@ Once connected, simply ask Claude about your real estate data:
|
||||
|
||||
Claude will automatically call the appropriate Fredy MCP tools.
|
||||
|
||||
> **Note:** Fredy's main web process does not need to be running — the stdio transport opens its own database connection directly. But the SQLite database file must exist and migrations must have been applied.
|
||||
> **Note:** Fredy's main web process does not need to be running - the stdio transport opens its own database connection directly. But the SQLite database file must exist and migrations must have been applied.
|
||||
|
||||
---
|
||||
|
||||
@@ -252,7 +252,7 @@ Example list response:
|
||||
```
|
||||
**Tool:** list_listings | **Status:** OK
|
||||
|
||||
Found **85** listing(s). Showing page 1 of 2 (50 on this page). More pages available — use page=2 to continue.
|
||||
Found **85** listing(s). Showing page 1 of 2 (50 on this page). More pages available - use page=2 to continue.
|
||||
|
||||
| ID | Title | Address | Price | Size | Provider | Active | Created | Job |
|
||||
|----|-------|---------|-------|------|----------|--------|---------|-----|
|
||||
|
||||
@@ -49,7 +49,7 @@ export function createMcpServer() {
|
||||
'list_listings to search listings (supports time filters like createdAfter/createdBefore), ' +
|
||||
'and get_listing for full details of a single listing. ' +
|
||||
'Responses are formatted as markdown with a summary, data (tables for lists, key-value for details), and pagination info. ' +
|
||||
'Always present results to the user as soon as you have them — do NOT call the tool again unless you need additional pages or different data.',
|
||||
'Always present results to the user as soon as you have them - do NOT call the tool again unless you need additional pages or different data.',
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (c) 2026 by Christian Kellner.
|
||||
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
||||
*/
|
||||
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||
import { createMcpServer } from './mcpAdapter.js';
|
||||
import { authenticateRequest } from './mcpAuthentication.js';
|
||||
@@ -15,16 +11,13 @@ import crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Active transports keyed by session id.
|
||||
* Each session gets its own McpServer + StreamableHTTPServerTransport pair.
|
||||
* @type {Map<string, { server: McpServer, transport: StreamableHTTPServerTransport }>}
|
||||
*/
|
||||
const sessions = new Map();
|
||||
|
||||
/**
|
||||
* Get or create a session for the given session id with authentication.
|
||||
* @param {string|undefined} sessionId
|
||||
* @param {{ userId: string }} auth
|
||||
* @returns {{ server: McpServer, transport: StreamableHTTPServerTransport }}
|
||||
*/
|
||||
function getOrCreateSession(sessionId, auth) {
|
||||
if (sessionId && sessions.has(sessionId)) {
|
||||
@@ -54,77 +47,67 @@ function getOrCreateSession(sessionId, auth) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register MCP Streamable HTTP routes on a restana service.
|
||||
* Register MCP Streamable HTTP routes on a fastify instance.
|
||||
*
|
||||
* Mounts handlers at /api/mcp to handle the MCP Streamable HTTP protocol:
|
||||
* - POST /api/mcp – JSON-RPC messages (initialize, tool calls, etc.)
|
||||
* - GET /api/mcp – SSE stream for server-initiated notifications
|
||||
* - DELETE /api/mcp – session termination
|
||||
* POST /api/mcp – JSON-RPC messages
|
||||
* GET /api/mcp – SSE stream for server-initiated notifications
|
||||
* DELETE /api/mcp – session termination
|
||||
*
|
||||
* All endpoints require a valid Bearer token in the Authorization header.
|
||||
*
|
||||
* @param {import('restana').Service} service - The restana service instance.
|
||||
* @param {import('fastify').FastifyInstance} fastify
|
||||
*/
|
||||
export function registerMcpRoutes(service) {
|
||||
// POST – main JSON-RPC endpoint
|
||||
service.post('/api/mcp', async (req, res) => {
|
||||
const auth = authenticateRequest(req);
|
||||
export function registerMcpRoutes(fastify) {
|
||||
fastify.post('/api/mcp', async (request, reply) => {
|
||||
const auth = authenticateRequest(request.raw);
|
||||
if (!auth) {
|
||||
res.statusCode = 401;
|
||||
return res.send({ error: 'Unauthorized. Provide a valid Bearer token.' });
|
||||
return reply.code(401).send({ error: 'Unauthorized. Provide a valid Bearer token.' });
|
||||
}
|
||||
|
||||
const sessionId = req.headers['mcp-session-id'];
|
||||
const sessionId = request.raw.headers['mcp-session-id'];
|
||||
const { server, transport } = getOrCreateSession(sessionId, auth);
|
||||
|
||||
// Connect server to transport if not already connected
|
||||
if (!transport.onmessage) {
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
// Inject authInfo so tools can access the authenticated user
|
||||
req.auth = { userId: auth.userId };
|
||||
request.raw.auth = { userId: auth.userId };
|
||||
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
reply.hijack();
|
||||
await transport.handleRequest(request.raw, reply.raw, request.body);
|
||||
});
|
||||
|
||||
// GET – SSE stream for server-initiated messages
|
||||
service.get('/api/mcp', async (req, res) => {
|
||||
const auth = authenticateRequest(req);
|
||||
fastify.get('/api/mcp', async (request, reply) => {
|
||||
const auth = authenticateRequest(request.raw);
|
||||
if (!auth) {
|
||||
res.statusCode = 401;
|
||||
return res.send({ error: 'Unauthorized. Provide a valid Bearer token.' });
|
||||
return reply.code(401).send({ error: 'Unauthorized. Provide a valid Bearer token.' });
|
||||
}
|
||||
|
||||
const sessionId = req.headers['mcp-session-id'];
|
||||
const sessionId = request.raw.headers['mcp-session-id'];
|
||||
if (!sessionId || !sessions.has(sessionId)) {
|
||||
res.statusCode = 400;
|
||||
return res.send({ error: 'Invalid or missing session. Send an initialize request first.' });
|
||||
return reply.code(400).send({ error: 'Invalid or missing session. Send an initialize request first.' });
|
||||
}
|
||||
|
||||
const { transport } = sessions.get(sessionId);
|
||||
await transport.handleRequest(req, res);
|
||||
reply.hijack();
|
||||
await transport.handleRequest(request.raw, reply.raw);
|
||||
});
|
||||
|
||||
// DELETE – terminate session
|
||||
service.delete('/api/mcp', async (req, res) => {
|
||||
const auth = authenticateRequest(req);
|
||||
fastify.delete('/api/mcp', async (request, reply) => {
|
||||
const auth = authenticateRequest(request.raw);
|
||||
if (!auth) {
|
||||
res.statusCode = 401;
|
||||
return res.send({ error: 'Unauthorized. Provide a valid Bearer token.' });
|
||||
return reply.code(401).send({ error: 'Unauthorized. Provide a valid Bearer token.' });
|
||||
}
|
||||
|
||||
const sessionId = req.headers['mcp-session-id'];
|
||||
const sessionId = request.raw.headers['mcp-session-id'];
|
||||
if (!sessionId || !sessions.has(sessionId)) {
|
||||
res.statusCode = 404;
|
||||
return res.send({ error: 'Session not found.' });
|
||||
return reply.code(404).send({ error: 'Session not found.' });
|
||||
}
|
||||
|
||||
const { transport } = sessions.get(sessionId);
|
||||
await transport.close();
|
||||
sessions.delete(sessionId);
|
||||
res.statusCode = 200;
|
||||
res.send({ ok: true });
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
logger.debug('MCP Streamable HTTP endpoint registered at /api/mcp');
|
||||
|
||||
@@ -70,7 +70,7 @@ export function normalizeListJobs(queryResult, { page, pageSize }) {
|
||||
|
||||
let md = `**Tool:** list_jobs | **Status:** OK\n\n`;
|
||||
md += `Found **${queryResult.totalNumber}** job(s). Showing page ${page} of ${maxPage} (${jobs.length} on this page).`;
|
||||
if (hasMore) md += ` More pages available — use page=${page + 1} to continue.`;
|
||||
if (hasMore) md += ` More pages available - use page=${page + 1} to continue.`;
|
||||
md += '\n\n';
|
||||
|
||||
if (jobs.length > 0) {
|
||||
@@ -120,7 +120,7 @@ export function normalizeListListings(queryResult, { page, pageSize }) {
|
||||
|
||||
let md = `**Tool:** list_listings | **Status:** OK\n\n`;
|
||||
md += `Found **${queryResult.totalNumber}** listing(s). Showing page ${page} of ${maxPage} (${listings.length} on this page).`;
|
||||
if (hasMore) md += ` More pages available — use page=${page + 1} to continue.`;
|
||||
if (hasMore) md += ` More pages available - use page=${page + 1} to continue.`;
|
||||
md += '\n\n';
|
||||
|
||||
if (listings.length > 0) {
|
||||
|
||||
@@ -17,6 +17,6 @@ Multiple recipients:
|
||||
|
||||
Common SMTP settings:
|
||||
|
||||
- **Gmail** — `smtp.gmail.com`, port 587, secure: false
|
||||
- **Outlook** — `smtp.office365.com`, port 587, secure: false
|
||||
- **Yahoo** — `smtp.mail.yahoo.com`, port 465, secure: true
|
||||
- **Gmail** - `smtp.gmail.com`, port 587, secure: false
|
||||
- **Outlook** - `smtp.office365.com`, port 587, secure: false
|
||||
- **Yahoo** - `smtp.mail.yahoo.com`, port 465, secure: true
|
||||
|
||||
@@ -94,7 +94,7 @@ export async function applyBotPreventionToPage(page, cfg) {
|
||||
// webdriver
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
|
||||
|
||||
// chrome runtime — expose loadTimes, csi and app like real Chrome
|
||||
// chrome runtime - expose loadTimes, csi and app like real Chrome
|
||||
// @ts-ignore
|
||||
window.chrome = {
|
||||
runtime: {},
|
||||
@@ -129,7 +129,7 @@ export async function applyBotPreventionToPage(page, cfg) {
|
||||
get: () => (window.localStorage.getItem('__LANGS__') || 'de-DE,de').split(','),
|
||||
});
|
||||
|
||||
// plugins — mimic real Chrome's built-in PDF plugins
|
||||
// plugins - mimic real Chrome's built-in PDF plugins
|
||||
const makePlugin = (name, filename, description, mimeType, mimeTypeSuffix) => {
|
||||
const mimeObj = { type: mimeType, suffixes: mimeTypeSuffix, description, enabledPlugin: null };
|
||||
const plugin = { name, filename, description, length: 1, 0: mimeObj };
|
||||
@@ -274,14 +274,14 @@ export async function applyBotPreventionToPage(page, cfg) {
|
||||
//noop
|
||||
}
|
||||
|
||||
// document.hasFocus — headless returns false; real active tabs return true
|
||||
// document.hasFocus - headless returns false; real active tabs return true
|
||||
try {
|
||||
document.hasFocus = () => true;
|
||||
} catch {
|
||||
//noop
|
||||
}
|
||||
|
||||
// screen color depth — normalise in case headless reports 0
|
||||
// screen color depth - normalise in case headless reports 0
|
||||
try {
|
||||
Object.defineProperty(screen, 'colorDepth', { get: () => 24 });
|
||||
Object.defineProperty(screen, 'pixelDepth', { get: () => 24 });
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function launchBrowser(url, options) {
|
||||
removeUserDataDir = true;
|
||||
}
|
||||
|
||||
// On ARM64 Docker, Chrome for Testing has no native binary — use system Chromium instead.
|
||||
// On ARM64 Docker, Chrome for Testing has no native binary - use system Chromium instead.
|
||||
const executablePath =
|
||||
options?.executablePath ||
|
||||
(process.arch === 'arm64' && process.env.IS_DOCKER === 'true' ? '/usr/bin/chromium' : undefined);
|
||||
|
||||
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fredy",
|
||||
"version": "21.0.2",
|
||||
"version": "21.1.0",
|
||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
@@ -65,6 +65,10 @@
|
||||
"@douyinfe/semi-icons": "^2.95.1",
|
||||
"@douyinfe/semi-ui": "2.95.1",
|
||||
"@douyinfe/semi-ui-19": "^2.95.1",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/helmet": "^13.0.2",
|
||||
"@fastify/session": "^11.1.1",
|
||||
"@fastify/static": "^9.1.3",
|
||||
"@mapbox/mapbox-gl-draw": "^1.5.1",
|
||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||
"@sendgrid/mail": "8.1.6",
|
||||
@@ -72,17 +76,16 @@
|
||||
"@vitejs/plugin-react": "6.0.1",
|
||||
"adm-zip": "^0.5.17",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"body-parser": "2.2.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"cheerio": "^1.2.0",
|
||||
"cookie-session": "2.1.1",
|
||||
"fastify": "^5.8.5",
|
||||
"handlebars": "4.7.9",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"nanoid": "5.1.9",
|
||||
"node-cron": "^4.2.1",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-mailjet": "6.0.11",
|
||||
"nodemailer": "^8.0.6",
|
||||
"nodemailer": "^8.0.7",
|
||||
"p-throttle": "^8.1.0",
|
||||
"package-up": "^5.0.0",
|
||||
"puppeteer": "^24.42.0",
|
||||
@@ -96,9 +99,7 @@
|
||||
"react-router": "7.14.2",
|
||||
"react-router-dom": "7.14.2",
|
||||
"resend": "^6.12.2",
|
||||
"restana": "5.2.0",
|
||||
"semver": "^7.7.4",
|
||||
"serve-static": "2.2.1",
|
||||
"slack": "11.0.2",
|
||||
"vite": "8.0.10",
|
||||
"x-var": "^3.0.1",
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('#immobilien.de testsuite()', () => {
|
||||
expect(listing.link).toContain('https://www.immobilien.de');
|
||||
expect(listing.address).toBeTypeOf('string');
|
||||
expect(listing.address).not.toBe('');
|
||||
// description may be null if selectors don't match yet — falls back gracefully
|
||||
// description may be null if selectors don't match yet - falls back gracefully
|
||||
if (listing.description != null) {
|
||||
expect(listing.description).toBeTypeOf('string');
|
||||
}
|
||||
|
||||
@@ -713,7 +713,7 @@
|
||||
|
||||
</div><!-- /srb-filters -->
|
||||
|
||||
<!-- Search button — CMS handles via onclick="{{submit()}}"; srbDoSearch() is
|
||||
<!-- Search button - CMS handles via onclick="{{submit()}}"; srbDoSearch() is
|
||||
a standalone fallback that skips empty params (used outside CMS context) -->
|
||||
<button type="button" class="srb-search-btn" id="srbSearchBtn">
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
@@ -740,7 +740,7 @@
|
||||
</div><!-- /srb-wrap -->
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════
|
||||
MODAL — WEITERE FILTER
|
||||
MODAL - WEITERE FILTER
|
||||
═══════════════════════════════════════════════════════════ -->
|
||||
<!-- Screen-reader live region: announces filter changes and modal state -->
|
||||
<div id="srbLiveRegion" role="status" aria-live="polite" aria-atomic="true" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap"></div>
|
||||
@@ -768,7 +768,7 @@
|
||||
<circle cx="12" cy="10" r="3" stroke="currentColor" stroke-width="2"></circle>
|
||||
</svg>
|
||||
<input type="text" class="srb-modal-location-input" id="srbModalLocation" placeholder="Ort, Postleitzahl, Stadt" autocomplete="off" b-event="keyup" role="combobox" aria-autocomplete="list" aria-controls="srbModalLocationList" aria-expanded="false">
|
||||
<!-- Mirror of the desktop grbWoHidden — Piglet updates this via b-model
|
||||
<!-- Mirror of the desktop grbWoHidden - Piglet updates this via b-model
|
||||
when select(entry) fires in the modal controller's own scope. -->
|
||||
<input type="hidden" id="srbModalWoHidden" value="district:2434,2695,2621,2700,2967,2734,2909,2955,2392,2746,2767,2982,2904,2612,2892,2587,2871,2975,2591,2887,2569,2640,2735">
|
||||
<div b-redrawable="autocomplete" class="srb-autocomplete-wrapper">
|
||||
@@ -899,7 +899,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Additional criteria — CMS server-side rendered per typ+objektart ── -->
|
||||
<!-- ── Additional criteria - CMS server-side rendered per typ+objektart ── -->
|
||||
<div class="srb-modal-criteria-groups">
|
||||
<div class="srb-modal-section srb-modal-section--animated srb-modal-section--criteria" data-valid-searches="["kaufen_grundstueck","kaufen_haus","kaufen_rendite","kaufen_wohnung","mieten_grundstueck","mieten_haus","mieten_waz","mieten_wohnung"]">
|
||||
<div class="srb-section-body">
|
||||
@@ -4393,7 +4393,7 @@
|
||||
void g.offsetHeight;
|
||||
g.classList.remove('lr-card__gallery--no-transition');
|
||||
|
||||
// counter total (CMS doesn't evaluate expressions in attrs — read from hidden span)
|
||||
// counter total (CMS doesn't evaluate expressions in attrs - read from hidden span)
|
||||
var totalEl = g.querySelector('.lr-card__gallery-total');
|
||||
var counter = g.querySelector('.lr-card__gallery-counter');
|
||||
if (counter && totalEl) counter.dataset.total = totalEl.textContent.trim();
|
||||
|
||||
@@ -1523,7 +1523,7 @@
|
||||
|
||||
|
||||
<div class="hidden print:flex print:items-center print:justify-between mb-2">
|
||||
<img src="/static/img/brand/logo.b187f1e54302.svg" class="w-32" alt="ohne-makler — Immobilien selbst vermarkten">
|
||||
<img src="/static/img/brand/logo.b187f1e54302.svg" class="w-32" alt="ohne-makler - Immobilien selbst vermarkten">
|
||||
<br>
|
||||
Diese Seite wurde ausgedruckt von:<br>
|
||||
https://www.ohne-makler.net/immobilien/wohnung-kaufen/nordrhein-westfalen/dusseldorf/
|
||||
|
||||
@@ -12,7 +12,7 @@ import './Index.less';
|
||||
|
||||
// Semi UI uses react-dom (not react-dom/client) internally for imperative renders
|
||||
// like Toast, Notification, etc. In React 19, createRoot was removed from react-dom
|
||||
// and lives only in react-dom/client — inject it so Toast can create its own root.
|
||||
// and lives only in react-dom/client - inject it so Toast can create its own root.
|
||||
semiGlobal.config.createRoot = createRoot;
|
||||
|
||||
const container = document.getElementById('fredy');
|
||||
|
||||
@@ -71,7 +71,7 @@ body {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
// Suppress focus outlines — Semi uses --semi-color-primary (our red) for all rings
|
||||
// Suppress focus outlines - Semi uses --semi-color-primary (our red) for all rings
|
||||
button:focus,
|
||||
button:focus-visible,
|
||||
.semi-button:focus,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 242 KiB |
BIN
ui/src/assets/no_image.png
Normal file
BIN
ui/src/assets/no_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -26,7 +26,7 @@ import {
|
||||
} from '@douyinfe/semi-icons';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import ListingDeletionModal from '../../ListingDeletionModal.jsx';
|
||||
import no_image from '../../../assets/no_image.jpg';
|
||||
import no_image from '../../../assets/no_image.png';
|
||||
import * as timeService from '../../../services/time/timeService.js';
|
||||
import { xhrDelete, xhrPost } from '../../../services/xhr.js';
|
||||
import { useActions, useSelector } from '../../../services/state/store.js';
|
||||
|
||||
@@ -136,9 +136,9 @@
|
||||
color: @color-muted !important;
|
||||
}
|
||||
|
||||
// Collapsed state — icons perfectly centered
|
||||
// Collapsed state - icons perfectly centered
|
||||
.semi-navigation-collapsed {
|
||||
// Text span is display:block and takes up flex space — must be removed so justify-content:center works
|
||||
// Text span is display:block and takes up flex space - must be removed so justify-content:center works
|
||||
.semi-navigation-item-text {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -165,7 +165,7 @@
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
// Semi adds margin-right to icons for text spacing — remove it when collapsed
|
||||
// Semi adds margin-right to icons for text spacing - remove it when collapsed
|
||||
.semi-navigation-item-icon,
|
||||
.semi-navigation-sub-title-icon {
|
||||
display: flex !important;
|
||||
@@ -179,13 +179,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Semi Nav.Footer — full width, no extra padding (our BEM class controls it)
|
||||
// Semi Nav.Footer - full width, no extra padding (our BEM class controls it)
|
||||
.semi-navigation-footer {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
// Collapsed submenu popup — actual class used by Semi UI is .semi-navigation-popover
|
||||
// Collapsed submenu popup - actual class used by Semi UI is .semi-navigation-popover
|
||||
.semi-navigation-popover {
|
||||
background: @color-elevated !important;
|
||||
border: 1px solid @color-border !important;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
padding: 16px 20px !important;
|
||||
}
|
||||
|
||||
// Semi input focus — subtle, not accent
|
||||
// Semi input focus - subtle, not accent
|
||||
.semi-input-wrapper:focus-within,
|
||||
.semi-select:focus-within {
|
||||
border-color: @color-border-bright !important;
|
||||
|
||||
@@ -299,7 +299,7 @@ const GeneralSettings = function GeneralSettings() {
|
||||
|
||||
<SegmentPart
|
||||
name="Analytics"
|
||||
helpText="Anonymous usage data to help improve Fredy — provider names, adapter names, OS, Node version, and architecture."
|
||||
helpText="Anonymous usage data to help improve Fredy - provider names, adapter names, OS, Node version, and architecture."
|
||||
>
|
||||
<Checkbox checked={analyticsEnabled} onChange={(e) => setAnalyticsEnabled(e.target.checked)}>
|
||||
Enable analytics
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// TimePicker fix — scoped so it doesn't pollute modal headers
|
||||
// TimePicker fix - scoped so it doesn't pollute modal headers
|
||||
.semi-timepicker .semi-input-wrapper,
|
||||
.semi-timepicker .semi-input-inset-label-wrapper {
|
||||
background: @color-elevated !important;
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from '@douyinfe/semi-icons';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import no_image from '../../assets/no_image.jpg';
|
||||
import no_image from '../../assets/no_image.png';
|
||||
import * as timeService from '../../services/time/timeService.js';
|
||||
import { distanceMeters, getBoundsFromCoords } from './mapUtils.js';
|
||||
import { xhrPost } from '../../services/xhr.js';
|
||||
@@ -337,10 +337,16 @@ export default function ListingDetail() {
|
||||
<Col span={24} lg={12}>
|
||||
<div className="listing-detail__image-container">
|
||||
<Image
|
||||
src={listing.image_url}
|
||||
fallback={no_image}
|
||||
src={listing.image_url ?? no_image}
|
||||
fallback={
|
||||
<img
|
||||
src={no_image}
|
||||
alt="No image available"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
preview={true}
|
||||
preview={!!listing.image_url}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { distanceMeters, generateCircleCoords, getBoundsFromCenter, getBoundsFro
|
||||
import { Banner, Select, Switch, Toast, Typography } from '@douyinfe/semi-ui-19';
|
||||
import { IconDelete, IconEyeOpened, IconLink } from '@douyinfe/semi-icons';
|
||||
|
||||
import no_image from '../../assets/no_image.jpg';
|
||||
import no_image from '../../assets/no_image.png';
|
||||
import _RangeSlider from 'react-range-slider-input';
|
||||
import 'react-range-slider-input/dist/style.css';
|
||||
import './Map.less';
|
||||
|
||||
474
yarn.lock
474
yarn.lock
@@ -2,15 +2,6 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"0http@^4.4.0":
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/0http/-/0http-4.4.0.tgz#0929a272d32ac931dfab538be671c1520be5b194"
|
||||
integrity sha512-Zs3pTtQFZL8ishQREXX+9cMo1zRgaFF8g6AtnEL0iPUhzi2thtHf3P+487uhIieJm+wyPMu5QHJi8PCrt2zXJQ==
|
||||
dependencies:
|
||||
lru-cache "^11.2.1"
|
||||
regexparam "^3.0.0"
|
||||
trouter "^4.0.0"
|
||||
|
||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz"
|
||||
@@ -1136,6 +1127,99 @@
|
||||
"@eslint/core" "^1.2.1"
|
||||
levn "^0.4.1"
|
||||
|
||||
"@fastify/accept-negotiator@^2.0.0":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz#77afd6254ba77f6c22c6f35c4fb0c1b6d005199b"
|
||||
integrity sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==
|
||||
|
||||
"@fastify/ajv-compiler@^4.0.5":
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz#fdb0887a7af51abaae8c1829e8099d34f8ddd302"
|
||||
integrity sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==
|
||||
dependencies:
|
||||
ajv "^8.12.0"
|
||||
ajv-formats "^3.0.1"
|
||||
fast-uri "^3.0.0"
|
||||
|
||||
"@fastify/cookie@^11.0.2":
|
||||
version "11.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/cookie/-/cookie-11.0.2.tgz#fa772c652e51f9252addd788289aa16627ebe5f1"
|
||||
integrity sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==
|
||||
dependencies:
|
||||
cookie "^1.0.0"
|
||||
fastify-plugin "^5.0.0"
|
||||
|
||||
"@fastify/error@^4.0.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/error/-/error-4.2.0.tgz#d40f46ba75f541fdcc4dc276b7308bbc8e8e6d7a"
|
||||
integrity sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==
|
||||
|
||||
"@fastify/fast-json-stringify-compiler@^5.0.0":
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz#fae495bf30dbbd029139839ec5c2ea111bde7d3f"
|
||||
integrity sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==
|
||||
dependencies:
|
||||
fast-json-stringify "^6.0.0"
|
||||
|
||||
"@fastify/forwarded@^3.0.0":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/forwarded/-/forwarded-3.0.1.tgz#9662b7bd4a59f6d123cc3487494f75f635c32d23"
|
||||
integrity sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==
|
||||
|
||||
"@fastify/helmet@^13.0.2":
|
||||
version "13.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/helmet/-/helmet-13.0.2.tgz#c1b2e4dc28ddfb596121e311a0c9f318773b7f5b"
|
||||
integrity sha512-tO1QMkOfNeCt9l4sG/FiWErH4QMm+RjHzbMTrgew1DYOQ2vb/6M1G2iNABBrD7Xq6dUk+HLzWW8u+rmmhQHifA==
|
||||
dependencies:
|
||||
fastify-plugin "^5.0.0"
|
||||
helmet "^8.0.0"
|
||||
|
||||
"@fastify/merge-json-schemas@^0.2.0":
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz#3aa30d2f0c81a8ac5995b6d94ed4eaa2c3055824"
|
||||
integrity sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
"@fastify/proxy-addr@^5.0.0":
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz#f5360b5dd83c7de3d41b415be4aab84ae44aa106"
|
||||
integrity sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==
|
||||
dependencies:
|
||||
"@fastify/forwarded" "^3.0.0"
|
||||
ipaddr.js "^2.1.0"
|
||||
|
||||
"@fastify/send@^4.0.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/send/-/send-4.1.0.tgz#d9c283b86e12080c0dcc160bbc16106debf1f0d3"
|
||||
integrity sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==
|
||||
dependencies:
|
||||
"@lukeed/ms" "^2.0.2"
|
||||
escape-html "~1.0.3"
|
||||
fast-decode-uri-component "^1.0.1"
|
||||
http-errors "^2.0.0"
|
||||
mime "^3"
|
||||
|
||||
"@fastify/session@^11.1.1":
|
||||
version "11.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/session/-/session-11.1.1.tgz#591071a567fcd86b96f49b4321dc0198e2f4e226"
|
||||
integrity sha512-nuKwTHxh3eJsI4NJeXoYVGzXUsg+kH1WfHgS7IofVyVhmjc+A6qGr+29WQy8hYZiNtmCjfG415COpf5xTBkW4Q==
|
||||
dependencies:
|
||||
fastify-plugin "^5.0.1"
|
||||
safe-stable-stringify "^2.4.3"
|
||||
|
||||
"@fastify/static@^9.1.3":
|
||||
version "9.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/static/-/static-9.1.3.tgz#d878694333b75c646d40670b7416044de09f09ad"
|
||||
integrity sha512-aXrYtsiryLhRxRNaxNqsn7FUISeb7rB9q4eHUPIot5aeQBLNahnz1m6thzm7JWC1poSGXS9XrX8DvuMivp2hkQ==
|
||||
dependencies:
|
||||
"@fastify/accept-negotiator" "^2.0.0"
|
||||
"@fastify/send" "^4.0.0"
|
||||
content-disposition "^1.0.1"
|
||||
fastify-plugin "^5.0.0"
|
||||
fastq "^1.17.1"
|
||||
glob "^13.0.0"
|
||||
|
||||
"@floating-ui/core@^1.7.3":
|
||||
version "1.7.3"
|
||||
resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz"
|
||||
@@ -1228,6 +1312,11 @@
|
||||
resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz"
|
||||
integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==
|
||||
|
||||
"@lukeed/ms@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@lukeed/ms/-/ms-2.0.2.tgz#07f09e59a74c52f4d88c6db5c1054e819538e2a8"
|
||||
integrity sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==
|
||||
|
||||
"@mapbox/geojson-area@^0.2.2":
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz#18d7814aa36bf23fbbcc379f8e26a22927debf10"
|
||||
@@ -1418,6 +1507,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.127.0.tgz#8374fcdfb4a641861218daa5700c447c00b66663"
|
||||
integrity sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==
|
||||
|
||||
"@pinojs/redact@^0.4.0":
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@pinojs/redact/-/redact-0.4.0.tgz#c3de060dd12640dcc838516aa2a6803cc7b2e9d6"
|
||||
integrity sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==
|
||||
|
||||
"@puppeteer/browsers@2.13.0":
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@puppeteer/browsers/-/browsers-2.13.0.tgz#10f980c6d65efeff77f8a3cac6e1a7ac10604500"
|
||||
@@ -2183,6 +2277,11 @@
|
||||
convert-source-map "^2.0.0"
|
||||
tinyrainbow "^3.1.0"
|
||||
|
||||
abstract-logging@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839"
|
||||
integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==
|
||||
|
||||
accepts@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895"
|
||||
@@ -2243,6 +2342,16 @@ ajv@^8.0.0, ajv@^8.17.1:
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
ajv@^8.12.0:
|
||||
version "8.20.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.20.0.tgz#304b3636add88ba7d936760dd50ece006dea95f9"
|
||||
integrity sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
fast-uri "^3.0.1"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
ansi-escapes@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz"
|
||||
@@ -2400,6 +2509,11 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
atomic-sleep@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
|
||||
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
|
||||
|
||||
available-typed-arrays@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz"
|
||||
@@ -2407,6 +2521,14 @@ available-typed-arrays@^1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
avvio@^9.0.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/avvio/-/avvio-9.2.0.tgz#16bb653c022237d1aeb984b00d3cbe2d96b77c20"
|
||||
integrity sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==
|
||||
dependencies:
|
||||
"@fastify/error" "^4.0.0"
|
||||
fastq "^1.17.1"
|
||||
|
||||
axios@^1.12.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz"
|
||||
@@ -2565,7 +2687,7 @@ bl@^4.0.3:
|
||||
inherits "^2.0.4"
|
||||
readable-stream "^3.4.0"
|
||||
|
||||
body-parser@2.2.2, body-parser@^2.2.1:
|
||||
body-parser@^2.2.1:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz"
|
||||
integrity sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==
|
||||
@@ -2600,6 +2722,13 @@ brace-expansion@^5.0.2:
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
brace-expansion@^5.0.5:
|
||||
version "5.0.5"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.5.tgz#dcc3a37116b79f3e1b46db994ced5d570e930fdb"
|
||||
integrity sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
braces@~3.0.2:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
|
||||
@@ -2875,6 +3004,11 @@ content-disposition@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.1.tgz#a8b7bbeb2904befdfb6787e5c0c086959f605f9b"
|
||||
integrity sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==
|
||||
|
||||
content-disposition@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.1.0.tgz#f3db789c752d45564cc7e9e1e0b31790d4a38e17"
|
||||
integrity sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==
|
||||
|
||||
content-type@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz"
|
||||
@@ -2885,16 +3019,6 @@ convert-source-map@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
|
||||
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
||||
|
||||
cookie-session@2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/cookie-session/-/cookie-session-2.1.1.tgz"
|
||||
integrity sha512-ji3kym/XZaFVew1+tIZk5ZLp9Z/fLv9rK1aZmpug0FsgE7Cu3ZDrUdRo7FT9vFjMYfNimrrUHJzywDwT7XEFlg==
|
||||
dependencies:
|
||||
cookies "0.9.1"
|
||||
debug "3.2.7"
|
||||
on-headers "~1.1.0"
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
cookie-signature@^1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793"
|
||||
@@ -2905,19 +3029,11 @@ cookie@^0.7.1:
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
||||
|
||||
cookie@^1.0.1:
|
||||
cookie@^1.0.0, cookie@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.1.1.tgz#3bb9bdfc82369db9c2f69c93c9c3ceb310c88b3c"
|
||||
integrity sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==
|
||||
|
||||
cookies@0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz"
|
||||
integrity sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==
|
||||
dependencies:
|
||||
depd "~2.0.0"
|
||||
keygrip "~1.1.0"
|
||||
|
||||
copy-anything@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
|
||||
@@ -3039,13 +3155,6 @@ date-fns@^2.29.3:
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
|
||||
debug@3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
|
||||
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@4, debug@^4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5, debug@^4.4.0, debug@^4.4.3:
|
||||
version "4.4.3"
|
||||
resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz"
|
||||
@@ -3124,7 +3233,7 @@ depd@2.0.0, depd@^2.0.0, depd@~2.0.0:
|
||||
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
|
||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||
|
||||
dequal@^2.0.0:
|
||||
dequal@^2.0.0, dequal@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz"
|
||||
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||
@@ -3441,9 +3550,9 @@ escalade@^3.1.1, escalade@^3.2.0:
|
||||
resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
|
||||
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
|
||||
|
||||
escape-html@^1.0.3:
|
||||
escape-html@^1.0.3, escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
||||
|
||||
escape-string-regexp@^4.0.0:
|
||||
@@ -3763,6 +3872,11 @@ extract-zip@^2.0.1:
|
||||
resolved "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz"
|
||||
integrity sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==
|
||||
|
||||
fast-decode-uri-component@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543"
|
||||
integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
|
||||
@@ -3783,21 +3897,73 @@ fast-json-stable-stringify@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
|
||||
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
|
||||
|
||||
fast-json-stringify@^6.0.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz#e59f2fbd558842d7ec085276444d15e6500c16d4"
|
||||
integrity sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==
|
||||
dependencies:
|
||||
"@fastify/merge-json-schemas" "^0.2.0"
|
||||
ajv "^8.12.0"
|
||||
ajv-formats "^3.0.1"
|
||||
fast-uri "^3.0.0"
|
||||
json-schema-ref-resolver "^3.0.0"
|
||||
rfdc "^1.2.0"
|
||||
|
||||
fast-levenshtein@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fast-querystring@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.2.tgz#a6d24937b4fc6f791b4ee31dcb6f53aeafb89f53"
|
||||
integrity sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==
|
||||
dependencies:
|
||||
fast-decode-uri-component "^1.0.1"
|
||||
|
||||
fast-sha256@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-sha256/-/fast-sha256-1.3.0.tgz#7916ba2054eeb255982608cccd0f6660c79b7ae6"
|
||||
integrity sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==
|
||||
|
||||
fast-uri@^3.0.1:
|
||||
fast-uri@^3.0.0, fast-uri@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
|
||||
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
|
||||
|
||||
fastify-plugin@^5.0.0, fastify-plugin@^5.0.1:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-5.1.0.tgz#7083e039d6418415f9a669f8c25e72fc5bf2d3e7"
|
||||
integrity sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==
|
||||
|
||||
fastify@^5.8.5:
|
||||
version "5.8.5"
|
||||
resolved "https://registry.yarnpkg.com/fastify/-/fastify-5.8.5.tgz#c452224295e0ca550bcd0efc3f7d3e90e9c11955"
|
||||
integrity sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==
|
||||
dependencies:
|
||||
"@fastify/ajv-compiler" "^4.0.5"
|
||||
"@fastify/error" "^4.0.0"
|
||||
"@fastify/fast-json-stringify-compiler" "^5.0.0"
|
||||
"@fastify/proxy-addr" "^5.0.0"
|
||||
abstract-logging "^2.0.1"
|
||||
avvio "^9.0.0"
|
||||
fast-json-stringify "^6.0.0"
|
||||
find-my-way "^9.0.0"
|
||||
light-my-request "^6.0.0"
|
||||
pino "^9.14.0 || ^10.1.0"
|
||||
process-warning "^5.0.0"
|
||||
rfdc "^1.3.1"
|
||||
secure-json-parse "^4.0.0"
|
||||
semver "^7.6.0"
|
||||
toad-cache "^3.7.0"
|
||||
|
||||
fastq@^1.17.1:
|
||||
version "1.20.1"
|
||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675"
|
||||
integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==
|
||||
dependencies:
|
||||
reusify "^1.0.4"
|
||||
|
||||
fd-slicer@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz"
|
||||
@@ -3854,6 +4020,15 @@ finalhandler@^2.1.0:
|
||||
parseurl "^1.3.3"
|
||||
statuses "^2.0.1"
|
||||
|
||||
find-my-way@^9.0.0:
|
||||
version "9.5.0"
|
||||
resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-9.5.0.tgz#3e6819bf4310b5293f490c032e70be0b506d0dc8"
|
||||
integrity sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
fast-querystring "^1.0.0"
|
||||
safe-regex2 "^5.0.0"
|
||||
|
||||
find-up-simple@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz"
|
||||
@@ -4076,6 +4251,15 @@ glob-parent@~5.1.2:
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^13.0.0:
|
||||
version "13.0.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d"
|
||||
integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==
|
||||
dependencies:
|
||||
minimatch "^10.2.2"
|
||||
minipass "^7.1.3"
|
||||
path-scurry "^2.0.2"
|
||||
|
||||
glob@^7.0.0, glob@^7.1.3:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
|
||||
@@ -4216,6 +4400,11 @@ hast-util-whitespace@^3.0.0:
|
||||
dependencies:
|
||||
"@types/hast" "^3.0.0"
|
||||
|
||||
helmet@^8.0.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/helmet/-/helmet-8.1.0.tgz#f96d23fedc89e9476ecb5198181009c804b8b38c"
|
||||
integrity sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==
|
||||
|
||||
history@5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.npmjs.org/history/-/history-5.3.0.tgz"
|
||||
@@ -4375,6 +4564,11 @@ ipaddr.js@1.9.1:
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||
|
||||
ipaddr.js@^2.1.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz#71dce70e1398122208996d1c22f2ba46a24b1abc"
|
||||
integrity sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==
|
||||
|
||||
is-alphabetical@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz"
|
||||
@@ -4702,6 +4896,13 @@ json-parse-even-better-errors@^2.3.0:
|
||||
resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz"
|
||||
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
|
||||
|
||||
json-schema-ref-resolver@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz#28f6a410122cde9238762a5e9296faa38be28708"
|
||||
integrity sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
json-schema-traverse@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
|
||||
@@ -4761,13 +4962,6 @@ kdbush@^4.0.2:
|
||||
resolved "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz"
|
||||
integrity sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==
|
||||
|
||||
keygrip@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz"
|
||||
integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==
|
||||
dependencies:
|
||||
tsscmp "1.0.6"
|
||||
|
||||
keyv@^4.5.4:
|
||||
version "4.5.4"
|
||||
resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz"
|
||||
@@ -4823,6 +5017,15 @@ levn@^0.4.1:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "~0.4.0"
|
||||
|
||||
light-my-request@^6.0.0:
|
||||
version "6.6.0"
|
||||
resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-6.6.0.tgz#c9448772323f65f33720fb5979c7841f14060add"
|
||||
integrity sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==
|
||||
dependencies:
|
||||
cookie "^1.0.1"
|
||||
process-warning "^4.0.0"
|
||||
set-cookie-parser "^2.6.0"
|
||||
|
||||
lightningcss-android-arm64@1.32.0:
|
||||
version "1.32.0"
|
||||
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
|
||||
@@ -4983,7 +5186,7 @@ lottie-web@^5.13.0:
|
||||
resolved "https://registry.yarnpkg.com/lottie-web/-/lottie-web-5.13.0.tgz#441d3df217cc8ba302338c3f168e1a3af0f221d3"
|
||||
integrity sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ==
|
||||
|
||||
lru-cache@^11.2.1:
|
||||
lru-cache@^11.0.0:
|
||||
version "11.3.5"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.3.5.tgz#29047d348c0b2793e3112a01c739bb7c6d855637"
|
||||
integrity sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==
|
||||
@@ -5690,6 +5893,11 @@ mime@^1.4.1:
|
||||
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
mime@^3:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
|
||||
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
|
||||
|
||||
mimic-function@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz"
|
||||
@@ -5707,6 +5915,13 @@ minimatch@^10.2.1, minimatch@^10.2.4:
|
||||
dependencies:
|
||||
brace-expansion "^5.0.2"
|
||||
|
||||
minimatch@^10.2.2:
|
||||
version "10.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1"
|
||||
integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.5"
|
||||
|
||||
minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
|
||||
@@ -5719,6 +5934,11 @@ minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.8:
|
||||
resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz"
|
||||
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
|
||||
|
||||
minipass@^7.1.2, minipass@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b"
|
||||
integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==
|
||||
|
||||
mitt@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz"
|
||||
@@ -5737,7 +5957,7 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
|
||||
resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz"
|
||||
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
|
||||
|
||||
ms@^2.1.1, ms@^2.1.3:
|
||||
ms@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
@@ -5835,7 +6055,7 @@ node-releases@^2.0.27:
|
||||
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz"
|
||||
integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==
|
||||
|
||||
nodemailer@^8.0.6:
|
||||
nodemailer@^8.0.7:
|
||||
version "8.0.7"
|
||||
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-8.0.7.tgz#538729a79444e538331bca8a6fc3e5c034eaebc6"
|
||||
integrity sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==
|
||||
@@ -5930,6 +6150,11 @@ obug@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/obug/-/obug-2.1.1.tgz#2cba74ff241beb77d63055ddf4cd1e9f90b538be"
|
||||
integrity sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==
|
||||
|
||||
on-exit-leak-free@^2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz#fed195c9ebddb7d9e4c3842f93f281ac8dadd3b8"
|
||||
integrity sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==
|
||||
|
||||
on-finished@^2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz"
|
||||
@@ -5937,11 +6162,6 @@ on-finished@^2.4.1:
|
||||
dependencies:
|
||||
ee-first "1.1.1"
|
||||
|
||||
on-headers@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz"
|
||||
integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==
|
||||
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
|
||||
@@ -6112,6 +6332,14 @@ path-parse@^1.0.7:
|
||||
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-scurry@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85"
|
||||
integrity sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==
|
||||
dependencies:
|
||||
lru-cache "^11.0.0"
|
||||
minipass "^7.1.2"
|
||||
|
||||
path-to-regexp@^8.0.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f"
|
||||
@@ -6159,6 +6387,35 @@ pify@^4.0.1:
|
||||
resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz"
|
||||
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
|
||||
|
||||
pino-abstract-transport@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz#b21e5f33a297e8c4c915c62b3ce5dd4a87a52c23"
|
||||
integrity sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==
|
||||
dependencies:
|
||||
split2 "^4.0.0"
|
||||
|
||||
pino-std-serializers@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz#a7b0cd65225f29e92540e7853bd73b07479893fc"
|
||||
integrity sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==
|
||||
|
||||
"pino@^9.14.0 || ^10.1.0":
|
||||
version "10.3.1"
|
||||
resolved "https://registry.yarnpkg.com/pino/-/pino-10.3.1.tgz#6552c8f8d8481844c9e452e7bf0be90bff1939ce"
|
||||
integrity sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==
|
||||
dependencies:
|
||||
"@pinojs/redact" "^0.4.0"
|
||||
atomic-sleep "^1.0.0"
|
||||
on-exit-leak-free "^2.1.0"
|
||||
pino-abstract-transport "^3.0.0"
|
||||
pino-std-serializers "^7.0.0"
|
||||
process-warning "^5.0.0"
|
||||
quick-format-unescaped "^4.0.3"
|
||||
real-require "^0.2.0"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
sonic-boom "^4.0.1"
|
||||
thread-stream "^4.0.0"
|
||||
|
||||
pkce-challenge@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d"
|
||||
@@ -6237,6 +6494,16 @@ prismjs@^1.29.0:
|
||||
resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz"
|
||||
integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==
|
||||
|
||||
process-warning@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.1.tgz#5c1db66007c67c756e4e09eb170cdece15da32fb"
|
||||
integrity sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==
|
||||
|
||||
process-warning@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7"
|
||||
integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==
|
||||
|
||||
progress@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz"
|
||||
@@ -6565,6 +6832,11 @@ query-string@9.3.1:
|
||||
filter-obj "^5.1.0"
|
||||
split-on-first "^3.0.0"
|
||||
|
||||
quick-format-unescaped@^4.0.3:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7"
|
||||
integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==
|
||||
|
||||
quickselect@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz"
|
||||
@@ -6680,6 +6952,11 @@ readdirp@~3.6.0:
|
||||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
real-require@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78"
|
||||
integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==
|
||||
|
||||
rechoir@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz"
|
||||
@@ -6765,11 +7042,6 @@ regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4:
|
||||
gopd "^1.2.0"
|
||||
set-function-name "^2.0.2"
|
||||
|
||||
regexparam@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz"
|
||||
integrity sha512-RSYAtP31mvYLkAHrOlh25pCNQ5hWnT106VukGaaFfuJrZFkGRX5GhUAdPqpSDXxOhA2c4akmRuplv1mRqnBn6Q==
|
||||
|
||||
regexpu-core@^6.3.1:
|
||||
version "6.4.0"
|
||||
resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz"
|
||||
@@ -6901,13 +7173,6 @@ resolve@^2.0.0-next.5:
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
restana@5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/restana/-/restana-5.2.0.tgz#71921e0fe48d6f59b2c212c5dd31524e157f7e8b"
|
||||
integrity sha512-IY/ibi45o5fNLtC0lrYmLe0ncqknInePUBCt7Y8bzmhINp7ZxfGAB3p0YQv4JzZh7aYll0335lffOYlXpkjO/Q==
|
||||
dependencies:
|
||||
"0http" "^4.4.0"
|
||||
|
||||
restore-cursor@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz"
|
||||
@@ -6916,9 +7181,19 @@ restore-cursor@^5.0.0:
|
||||
onetime "^7.0.0"
|
||||
signal-exit "^4.1.0"
|
||||
|
||||
rfdc@^1.4.1:
|
||||
ret@~0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/ret/-/ret-0.5.0.tgz#30a4d38a7e704bd96dc5ffcbe7ce2a9274c41c95"
|
||||
integrity sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==
|
||||
|
||||
reusify@^1.0.4:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f"
|
||||
integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
|
||||
|
||||
rfdc@^1.2.0, rfdc@^1.3.1, rfdc@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz"
|
||||
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
|
||||
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
|
||||
|
||||
rimraf@^3.0.2:
|
||||
@@ -7013,7 +7288,7 @@ safe-array-concat@^1.1.3:
|
||||
has-symbols "^1.1.0"
|
||||
isarray "^2.0.5"
|
||||
|
||||
safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||
safe-buffer@^5.0.1, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
@@ -7035,6 +7310,18 @@ safe-regex-test@^1.1.0:
|
||||
es-errors "^1.3.0"
|
||||
is-regex "^1.2.1"
|
||||
|
||||
safe-regex2@^5.0.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-5.1.1.tgz#a5f3a6e35b8d84d0f41fa22efd5b6d30b367bbc7"
|
||||
integrity sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==
|
||||
dependencies:
|
||||
ret "~0.5.0"
|
||||
|
||||
safe-stable-stringify@^2.3.1, safe-stable-stringify@^2.4.3:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd"
|
||||
integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
|
||||
@@ -7057,6 +7344,11 @@ scroll-into-view-if-needed@^2.2.24:
|
||||
dependencies:
|
||||
compute-scroll-into-view "^1.0.20"
|
||||
|
||||
secure-json-parse@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-4.1.0.tgz#4f1ab41c67a13497ea1b9131bb4183a22865477c"
|
||||
integrity sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==
|
||||
|
||||
semver@^5.6.0:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz"
|
||||
@@ -7072,7 +7364,7 @@ semver@^7.3.5, semver@^7.5.3:
|
||||
resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
|
||||
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
|
||||
|
||||
semver@^7.7.4:
|
||||
semver@^7.6.0, semver@^7.7.4:
|
||||
version "7.7.4"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
||||
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
|
||||
@@ -7111,7 +7403,7 @@ send@^1.2.0:
|
||||
range-parser "^1.2.1"
|
||||
statuses "^2.0.1"
|
||||
|
||||
serve-static@2.2.1, serve-static@^2.2.0:
|
||||
serve-static@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz"
|
||||
integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==
|
||||
@@ -7301,6 +7593,13 @@ socks@^2.8.3:
|
||||
ip-address "^10.0.1"
|
||||
smart-buffer "^4.2.0"
|
||||
|
||||
sonic-boom@^4.0.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-4.2.1.tgz#28598250df4899c0ac572d7e2f0460690ba6a030"
|
||||
integrity sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==
|
||||
dependencies:
|
||||
atomic-sleep "^1.0.0"
|
||||
|
||||
source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
|
||||
@@ -7326,6 +7625,11 @@ split-on-first@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz"
|
||||
integrity sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==
|
||||
|
||||
split2@^4.0.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
|
||||
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
|
||||
|
||||
stackback@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
|
||||
@@ -7584,6 +7888,13 @@ text-decoder@^1.1.0:
|
||||
dependencies:
|
||||
b4a "^1.6.4"
|
||||
|
||||
thread-stream@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-4.0.0.tgz#732f007c24da7084f729d6e3a7e3f5934a7380b7"
|
||||
integrity sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==
|
||||
dependencies:
|
||||
real-require "^0.2.0"
|
||||
|
||||
tiny-json-http@^7.0.2:
|
||||
version "7.5.1"
|
||||
resolved "https://registry.npmjs.org/tiny-json-http/-/tiny-json-http-7.5.1.tgz"
|
||||
@@ -7632,6 +7943,11 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
toad-cache@^3.7.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/toad-cache/-/toad-cache-3.7.0.tgz#b9b63304ea7c45ec34d91f1d2fa513517025c441"
|
||||
integrity sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==
|
||||
|
||||
toidentifier@1.0.1, toidentifier@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz"
|
||||
@@ -7652,23 +7968,11 @@ trough@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz"
|
||||
integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==
|
||||
|
||||
trouter@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/trouter/-/trouter-4.0.0.tgz"
|
||||
integrity sha512-bwwr76BThfiVwAFZqks5cJ+VoKNM3/2Yg1ZwJslkdmAUQ6S0UNoCoGYFDxdw+u1skfexggdmD2p35kW5Td4Cug==
|
||||
dependencies:
|
||||
regexparam "^3.0.0"
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.4.0, tslib@^2.8.1:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
|
||||
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
|
||||
|
||||
tsscmp@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz"
|
||||
integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
|
||||
|
||||
tunnel-agent@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user