Merge branch 'main' into spotify-app

This commit is contained in:
Ömer Faruk Aydın
2023-03-23 15:58:50 +03:00
committed by GitHub
43 changed files with 957 additions and 357 deletions

View File

@@ -59,6 +59,7 @@
"nodemailer": "6.7.0",
"oauth-1.0a": "^2.2.6",
"objection": "^3.0.0",
"paddle-sdk": "^3.3.0",
"pg": "^8.7.1",
"stripe": "^11.13.0",
"winston": "^3.7.1"

View File

@@ -40,13 +40,15 @@ app.use(
},
})
);
app.use(express.urlencoded({
extended: false,
limit: appConfig.requestBodySizeLimit,
verify(req, res, buf) {
(req as IRequest).rawBody = buf;
},
}));
app.use(
express.urlencoded({
extended: true,
limit: appConfig.requestBodySizeLimit,
verify(req, res, buf) {
(req as IRequest).rawBody = buf;
},
})
);
app.use(cors(corsOptions));
app.use('/', router);

View File

@@ -39,10 +39,9 @@ type AppConfig = {
smtpPassword: string;
fromEmail: string;
isCloud: boolean;
stripeSecretKey: string;
stripeSigningSecret: string;
stripeStarterPriceKey: string;
stripeGrowthPriceKey: string;
paddleVendorId: number;
paddleVendorAuthCode: string;
paddlePublicKey: string;
licenseKey: string;
sentryDsn: string;
};
@@ -111,10 +110,9 @@ const appConfig: AppConfig = {
smtpPassword: process.env.SMTP_PASSWORD,
fromEmail: process.env.FROM_EMAIL,
isCloud: process.env.AUTOMATISCH_CLOUD === 'true',
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
stripeSigningSecret: process.env.STRIPE_SIGNING_SECRET,
stripeStarterPriceKey: process.env.STRIPE_STARTER_PRICE_KEY,
stripeGrowthPriceKey: process.env.STRIPE_GROWTH_PRICE_KEY,
paddleVendorId: Number(process.env.PADDLE_VENDOR_ID),
paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE,
paddlePublicKey: process.env.PADDLE_PUBLIC_KEY,
licenseKey: process.env.LICENSE_KEY,
sentryDsn: process.env.SENTRY_DSN,
};

View File

@@ -0,0 +1,14 @@
import { Response } from 'express';
import { IRequest } from '@automatisch/types';
import Billing from '../../helpers/billing/index.ee';
export default async (request: IRequest, response: Response) => {
const isVerified = Billing.paddleClient.verifyWebhookData(request.body);
if (!isVerified) {
return response.sendStatus(401);
}
// TODO: Handle Paddle webhooks
return response.sendStatus(200);
};

View File

@@ -1,27 +0,0 @@
import { Response } from 'express';
import { IRequest } from '@automatisch/types';
import * as Sentry from '../../helpers/sentry.ee';
import Billing from '../../helpers/billing/index.ee';
import appConfig from '../../config/app';
import logger from '../../helpers/logger';
export default async (request: IRequest, response: Response) => {
const signature = request.headers['stripe-signature'];
try {
const event = Billing.stripe.webhooks.constructEvent(
request.rawBody,
signature,
appConfig.stripeSigningSecret
);
await Billing.handleWebhooks(event);
return response.sendStatus(200);
} catch (error) {
logger.error(`Webhook Error: ${error.message}`);
Sentry.captureException(error);
return response.sendStatus(400);
}
};

View File

@@ -0,0 +1,18 @@
import { Knex } from 'knex';
import appConfig from '../../config/app';
export async function up(knex: Knex): Promise<void> {
if (!appConfig.isCloud) return;
return knex.schema.table('users', (table) => {
table.date('trial_expiry_date');
});
}
export async function down(knex: Knex): Promise<void> {
if (!appConfig.isCloud) return;
return knex.schema.table('users', (table) => {
table.dropColumn('trial_expiry_date');
});
}

View File

@@ -1,6 +1,4 @@
import User from '../../models/user';
import Billing from '../../helpers/billing/index.ee';
import appConfig from '../../config/app';
type Params = {
input: {
@@ -26,10 +24,6 @@ const createUser = async (_parent: unknown, params: Params) => {
role: 'user',
});
if (appConfig.isCloud) {
await Billing.createSubscription(user);
}
return user;
};

View File

@@ -0,0 +1,10 @@
import appConfig from '../../config/app';
import Billing from '../../helpers/billing/index.ee';
const getPaddleInfo = async () => {
if (!appConfig.isCloud) return;
return Billing.paddleInfo;
};
export default getPaddleInfo;

View File

