Compare commits

...

4 Commits

Author SHA1 Message Date
Kenrick Tandrian 517425646f
Task/upgrade Storybook to version 10.5.7 (#7641) 2 days ago
Thomas Kaul 9d9d4450f4
Task/extend personal finance tools (20260816) (#7644) 2 days ago
Kenrick Tandrian 39ba6b98e6
Task/improve type safety in auth and user services (#7630) 2 days ago
Kenrick Tandrian 403b97a3e3
Task/upgrade to Nx to version 23.1.1 (#7640) 2 days ago
  1. 5
      CHANGELOG.md
  2. 18
      apps/api/src/app/auth/api-key.strategy.ts
  3. 21
      apps/api/src/app/auth/auth.module.ts
  4. 31
      apps/api/src/app/auth/oidc-state.store.ts
  5. 14
      apps/api/src/app/auth/oidc.strategy.ts
  6. 9
      apps/api/src/app/auth/web-auth.service.ts
  7. 13
      apps/api/src/app/user/user.service.ts
  8. 186
      libs/common/src/lib/personal-finance-tools.ts
  9. 3193
      package-lock.json
  10. 32
      package.json

5
CHANGELOG.md

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Changed
- Upgraded `Nx` from version `23.0.2` to `23.1.1`
- Upgraded `storybook` from version `10.1.10` to `10.5.7`
### Fixed ### Fixed
- Fixed the internal server error on a failed social login by redirecting to the login page - Fixed the internal server error on a failed social login by redirecting to the login page

18
apps/api/src/app/auth/api-key.strategy.ts

@ -56,22 +56,20 @@ export class ApiKeyStrategy extends PassportStrategy(
} }
private async validateApiKey(apiKey: string) { private async validateApiKey(apiKey: string) {
if (!apiKey) { if (apiKey) {
throw new HttpException(
getReasonPhrase(StatusCodes.UNAUTHORIZED),
StatusCodes.UNAUTHORIZED
);
}
try { try {
const { id } = await this.apiKeyService.getUserByApiKey(apiKey); const { id } = await this.apiKeyService.getUserByApiKey(apiKey);
const user = await this.userService.user({ id });
if (user) {
return user;
}
} catch {}
}
return this.userService.user({ id });
} catch {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.UNAUTHORIZED), getReasonPhrase(StatusCodes.UNAUTHORIZED),
StatusCodes.UNAUTHORIZED StatusCodes.UNAUTHORIZED
); );
} }
} }
}

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

@ -69,7 +69,7 @@ import { OidcStrategy } from './oidc.strategy';
const issuer = configurationService.get('OIDC_ISSUER'); const issuer = configurationService.get('OIDC_ISSUER');
const scope = configurationService.get('OIDC_SCOPE'); const scope = configurationService.get('OIDC_SCOPE');
const callbackUrl = const callbackURL =
configurationService.get('OIDC_CALLBACK_URL') || configurationService.get('OIDC_CALLBACK_URL') ||
`${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`; `${configurationService.get('ROOT_URL')}/api/auth/oidc/callback`;
@ -114,15 +114,26 @@ import { OidcStrategy } from './oidc.strategy';
} }
} }
const clientID = configurationService.get('OIDC_CLIENT_ID');
const clientSecret = configurationService.get('OIDC_CLIENT_SECRET');
if (!clientID || !clientSecret || !issuer) {
logger.error(
'OIDC configuration incomplete: issuer, clientID, or clientSecret missing'
);
throw new Error('OIDC configuration incomplete');
}
const options: StrategyOptions = { const options: StrategyOptions = {
authorizationURL, authorizationURL,
callbackURL,
clientID,
clientSecret,
issuer, issuer,
scope, scope,
tokenURL, tokenURL,
userInfoURL, userInfoURL
callbackURL: callbackUrl,
clientID: configurationService.get('OIDC_CLIENT_ID'),
clientSecret: configurationService.get('OIDC_CLIENT_SECRET')
}; };
return new OidcStrategy(authService, options); return new OidcStrategy(authService, options);

31
apps/api/src/app/auth/oidc-state.store.ts

