Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f2cf0be
wip
flohoch Nov 11, 2023
1dcf3d0
wip
flohoch Nov 11, 2023
4aab5f8
wip
flohoch Nov 17, 2023
66e67e9
wip
flohoch Dec 6, 2023
67aa65d
Merge branch 'main' into feature/stripe-enabled-flag
flohoch Dec 6, 2023
26f9efe
wip
flohoch Dec 6, 2023
339f7e2
Merge branch 'feature/stripe-enabled-flag' of https://github.com/kula…
flohoch Dec 6, 2023
4b35c0a
wip
flohoch Dec 6, 2023
e2391be
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 7, 2023
fa213fc
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 7, 2023
c467472
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 7, 2023
e2bbee7
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 8, 2023
4ed6329
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 8, 2023
22d32a3
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 8, 2023
6b439b2
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 8, 2023
6db6f45
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 9, 2023
8eb086e
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 9, 2023
ccb3e90
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 10, 2023
bbe4e03
Update config/loadConfig.ts
flohoch Dec 10, 2023
d14f108
Update config/loadConfig.ts
flohoch Dec 10, 2023
d99a290
Update pages/api/frontend/v0.1/stripe/checkoutSession.ts
flohoch Dec 10, 2023
34f4bd2
updated according to pull request feedback
flohoch Dec 10, 2023
bc92ac9
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 10, 2023
3e0e499
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 11, 2023
c436a1b
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 11, 2023
5837f8e
Merge branch 'main' into feature/stripe-enabled-flag
kodiakhq[bot] Dec 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ SMTP_FROM_EMAIL_ADDRESS="flo@kula.app"
SMTP_FROM_NAME="Flo Ho"

