Browse Source

Merge remote-tracking branch 'origin/main' into task/upgrade-jest-preset-angular

pull/7645/head
KenTandrian 2 days ago
parent
commit
cef806102e
  1. 1
      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. 1281
      package-lock.json
  10. 10
      package.json

1
CHANGELOG.md

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Upgraded `Nx` from version `23.0.2` to `23.1.1`
- Upgraded `storybook` from version `10.1.10` to `10.5.7`
### Fixed

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

@ -56,22 +56,20 @@ export class ApiKeyStrategy extends PassportStrategy(
}
private async validateApiKey(apiKey: string) {
if (!apiKey) {
throw new HttpException(
getReasonPhrase(StatusCodes.UNAUTHORIZED),
StatusCodes.UNAUTHORIZED
);
}
if (apiKey) {
try {
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(
getReasonPhrase(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 scope = configurationService.get('OIDC_SCOPE');
const callbackUrl =
const callbackURL =
configurationService.get('OIDC_CALLBACK_URL') ||
`${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 = {
authorizationURL,
callbackURL,
clientID,
clientSecret,
issuer,
scope,
tokenURL,
userInfoURL,
callbackURL: callbackUrl,
clientID: configurationService.get('OIDC_CLIENT_ID'),
clientSecret: configurationService.get('OIDC_CLIENT_SECRET')
userInfoURL
};
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 type {
SessionStore,
SessionStoreCallback,
SessionStoreContext,
SessionVerifyCallback
} from 'passport-openidconnect';
/**
* Custom state store for OIDC authentication that doesn't rely on express-session.
* 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 stateMap = new Map<
string,
{
appState?: unknown;
ctx: { issued?: Date; maxAge?: number; nonce?: string };
ctx: SessionStoreContext;
meta?: unknown;
timestamp: number;
}
@ -19,14 +26,13 @@ export class OidcStateStore {
/**
* Store request state.
* Signature matches passport-openidconnect SessionStore
*/
public store(
_req: unknown,
_meta: unknown,
_req: Request,
ctx: SessionStoreContext,
appState: unknown,
ctx: { maxAge?: number; nonce?: string; issued?: Date },
callback: (err: Error | null, handle?: string) => void
meta: unknown,
callback: SessionStoreCallback
) {
try {
// Generate a unique handle for this state
@ -35,7 +41,7 @@ export class OidcStateStore {
this.stateMap.set(handle, {
appState,
ctx,
meta: _meta,
meta,
timestamp: Date.now()
});
@ -50,16 +56,11 @@ export class OidcStateStore {
/**
* Verify request state.
* Signature matches passport-openidconnect SessionStore
*/
public verify(
_req: unknown,
_req: Request,
handle: string,
callback: (
err: Error | null,
appState?: unknown,
ctx?: { maxAge?: number; nonce?: string; issued?: Date }
) => void
callback: SessionVerifyCallback
) {
try {
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()
export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
private readonly logger = new Logger(OidcStrategy.name);
private static readonly stateStore = new OidcStateStore();
private readonly logger = new Logger(OidcStrategy.name);
public constructor(
private readonly authService: AuthService,
options: StrategyOptions
@ -48,11 +48,6 @@ export class OidcStrategy extends PassportStrategy(Strategy, 'oidc') {
params?.sub ??
context?.claims?.sub;
const jwt = await this.authService.validateOAuthLogin({
thirdPartyId,
provider: Provider.OIDC
});
if (!thirdPartyId) {
this.logger.error(
`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');
}
const jwt = await this.authService.validateOAuthLogin({
thirdPartyId,
provider: Provider.OIDC
});
return { jwt };
} catch (error) {
this.logger.error(error);

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

@ -99,6 +99,11 @@ export class WebAuthService {
): Promise<AuthDeviceDto> {
const user = this.request.user;
const expectedChallenge = user.authChallenge;
if (!expectedChallenge) {
throw new Error('Missing authentication challenge');
}
let verification: VerifiedRegistrationResponse;
try {
@ -213,7 +218,7 @@ export class WebAuthService {
id: isoBase64URL.fromBuffer(device.credentialId),
publicKey: device.credentialPublicKey
},
expectedChallenge: `${user.authChallenge}`,
expectedChallenge: `${user?.authChallenge}`,
expectedOrigin: this.expectedOrigin,
expectedRPID: this.rpID,
requireUserVerification: false,
@ -243,7 +248,7 @@ export class WebAuthService {
});
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
});
let referralPartners: ReferralPartner[];
let referralPartners: ReferralPartner[] = [];
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === SubscriptionType.Basic
subscription?.type === SubscriptionType.Basic
) {
referralPartners = await this.propertyService.getByKey<ReferralPartner[]>(
PROPERTY_REFERRAL_PARTNERS
);
}
let systemMessage: SystemMessage;
let systemMessage: SystemMessage | undefined;
const systemMessageProperty =
await this.propertyService.getByKey<SystemMessage>(
PROPERTY_SYSTEM_MESSAGE
);
if (systemMessageProperty?.targetGroups?.includes(subscription?.type)) {
if (
subscription?.type &&
systemMessageProperty?.targetGroups?.includes(subscription.type)
) {
systemMessage = systemMessageProperty;
}
@ -197,7 +200,7 @@ export class UserService {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
subscription.type === SubscriptionType.Basic
subscription?.type === SubscriptionType.Basic
) {
tags = tags.filter(({ 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',
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'],
founded: 2018,
@ -178,6 +190,18 @@ export const personalFinanceTools: Product[] = [
slogan: 'Stock Portfolio Tracker for Smart Investors',
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'],
founded: 2024,
@ -418,6 +442,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Your personal Dividend Calendar',
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'],
founded: 2009,
@ -487,6 +524,20 @@ export const personalFinanceTools: Product[] = [
slogan: 'Investment Management Platforms',
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'],
founded: 2018,
@ -794,6 +845,18 @@ export const personalFinanceTools: Product[] = [
url: 'https://invmon.com',
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'],
founded: 2011,
@ -1076,6 +1139,24 @@ export const personalFinanceTools: Product[] = [
'Track your equity, fund, investment trust, ETF and pension investments in one place.',
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'],
hasFreePlan: true,
@ -1088,6 +1169,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Your Personal Finance Command Center',
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'],
founded: 2020,
@ -1148,6 +1242,20 @@ export const personalFinanceTools: Product[] = [
slogan: 'Dein Vermögen immer im Blick',
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'],
hasFreePlan: true,
@ -1252,6 +1360,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Know where your money is going',
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'],
hasFreePlan: true,
@ -1278,6 +1399,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'Manage all your portfolios',
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'],
hasFreePlan: true,
@ -1372,6 +1506,19 @@ export const personalFinanceTools: Product[] = [
'Your entire financial life in one app, monitored continuously by agents',
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'],
founded: 2015,
@ -1589,6 +1736,32 @@ export const personalFinanceTools: Product[] = [
slogan: 'Your key to empowered wealth management',
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,
hasFreePlan: false,
@ -1601,6 +1774,19 @@ export const personalFinanceTools: Product[] = [
slogan: 'The Trading Journal to Improve Your Trading Performance',
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'],
hasFreePlan: true,

1281
package-lock.json

File diff suppressed because it is too large

10
package.json

@ -174,9 +174,9 @@
"@nx/workspace": "23.1.1",
"@prisma/config": "7.9.1",
"@schematics/angular": "21.2.6",
"@storybook/addon-docs": "10.1.10",
"@storybook/addon-themes": "10.1.10",
"@storybook/angular": "10.1.10",
"@storybook/addon-docs": "10.5.7",
"@storybook/addon-themes": "10.5.7",
"@storybook/angular": "10.5.7",
"@trivago/prettier-plugin-sort-imports": "6.0.2",
"@types/big.js": "7.0.0",
"@types/cookie-parser": "1.4.10",
@ -194,7 +194,7 @@
"eslint": "9.35.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-storybook": "10.1.10",
"eslint-plugin-storybook": "10.5.7",
"husky": "9.1.7",
"jest": "30.3.0",
"jest-environment-jsdom": "30.2.0",
@ -207,7 +207,7 @@
"react-dom": "18.2.0",
"replace-in-file": "8.4.0",
"shx": "0.4.0",
"storybook": "10.1.10",
"storybook": "10.5.7",
"ts-jest": "29.4.0",
"ts-node": "10.9.2",
"tslib": "2.8.1",

Loading…
Cancel
Save