@@ -0,0 +1,10 @@
import appConfig from '../../config/app';
import Billing from '../../helpers/billing/index.ee';
const getPaymentPlans = async () => {
if (!appConfig.isCloud) return;
return Billing.paddlePlans;
};
export default getPaymentPlans;

View File

@@ -1,16 +0,0 @@
import appConfig from '../../config/app';
import Context from '../../types/express/context';
import Billing from '../../helpers/billing/index.ee';
const getPaymentPortalUrl = async (
_parent: unknown,
_params: unknown,
context: Context
) => {
if (!appConfig.isCloud) return;
const url = Billing.createPaymentPortalUrl(context.currentUser);
return { url };
};
export default getPaymentPortalUrl;

View File

@@ -12,7 +12,8 @@ import getDynamicData from './queries/get-dynamic-data';
import getDynamicFields from './queries/get-dynamic-fields';
import getCurrentUser from './queries/get-current-user';
import getUsageData from './queries/get-usage-data.ee';
import getPaymentPortalUrl from './queries/get-payment-portal-url.ee';
import getPaymentPlans from './queries/get-payment-plans.ee';
import getPaddleInfo from './queries/get-paddle-info.ee';
import getAutomatischInfo from './queries/get-automatisch-info';
import healthcheck from './queries/healthcheck';
@@ -31,7 +32,8 @@ const queryResolvers = {
getDynamicFields,
getCurrentUser,
getUsageData,
getPaymentPortalUrl,
getPaymentPlans,
getPaddleInfo,
getAutomatischInfo,
healthcheck,
};

View File

