Browse Source

Feature/warm up portfolio snapshot calculation during biometric authentication (#7427)

* Warm up portfolio snapshot calculation during biometric authentication

* Update changelog
pull/7554/head^2
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
bb946de25c
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 6
      apps/api/src/app/auth/auth.module.ts
  3. 69
      apps/api/src/app/auth/web-auth.service.ts

1
CHANGELOG.md

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added the country flag to the currency selector - Added the country flag to the currency selector
- Added a _Storybook_ story for the currency selector component - Added a _Storybook_ story for the currency selector component
- Extended the entity logo component by a `hasPlaceholder` attribute to reserve the space of a missing logo - Extended the entity logo component by a `hasPlaceholder` attribute to reserve the space of a missing logo
- Warmed up the portfolio snapshot calculation in the background during the biometric authentication
### Changed ### Changed

6
apps/api/src/app/auth/auth.module.ts

@ -1,14 +1,17 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service'; import { WebAuthService } from '@ghostfolio/api/app/auth/web-auth.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module'; import { SubscriptionModule } from '@ghostfolio/api/app/subscription/subscription.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service'; import { ApiKeyService } from '@ghostfolio/api/services/api-key/api-key.service';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module'; import { FetchModule } from '@ghostfolio/api/services/fetch/fetch.module';
import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service'; import { FetchService } from '@ghostfolio/api/services/fetch/fetch.service';
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module'; import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
import { PropertyModule } from '@ghostfolio/api/services/property/property.module'; import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
import { PortfolioSnapshotQueueModule } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.module';
import { Logger, Module } from '@nestjs/common'; import { Logger, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt'; import { JwtModule } from '@nestjs/jwt';
@ -24,14 +27,17 @@ import { OidcStrategy } from './oidc.strategy';
@Module({ @Module({
controllers: [AuthController], controllers: [AuthController],
imports: [ imports: [
ApiModule,
ConfigurationModule, ConfigurationModule,
FetchModule, FetchModule,
JwtModule.register({ JwtModule.register({
secret: process.env.JWT_SECRET_KEY, secret: process.env.JWT_SECRET_KEY,
signOptions: { expiresIn: '180 days' } signOptions: { expiresIn: '180 days' }
}), }),
PortfolioSnapshotQueueModule,
PrismaModule, PrismaModule,
PropertyModule, PropertyModule,
RedisCacheModule,
SubscriptionModule, SubscriptionModule,
UserModule UserModule
], ],

69
apps/api/src/app/auth/web-auth.service.ts

@ -1,6 +1,15 @@
import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service'; import { AuthDeviceService } from '@ghostfolio/api/app/auth-device/auth-device.service';
import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ApiService } from '@ghostfolio/api/services/api/api.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import {
PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW,
PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS
} from '@ghostfolio/common/config';
import { AuthDeviceDto } from '@ghostfolio/common/dtos'; import { AuthDeviceDto } from '@ghostfolio/common/dtos';
import { import {
AssertionCredentialJSON, AssertionCredentialJSON,
@ -29,6 +38,7 @@ import {
VerifyRegistrationResponseOpts VerifyRegistrationResponseOpts
} from '@simplewebauthn/server'; } from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers'; import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';
import { isPast } from 'date-fns';
import ms from 'ms'; import ms from 'ms';
@Injectable() @Injectable()
@ -36,9 +46,12 @@ export class WebAuthService {
private readonly logger = new Logger(WebAuthService.name); private readonly logger = new Logger(WebAuthService.name);
public constructor( public constructor(
private readonly apiService: ApiService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly deviceService: AuthDeviceService, private readonly deviceService: AuthDeviceService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly portfolioSnapshotService: PortfolioSnapshotService,
private readonly redisCacheService: RedisCacheService,
private readonly userService: UserService, private readonly userService: UserService,
@Inject(REQUEST) private readonly request: RequestWithUser @Inject(REQUEST) private readonly request: RequestWithUser
) {} ) {}
@ -155,6 +168,9 @@ export class WebAuthService {
throw new Error('Device not found'); throw new Error('Device not found');
} }
// Compute in the background during the biometric authentication
void this.warmUpPortfolioSnapshot({ userId: device.userId });
const opts: GenerateAuthenticationOptionsOpts = { const opts: GenerateAuthenticationOptionsOpts = {
allowCredentials: [], allowCredentials: [],
rpID: this.rpID, rpID: this.rpID,
@ -233,4 +249,57 @@ export class WebAuthService {
throw new Error(); throw new Error();
} }
private async isPortfolioSnapshotExpired(portfolioSnapshotKey: string) {
try {
const { expiration }: PortfolioSnapshotValue = JSON.parse(
await this.redisCacheService.get(portfolioSnapshotKey)
);
return isPast(new Date(expiration));
} catch {
return true;
}
}
private async warmUpPortfolioSnapshot({ userId }: { userId: string }) {
try {
const user = await this.userService.user({ id: userId });
if (!user) {
return;
}
const userSettings = user.settings.settings;
const filters = this.apiService.buildFiltersFromUserSettings({
userSettings
});
const portfolioSnapshotKey =
this.redisCacheService.getPortfolioSnapshotKey({ filters, userId });
if (await this.isPortfolioSnapshotExpired(portfolioSnapshotKey)) {
await this.portfolioSnapshotService.addJobToQueue({
data: {
filters,
userId,
calculationType: userSettings.performanceCalculationType,
userCurrency: userSettings.baseCurrency
},
name: PORTFOLIO_SNAPSHOT_PROCESS_JOB_NAME,
opts: {
...PORTFOLIO_SNAPSHOT_PROCESS_JOB_OPTIONS,
jobId: portfolioSnapshotKey,
priority: PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW
}
});
}
} catch (error) {
this.logger.error(
`Portfolio snapshot of user '${userId}' could not be warmed up`,
error
);
}
}
} }

Loading…
Cancel
Save