Browse Source

Task/improve Stripe checkout session verification (#6872)

* Improve verification of stripeCheckoutSessionId

* Update changelog
pull/6919/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
8e3778cc66
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 2
      apps/api/src/app/subscription/subscription.controller.ts
  3. 32
      apps/api/src/app/subscription/subscription.service.ts
  4. 5
      prisma/migrations/20260516074856_added_stripe_checkout_session_to_subscription/migration.sql
  5. 1
      prisma/schema.prisma

1
CHANGELOG.md

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Randomized the placeholder in the assistant - Randomized the placeholder in the assistant
- Filtered out sectors with zero weight for ETF and mutual fund assets in the _Yahoo Finance_ data enhancer - Filtered out sectors with zero weight for ETF and mutual fund assets in the _Yahoo Finance_ data enhancer
- Enabled the _Bull Dashboard_ in the admin control panel without requiring an environment variable (experimental) - Enabled the _Bull Dashboard_ in the admin control panel without requiring an environment variable (experimental)
- Improved the verification of the _Stripe_ checkout session when creating a subscription
- Relaxed the URL validation in the asset profile DTOs to accept both `HTTP` and `HTTPS` protocols - Relaxed the URL validation in the asset profile DTOs to accept both `HTTP` and `HTTPS` protocols
- Relaxed the URL validation in the platform DTOs to accept both `HTTP` and `HTTPS` protocols - Relaxed the URL validation in the platform DTOs to accept both `HTTP` and `HTTPS` protocols
- Extracted the page tabs to a reusable component - Extracted the page tabs to a reusable component

2
apps/api/src/app/subscription/subscription.controller.ts

@ -100,10 +100,12 @@ export class SubscriptionController {
request.query.checkoutSessionId as string request.query.checkoutSessionId as string
); );
if (userId) {
Logger.log( Logger.log(
`Subscription for user '${userId}' has been created via Stripe`, `Subscription for user '${userId}' has been created via Stripe`,
'SubscriptionController' 'SubscriptionController'
); );
}
response.redirect( response.redirect(
`${this.configurationService.get( `${this.configurationService.get(

32
apps/api/src/app/subscription/subscription.service.ts

@ -17,7 +17,7 @@ import {
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Subscription } from '@prisma/client'; import { Prisma, Subscription } from '@prisma/client';
import { addMilliseconds, isBefore } from 'date-fns'; import { addMilliseconds, isBefore } from 'date-fns';
import ms, { StringValue } from 'ms'; import ms, { StringValue } from 'ms';
import Stripe from 'stripe'; import Stripe from 'stripe';
@ -108,11 +108,13 @@ export class SubscriptionService {
duration = '1 year', duration = '1 year',
durationExtension, durationExtension,
price, price,
stripeCheckoutSessionId,
userId userId
}: { }: {
duration?: StringValue; duration?: StringValue;
durationExtension?: StringValue; durationExtension?: StringValue;
price: number; price: number;
stripeCheckoutSessionId?: string;
userId: string; userId: string;
}) { }) {
let expiresAt = addMilliseconds(new Date(), ms(duration)); let expiresAt = addMilliseconds(new Date(), ms(duration));
@ -125,6 +127,7 @@ export class SubscriptionService {
data: { data: {
expiresAt, expiresAt,
price, price,
stripeCheckoutSessionId,
user: { user: {
connect: { connect: {
id: userId id: userId
@ -136,24 +139,41 @@ export class SubscriptionService {
public async createSubscriptionViaStripe(aCheckoutSessionId: string) { public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
try { try {
let durationExtension: StringValue;
const session = const session =
await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId); await this.stripe.checkout.sessions.retrieve(aCheckoutSessionId);
if (session.payment_status !== 'paid' || session.status !== 'complete') {
throw new Error(
`Stripe Checkout Session '${aCheckoutSessionId}' has not been paid (status=${session.status}, payment_status=${session.payment_status})`
);
}
const subscriptionOffer: SubscriptionOffer = JSON.parse( const subscriptionOffer: SubscriptionOffer = JSON.parse(
session.metadata.subscriptionOffer ?? '{}' session.metadata.subscriptionOffer ?? '{}'
); );
if (subscriptionOffer) { const durationExtension = subscriptionOffer?.durationExtension;
durationExtension = subscriptionOffer.durationExtension;
}
try {
await this.createSubscription({ await this.createSubscription({
durationExtension, durationExtension,
price: session.amount_total / 100, price: session.amount_total / 100,
stripeCheckoutSessionId: session.id,
userId: session.client_reference_id userId: session.client_reference_id
}); });
} catch (error) {
if (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === 'P2002'
) {
Logger.log(
`Stripe Checkout Session '${session.id}' has already been redeemed`,
'SubscriptionService'
);
} else {
throw error;
}
}
return session.client_reference_id; return session.client_reference_id;
} catch (error) { } catch (error) {

5
prisma/migrations/20260516074856_added_stripe_checkout_session_to_subscription/migration.sql

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Subscription" ADD COLUMN "stripeCheckoutSessionId" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "Subscription_stripeCheckoutSessionId_key" ON "Subscription"("stripeCheckoutSessionId");

1
prisma/schema.prisma

@ -240,6 +240,7 @@ model Subscription {
expiresAt DateTime expiresAt DateTime
id String @id @default(uuid()) id String @id @default(uuid())
price Float? price Float?
stripeCheckoutSessionId String? @unique
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
user User @relation(fields: [userId], onDelete: Cascade, references: [id]) user User @relation(fields: [userId], onDelete: Cascade, references: [id])
userId String userId String

Loading…
Cancel
Save