feat: storage and token limiting

This commit is contained in:
Vasily Zubarev
2025-04-21 13:50:45 +02:00
parent 62bad46e58
commit 73e83221b8
25 changed files with 232 additions and 65 deletions

View File

@@ -1,6 +1,12 @@
import { stripeClient } from "@better-auth/stripe/client"
import { createAuthClient } from "better-auth/client"
import { emailOTPClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
plugins: [emailOTPClient()],
plugins: [
emailOTPClient(),
stripeClient({
subscription: true,
}),
],
})

View File

@@ -1,6 +1,7 @@
import config from "@/lib/config"
import { createUserDefaults } from "@/models/defaults"
import { getSelfHostedUser, getUserByEmail } from "@/models/users"
import { stripe } from "@better-auth/stripe"
import { User } from "@prisma/client"
import { betterAuth } from "better-auth"
import { prismaAdapter } from "better-auth/adapters/prisma"
@@ -11,6 +12,7 @@ import { headers } from "next/headers"
import { redirect } from "next/navigation"
import { prisma } from "./db"
import { resend, sendOTPCodeEmail } from "./email"
import { isStripeEnabled, stripeClient } from "./stripe"
export type UserProfile = {
id: string
@@ -18,6 +20,7 @@ export type UserProfile = {
email: string
avatar?: string
storageUsed?: number
tokenBalance?: number
}
export const auth = betterAuth({
@@ -65,6 +68,13 @@ export const auth = betterAuth({
await sendOTPCodeEmail({ email, otp })
},
}),
isStripeEnabled(stripeClient)
? stripe({
stripeClient: stripeClient!,
stripeWebhookSecret: config.stripe.webhookSecret,
createCustomerOnSignUp: true,
})
: { id: "stripe", endpoints: {} },
nextCookies(), // make sure this is the last plugin in the array
],
})

View File

@@ -13,6 +13,8 @@ const envSchema = z.object({
RESEND_API_KEY: z.string().default("please-set-your-resend-api-key-here"),
RESEND_FROM_EMAIL: z.string().default("TaxHacker <user@localhost>"),
RESEND_AUDIENCE_ID: z.string().default(""),
STRIPE_SECRET_KEY: z.string().default(""),
STRIPE_WEBHOOK_SECRET: z.string().default(""),
})
const env = envSchema.parse(process.env)
@@ -40,6 +42,10 @@ const config = {
loginUrl: "/enter",
disableSignup: env.DISABLE_SIGNUP === "true" || env.SELF_HOSTED_MODE === "true",
},
stripe: {
secretKey: env.STRIPE_SECRET_KEY,
webhookSecret: env.STRIPE_WEBHOOK_SECRET,
},
email: {
apiKey: env.RESEND_API_KEY,
from: env.RESEND_FROM_EMAIL,

View File

@@ -1,6 +1,7 @@
import { File, Transaction, User } from "@prisma/client"
import { access, constants, readdir, stat } from "fs/promises"
import path from "path"
import config from "./config"
export const FILE_UPLOAD_PATH = path.resolve(process.env.UPLOAD_PATH || "./uploads")
export const FILE_UNSORTED_DIRECTORY_NAME = "unsorted"
@@ -70,3 +71,10 @@ export async function getDirectorySize(directoryPath: string) {
await calculateSize(directoryPath)
return totalSize
}
export function isEnoughStorageToUploadFile(user: User, fileSize: number) {
if (config.selfHosted.isEnabled || user.storageLimit < 0) {
return true
}
return user.storageUsed + fileSize <= user.storageLimit
}

13
lib/stripe.ts Normal file
View File

@@ -0,0 +1,13 @@
import Stripe from "stripe"
import config from "./config"
export const stripeClient: Stripe | null = config.stripe.secretKey
? new Stripe(config.stripe.secretKey, {
apiVersion: "2025-03-31.basil",
})
: null
// Type guard to check if Stripe is initialized
export const isStripeEnabled = (client: Stripe | null): client is Stripe => {
return client !== null
}

View File

@@ -2,12 +2,14 @@ import { clsx, type ClassValue } from "clsx"
import slugify from "slugify"
import { twMerge } from "tailwind-merge"
const LOCALE = "en-US"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function formatCurrency(total: number, currency: string) {
return new Intl.NumberFormat("en-US", {
return new Intl.NumberFormat(LOCALE, {
style: "currency",
currency: currency,
minimumFractionDigits: 2,
@@ -28,6 +30,12 @@ export function formatBytes(bytes: number) {
return `${parseFloat(value.toFixed(2))} ${sizes[i]}`
}
export function formatNumber(number: number) {
return new Intl.NumberFormat(LOCALE, {
useGrouping: true,
}).format(number)
}
export function codeFromName(name: string, maxLength: number = 16) {
const code = slugify(name, {
replacement: "_",