Skip to content

Commit

Permalink
Refactor all external sdks to be injected through dependency injection (
Browse files Browse the repository at this point in the history
#838)

Previously, the S3 repository and Stripe code both created the SDKs on
their own. These should instead be provided through dependency
injection.

Also removes the /lib favor in core of one named external.
  • Loading branch information
junlarsen authored Mar 16, 2024
1 parent 50468e4 commit 3bb8961
Show file tree
Hide file tree
Showing 13 changed files with 93 additions and 110 deletions.
3 changes: 0 additions & 3 deletions packages/core/logger.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./errors/errors"
export * from "./lib/stripe"
export * from "./utils/db-utils"
export * from "./modules/core"
50 changes: 0 additions & 50 deletions packages/core/src/lib/stripe.ts

This file was deleted.

34 changes: 26 additions & 8 deletions packages/core/src/modules/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,15 @@ import {
} from "./user/privacy-permissions-repository"
import { type UserRepository, UserRepositoryImpl } from "./user/user-repository"
import { type UserService, UserServiceImpl } from "./user/user-service"
import { type S3Repository, s3RepositoryImpl } from "../lib/s3/s3-repository"
import { type S3Repository, S3RepositoryImpl } from "./external/s3-repository"
import { type InterestGroupRepository, InterestGroupRepositoryImpl } from "./interest-group/interest-group-repository"
import { type InterestGroupService, InterestGroupServiceImpl } from "./interest-group/interest-group-service"
import { Auth0RepositoryImpl, Auth0Repository } from "../lib/auth0-repository"
import { Auth0RepositoryImpl, Auth0Repository } from "./external/auth0-repository"
import { ManagementClient } from "auth0"
import { env } from "@dotkomonline/env"
import { Auth0SynchronizationService, Auth0SynchronizationServiceImpl } from "../lib/auth0-synchronization-service"
import { Auth0SynchronizationService, Auth0SynchronizationServiceImpl } from "./external/auth0-synchronization-service"
import { S3Client } from "@aws-sdk/client-s3"
import Stripe from "stripe"

export type ServiceLayer = Awaited<ReturnType<typeof createServiceLayer>>

Expand All @@ -73,14 +75,31 @@ export interface ServerLayerOptions {
}

export const createServiceLayer = async ({ db }: ServerLayerOptions) => {
const s3Repository: S3Repository = new s3RepositoryImpl()
const s3Client = new S3Client({
region: env.AWS_REGION,
})
const auth0ManagementClient = new ManagementClient({
domain: "onlineweb.eu.auth0.com",
clientSecret: env.GTX_AUTH0_CLIENT_SECRET,
clientId: env.GTX_AUTH0_CLIENT_ID,
})
const auth0Repository: Auth0Repository = new Auth0RepositoryImpl(auth0ManagementClient)
const trikomStripeSdk = new Stripe(env.TRIKOM_STRIPE_SECRET_KEY, { apiVersion: "2023-08-16" })
const fagkomStripeSdk = new Stripe(env.FAGKOM_STRIPE_SECRET_KEY, { apiVersion: "2023-08-16" })
const stripeAccounts = {
trikom: {
stripe: trikomStripeSdk,
publicKey: env.TRIKOM_STRIPE_PUBLIC_KEY,
webhookSecret: env.TRIKOM_STRIPE_WEBHOOK_SECRET,
},
fagkom: {
stripe: fagkomStripeSdk,
publicKey: env.FAGKOM_STRIPE_PUBLIC_KEY,
webhookSecret: env.FAGKOM_STRIPE_WEBHOOK_SECRET,
},
}

const s3Repository: S3Repository = new S3RepositoryImpl(s3Client)
const auth0Repository: Auth0Repository = new Auth0RepositoryImpl(auth0ManagementClient)
const eventRepository: EventRepository = new EventRepositoryImpl(db)
const committeeRepository: CommitteeRepository = new CommitteeRepositoryImpl(db)
const jobListingRepository: JobListingRepository = new JobListingRepositoryImpl(db)
Expand Down Expand Up @@ -109,7 +128,6 @@ export const createServiceLayer = async ({ db }: ServerLayerOptions) => {
const articleRepository: ArticleRepository = new ArticleRepositoryImpl(db)
const articleTagRepository: ArticleTagRepository = new ArticleTagRepositoryImpl(db)
const articleTagLinkRepository: ArticleTagLinkRepository = new ArticleTagLinkRepositoryImpl(db)

const userService: UserService = new UserServiceImpl(
userRepository,
privacyPermissionsRepository,
Expand All @@ -132,7 +150,8 @@ export const createServiceLayer = async ({ db }: ServerLayerOptions) => {
paymentRepository,
productRepository,
eventRepository,
refundRequestRepository
refundRequestRepository,
stripeAccounts
)
const productPaymentProviderService: ProductPaymentProviderService = new ProductPaymentProviderServiceImpl(
productPaymentProviderRepository
Expand All @@ -151,7 +170,6 @@ export const createServiceLayer = async ({ db }: ServerLayerOptions) => {
articleTagRepository,
articleTagLinkRepository
)

const interestGroupRepository: InterestGroupRepository = new InterestGroupRepositoryImpl(db)
const interestGroupService: InterestGroupService = new InterestGroupServiceImpl(interestGroupRepository)
const auth0SynchronizationService: Auth0SynchronizationService = new Auth0SynchronizationServiceImpl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ describe("EventService", () => {
vi.spyOn(eventRepository, "create").mockResolvedValueOnce({ id, ...eventPayload })
const event = await eventService.createEvent(eventPayload)
expect(event).toEqual({ id, ...eventPayload })
expect(eventRepository.create).toHaveBeenCalledWith(eventPayload)
expect(eventRepository.create).toHaveBeenCalledWith({
...eventPayload,
// This is because extras is a jsonb column in the database
extras: "null",
})
})

it("finds events by id", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,23 @@ export interface Auth0Repository {
getBySubject(sub: string): Promise<OidcUser | null>
}

export const mapToOidcUser = (payload: unknown) => OidcUser.parse(payload)

export class Auth0RepositoryImpl implements Auth0Repository {
constructor(private readonly client: ManagementClient) {}
async getBySubject(sub: string) {
try {
const res = await this.client.users.get({
const user = await this.client.users.get({
id: sub,
})

const parsed = OidcUser.parse({
email: res.data.email,
subject: res.data.user_id,
name: res.data.name,
// familyName: res.data.family_name,
// givenName: res.data.given_name,
})

return parsed
return mapToOidcUser(user)
} catch (e) {
if (e instanceof ManagementApiError) {
if (e.errorCode === "inexistent_user") {
return null
}
}

// Error was caused by other reasons than user not existing
throw e
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { User, UserWrite } from "@dotkomonline/types"
import { UserService } from "../modules/user/user-service"
import { UserService } from "../user/user-service"
import { Auth0Repository } from "./auth0-repository"
import { logger } from "../../logger"
import { getLogger, Logger } from "@dotkomonline/logger"

// Id token returned from Auth0. We don't want core to depend on next-auth, so we duplicate the type here.
type Auth0IdToken = {
Expand All @@ -12,22 +12,21 @@ type Auth0IdToken = {
familyName?: string | null
}

/**
* Synchronize users in a local database user table with Auth0.
*/
export interface Auth0SynchronizationService {
/**
* If no record of the user exists in the local database, save it to the database.
*/
createUser: (token: Auth0IdToken) => Promise<User>
createUser(token: Auth0IdToken): Promise<User>

/**
* Synchronize record of user in database with user data from Auth0.
*/
synchronizeUser: (user: User) => Promise<User | null>
synchronizeUser(user: User): Promise<User | null>
}

export class Auth0SynchronizationServiceImpl implements Auth0SynchronizationService {
private readonly logger: Logger = getLogger(Auth0SynchronizationServiceImpl.name)

constructor(
private readonly userService: UserService,
private readonly auth0Repository: Auth0Repository
Expand Down Expand Up @@ -62,16 +61,16 @@ export class Auth0SynchronizationServiceImpl implements Auth0SynchronizationServ
const oneDay = 1000 * 60 * 60 * 24
const oneDayAgo = new Date(Date.now() - oneDay)
if (!user.lastSyncedAt || user.lastSyncedAt < oneDayAgo) {
logger.log("info", "Synchronizing user with Auth0", { userId: user.id })
const idpUser = await this.auth0Repository.getBySubject(user.auth0Sub)
this.logger.log("info", "Synchronizing user with Auth0", { userId: user.id })
const auth0User = await this.auth0Repository.getBySubject(user.auth0Sub)

if (idpUser === null) {
if (auth0User === null) {
throw new Error("User does not exist in Auth0")
}

return this.userService.updateUser(user.id, {
email: idpUser.email,
name: idpUser.name,
email: auth0User.email,
name: auth0User.name,
lastSyncedAt: new Date(),
// givenName: idpUser.givenName,
// familyName: idpUser.familyName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import { type PresignedPost, createPresignedPost as _createPresignedPost } from "@aws-sdk/s3-presigned-post"
import { S3Client } from "@aws-sdk/client-s3"
import { env } from "@dotkomonline/env"

export interface S3Repository {
createPresignedPost(bucket: string, filename: string, mimeType: string, maxSizeMB: number): Promise<PresignedPost>
}

export class s3RepositoryImpl implements S3Repository {
export class S3RepositoryImpl implements S3Repository {
public constructor(private readonly s3Client: S3Client) {}

async createPresignedPost(
bucket: string,
filepath: string,
mimeType: string,
maxSizeMB: number
): Promise<PresignedPost> {
const s3 = new S3Client({
region: env.AWS_REGION,
})

return await _createPresignedPost(s3, {
return await _createPresignedPost(this.s3Client, {
Bucket: bucket,
Key: filepath,
Fields: {
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/modules/offline/offline-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { env } from "@dotkomonline/env"
import { type OfflineRepository } from "./offline-repository"
import { type Cursor } from "../../utils/db-utils"
import { NotFoundError } from "../../errors/errors"
import { type S3Repository } from "../../lib/s3/s3-repository"
import { type S3Repository } from "../external/s3-repository"

type Fields = Record<string, string>

Expand Down
16 changes: 11 additions & 5 deletions packages/core/src/modules/payment/__test__/payment-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Kysely } from "kysely"
import Stripe from "stripe"
import { paymentProvidersPayload } from "./product-payment-provider.spec"
import { productPayload } from "./product-service.spec"
import * as LocalStripeLib from "../../../lib/stripe"
import { EventRepositoryImpl } from "../../event/event-repository"
import { PaymentRepositoryImpl } from "../payment-repository"
import { PaymentServiceImpl } from "../payment-service"
Expand Down Expand Up @@ -182,15 +181,22 @@ describe("PaymentService", () => {
const productRepository = new ProductRepositoryImpl(db)
const eventRepository = new EventRepositoryImpl(db)
const refundRequestRepository = new RefundRequestRepositoryImpl(db)
const stripe = new Stripe("doesntmatter", { apiVersion: "2023-08-16" })
const stripeAccounts = {
doesntmatter: {
stripe,
publicKey: "obviouslyInvalidPaymentProviderId",
webhookSecret: "doesntmatterWebhookSecret",
},
}
const paymentService = new PaymentServiceImpl(
paymentRepository,
productRepository,
eventRepository,
refundRequestRepository
refundRequestRepository,
stripeAccounts
)

const stripe = new Stripe("doesntmatter", { apiVersion: "2023-08-16" })
vi.spyOn(LocalStripeLib, "getStripeObject").mockResolvedValue(stripe)
vi.spyOn(paymentService, "findStripeSdkByPublicKey").mockReturnValue(stripe)

const paymentPayloadExtended: Payment = {
...paymentPayload,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ describe("RefundRequestService", () => {
paymentRepository,
productRepository,
eventRepository,
refundRequestRepository
refundRequestRepository,
{}
)
const refundRequestService = new RefundRequestServiceImpl(
refundRequestRepository,
Expand Down
32 changes: 25 additions & 7 deletions packages/core/src/modules/payment/payment-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ import { type Payment, type PaymentProvider, type Product, type ProductId, type
import { type PaymentRepository } from "./payment-repository.js"
import { type ProductRepository } from "./product-repository.js"
import { type RefundRequestRepository } from "./refund-request-repository.js"
import { getStripeObject, readableStripeAccounts } from "../../lib/stripe"
import { type Cursor } from "../../utils/db-utils"
import { type EventRepository } from "../event/event-repository"
import Stripe from "stripe"

export interface StripeAccount {
stripe: Stripe
publicKey: string
webhookSecret: string
}

export interface PaymentService {
findStripeSdkByPublicKey(publicKey: string): Stripe | null
findWebhookSecretByPublicKey(publicKey: string): string | null
getPaymentProviders(): (PaymentProvider & { paymentAlias: string })[]
getPayments(take: number, cursor?: Cursor): Promise<Payment[]>
createStripeCheckoutSessionForProductId(
Expand All @@ -29,14 +37,23 @@ export class PaymentServiceImpl implements PaymentService {
private readonly paymentRepository: PaymentRepository,
private readonly productRepository: ProductRepository,
private readonly eventRepository: EventRepository,
private readonly refundRequestRepository: RefundRequestRepository
private readonly refundRequestRepository: RefundRequestRepository,
private readonly stripeAccounts: Record<string, StripeAccount>
) {}

findStripeSdkByPublicKey(publicKey: string): Stripe | null {
return Object.values(this.stripeAccounts).find((account) => account.publicKey === publicKey)?.stripe ?? null
}

findWebhookSecretByPublicKey(publicKey: string): string | null {
return Object.values(this.stripeAccounts).find((account) => account.publicKey === publicKey)?.webhookSecret ?? null
}

getPaymentProviders(): (PaymentProvider & { paymentAlias: string })[] {
return readableStripeAccounts.map(({ alias, publicKey }) => ({
return Object.entries(this.stripeAccounts).map(([alias, account]) => ({
paymentAlias: alias,
paymentProvider: "STRIPE",
paymentProviderId: publicKey,
paymentProviderId: account.publicKey,
}))
}

Expand Down Expand Up @@ -77,9 +94,10 @@ export class PaymentServiceImpl implements PaymentService {
}
}

const stripe = getStripeObject(stripePublicKey)
const stripe = this.findStripeSdkByPublicKey(stripePublicKey)
if (!stripe) {
throw new Error("No stripe account found for the given public key")
console.log(stripePublicKey)
throw new Error(`No stripe account found for public key ${stripePublicKey}`)
}

// Tests requires stripe to be awaited first but otherwise it works fine without.
Expand Down Expand Up @@ -225,7 +243,7 @@ export class PaymentServiceImpl implements PaymentService {
}

async refundStripePayment(payment: Payment) {
const stripe = getStripeObject(payment.paymentProviderId)
const stripe = this.findStripeSdkByPublicKey(payment.paymentProviderId)
if (!stripe) {
throw new Error("No stripe account found for the given public key")
}
Expand Down
Loading

0 comments on commit 3bb8961

Please sign in to comment.