# add your stripe API key here
NEXT_PUBLIC_STRIPE_ENABLED=false
STRIPE_ENABLED=false
STRIPE_WEBHOOK_SECRET=
STRIPE_SECRET_KEY=
STRIPE_USE_AUTOMATIC_TAX=
Expand Down
1 change: 1 addition & 0 deletions config/interfaces/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { UsageReportConfig } from "./UsageReportConfig";
export interface Config {
client: {
sentryConfig: SentryConfig;
stripeConfig: StripeConfig;
};
server: {
database: DatabaseConfig;
Expand Down
9 changes: 5 additions & 4 deletions config/interfaces/StripeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export interface StripeConfig {
apiVersion: string;
webhookSecret: string;
secretKey: string;
useAutomaticTax: boolean;
isEnabled: boolean;
apiVersion?: string;
webhookSecret?: string;
secretKey?: string;
useAutomaticTax?: boolean;
dynamicTaxRates?: Array<string>;
taxRates?: Array<string>;
}
7 changes: 7 additions & 0 deletions config/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export function loadConfig(): Config {
env.NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE
) ?? 0.2,
},
stripeConfig: {
isEnabled:
parseBooleanEnvValue(env.STRIPE_ENABLED) ??
parseBooleanEnvValue(env.NEXT_PUBLIC_STRIPE_ENABLED) ??
false,
},
},
server: {
nextAuth: {
Expand Down Expand Up @@ -107,6 +113,7 @@ export function loadConfig(): Config {
sentinelPassword: env.REDIS_SENTINEL_PASSWORD,
},
stripeConfig: {
isEnabled: parseBooleanEnvValue(env.STRIPE_ENABLED) ?? false,
apiVersion: env.STRIPE_API_VERSION || "",
webhookSecret: env.STRIPE_WEBHOOK_SECRET || "",
secretKey: env.STRIPE_SECRET_KEY || "",
Expand Down
22 changes: 15 additions & 7 deletions pages/api/frontend/v0.1/stripe/checkoutSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,15 @@ export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const stripeConfig = loadConfig().server.stripeConfig;
const logger = new Logger(__filename);
const stripeConfig = loadConfig().server.stripeConfig;

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

const userInOrg = await getUserWithRoleFromRequest(req, res);

Expand All @@ -30,6 +37,9 @@ export default async function handler(

switch (req.method) {
case "POST":
if (!stripeConfig.secretKey) {
throw new Error("Stripe secret key is not configured");
}
const stripe = new Stripe(stripeConfig.secretKey, {
apiVersion: "2023-08-16",
});
Expand All @@ -44,11 +54,9 @@ export default async function handler(

if (!org) {
logger.error(`No organisation found with id ${userInOrg.orgId}`);
return res
.status(StatusCodes.NOT_FOUND)
.json({
message: `No organisation found with id ${userInOrg.orgId}`,
});
return res.status(StatusCodes.NOT_FOUND).json({
message: `No organisation found with id ${userInOrg.orgId}`,
});
}

if (!req.body.products || !Array.isArray(req.body.products)) {
Expand Down Expand Up @@ -96,7 +104,7 @@ export default async function handler(
let sessionOptions: Stripe.Checkout.SessionCreateParams = {
allow_promotion_codes: true,
automatic_tax: {
enabled: stripeConfig.useAutomaticTax,
enabled: !!stripeConfig.useAutomaticTax,
},
billing_address_collection: "required",
client_reference_id: req.body.orgId,
Expand Down
13 changes: 12 additions & 1 deletion pages/api/frontend/v0.1/stripe/customerPortal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ export default async function handler(
const config = loadConfig();
const logger = new Logger(__filename);

if (!config.server.stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

const user = await getUserWithRoleFromRequest(req, res);

if (!user) {
Expand All @@ -31,7 +38,11 @@ export default async function handler(

switch (req.method) {
case "POST":
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
const stripeConfig = config.server.stripeConfig;
if (!stripeConfig.secretKey) {
throw new Error("Stripe secret key is not configured");
}
const stripe = new Stripe(stripeConfig.secretKey, {
apiVersion: "2023-08-16",
});

Expand Down
17 changes: 16 additions & 1 deletion pages/api/frontend/v0.1/stripe/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ const logger = new Logger(__filename);

export async function getProducts(): Promise<Product[]> {
const config = loadConfig();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {

const stripeConfig = loadConfig().server.stripeConfig;
if (!stripeConfig.secretKey) {
throw new Error("Stripe secret key is not configured");
}
const stripe = new Stripe(stripeConfig.secretKey, {
apiVersion: "2023-08-16",
});

const freeProduct: Product = {
id: "FREE",
description: "For checking it out",
Expand Down Expand Up @@ -150,6 +156,15 @@ export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const stripeConfig = loadConfig().server.stripeConfig;

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

switch (req.method) {
case "GET":
try {
Expand Down
9 changes: 9 additions & 0 deletions pages/api/frontend/v0.1/stripe/subscriptions/[orgId].ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { StatusCodes } from "http-status-codes";
import type { NextApiRequest, NextApiResponse } from "next";
import { loadConfig } from "../../../../../../config/loadConfig";
import prisma from "../../../../../../lib/services/db";
import { getUserWithRoleFromRequest } from "../../../../../../util/auth";
import { Logger } from "../../../../../../util/logger";
Expand All @@ -9,6 +10,14 @@ export default async function handler(
res: NextApiResponse
) {
const logger = new Logger(__filename);
const stripeConfig = loadConfig().server.stripeConfig;

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

const userInOrg = await getUserWithRoleFromRequest(req, res);

Expand Down
10 changes: 9 additions & 1 deletion pages/api/usageReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const usageReportingConfig = loadConfig().server.usageReport;
const logger = new Logger(__filename);
const usageReportingConfig = loadConfig().server.usageReport;
const stripeConfig = loadConfig().server.stripeConfig;

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

if (
req.headers.authorization !== `Bearer ${usageReportingConfig.apiKey}` &&
Expand Down
148 changes: 75 additions & 73 deletions pages/api/v0.1/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,97 +128,99 @@ export default async function handler(
}

// Start of quota limitation
try {
const products = await getProducts();
if (config.server.stripeConfig.isEnabled) {
try {
const products = await getProducts();

// Check if there is a subItem with isMetered set to true
// Metered subItems do not have a limit
let hasMeteredSubItem = false;
// There should be 0 or 1 sub
let subFromDb = app?.organisation?.subs[0];
// Check if there is a subItem with isMetered set to true
// Metered subItems do not have a limit
let hasMeteredSubItem = false;
// There should be 0 or 1 sub
let subFromDb = app?.organisation?.subs[0];

if (app?.organisation?.subs) {
for (const sub of app.organisation.subs) {
if (sub.subItems?.some((subItem) => subItem.metered === true)) {
hasMeteredSubItem = true;
break;
if (app?.organisation?.subs) {
for (const sub of app.organisation.subs) {
if (sub.subItems?.some((subItem) => subItem.metered === true)) {
hasMeteredSubItem = true;
break;
}
}
}
}

// If not metered, check for the limit
if (!hasMeteredSubItem) {
let countingStartDate = new Date();
// If not metered, check for the limit
if (!hasMeteredSubItem) {
let countingStartDate = new Date();

// Free version counts back plainly one month
if (!subFromDb) {
countingStartDate.setMonth(countingStartDate.getMonth() - 1);
} else {
// use current period start of active subscription
countingStartDate = subFromDb.currentPeriodStart;
}
// Free version counts back plainly one month
if (!subFromDb) {
countingStartDate.setMonth(countingStartDate.getMonth() - 1);
} else {
// use current period start of active subscription
countingStartDate = subFromDb.currentPeriodStart;
}

// Prepare array of app ids of organisation
const appIds = app?.organisation?.apps?.map((app) => app.id) || [];
// Prepare array of app ids of organisation
const appIds = app?.organisation?.apps?.map((app) => app.id) || [];

// Count requests across all apps of the org
const requestCount = await prisma.loggedApiRequests.count({
where: {
appId: {
in: appIds,
},
createdAt: {
gte: countingStartDate,
// Count requests across all apps of the org
const requestCount = await prisma.loggedApiRequests.count({
where: {
appId: {
in: appIds,
},
createdAt: {
gte: countingStartDate,
},
},
},
});
logger.log(
`Request count for org with id '${app.orgId}' is ${requestCount}`
);

let isLimitReached = false;

// Check whether quota/limit for the request has been met (active subscription)
if (subFromDb) {
const targetProduct = products.find(
(product: { id: string | undefined }) =>
product.id === subFromDb?.subItems[0].productId
});
logger.log(
`Request count for org with id '${app.orgId}' is ${requestCount}`
);

if (!targetProduct) {
logger.error(
`No product found for org with id '${app.orgId}' and active sub with id '${subFromDb.subId}'`
let isLimitReached = false;

// Check whether quota/limit for the request has been met (active subscription)
if (subFromDb) {
const targetProduct = products.find(
(product: { id: string | undefined }) =>
product.id === subFromDb?.subItems[0].productId
);
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ message: "Please try again later" });
}

logger.log(
`Request limit for org with id '${app.orgId}' is ${targetProduct.requests}`
);
if (requestCount >= Number(targetProduct.requests)) {
if (!targetProduct) {
logger.error(
`No product found for org with id '${app.orgId}' and active sub with id '${subFromDb.subId}'`
);
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ message: "Please try again later" });
}

logger.log(
`Request limit for org with id '${app.orgId}' is ${targetProduct.requests}`
);
if (requestCount >= Number(targetProduct.requests)) {
isLimitReached = true;
}
} else if (!subFromDb && requestCount >= FREE_SUB_REQUEST_LIMIT) {
// Check quota/limit for free version
isLimitReached = true;
}
} else if (!subFromDb && requestCount >= FREE_SUB_REQUEST_LIMIT) {
// Check quota/limit for free version
isLimitReached = true;
}

// Return error if limit has been reached and the request cannot be served
if (isLimitReached) {
logger.log(
`The limit has been currently reached for org with id '${app?.orgId}'`
);
return res.status(StatusCodes.PAYMENT_REQUIRED).json({
message: "The limit for the current abo has been reached.",
});
// Return error if limit has been reached and the request cannot be served
if (isLimitReached) {
logger.log(
`The limit has been currently reached for org with id '${app?.orgId}'`
);
return res.status(StatusCodes.PAYMENT_REQUIRED).json({
message: "The limit for the current abo has been reached.",
});
}
}
} catch (error: any) {
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ message: error.message });
}
} catch (error: any) {
return res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ message: error.message });
}

logger.log(`Looking up all messages for app with id '${app.id}'`);
Expand Down
10 changes: 10 additions & 0 deletions pages/api/webhooks/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,20 @@ export default async function handler(
const logger = new Logger(__filename);

const stripeConfig = loadConfig().server.stripeConfig;
if (!stripeConfig.secretKey) {
throw new Error("Stripe secret key is not configured");
}
const stripe = new Stripe(stripeConfig.secretKey, {
apiVersion: "2023-08-16",
});

if (!stripeConfig.isEnabled) {
logger.error("stripe is disabled but endpoint has been called");
return res
.status(StatusCodes.SERVICE_UNAVAILABLE)
.json({ message: "Endpoint is disabled" });
}

switch (req.method) {
case "POST":
const buf = await buffer(req);
Expand Down
Loading