Browse Source

Initial setup

pull/6501/head
Thomas Kaul 3 weeks ago
parent
commit
861e6760ee
  1. 58
      apps/api/src/app/app.module.ts
  2. 2
      apps/api/src/main.ts
  3. 10
      apps/api/src/services/queues/data-gathering/data-gathering.module.ts
  4. 10
      apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts
  5. 4
      apps/client/proxy.conf.json
  6. 12
      apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
  7. 9
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  8. 72
      package-lock.json
  9. 3
      package.json

58
apps/api/src/app/app.module.ts

@ -13,15 +13,19 @@ import {
DEFAULT_LANGUAGE_CODE,
SUPPORTED_LANGUAGE_CODES
} from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { ExpressAdapter } from '@bull-board/express';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullModule } from '@nestjs/bull';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { StatusCodes } from 'http-status-codes';
import { getReasonPhrase, StatusCodes } from 'http-status-codes';
import { join } from 'node:path';
import passport from 'passport';
import { AccessModule } from './access/access.module';
import { AccountModule } from './account/account.module';
@ -70,6 +74,51 @@ import { UserModule } from './user/user.module';
AuthDeviceModule,
AuthModule,
BenchmarksModule,
BullBoardModule.forRoot({
adapter: ExpressAdapter,
boardOptions: {
uiConfig: {
boardLogo: {
height: 0,
path: '',
width: 0
},
boardTitle: 'Job Queue',
favIcon: {
alternative: '/assets/favicon-32x32.png',
default: '/assets/favicon-32x32.png'
}
}
},
middleware: (req, res, next) => {
const token = req.headers.cookie
?.split(';')
.map((c) => c.trim())
.find((c) => c.startsWith('bull_board_token='))
?.split('=')[1];
if (token) {
req.headers.authorization = `Bearer ${token}`;
}
passport.authenticate('jwt', { session: false }, (error, user) => {
if (
error ||
!user ||
!hasPermission(user.permissions, permissions.accessAdminControl)
) {
res
.status(StatusCodes.FORBIDDEN)
.json({ message: getReasonPhrase(StatusCodes.FORBIDDEN) });
return;
}
next();
})(req, res, next);
},
route: '/admin/queues'
}),
BullModule.forRoot({
redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10),
@ -105,7 +154,12 @@ import { UserModule } from './user/user.module';
RedisCacheModule,
ScheduleModule.forRoot(),
ServeStaticModule.forRoot({
exclude: ['/.well-known/*wildcard', '/api/*wildcard', '/sitemap.xml'],
exclude: [
'/.well-known/*wildcard',
'/admin/queues/*wildcard',
'/api/*wildcard',
'/sitemap.xml'
],
rootPath: join(__dirname, '..', 'client'),
serveStaticOptions: {
setHeaders: (res) => {

2
apps/api/src/main.ts

@ -46,6 +46,7 @@ async function bootstrap() {
});
app.setGlobalPrefix('api', {
exclude: [
'admin/queues{/*wildcard}',
'sitemap.xml',
...SUPPORTED_LANGUAGE_CODES.map((languageCode) => {
// Exclude language-specific routes with an optional wildcard
@ -53,6 +54,7 @@ async function bootstrap() {
})
]
});
app.useGlobalPipes(
new ValidationPipe({
forbidNonWhitelisted: true,

10
apps/api/src/services/queues/data-gathering/data-gathering.module.ts

@ -9,6 +9,8 @@ import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathe
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
import { DATA_GATHERING_QUEUE } from '@ghostfolio/common/config';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import ms from 'ms';
@ -17,6 +19,14 @@ import { DataGatheringProcessor } from './data-gathering.processor';
@Module({
imports: [
BullBoardModule.forFeature({
adapter: BullAdapter,
name: DATA_GATHERING_QUEUE,
options: {
displayName: 'Data Gathering',
readOnlyMode: true
}
}),
BullModule.registerQueue({
limiter: {
duration: ms('4 seconds'),

10
apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.module.ts

@ -13,6 +13,8 @@ import {
PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE
} from '@ghostfolio/common/config';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
@ -23,6 +25,14 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor';
imports: [
AccountBalanceModule,
ActivitiesModule,
BullBoardModule.forFeature({
adapter: BullAdapter,
name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE,
options: {
displayName: 'Portfolio Snapshot Computation',
readOnlyMode: true
}
}),
BullModule.registerQueue({
name: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE,
settings: {

4
apps/client/proxy.conf.json

@ -1,4 +1,8 @@
{
"/admin/queues": {
"target": "http://0.0.0.0:3333",
"secure": false
},
"/api": {
"target": "http://0.0.0.0:3333",
"secure": false

12
apps/client/src/app/components/admin-jobs/admin-jobs.component.ts

@ -1,3 +1,4 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
@ -41,6 +42,7 @@ import {
chevronUpCircleOutline,
ellipsisHorizontal,
ellipsisVertical,
openOutline,
pauseOutline,
playOutline,
removeCircleOutline,
@ -104,6 +106,7 @@ export class GfAdminJobsComponent implements OnDestroy, OnInit {
private changeDetectorRef: ChangeDetectorRef,
private formBuilder: FormBuilder,
private notificationService: NotificationService,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
this.userService.stateChanged
@ -126,6 +129,7 @@ export class GfAdminJobsComponent implements OnDestroy, OnInit {
chevronUpCircleOutline,
ellipsisHorizontal,
ellipsisVertical,
openOutline,
pauseOutline,
playOutline,
removeCircleOutline,
@ -177,6 +181,14 @@ export class GfAdminJobsComponent implements OnDestroy, OnInit {
});
}
public onOpenBullBoard() {
const token = this.tokenStorageService.getToken();
document.cookie = `bull_board_token=${token}; path=/admin/queues; SameSite=Strict`;
window.open('/admin/queues', '_blank');
}
public onViewData(aData: AdminJobs['jobs'][0]['data']) {
this.notificationService.alert({
title: JSON.stringify(aData, null, ' ')

9
apps/client/src/app/components/admin-jobs/admin-jobs.html

@ -1,6 +1,15 @@
<div class="container">
<div class="row">
<div class="col">
@if (user?.settings?.isExperimentalFeatures) {
<div class="d-flex justify-content-end mb-3">
<button mat-stroked-button (click)="onOpenBullBoard()">
<span><ng-container i18n>Overview</ng-container></span>
<ion-icon class="ml-2" name="open-outline" />
</button>
</div>
}
<form class="align-items-center d-flex" [formGroup]="filterForm">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select formControlName="status">

72
package-lock.json

@ -21,6 +21,9 @@
"@angular/platform-browser-dynamic": "21.1.1",
"@angular/router": "21.1.1",
"@angular/service-worker": "21.1.1",
"@bull-board/api": "6.20.3",
"@bull-board/express": "6.20.3",
"@bull-board/nestjs": "6.20.3",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.5",
@ -3641,6 +3644,53 @@
"devOptional": true,
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@bull-board/api": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@bull-board/api/-/api-6.20.3.tgz",
"integrity": "sha512-cDrsJJsmF4DbbY8/5oHxO4qFtyFjxexsWQKHowsud/8H4mtZN7MZg4fCmNzfaxc9Ov7V6r9Y9F5G2Mq6t7ZEJg==",
"license": "MIT",
"dependencies": {
"redis-info": "^3.1.0"
},
"peerDependencies": {
"@bull-board/ui": "6.20.3"
}
},
"node_modules/@bull-board/express": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@bull-board/express/-/express-6.20.3.tgz",
"integrity": "sha512-S6BGeSf/PLwjx5W1IrKxoV8G6iiMmLqT/pldZ6BiC1IDldedisTtAdL1z117swXPv1H7/3hy0vr03dUr8bUCPg==",
"license": "MIT",
"dependencies": {
"@bull-board/api": "6.20.3",
"@bull-board/ui": "6.20.3",
"ejs": "^3.1.10",
"express": "^5.2.1"
}
},
"node_modules/@bull-board/nestjs": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@bull-board/nestjs/-/nestjs-6.20.3.tgz",
"integrity": "sha512-VFi96Z2M8k3G26H1ivzQnpjKszxh90vrUm78VtMZH/sh8wjm88mJFDXcOgFutOaddx7cc9VNXlKsTTcu6okPFQ==",
"license": "MIT",
"peerDependencies": {
"@bull-board/api": "^6.20.3",
"@nestjs/bull-shared": "^10.0.0 || ^11.0.0",
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
"reflect-metadata": "^0.1.13 || ^0.2.0",
"rxjs": "^7.8.1"
}
},
"node_modules/@bull-board/ui": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-6.20.3.tgz",
"integrity": "sha512-oANyYoW0X+xd0j/09DRyh3u7Q3wqBtXiLEWyZUJIi/Bjp/hINwiw20RwWuRcaFkqkFylEJL9l+pjmeSA9X5L2A==",
"license": "MIT",
"dependencies": {
"@bull-board/api": "6.20.3"
}
},
"node_modules/@chevrotain/cst-dts-gen": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz",
@ -14979,7 +15029,6 @@
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"dev": true,
"license": "MIT"
},
"node_modules/async-function": {
@ -15373,7 +15422,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
@ -15627,7 +15675,6 @@
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -16852,7 +16899,6 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/confbox": {
@ -18812,7 +18858,6 @@
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"jake": "^10.8.5"
@ -20253,7 +20298,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
"integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"minimatch": "^5.0.1"
@ -20263,7 +20307,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@ -20273,7 +20316,6 @@
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@ -21485,7 +21527,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@ -23125,7 +23166,6 @@
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz",
"integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.3",
@ -23144,7 +23184,6 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@ -25916,7 +25955,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -29532,6 +29570,15 @@
"node": ">=4"
}
},
"node_modules/redis-info": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redis-info/-/redis-info-3.1.0.tgz",
"integrity": "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.11"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
@ -32233,7 +32280,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"

3
package.json

@ -66,6 +66,9 @@
"@angular/platform-browser-dynamic": "21.1.1",
"@angular/router": "21.1.1",
"@angular/service-worker": "21.1.1",
"@bull-board/api": "6.20.3",
"@bull-board/express": "6.20.3",
"@bull-board/nestjs": "6.20.3",
"@codewithdan/observable-store": "2.2.15",
"@date-fns/utc": "2.1.1",
"@internationalized/number": "3.6.5",

Loading…
Cancel
Save