Merge branch 'main' of github.com:ghostfolio/ghostfolio
All checks were successful
Docker image CD / build_and_push (push) Successful in 21m57s
All checks were successful
Docker image CD / build_and_push (push) Successful in 21m57s
This commit is contained in:
commit
c393aeeb9a
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Upgraded `nestjs` from version `10.4.15` to `11.0.12`
|
||||||
|
|
||||||
## 2.161.0 - 2025-05-06
|
## 2.161.0 - 2025-05-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -21,37 +21,38 @@ export class ApiKeyStrategy extends PassportStrategy(
|
|||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {
|
) {
|
||||||
super(
|
super({ header: HEADER_KEY_TOKEN, prefix: 'Api-Key ' }, true);
|
||||||
{ header: HEADER_KEY_TOKEN, prefix: 'Api-Key ' },
|
}
|
||||||
true,
|
|
||||||
async (apiKey: string, done: (error: any, user?: any) => void) => {
|
|
||||||
try {
|
|
||||||
const user = await this.validateApiKey(apiKey);
|
|
||||||
|
|
||||||
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
public async validate(
|
||||||
if (hasRole(user, 'INACTIVE')) {
|
apiKey: string,
|
||||||
throw new HttpException(
|
done: (error: any, user?: any) => void
|
||||||
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
|
) {
|
||||||
StatusCodes.TOO_MANY_REQUESTS
|
try {
|
||||||
);
|
const user = await this.validateApiKey(apiKey);
|
||||||
}
|
|
||||||
|
|
||||||
await this.prismaService.analytics.upsert({
|
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
|
||||||
create: { User: { connect: { id: user.id } } },
|
if (hasRole(user, 'INACTIVE')) {
|
||||||
update: {
|
throw new HttpException(
|
||||||
activityCount: { increment: 1 },
|
getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS),
|
||||||
lastRequestAt: new Date()
|
StatusCodes.TOO_MANY_REQUESTS
|
||||||
},
|
);
|
||||||
where: { userId: user.id }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
done(null, user);
|
|
||||||
} catch (error) {
|
|
||||||
done(error, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.prismaService.analytics.upsert({
|
||||||
|
create: { User: { connect: { id: user.id } } },
|
||||||
|
update: {
|
||||||
|
activityCount: { increment: 1 },
|
||||||
|
lastRequestAt: new Date()
|
||||||
|
},
|
||||||
|
where: { userId: user.id }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
done(null, user);
|
||||||
|
} catch (error) {
|
||||||
|
done(error, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async validateApiKey(apiKey: string) {
|
private async validateApiKey(apiKey: string) {
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
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 { createKeyv } from '@keyv/redis';
|
||||||
import { CacheModule } from '@nestjs/cache-manager';
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { redisStore } from 'cache-manager-redis-yet';
|
|
||||||
import type { RedisClientOptions } from 'redis';
|
|
||||||
|
|
||||||
import { RedisCacheService } from './redis-cache.service';
|
import { RedisCacheService } from './redis-cache.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
exports: [RedisCacheService],
|
exports: [RedisCacheService],
|
||||||
imports: [
|
imports: [
|
||||||
CacheModule.registerAsync<RedisClientOptions>({
|
CacheModule.registerAsync({
|
||||||
imports: [ConfigurationModule],
|
imports: [ConfigurationModule],
|
||||||
inject: [ConfigurationService],
|
inject: [ConfigurationService],
|
||||||
useFactory: async (configurationService: ConfigurationService) => {
|
useFactory: async (configurationService: ConfigurationService) => {
|
||||||
@ -20,10 +19,13 @@ import { RedisCacheService } from './redis-cache.service';
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
store: redisStore,
|
stores: [
|
||||||
ttl: configurationService.get('CACHE_TTL'),
|
createKeyv(
|
||||||
url: `redis://${redisPassword ? `:${redisPassword}` : ''}@${configurationService.get('REDIS_HOST')}:${configurationService.get('REDIS_PORT')}/${configurationService.get('REDIS_DB')}`
|
`redis://${redisPassword ? `:${redisPassword}` : ''}@${configurationService.get('REDIS_HOST')}:${configurationService.get('REDIS_PORT')}/${configurationService.get('REDIS_DB')}`
|
||||||
} as RedisClientOptions;
|
)
|
||||||
|
],
|
||||||
|
ttl: configurationService.get('CACHE_TTL')
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
ConfigurationModule
|
ConfigurationModule
|
||||||
|
@ -2,20 +2,18 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
|
|||||||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||||
import { AssetProfileIdentifier, Filter } from '@ghostfolio/common/interfaces';
|
import { AssetProfileIdentifier, Filter } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { Milliseconds } from 'cache-manager';
|
|
||||||
import { RedisCache } from 'cache-manager-redis-yet';
|
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RedisCacheService {
|
export class RedisCacheService {
|
||||||
public constructor(
|
public constructor(
|
||||||
@Inject(CACHE_MANAGER) private readonly cache: RedisCache,
|
@Inject(CACHE_MANAGER) private readonly cache: Cache,
|
||||||
private readonly configurationService: ConfigurationService
|
private readonly configurationService: ConfigurationService
|
||||||
) {
|
) {
|
||||||
const client = cache.store.client;
|
const client = cache.stores[0];
|
||||||
|
|
||||||
client.on('error', (error) => {
|
client.on('error', (error) => {
|
||||||
Logger.error(error, 'RedisCacheService');
|
Logger.error(error, 'RedisCacheService');
|
||||||
@ -27,13 +25,33 @@ export class RedisCacheService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getKeys(aPrefix?: string): Promise<string[]> {
|
public async getKeys(aPrefix?: string): Promise<string[]> {
|
||||||
let prefix = aPrefix;
|
const keys: string[] = [];
|
||||||
|
const prefix = aPrefix;
|
||||||
|
|
||||||
if (prefix) {
|
this.cache.stores[0].deserialize = (value) => {
|
||||||
prefix = `${prefix}*`;
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof SyntaxError) {
|
||||||
|
Logger.debug(
|
||||||
|
`Failed to parse json, returning the value as String: ${value}`,
|
||||||
|
'RedisCacheService'
|
||||||
|
);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for await (const [key] of this.cache.stores[0].iterator({})) {
|
||||||
|
if ((prefix && key.startsWith(prefix)) || !prefix) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.cache.store.keys(prefix);
|
return keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPortfolioSnapshotKey({
|
public getPortfolioSnapshotKey({
|
||||||
@ -62,10 +80,8 @@ export class RedisCacheService {
|
|||||||
|
|
||||||
public async isHealthy() {
|
public async isHealthy() {
|
||||||
try {
|
try {
|
||||||
const client = this.cache.store.client;
|
|
||||||
|
|
||||||
const isHealthy = await Promise.race([
|
const isHealthy = await Promise.race([
|
||||||
client.ping(),
|
this.getKeys(),
|
||||||
new Promise((_, reject) =>
|
new Promise((_, reject) =>
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => reject(new Error('Redis health check timeout')),
|
() => reject(new Error('Redis health check timeout')),
|
||||||
@ -93,16 +109,14 @@ export class RedisCacheService {
|
|||||||
`${this.getPortfolioSnapshotKey({ userId })}`
|
`${this.getPortfolioSnapshotKey({ userId })}`
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const key of keys) {
|
return this.cache.mdel(keys);
|
||||||
await this.remove(key);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async reset() {
|
public async reset() {
|
||||||
return this.cache.reset();
|
return this.cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async set(key: string, value: string, ttl?: Milliseconds) {
|
public async set(key: string, value: string, ttl?: number) {
|
||||||
return this.cache.set(
|
return this.cache.set(
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
|
2934
package-lock.json
generated
2934
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -76,17 +76,18 @@
|
|||||||
"@dfinity/principal": "0.15.7",
|
"@dfinity/principal": "0.15.7",
|
||||||
"@dinero.js/currencies": "2.0.0-alpha.8",
|
"@dinero.js/currencies": "2.0.0-alpha.8",
|
||||||
"@internationalized/number": "3.6.0",
|
"@internationalized/number": "3.6.0",
|
||||||
"@nestjs/bull": "10.2.3",
|
"@keyv/redis": "4.3.4",
|
||||||
"@nestjs/cache-manager": "2.3.0",
|
"@nestjs/bull": "11.0.2",
|
||||||
"@nestjs/common": "10.4.15",
|
"@nestjs/cache-manager": "3.0.1",
|
||||||
"@nestjs/config": "3.3.0",
|
"@nestjs/common": "11.1.0",
|
||||||
"@nestjs/core": "10.4.15",
|
"@nestjs/config": "4.0.2",
|
||||||
"@nestjs/event-emitter": "2.1.1",
|
"@nestjs/core": "11.1.0",
|
||||||
"@nestjs/jwt": "10.2.0",
|
"@nestjs/event-emitter": "3.0.1",
|
||||||
"@nestjs/passport": "10.0.3",
|
"@nestjs/jwt": "11.0.0",
|
||||||
"@nestjs/platform-express": "10.4.15",
|
"@nestjs/passport": "11.0.5",
|
||||||
"@nestjs/schedule": "4.1.2",
|
"@nestjs/platform-express": "11.1.0",
|
||||||
"@nestjs/serve-static": "4.0.2",
|
"@nestjs/schedule": "6.0.0",
|
||||||
|
"@nestjs/serve-static": "5.0.3",
|
||||||
"@prisma/client": "6.7.0",
|
"@prisma/client": "6.7.0",
|
||||||
"@simplewebauthn/browser": "13.1.0",
|
"@simplewebauthn/browser": "13.1.0",
|
||||||
"@simplewebauthn/server": "13.1.1",
|
"@simplewebauthn/server": "13.1.1",
|
||||||
@ -95,8 +96,6 @@
|
|||||||
"big.js": "6.2.2",
|
"big.js": "6.2.2",
|
||||||
"bootstrap": "4.6.2",
|
"bootstrap": "4.6.2",
|
||||||
"bull": "4.16.5",
|
"bull": "4.16.5",
|
||||||
"cache-manager": "5.7.6",
|
|
||||||
"cache-manager-redis-yet": "5.1.4",
|
|
||||||
"chart.js": "4.4.9",
|
"chart.js": "4.4.9",
|
||||||
"chartjs-adapter-date-fns": "3.0.0",
|
"chartjs-adapter-date-fns": "3.0.0",
|
||||||
"chartjs-chart-treemap": "3.1.0",
|
"chartjs-chart-treemap": "3.1.0",
|
||||||
@ -153,8 +152,8 @@
|
|||||||
"@angular/pwa": "19.2.1",
|
"@angular/pwa": "19.2.1",
|
||||||
"@eslint/eslintrc": "3.3.1",
|
"@eslint/eslintrc": "3.3.1",
|
||||||
"@eslint/js": "9.24.0",
|
"@eslint/js": "9.24.0",
|
||||||
"@nestjs/schematics": "10.2.3",
|
"@nestjs/schematics": "11.0.5",
|
||||||
"@nestjs/testing": "10.4.15",
|
"@nestjs/testing": "11.1.0",
|
||||||
"@nx/angular": "20.8.1",
|
"@nx/angular": "20.8.1",
|
||||||
"@nx/cypress": "20.8.1",
|
"@nx/cypress": "20.8.1",
|
||||||
"@nx/eslint-plugin": "20.8.1",
|
"@nx/eslint-plugin": "20.8.1",
|
||||||
@ -173,7 +172,6 @@
|
|||||||
"@storybook/core-server": "8.6.12",
|
"@storybook/core-server": "8.6.12",
|
||||||
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "5.2.2",
|
||||||
"@types/big.js": "6.2.2",
|
"@types/big.js": "6.2.2",
|
||||||
"@types/cache-manager": "4.0.6",
|
|
||||||
"@types/google-spreadsheet": "3.1.5",
|
"@types/google-spreadsheet": "3.1.5",
|
||||||
"@types/jest": "29.5.13",
|
"@types/jest": "29.5.13",
|
||||||
"@types/lodash": "4.17.16",
|
"@types/lodash": "4.17.16",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user