diff --git a/src/config/configuration.interface.ts b/src/config/configuration.interface.ts index 3d98344d7..da8ed65a1 100644 --- a/src/config/configuration.interface.ts +++ b/src/config/configuration.interface.ts @@ -2,14 +2,13 @@ import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool'; import Stripe from 'stripe'; export interface Configuration { - /** - * Frontend URL (Staart UI or compatible app) - * Used for redirects and links in emails - */ - frontendUrl: string; - /** Project name and other metadata */ meta: { + /** + * Frontend URL (Staart UI or compatible app) + * Used for redirects and links in emails + */ + frontendUrl: string; /** * Title cased application name * Used as the "Issuer" in MFA TOTP and in SMS OTPs diff --git a/src/config/configuration.ts b/src/config/configuration.ts index e3111aba9..265bd1c73 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -23,8 +23,8 @@ const bool = (val: string | undefined, bool: boolean): boolean => /** Central configuration object */ const configuration: Configuration = { - frontendUrl: process.env.FRONTEND_URL ?? 'http://localhost:3000', meta: { + frontendUrl: process.env.FRONTEND_URL ?? 'http://localhost:3000', appName: process.env.APP_NAME ?? 'Staart', domainVerificationFile: process.env.DOMAIN_VERIFICATION_FILE ?? diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 65313c8f7..3f0b6ee8e 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -4,7 +4,7 @@ import { Injectable, NotFoundException, UnauthorizedException, - UnprocessableEntityException, + UnprocessableEntityException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Authenticator } from '@otplib/core'; @@ -18,6 +18,7 @@ import { authenticator } from 'otplib'; import qrcode from 'qrcode'; import randomColor from 'randomcolor'; import { UAParser } from 'ua-parser-js'; +import { Configuration } from '../../config/configuration.interface'; import { COMPROMISED_PASSWORD, EMAIL_USER_CONFLICT, @@ -33,7 +34,7 @@ import { SESSION_NOT_FOUND, UNVERIFIED_EMAIL, UNVERIFIED_LOCATION, - USER_NOT_FOUND, + USER_NOT_FOUND } from '../../errors/errors.constants'; import { safeEmail } from '../../helpers/safe-email'; import { GeolocationService } from '../../providers/geolocation/geolocation.service'; @@ -48,7 +49,7 @@ import { LOGIN_ACCESS_TOKEN, MERGE_ACCOUNTS_TOKEN, MULTI_FACTOR_TOKEN, - PASSWORD_RESET_TOKEN, + PASSWORD_RESET_TOKEN } from '../../providers/tokens/tokens.constants'; import { TokensService } from '../../providers/tokens/tokens.service'; import { TwilioService } from '../../providers/twilio/twilio.service'; @@ -58,12 +59,16 @@ import { AccessTokenClaims, MfaTokenPayload, TokenResponse, - TotpTokenResponse, + TotpTokenResponse } from './auth.interface'; @Injectable() export class AuthService { - authenticator: Authenticator; + private authenticator: Authenticator; + private securityConfig = this.configService.get( + 'security', + ); + private metaConfig = this.configService.get('meta'); constructor( private prisma: PrismaService, @@ -77,8 +82,8 @@ export class AuthService { ) { this.authenticator = authenticator.create({ window: [ - this.configService.get('security.totpWindowPast') ?? 0, - this.configService.get('security.totpWindowFuture') ?? 0, + this.securityConfig.totpWindowPast, + this.securityConfig.totpWindowFuture, ], }); } @@ -208,9 +213,9 @@ export class AuthService { data: { name: emailDetails.user.name, days: 7, - link: `${this.configService.get( - 'frontendUrl', - )}/auth/link/verify-email?token=${this.tokensService.signJwt( + link: `${ + this.metaConfig.frontendUrl + }/auth/link/verify-email?token=${this.tokensService.signJwt( EMAIL_VERIFY_TOKEN, { id: emailDetails.id }, '7d', @@ -352,9 +357,9 @@ export class AuthService { data: { name: emailDetails.user.name, minutes: 30, - link: `${this.configService.get( - 'frontendUrl', - )}/auth/link/reset-password?token=${this.tokensService.signJwt( + link: `${ + this.metaConfig.frontendUrl + }/auth/link/reset-password?token=${this.tokensService.signJwt( PASSWORD_RESET_TOKEN, { id: emailDetails.user.id }, '30m', @@ -423,9 +428,7 @@ export class AuthService { data: { name: result.user.name, group: group.name, - link: `${this.configService.get('frontendUrl')}/groups/${ - group.id - }`, + link: `${this.metaConfig.frontendUrl}/groups/${group.id}`, }, }); } @@ -481,9 +484,7 @@ export class AuthService { data: { name: user.name, locationName, - link: `${this.configService.get( - 'frontendUrl', - )}/users/${id}/sessions`, + link: `${this.metaConfig.frontendUrl}/users/${id}/sessions`, }, }); } @@ -496,13 +497,13 @@ export class AuthService { private async getAccessToken(user: User): Promise { const scopes = await this.getScopes(user); const payload: AccessTokenClaims = { - sub: `acct:${user.id}@${this.configService.get('security.issuerDomain')}`, + sub: `acct:${user.id}@${this.securityConfig.issuerDomain}`, scopes, }; return this.tokensService.signJwt( LOGIN_ACCESS_TOKEN, payload, - this.configService.get('security.accessTokenExpiry'), + this.securityConfig.accessTokenExpiry, ); } @@ -564,9 +565,9 @@ export class AuthService { minutes: parseInt( this.configService.get('security.mfaTokenExpiry') ?? '', ), - link: `${this.configService.get( - 'frontendUrl', - )}/auth/link/login%2Ftoken?token=${this.tokensService.signJwt( + link: `${ + this.metaConfig.frontendUrl + }/auth/link/login%2Ftoken?token=${this.tokensService.signJwt( EMAIL_MFA_TOKEN, { id: user.id }, '30m', @@ -625,9 +626,9 @@ export class AuthService { name: user.name, locationName, minutes: 30, - link: `${this.configService.get( - 'frontendUrl', - )}/auth/link/reset-password?token=${this.tokensService.signJwt( + link: `${ + this.metaConfig.frontendUrl + }/auth/link/reset-password?token=${this.tokensService.signJwt( APPROVE_SUBNET_TOKEN, { id }, '30m', @@ -643,18 +644,12 @@ export class AuthService { ignorePwnedPassword: boolean, ): Promise { if (!ignorePwnedPassword) { - if (!this.configService.get('security.passwordPwnedCheck')) - return await hash( - password, - this.configService.get('security.saltRounds') ?? 10, - ); + if (!this.securityConfig.passwordPwnedCheck) + return await hash(password, this.securityConfig.saltRounds ?? 10); if (!(await this.pwnedService.isPasswordSafe(password))) throw new BadRequestException(COMPROMISED_PASSWORD); } - return await hash( - password, - this.configService.get('security.saltRounds') ?? 10, - ); + return await hash(password, this.securityConfig.saltRounds ?? 10); } async getScopes(user: User): Promise { diff --git a/src/modules/memberships/memberships.service.ts b/src/modules/memberships/memberships.service.ts index 50cafac79..57b67ddde 100644 --- a/src/modules/memberships/memberships.service.ts +++ b/src/modules/memberships/memberships.service.ts @@ -22,9 +22,12 @@ import { ApiKeysService } from '../api-keys/api-keys.service'; import { AuthService } from '../auth/auth.service'; import { GroupsService } from '../groups/groups.service'; import { CreateMembershipInput } from './memberships.interface'; +import { Configuration } from '../../config/configuration.interface'; @Injectable() export class MembershipsService { + private metaConfig = this.configService.get('meta'); + constructor( private prisma: PrismaService, private auth: AuthService, @@ -183,9 +186,7 @@ export class MembershipsService { data: { name: user.name, group: result.group.name, - link: `${this.configService.get( - 'frontendUrl', - )}/groups/${groupId}`, + link: `${this.metaConfig.frontendUrl}/groups/${groupId}`, }, }); return this.prisma.expose(result); diff --git a/src/modules/stripe/stripe.service.ts b/src/modules/stripe/stripe.service.ts index c131e2f8e..0e3e2c05a 100644 --- a/src/modules/stripe/stripe.service.ts +++ b/src/modules/stripe/stripe.service.ts @@ -7,6 +7,7 @@ import { } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import Stripe from 'stripe'; +import { Configuration } from '../../config/configuration.interface'; import { BILLING_ACCOUNT_CREATED_CONFLICT, BILLING_NOT_FOUND, @@ -22,6 +23,7 @@ import { PrismaService } from '../../providers/prisma/prisma.service'; export class StripeService { stripe: Stripe; logger = new Logger(StripeService.name); + private metaConfig = this.configService.get('meta'); constructor( private configService: ConfigService, @@ -83,9 +85,7 @@ export class StripeService { const stripeId = await this.stripeId(groupId); return this.stripe.billingPortal.sessions.create({ customer: stripeId, - return_url: `${this.configService.get( - 'frontendUrl', - )}/groups/${groupId}`, + return_url: `${this.metaConfig.frontendUrl}/groups/${groupId}`, }); } @@ -186,12 +186,8 @@ export class StripeService { payment_method_types: this.configService.get< Array >('payments.paymentMethodTypes') ?? ['card'], - success_url: `${this.configService.get( - 'frontendUrl', - )}/groups/${groupId}`, - cancel_url: `${this.configService.get( - 'frontendUrl', - )}/groups/${groupId}`, + success_url: `${this.metaConfig.frontendUrl}/groups/${groupId}`, + cancel_url: `${this.metaConfig.frontendUrl}/groups/${groupId}`, }; if (mode === 'subscription') data.line_items = [{ quantity: 1, price: planId }]; diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index ebeadde77..376bc14b0 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -7,6 +7,7 @@ import { ConfigService } from '@nestjs/config'; import type { Prisma } from '@prisma/client'; import { User } from '@prisma/client'; import { compare } from 'bcrypt'; +import { Configuration } from '../../config/configuration.interface'; import { CURRENT_PASSWORD_REQUIRED, INVALID_CREDENTIALS, @@ -23,6 +24,11 @@ import { PasswordUpdateInput } from './users.interface'; @Injectable() export class UsersService { + private metaConfig = this.configService.get('meta'); + private securityConfig = this.configService.get( + 'security', + ); + constructor( private prisma: PrismaService, private auth: AuthService, @@ -113,18 +119,16 @@ export class UsersService { }); if (!user) throw new NotFoundException(USER_NOT_FOUND); if (user.id === userId) throw new NotFoundException(USER_NOT_FOUND); - const minutes = parseInt( - this.configService.get('security.mergeUsersTokenExpiry') ?? '', - ); + const minutes = this.securityConfig.mergeUsersTokenExpiry; this.email.send({ to: `"${user.name}" <${user.prefersEmail.email}>`, template: 'auth/mfa-code', data: { name: user.name, minutes, - link: `${this.configService.get( - 'frontendUrl', - )}/auth/link/merge-accounts?token=${this.tokensService.signJwt( + link: `${ + this.metaConfig.frontendUrl + }/auth/link/merge-accounts?token=${this.tokensService.signJwt( MERGE_ACCOUNTS_TOKEN, { baseUserId: userId, mergeUserId: user.id }, `${minutes}m`,