mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7d0037edd | ||
|
|
f339a2e2cf | ||
|
|
da8fd13973 |
@@ -1 +1 @@
|
|||||||
{"interval":"60","port":9998,"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":null,"sqlitepath":"/db"}
|
{"interval":"60","port":9998,"workingHours":{"from":"","to":""},"demoMode":false,"analyticsEnabled":true,"sqlitepath":"/db"}
|
||||||
@@ -7,6 +7,7 @@ import { loginRouter } from './routes/loginRoute.js';
|
|||||||
import { config } from '../utils.js';
|
import { config } from '../utils.js';
|
||||||
import { userRouter } from './routes/userRoute.js';
|
import { userRouter } from './routes/userRoute.js';
|
||||||
import { jobRouter } from './routes/jobRouter.js';
|
import { jobRouter } from './routes/jobRouter.js';
|
||||||
|
import { versionRouter } from './routes/versionRouter.js';
|
||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import restana from 'restana';
|
import restana from 'restana';
|
||||||
import files from 'serve-static';
|
import files from 'serve-static';
|
||||||
@@ -23,6 +24,7 @@ service.use(cookieSession());
|
|||||||
service.use(staticService);
|
service.use(staticService);
|
||||||
service.use('/api/admin', authInterceptor());
|
service.use('/api/admin', authInterceptor());
|
||||||
service.use('/api/jobs', authInterceptor());
|
service.use('/api/jobs', authInterceptor());
|
||||||
|
service.use('/api/version', authInterceptor());
|
||||||
// /admin can only be accessed when user is having admin permissions
|
// /admin can only be accessed when user is having admin permissions
|
||||||
service.use('/api/admin', adminInterceptor());
|
service.use('/api/admin', adminInterceptor());
|
||||||
service.use('/api/jobs/notificationAdapter', notificationAdapterRouter);
|
service.use('/api/jobs/notificationAdapter', notificationAdapterRouter);
|
||||||
@@ -30,6 +32,7 @@ service.use('/api/admin/generalSettings', generalSettingsRouter);
|
|||||||
service.use('/api/jobs/provider', providerRouter);
|
service.use('/api/jobs/provider', providerRouter);
|
||||||
service.use('/api/jobs/insights', analyticsRouter);
|
service.use('/api/jobs/insights', analyticsRouter);
|
||||||
service.use('/api/admin/users', userRouter);
|
service.use('/api/admin/users', userRouter);
|
||||||
|
service.use('/api/version', versionRouter);
|
||||||
service.use('/api/jobs', jobRouter);
|
service.use('/api/jobs', jobRouter);
|
||||||
service.use('/api/login', loginRouter);
|
service.use('/api/login', loginRouter);
|
||||||
//this route is unsecured intentionally as it is being queried from the login page
|
//this route is unsecured intentionally as it is being queried from the login page
|
||||||
|
|||||||
30
lib/api/routes/versionRouter.js
Normal file
30
lib/api/routes/versionRouter.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import restana from 'restana';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import { getPackageVersion } from '../../utils.js';
|
||||||
|
|
||||||
|
const service = restana();
|
||||||
|
const versionRouter = service.newRouter();
|
||||||
|
|
||||||
|
versionRouter.get('/', async (req, res) => {
|
||||||
|
const versionPayload = await getCurrentVersionFromGithub();
|
||||||
|
res.body = versionPayload == null ? { newVersion: false } : versionPayload;
|
||||||
|
res.send();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getCurrentVersionFromGithub() {
|
||||||
|
const raw = await fetch('https://api.github.com/repos/orangecoding/fredy/releases/latest');
|
||||||
|
const data = await raw.json();
|
||||||
|
const localFredyVersion = await getPackageVersion();
|
||||||
|
if (localFredyVersion === data.tag_name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
newVersion: true,
|
||||||
|
version: data.tag_name,
|
||||||
|
url: data.html_url,
|
||||||
|
body: data.body,
|
||||||
|
localFredyVersion,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { versionRouter };
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* The mobile API provides the following endpoints:
|
* The mobile API provides the following endpoints:
|
||||||
* - GET /search/total?{search parameters}: Returns the total number of listings for the given query
|
* - GET /search/total?{search parameters}: Returns the total number of listings for the given query
|
||||||
* Example: `curl -H "User-Agent: ImmoScout24_1410_30_._" https://api.mobile.immobilienscout24.de/search/total?searchType=region&realestatetype=apartmentrent&pricetype=calculatedtotalrent&geocodes=%2Fde%2Fberlin%2Fberlin `
|
* Example: `curl -H "User-Agent: ImmoScout_27.3_26.0_._" https://api.mobile.immobilienscout24.de/search/total?searchType=region&realestatetype=apartmentrent&pricetype=calculatedtotalrent&geocodes=%2Fde%2Fberlin%2Fberlin `
|
||||||
*
|
*
|
||||||
* - POST /search/list?{search parameters}: Actually retrieves the listings. Body is json encoded and contains
|
* - POST /search/list?{search parameters}: Actually retrieves the listings. Body is json encoded and contains
|
||||||
* data specifying additional results (advertisements) to return. The format is as follows:
|
* data specifying additional results (advertisements) to return. The format is as follows:
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
* ```
|
* ```
|
||||||
* It is not necessary to provide data for the specified keys.
|
* It is not necessary to provide data for the specified keys.
|
||||||
*
|
*
|
||||||
* Example: `curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calculatedtotalrent&realestatetype=apartmentrent&searchType=region&geocodes=%2Fde%2Fberlin%2Fberlin&pagenumber=1' -H "Connection: keep-alive" -H "User-Agent: ImmoScout24_1410_30_._" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"supportedResultListType": [], "userData": {}}'`
|
* Example: `curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calculatedtotalrent&realestatetype=apartmentrent&searchType=region&geocodes=%2Fde%2Fberlin%2Fberlin&pagenumber=1' -H "Connection: keep-alive" -H "User-Agent: ImmoScout_27.3_26.0_._" -H "Accept: application/json" -H "Content-Type: application/json" -d '{"supportedResultListType": [], "userData": {}}'`
|
||||||
|
|
||||||
* - GET /expose/{id} - Returns the details of a listing. The response contains additional details not included in the
|
* - GET /expose/{id} - Returns the details of a listing. The response contains additional details not included in the
|
||||||
* listing response.
|
* listing response.
|
||||||
*
|
*
|
||||||
* Example: `curl -H "User-Agent: ImmoScout24_1410_30_._" "https://api.mobile.immobilienscout24.de/expose/158382494"`
|
* Example: `curl -H "User-Agent: ImmoScout_27.3_26.0_._" "https://api.mobile.immobilienscout24.de/expose/158382494"`
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* It is necessary to set the correct User Agent (see `getListings`) in the request header.
|
* It is necessary to set the correct User Agent (see `getListings`) in the request header.
|
||||||
@@ -44,7 +44,7 @@ async function getListings(url) {
|
|||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'ImmoScout24_1410_30_._',
|
'User-Agent': 'ImmoScout_27.3_26.0_._',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { getJobs } from '../storage/jobStorage.js';
|
import { getJobs } from '../storage/jobStorage.js';
|
||||||
import { getUniqueId } from './uniqueId.js';
|
import { getUniqueId } from './uniqueId.js';
|
||||||
import { config, inDevMode } from '../../utils.js';
|
import { config, getPackageVersion, inDevMode } from '../../utils.js';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { packageUp } from 'package-up';
|
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import logger from '../logger.js';
|
import logger from '../logger.js';
|
||||||
|
|
||||||
@@ -77,15 +75,3 @@ function enrichTrackingObject(trackingObject) {
|
|||||||
version,
|
version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPackageVersion() {
|
|
||||||
try {
|
|
||||||
const packagePath = await packageUp();
|
|
||||||
const packageJson = readFileSync(packagePath, 'utf8');
|
|
||||||
const json = JSON.parse(packageJson);
|
|
||||||
return json.version;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error reading version from package.json', error);
|
|
||||||
}
|
|
||||||
return 'N/A';
|
|
||||||
}
|
|
||||||
|
|||||||
20
lib/utils.js
20
lib/utils.js
@@ -3,8 +3,9 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { DEFAULT_CONFIG } from './defaultConfig.js';
|
import { DEFAULT_CONFIG } from './defaultConfig.js';
|
||||||
import fs from 'fs';
|
import fs, { readFileSync } from 'fs';
|
||||||
import logger from './services/logger.js';
|
import logger from './services/logger.js';
|
||||||
|
import { packageUp } from 'package-up';
|
||||||
|
|
||||||
const RE_GT = />/g;
|
const RE_GT = />/g;
|
||||||
const RE_WEBP = /\/format\/webp/gi;
|
const RE_WEBP = /\/format\/webp/gi;
|
||||||
@@ -196,6 +197,22 @@ const normalizeImageUrl = (url) => {
|
|||||||
return u;
|
return u;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns Fredy's version
|
||||||
|
* @returns {Promise<*|string>}
|
||||||
|
*/
|
||||||
|
async function getPackageVersion() {
|
||||||
|
try {
|
||||||
|
const packagePath = await packageUp();
|
||||||
|
const packageJson = readFileSync(packagePath, 'utf8');
|
||||||
|
const json = JSON.parse(packageJson);
|
||||||
|
return json.version;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error reading version from package.json', error);
|
||||||
|
}
|
||||||
|
return 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
await refreshConfig();
|
await refreshConfig();
|
||||||
|
|
||||||
export { isOneOf };
|
export { isOneOf };
|
||||||
@@ -206,6 +223,7 @@ export { duringWorkingHoursOrNotSet };
|
|||||||
export { getDirName };
|
export { getDirName };
|
||||||
export { config };
|
export { config };
|
||||||
export { buildHash };
|
export { buildHash };
|
||||||
|
export { getPackageVersion };
|
||||||
export default {
|
export default {
|
||||||
isOneOf,
|
isOneOf,
|
||||||
nullOrEmpty,
|
nullOrEmpty,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "fredy",
|
"name": "fredy",
|
||||||
"version": "12.1.1",
|
"version": "12.1.3",
|
||||||
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
"description": "[F]ind [R]eal [E]states [d]amn eas[y].",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ Challenges:
|
|||||||
_Returns the total number of listings for the given query._
|
_Returns the total number of listings for the given query._
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -H "User-Agent: ImmoScout24_1410_30_._" \
|
curl -H "User-Agent: ImmoScout_27.3_26.0_._" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
"https://api.mobile.immobilienscout24.de/search/total?searchType=region&realestatetype=apartmentrent&pricetype=calculatedtotalrent&geocodes=%2Fde%2Fberlin%2Fberlin"
|
"https://api.mobile.immobilienscout24.de/search/total?searchType=region&realestatetype=apartmentrent&pricetype=calculatedtotalrent&geocodes=%2Fde%2Fberlin%2Fberlin"
|
||||||
```
|
```
|
||||||
@@ -63,7 +63,7 @@ _The body is json encoded and contains data specifying additional results (adver
|
|||||||
```
|
```
|
||||||
curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calculatedtotalrent&realestatetype=apartmentrent&searchType=region&geocodes=%2Fde%2Fberlin%2Fberlin&pagenumber=1' \
|
curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calculatedtotalrent&realestatetype=apartmentrent&searchType=region&geocodes=%2Fde%2Fberlin%2Fberlin&pagenumber=1' \
|
||||||
-H "Connection: keep-alive" \
|
-H "Connection: keep-alive" \
|
||||||
-H "User-Agent: ImmoScout24_1410_30_._" \
|
-H "User-Agent: ImmoScout_27.3_26.0_._" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"supportedResultListType":[],"userData":{}}'
|
-d '{"supportedResultListType":[],"userData":{}}'
|
||||||
@@ -78,7 +78,7 @@ curl -X POST 'https://api.mobile.immobilienscout24.de/search/list?pricetype=calc
|
|||||||
The response contains additional details not included in the listing response.
|
The response contains additional details not included in the listing response.
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -H "User-Agent: ImmoScout24_1410_30_._" \
|
curl -H "User-Agent: ImmoScout_27.3_26.0_._" \
|
||||||
-H "Accept: application/json" \
|
-H "Accept: application/json" \
|
||||||
"https://api.mobile.immobilienscout24.de/expose/158382494"
|
"https://api.mobile.immobilienscout24.de/expose/158382494"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ describe('#immoscout-mobile URL conversion', () => {
|
|||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'ImmoScout24_1410_30_._',
|
'User-Agent': 'ImmoScout_27.3_26.0_._',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ import Jobs from './views/jobs/Jobs';
|
|||||||
import './App.less';
|
import './App.less';
|
||||||
import TrackingModal from './components/tracking/TrackingModal.jsx';
|
import TrackingModal from './components/tracking/TrackingModal.jsx';
|
||||||
import { Banner } from '@douyinfe/semi-ui';
|
import { Banner } from '@douyinfe/semi-ui';
|
||||||
|
import VersionBanner from './components/version/VersionBanner.jsx';
|
||||||
|
|
||||||
export default function FredyApp() {
|
export default function FredyApp() {
|
||||||
const actions = useActions();
|
const actions = useActions();
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const currentUser = useSelector((state) => state.user.currentUser);
|
const currentUser = useSelector((state) => state.user.currentUser);
|
||||||
|
const versionUpdate = useSelector((state) => state.versionUpdate.versionUpdate);
|
||||||
const settings = useSelector((state) => state.generalSettings.settings);
|
const settings = useSelector((state) => state.generalSettings.settings);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -34,6 +36,7 @@ export default function FredyApp() {
|
|||||||
await actions.jobs.getProcessingTimes();
|
await actions.jobs.getProcessingTimes();
|
||||||
await actions.notificationAdapter.getAdapter();
|
await actions.notificationAdapter.getAdapter();
|
||||||
await actions.generalSettings.getGeneralSettings();
|
await actions.generalSettings.getGeneralSettings();
|
||||||
|
await actions.versionUpdate.getVersionUpdate();
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -53,7 +56,6 @@ export default function FredyApp() {
|
|||||||
<Route path="*" element={<Navigate to="/login" replace />} />
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|
||||||
return loading ? null : needsLogin() ? (
|
return loading ? null : needsLogin() ? (
|
||||||
login()
|
login()
|
||||||
) : (
|
) : (
|
||||||
@@ -62,7 +64,7 @@ export default function FredyApp() {
|
|||||||
<Logout />
|
<Logout />
|
||||||
<Logo width={190} white />
|
<Logo width={190} white />
|
||||||
<Menu isAdmin={isAdmin()} />
|
<Menu isAdmin={isAdmin()} />
|
||||||
|
{versionUpdate?.newVersion && <VersionBanner />}
|
||||||
{settings.demoMode && (
|
{settings.demoMode && (
|
||||||
<>
|
<>
|
||||||
<Banner
|
<Banner
|
||||||
|
|||||||
43
ui/src/components/version/VersionBanner.jsx
Normal file
43
ui/src/components/version/VersionBanner.jsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Banner, Descriptions } from '@douyinfe/semi-ui';
|
||||||
|
import { useSelector } from '../../services/state/store.js';
|
||||||
|
|
||||||
|
import './VersionBanner.less';
|
||||||
|
|
||||||
|
export default function VersionBanner() {
|
||||||
|
const versionUpdate = useSelector((state) => state.versionUpdate.versionUpdate);
|
||||||
|
return (
|
||||||
|
<Banner
|
||||||
|
className="versionBanner"
|
||||||
|
type="success"
|
||||||
|
icon={null}
|
||||||
|
description={
|
||||||
|
<div>
|
||||||
|
<p>A new version of Fredy is available. Update now to take advantage of the latest features and bug fixes.</p>
|
||||||
|
<Descriptions row size="small">
|
||||||
|
<Descriptions.Item itemKey="Your Version">{versionUpdate.localFredyVersion}</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey="Latest Version">{versionUpdate.version}</Descriptions.Item>
|
||||||
|
<Descriptions.Item itemKey="Github Release">
|
||||||
|
<a href={versionUpdate.url} target="_blank" rel="noreferrer">
|
||||||
|
{versionUpdate.url}
|
||||||
|
</a>{' '}
|
||||||
|
</Descriptions.Item>
|
||||||
|
</Descriptions>
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
<small>Release Notes</small>
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
|
<pre>{stripFullChangelog(versionUpdate.body)}</pre>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
function stripFullChangelog(text) {
|
||||||
|
if (text == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return text.replace(/(?:\r?\n)\*\*Full Changelog\*\*[\s\S]*$/u, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
3
ui/src/components/version/VersionBanner.less
Normal file
3
ui/src/components/version/VersionBanner.less
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.versionBanner {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
@@ -118,6 +118,18 @@ export const useFredyState = create(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
versionUpdate: {
|
||||||
|
async getVersionUpdate() {
|
||||||
|
try {
|
||||||
|
const response = await xhrGet('/api/version');
|
||||||
|
set((state) => ({
|
||||||
|
versionUpdate: { ...state.versionUpdate, versionUpdate: response.json },
|
||||||
|
}));
|
||||||
|
} catch (Exception) {
|
||||||
|
console.error('Error while trying to get resource for api/version. Error:', Exception);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
@@ -125,6 +137,7 @@ export const useFredyState = create(
|
|||||||
notificationAdapter: [],
|
notificationAdapter: [],
|
||||||
generalSettings: { settings: {} },
|
generalSettings: { settings: {} },
|
||||||
demoMode: { demoMode: false },
|
demoMode: { demoMode: false },
|
||||||
|
versionUpdate: {},
|
||||||
provider: [],
|
provider: [],
|
||||||
jobs: { jobs: [], insights: {}, processingTimes: {} },
|
jobs: { jobs: [], insights: {}, processingTimes: {} },
|
||||||
user: { users: [], currentUser: null },
|
user: { users: [], currentUser: null },
|
||||||
@@ -135,6 +148,7 @@ export const useFredyState = create(
|
|||||||
notificationAdapter: { ...effects.notificationAdapter },
|
notificationAdapter: { ...effects.notificationAdapter },
|
||||||
generalSettings: { ...effects.generalSettings },
|
generalSettings: { ...effects.generalSettings },
|
||||||
demoMode: { ...effects.demoMode },
|
demoMode: { ...effects.demoMode },
|
||||||
|
versionUpdate: { ...effects.versionUpdate },
|
||||||
provider: { ...effects.provider },
|
provider: { ...effects.provider },
|
||||||
jobs: { ...effects.jobs },
|
jobs: { ...effects.jobs },
|
||||||
user: { ...effects.user },
|
user: { ...effects.user },
|
||||||
|
|||||||
Reference in New Issue
Block a user