@ -1,17 +1,24 @@
import type { Request } from 'express';
import ms from 'ms'; import ms from 'ms';
import type {
SessionStore,
SessionStoreCallback,
SessionStoreContext,
SessionVerifyCallback
} from 'passport-openidconnect';
/** /**
* Custom state store for OIDC authentication that doesn't rely on express-session. * Custom state store for OIDC authentication that doesn't rely on express-session.
* This store manages OAuth2 state parameters in memory with automatic cleanup. * This store manages OAuth2 state parameters in memory with automatic cleanup.
*/ */
export class OidcStateStore { export class OidcStateStore implements SessionStore {
private readonly STATE_EXPIRY_MS = ms('10 minutes'); private readonly STATE_EXPIRY_MS = ms('10 minutes');
private stateMap = new Map< private stateMap = new Map<
string, string,
{ {
appState?: unknown; appState?: unknown;
ctx: { issued?: Date; maxAge?: number; nonce?: string }; ctx: SessionStoreContext;
meta?: unknown; meta?: unknown;
timestamp: number; timestamp: number;
} }
@ -19,14 +26,13 @@ export class OidcStateStore {
/** /**
* Store request state. * Store request state.
* Signature matches passport-openidconnect SessionStore
*/ */
public store( public store(
_req: unknown, _req: Request,
_meta: unknown, ctx: SessionStoreContext,
appState: unknown, appState: unknown,
ctx: { maxAge?: number; nonce?: string; issued?: Date }, meta: unknown,
callback: (err: Error | null, handle?: string) => void callback: SessionStoreCallback
) { ) {
try { try {
// Generate a unique handle for this state // Generate a unique handle for this state
@ -35,7 +41,7 @@ export class OidcStateStore {
this.stateMap.set(handle, { this.stateMap.set(handle, {
appState, appState,
ctx, ctx,
meta: _meta, meta,
timestamp: Date.now() timestamp: Date.now()
}); });
@ -50,16 +56,11 @@ export class OidcStateStore {
/** /**
* Verify request state. * Verify request state.
* Signature matches passport-openidconnect SessionStore
*/ */
public verify( public verify(
_req: unknown, _req: Request,
handle: string, handle: string,
callback: ( callback: SessionVerifyCallback
err: Error | null,
appState?: unknown,
ctx?: { maxAge?: number; nonce?: string; issued?: Date }
) => void
) { ) {
try { try {
const data = this.stateMap.get(handle); const data = this.stateMap.get(handle);

14
apps/api/src/app/auth/oidc.strategy.ts

@ -15,10 +15,10 @@ import { OidcStateStore } from './oidc-state.store';
@Injectable() @Injectable()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') { export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
private readonly logger = new Logger(OidcStrategy.name);
private static readonly stateStore = new OidcStateStore(); private static readonly stateStore = new OidcStateStore();
private readonly logger = new Logger(OidcStrategy.name);
public constructor( public constructor(
private readonly authService: AuthService, private readonly authService: AuthService,
options: StrategyOptions options: StrategyOptions
@ -48,11 +48,6 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
params?.sub ?? params?.sub ??
context?.claims?.sub; context?.claims?.sub;
const jwt = await this.authService.validateOAuthLogin({
thirdPartyId,
provider: Provider.OIDC
});
if (!thirdPartyId) { if (!thirdPartyId) {
this.logger.error( this.logger.error(
`Missing subject identifier in OIDC response from ${issuer}` `Missing subject identifier in OIDC response from ${issuer}`
@ -61,6 +56,11 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
throw new Error('Missing subject identifier in OIDC response'); throw new Error('Missing subject identifier in OIDC response');
} }
const jwt = await this.authService.validateOAuthLogin({
thirdPartyId,
provider: Provider.OIDC
});
return { jwt }; return { jwt };
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);

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

@ -99,6 +99,11 @@ export class WebAuthService {
): Promise<AuthDeviceDto> { ): Promise<AuthDeviceDto> {
const user = this.request.user; const user = this.request.user;
const expectedChallenge = user.authChallenge; const expectedChallenge = user.authChallenge;
if (!expectedChallenge) {
throw new Error('Missing authentication challenge');
}
let verification: VerifiedRegistrationResponse; let verification: VerifiedRegistrationResponse;
try { try {
@ -213,7 +218,7 @@ export class WebAuthService {
id: isoBase64URL.fromBuffer(device.credentialId), id: isoBase64URL.fromBuffer(device.credentialId),
publicKey: device.credentialPublicKey publicKey: device.credentialPublicKey
}, },
expectedChallenge: `${user.authChallenge}`, expectedChallenge: `${user?.authChallenge}`,
expectedOrigin: this.expectedOrigin, expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID, expectedRPID: this.rpID,
requireUserVerification: false, requireUserVerification: false,
@ -243,7 +248,7 @@ export class WebAuthService {
}); });
return this.jwtService.sign({ return this.jwtService.sign({
id: user.id id: user?.id
}); });
} }

13
apps/api/src/app/user/user.service.ts

@ -171,25 +171,28 @@ export class UserService {
userSettings: settings.settings as UserSettings userSettings: settings.settings as UserSettings
}); });
let referralPartners: ReferralPartner[]; let referralPartners: ReferralPartner[] = [];
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === SubscriptionType.Basic subscription?.type === SubscriptionType.Basic
) { ) {
referralPartners = await this.propertyService.getByKey<ReferralPartner[]>( referralPartners = await this.propertyService.getByKey<ReferralPartner[]>(
PROPERTY_REFERRAL_PARTNERS PROPERTY_REFERRAL_PARTNERS
); );
} }
let systemMessage: SystemMessage; let systemMessage: SystemMessage | undefined;
const systemMessageProperty = const systemMessageProperty =
await this.propertyService.getByKey<SystemMessage>( await this.propertyService.getByKey<SystemMessage>(
PROPERTY_SYSTEM_MESSAGE PROPERTY_SYSTEM_MESSAGE
); );
if (systemMessageProperty?.targetGroups?.includes(subscription?.type)) { if (
subscription?.type &&
systemMessageProperty?.targetGroups?.includes(subscription.type)
) {
systemMessage = systemMessageProperty; systemMessage = systemMessageProperty;
} }
@ -197,7 +200,7 @@ export class UserService {
if ( if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === SubscriptionType.Basic subscription?.type === SubscriptionType.Basic
) { ) {
tags = tags.filter(({ id }) => { tags = tags.filter(({ id }) => {
return [TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS].includes(id); return [TAG_ID_DRAFT, TAG_ID_EXCLUDE_FROM_ANALYSIS].includes(id);

186
libs/common/src/lib/personal-finance-tools.ts

@ -88,6 +88,18 @@ export const personalFinanceTools: Product[] = [
slogan: 'Portfolio Visualizer', slogan: 'Portfolio Visualizer',
url: 'https://amsflow.com' url: 'https://amsflow.com'
}, },
{
categories: ['ETF_TRACKING', 'STOCK_TRACKING'],
founded: 2025,
hasFreePlan: true,
key: 'anantys',
languages: ['English', 'Français'],
name: 'Anantys',
origin: 'FR',
platforms: ['WEB'],
slogan: 'The investment copilot with no conflict of interest',
url: 'https://anantys.com'
},
{ {
categories: ['STOCK_TRACKING'], categories: ['STOCK_TRACKING'],
founded: 2018, founded: 2018,
@ -178,6 +190,18 @@ export const personalFinanceTools: Product[] = [
slogan: 'Stock Portfolio Tracker for Smart Investors', slogan: 'Stock Portfolio Tracker for Smart Investors',
url: 'https://beanvest.com' url: 'https://beanvest.com'
}, },
{
categories: ['INVESTMENT_RESEARCH', 'STOCK_TRACKING'],
founded: 2021,
hasFreePlan: true,
key: 'blossom-social',
languages: ['English'],
name: 'Blossom Social',
origin: 'CA',
platforms: ['ANDROID', 'IOS'],
slogan: 'Real Portfolios, Trades & Market Insights',
url: 'https://blossomsocial.com'
},
{ {
categories: ['BUDGETING'], categories: ['BUDGETING'],
founded: 2024, founded: 2024,
@ -418,6 +442,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Your personal Dividend Calendar', slogan: 'Your personal Dividend Calendar',
url: 'https://divvydiary.com' url: 'https://divvydiary.com'
}, },
{
categories: ['CRYPTOCURRENCY', 'ETF_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'donkycapital',
languages: ['English', 'Italiano'],
name: 'DonkyCapital',
origin: 'IT',
platforms: ['ANDROID', 'WEB'],
pricingPerYear: '€57.48',
slogan: 'The Portfolio Tracker that replaces your spreadsheets',
url: 'https://www.donkycapital.com'
},
{ {
categories: ['FINANCIAL_PLANNING', 'NET_WORTH_TRACKING'], categories: ['FINANCIAL_PLANNING', 'NET_WORTH_TRACKING'],
founded: 2009, founded: 2009,
@ -487,6 +524,20 @@ export const personalFinanceTools: Product[] = [
slogan: 'Investment Management Platforms', slogan: 'Investment Management Platforms',
url: 'https://www.expersoft.com' url: 'https://www.expersoft.com'
}, },
{
categories: ['ETF_TRACKING', 'INVESTMENT_RESEARCH', 'STOCK_TRACKING'],
founded: 2008,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'extraetf',
languages: ['Deutsch', 'English'],
name: 'extraETF',
origin: 'DE',
platforms: ['ANDROID', 'IOS', 'WEB'],
pricingPerYear: '€89.99',
slogan: 'Die besten ETFs finden, vergleichen und beobachten',
url: 'https://extraetf.com'
},
{ {
categories: ['INVESTMENT_RESEARCH'], categories: ['INVESTMENT_RESEARCH'],
founded: 2018, founded: 2018,
@ -794,6 +845,18 @@ export const personalFinanceTools: Product[] = [
url: 'https://invmon.com', url: 'https://invmon.com',
useAnonymously: true useAnonymously: true
}, },
{
categories: ['CRYPTOCURRENCY', 'ETF_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'inwestuje',
languages: ['Polski'],
name: 'inwestuje.app',
origin: 'PL',
platforms: ['ANDROID', 'IOS', 'WEB'],
slogan: 'Inwestujesz? Śledź swój majątek w jednym miejscu',
url: 'https://inwestuje.app'
},
{ {
categories: ['ETF_TRACKING'], categories: ['ETF_TRACKING'],
founded: 2011, founded: 2011,
@ -1076,6 +1139,24 @@ export const personalFinanceTools: Product[] = [
'Track your equity, fund, investment trust, ETF and pension investments in one place.', 'Track your equity, fund, investment trust, ETF and pension investments in one place.',
url: 'https://www.morningstar.com/mm' url: 'https://www.morningstar.com/mm'
}, },
{
categories: ['CRYPTOCURRENCY', 'ETF_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
key: 'my-stocks-portfolio',
languages: [
'Deutsch',
'English',
'Français',
'Türkçe',
'简体中文',
'繁體中文'
],
name: 'My Stocks Portfolio & Market',
platforms: ['ANDROID', 'IOS'],
pricingPerYear: '$49.99',
slogan: 'Keep track of and visualize your investments throughout your day',
url: 'https://peeksoft.co'
},
{ {
categories: ['BUDGETING', 'NET_WORTH_TRACKING'], categories: ['BUDGETING', 'NET_WORTH_TRACKING'],
hasFreePlan: true, hasFreePlan: true,
@ -1088,6 +1169,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Your Personal Finance Command Center', slogan: 'Your Personal Finance Command Center',
url: 'https://myfinancetools.io' url: 'https://myfinancetools.io'
}, },
{
categories: ['CRYPTOCURRENCY', 'NET_WORTH_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'myfund',
languages: ['English', 'Polski'],
name: 'MyFund',
origin: 'PL',
platforms: ['ANDROID', 'IOS', 'WEB'],
pricingPerYear: 'PLN 36.10',
slogan: 'Manage all your assets and accounts in one place',
url: 'https://myfund.pl'
},
{ {
categories: ['CRYPTOCURRENCY'], categories: ['CRYPTOCURRENCY'],
founded: 2020, founded: 2020,
@ -1148,6 +1242,20 @@ export const personalFinanceTools: Product[] = [
slogan: 'Dein Vermögen immer im Blick', slogan: 'Dein Vermögen immer im Blick',
url: 'https://www.parqet.com' url: 'https://www.parqet.com'
}, },
{
categories: ['FINANCIAL_PLANNING', 'STOCK_TRACKING'],
founded: 2017,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'passiv',
languages: ['English'],
name: 'Passiv',
origin: 'CA',
platforms: ['WEB'],
pricingPerYear: '$99',
slogan: 'Autopilot for your Portfolio',
url: 'https://passiv.com'
},
{ {
categories: ['NET_WORTH_TRACKING'], categories: ['NET_WORTH_TRACKING'],
hasFreePlan: true, hasFreePlan: true,
@ -1252,6 +1360,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Know where your money is going', slogan: 'Know where your money is going',
url: 'https://www.pocketsmith.com' url: 'https://www.pocketsmith.com'
}, },
{
categories: ['BUDGETING', 'NET_WORTH_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'portfeo',
languages: ['Polski'],
name: 'Portfeo',
origin: 'PL',
platforms: ['WEB'],
pricingPerYear: 'PLN 168',
slogan: 'Wszystkie Twoje inwestycje w jednym miejscu',
url: 'https://www.portfeo.pl'
},
{ {
categories: ['FINANCIAL_PLANNING', 'NET_WORTH_TRACKING'], categories: ['FINANCIAL_PLANNING', 'NET_WORTH_TRACKING'],
hasFreePlan: true, hasFreePlan: true,
@ -1278,6 +1399,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Manage all your portfolios', slogan: 'Manage all your portfolios',
url: 'https://portfoliodividendtracker.com' url: 'https://portfoliodividendtracker.com'
}, },
{
categories: ['STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'portfolio-trader',
languages: ['English'],
name: 'Portfolio Trader',
origin: 'AU',
platforms: ['IOS'],
slogan:
'Professional stock portfolio tracking for iPhone, iPad and Apple Watch',
url: 'https://www.iportfoliotrader.com'
},
{ {
categories: ['INVESTMENT_RESEARCH'], categories: ['INVESTMENT_RESEARCH'],
hasFreePlan: true, hasFreePlan: true,
@ -1372,6 +1506,19 @@ export const personalFinanceTools: Product[] = [
'Your entire financial life in one app, monitored continuously by agents', 'Your entire financial life in one app, monitored continuously by agents',
url: 'https://rallies.ai' url: 'https://rallies.ai'
}, },
{
categories: ['DIVIDEND_TRACKING', 'NET_WORTH_TRACKING', 'STOCK_TRACKING'],
founded: 2014,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'rentablo',
languages: ['Deutsch'],
name: 'Rentablo',
origin: 'DE',
platforms: ['WEB'],
slogan: 'Das kostenfreie Tool für Ihren Finanzerfolg',
url: 'https://www.rentablo.de'
},
{ {
categories: ['BUDGETING', 'NET_WORTH_TRACKING'], categories: ['BUDGETING', 'NET_WORTH_TRACKING'],
founded: 2015, founded: 2015,
@ -1589,6 +1736,32 @@ export const personalFinanceTools: Product[] = [
slogan: 'Your key to empowered wealth management', slogan: 'Your key to empowered wealth management',
url: 'https://www.tinywallet.de' url: 'https://www.tinywallet.de'
}, },
{
categories: ['DIVIDEND_TRACKING', 'ETF_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'trackinv',
languages: ['English'],
name: 'TrackinV',
platforms: ['WEB'],
pricingPerYear: '€40',
slogan: 'See your real investment performance — across every broker',
url: 'https://trackinv.com'
},
{
categories: ['DIVIDEND_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'trackyourdividends',
languages: ['English'],
name: 'TrackYourDividends',
origin: 'US',
platforms: ['WEB'],
pricingPerYear: '$99.99',
slogan:
'Follow the Performance, Payments, and Diversification of Your Dividend Portfolio',
url: 'https://www.trackyourdividends.com'
},
{ {
founded: 2011, founded: 2011,
hasFreePlan: false, hasFreePlan: false,
@ -1601,6 +1774,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'The Trading Journal to Improve Your Trading Performance', slogan: 'The Trading Journal to Improve Your Trading Performance',
url: 'https://www.tradervue.com' url: 'https://www.tradervue.com'
}, },
{
categories: ['CRYPTOCURRENCY', 'ETF_TRACKING', 'STOCK_TRACKING'],
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'treestep',
languages: ['English', 'Français'],
name: 'Treestep',
origin: 'FR',
platforms: ['WEB'],
pricingPerYear: '€80.04',
slogan: 'Investing that never bores you',
url: 'https://www.treestep.fr'
},
{ {
categories: ['STOCK_TRACKING', 'TAX_REPORTING'], categories: ['STOCK_TRACKING', 'TAX_REPORTING'],
hasFreePlan: true, hasFreePlan: true,

3193
package-lock.json

File diff suppressed because it is too large

32
package.json

@ -162,21 +162,21 @@
"@eslint/js": "9.35.0", "@eslint/js": "9.35.0",
"@nestjs/schematics": "11.1.0", "@nestjs/schematics": "11.1.0",
"@nestjs/testing": "11.1.28", "@nestjs/testing": "11.1.28",
"@nx/angular": "23.0.2", "@nx/angular": "23.1.1",
"@nx/eslint-plugin": "23.0.2", "@nx/eslint-plugin": "23.1.1",
"@nx/jest": "23.0.2", "@nx/jest": "23.1.1",
"@nx/js": "23.0.2", "@nx/js": "23.1.1",
"@nx/module-federation": "23.0.2", "@nx/module-federation": "23.1.1",
"@nx/nest": "23.0.2", "@nx/nest": "23.1.1",
"@nx/node": "23.0.2", "@nx/node": "23.1.1",
"@nx/storybook": "23.0.2", "@nx/storybook": "23.1.1",
"@nx/web": "23.0.2", "@nx/web": "23.1.1",
"@nx/workspace": "23.0.2", "@nx/workspace": "23.1.1",
"@prisma/config": "7.9.1", "@prisma/config": "7.9.1",
"@schematics/angular": "21.2.6", "@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10", "@storybook/addon-docs": "10.5.7",
"@storybook/addon-themes": "10.1.10", "@storybook/addon-themes": "10.5.7",
"@storybook/angular": "10.1.10", "@storybook/angular": "10.5.7",
"@trivago/prettier-plugin-sort-imports": "6.0.2", "@trivago/prettier-plugin-sort-imports": "6.0.2",
"@types/big.js": "7.0.0", "@types/big.js": "7.0.0",
"@types/cookie-parser": "1.4.10", "@types/cookie-parser": "1.4.10",
@ -194,12 +194,12 @@
"eslint": "9.35.0", "eslint": "9.35.0",
"eslint-config-prettier": "10.1.8", "eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0", "eslint-plugin-import": "2.32.0",
"eslint-plugin-storybook": "10.1.10", "eslint-plugin-storybook": "10.5.7",
"husky": "9.1.7", "husky": "9.1.7",
"jest": "30.3.0", "jest": "30.3.0",
"jest-environment-jsdom": "30.2.0", "jest-environment-jsdom": "30.2.0",
"jest-preset-angular": "16.0.0", "jest-preset-angular": "16.0.0",
"nx": "23.0.2", "nx": "23.1.1",
"prettier": "3.9.6", "prettier": "3.9.6",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"prisma": "7.9.1", "prisma": "7.9.1",
@ -207,7 +207,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"replace-in-file": "8.4.0", "replace-in-file": "8.4.0",
"shx": "0.4.0", "shx": "0.4.0",
"storybook": "10.1.10", "storybook": "10.5.7",
"ts-jest": "29.4.0", "ts-jest": "29.4.0",
"ts-node": "10.9.2", "ts-node": "10.9.2",
"tslib": "2.8.1", "tslib": "2.8.1",

Loading…
Cancel
Save