@@ -35,7 +35,8 @@ type Query {
): [SubstepArgument]
getCurrentUser: User
getUsageData: GetUsageData
getPaymentPortalUrl: GetPaymentPortalUrl
getPaymentPlans: [PaymentPlan]
getPaddleInfo: GetPaddleInfo
getAutomatischInfo: GetAutomatischInfo
healthcheck: AppHealth
}
@@ -477,8 +478,16 @@ type GetUsageData {
nextResetAt: String
}
type GetPaymentPortalUrl {
url: String
type GetPaddleInfo {
sandbox: Boolean
vendorId: Int
}
type PaymentPlan {
name: String
limit: String
price: String
productId: String
}
schema {

View File

@@ -1,100 +1,16 @@
import Stripe from 'stripe';
import User from '../../models/user';
import PaymentPlan from '../../models/payment-plan.ee';
import UsageData from '../../models/usage-data.ee';
import appConfig from '../../config/app';
import handleWebhooks from './webhooks.ee';
import paddleClient from './paddle.ee';
import paddlePlans from './plans.ee';
const plans = [
{
price: appConfig.stripeStarterPriceKey,
name: 'Starter',
taskCount: 1000,
default: true,
},
{
price: appConfig.stripeGrowthPriceKey,
name: 'Growth',
taskCount: 10000,
default: false,
},
];
const stripe = new Stripe(appConfig.stripeSecretKey, {
apiVersion: '2022-11-15',
});
const createStripeCustomer = async (user: User) => {
const params: Stripe.CustomerCreateParams = {
email: user.email,
name: user.fullName,
description: `User ID: ${user.id}`,
};
return await stripe.customers.create(params);
};
const defaultPlan = plans.find((plan) => plan.default);
const createStripeSubscription = async (
user: User,
stripeCustomer: Stripe.Customer
) => {
const params: Stripe.SubscriptionCreateParams = {
customer: stripeCustomer.id,
items: [{ price: defaultPlan.price }],
};
return await stripe.subscriptions.create(params);
};
const createSubscription = async (user: User) => {
const stripeCustomer = await createStripeCustomer(user);
const stripeSubscription = await createStripeSubscription(
user,
stripeCustomer
);
await PaymentPlan.query().insert({
name: defaultPlan.name,
taskCount: defaultPlan.taskCount,
userId: user.id,
stripeCustomerId: stripeCustomer.id,
stripeSubscriptionId: stripeSubscription.id,
currentPeriodStartedAt: new Date(
stripeSubscription.current_period_start * 1000
).toISOString(),
currentPeriodEndsAt: new Date(
stripeSubscription.current_period_end * 1000
).toISOString(),
});
await UsageData.query().insert({
userId: user.id,
consumedTaskCount: 0,
nextResetAt: new Date(
stripeSubscription.current_period_end * 1000
).toISOString(),
});
};
const createPaymentPortalUrl = async (user: User) => {
const paymentPlan = await user.$relatedQuery('paymentPlan');
const userSession = await stripe.billingPortal.sessions.create({
customer: paymentPlan.stripeCustomerId,
return_url: 'https://cloud.automatisch.io/settings/billing',
});
return userSession.url;
const paddleInfo = {
sandbox: appConfig.isDev ? true : false,
vendorId: appConfig.paddleVendorId,
};
const billing = {
createSubscription,
createPaymentPortalUrl,
handleWebhooks,
stripe,
plans,
paddleClient,
paddlePlans,
paddleInfo,
};
export default billing;

View File

@@ -0,0 +1,10 @@
import PaddleSDK from 'paddle-sdk';
import appConfig from '../../config/app';
const paddleClient = new PaddleSDK(
appConfig.paddleVendorId.toString(),
appConfig.paddleVendorAuthCode,
appConfig.paddlePublicKey
);
export default paddleClient;

View File

@@ -0,0 +1,16 @@
const plans = [
{
name: '10k - monthly',
limit: '10,000',
price: '€20',
productId: '47384',
},
{
name: '30k - monthly',
limit: '30,000',
price: '€50',
productId: '47419',
},
];
export default plans;

View File

@@ -1,42 +0,0 @@
import Stripe from 'stripe';
import PaymentPlan from '../../models/payment-plan.ee';
import Billing from './index.ee';
const handleWebhooks = async (event: Stripe.Event) => {
const trackedWebhookTypes = [
'customer.subscription.created',
'customer.subscription.updated',
'customer.subscription.deleted',
];
if (!trackedWebhookTypes.includes(event.type)) {
return;
}
await updatePaymentPlan(event);
};
const updatePaymentPlan = async (event: Stripe.Event) => {
const subscription = event.data.object as Stripe.Subscription;
const priceKey = subscription.items.data[0].plan.id;
const plan = Billing.plans.find((plan) => plan.price === priceKey);
const paymentPlan = await PaymentPlan.query().findOne({
stripe_customer_id: subscription.customer,
});
await paymentPlan.$query().patchAndFetch({
name: plan.name,
taskCount: plan.taskCount,
stripeSubscriptionId: subscription.id,
});
const user = await paymentPlan.$relatedQuery('user');
const usageData = await user.$relatedQuery('usageData');
await usageData.$query().patchAndFetch({
nextResetAt: new Date(subscription.current_period_end * 1000).toISOString(),
});
};
export default handleWebhooks;

View File

@@ -1,4 +1,6 @@
import { QueryContext, ModelOptions } from 'objection';
import { DateTime } from 'luxon';
import appConfig from '../config/app';
import Base from './base';
import Connection from './connection';
import Flow from './flow';
@@ -17,6 +19,7 @@ class User extends Base {
role: string;
resetPasswordToken: string;
resetPasswordTokenSentAt: string;
trialExpiryDate: string;
connections?: Connection[];
flows?: Flow[];
steps?: Step[];
@@ -133,9 +136,17 @@ class User extends Base {
this.password = await bcrypt.hash(this.password, 10);
}
async startTrialPeriod() {
this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toFormat('D');
}
async $beforeInsert(queryContext: QueryContext) {
await super.$beforeInsert(queryContext);
await this.generateHash();
if (appConfig.isCloud) {
await this.startTrialPeriod();
}
}
async $beforeUpdate(opt: ModelOptions, queryContext: QueryContext) {

View File

@@ -1,12 +1,12 @@
import { Router } from 'express';
import graphQLInstance from '../helpers/graphql-instance';
import webhooksRouter from './webhooks';
import stripeRouter from './stripe.ee';
import paddleRouter from './paddle.ee';
const router = Router();
router.use('/graphql', graphQLInstance);
router.use('/webhooks', webhooksRouter);
router.use('/stripe', stripeRouter);
router.use('/paddle', paddleRouter);
export default router;

View File

@@ -0,0 +1,19 @@
import { Response, Router, NextFunction, RequestHandler } from 'express';
import { IRequest } from '@automatisch/types';
import webhooksHandler from '../controllers/paddle/webhooks.ee';
const router = Router();
const exposeError =
(handler: RequestHandler) =>
async (req: IRequest, res: Response, next: NextFunction) => {
try {
await handler(req, res, next);
} catch (err) {
next(err);
}
};
router.post('/webhooks', exposeError(webhooksHandler));
export default router;

View File

@@ -1,23 +0,0 @@
import express, { Router } from 'express';
import multer from 'multer';
import { IRequest } from '@automatisch/types';
import appConfig from '../config/app';
import stripeWebhooksAction from '../controllers/stripe/webhooks.ee';
const router = Router();
const upload = multer();
router.use(upload.none());
router.use(
express.text({
limit: appConfig.requestBodySizeLimit,
verify(req, res, buf) {
(req as IRequest).rawBody = buf;
},
})
);
router.post('/webhooks', stripeWebhooksAction);
export default router;