From 73e83221b8ee648821a21b3c0b5e45abb967a4bb Mon Sep 17 00:00:00 2001 From: Vasily Zubarev Date: Mon, 21 Apr 2025 13:50:45 +0200 Subject: [PATCH] feat: storage and token limiting --- .env.example | 4 ++ ai/analyze.ts | 11 +++-- app/(app)/files/actions.ts | 6 ++- app/(app)/layout.tsx | 2 + app/(app)/settings/profile/page.tsx | 2 +- app/(app)/transactions/actions.ts | 12 ++++- app/(app)/unsorted/actions.ts | 13 +++++- app/global-error.tsx | 23 +++++++--- components/forms/select-currency.tsx | 7 ++- components/forms/select-type.tsx | 8 ++-- components/forms/simple.tsx | 8 +++- .../settings/profile-settings-form copy.tsx | 33 ------------- components/settings/profile-settings-form.tsx | 44 ++++++++++++++++++ components/unsorted/analyze-form.tsx | 2 +- lib/auth-client.ts | 8 +++- lib/auth.ts | 10 ++++ lib/config.ts | 6 +++ lib/files.ts | 8 ++++ lib/stripe.ts | 13 ++++++ lib/utils.ts | 10 +++- package-lock.json | 46 ++++++++++++++++--- package.json | 2 + .../20250421102306_token_limit/migration.sql | 2 + .../migration.sql | 12 +++++ prisma/schema.prisma | 5 +- 25 files changed, 232 insertions(+), 65 deletions(-) delete mode 100644 components/settings/profile-settings-form copy.tsx create mode 100644 components/settings/profile-settings-form.tsx create mode 100644 lib/stripe.ts create mode 100644 prisma/migrations/20250421102306_token_limit/migration.sql create mode 100644 prisma/migrations/20250421113343_limits_not_null/migration.sql diff --git a/.env.example b/.env.example index f8e0422..af3b502 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,10 @@ OPENAI_API_KEY="" # Auth Config BETTER_AUTH_SECRET="random-secret-key" # please use any long random string here +# Stripe Configuration +STRIPE_SECRET_KEY="" +STRIPE_WEBHOOK_SECRET="" + # Resend Configuration (optional, use if you want to send emails) RESEND_API_KEY="" RESEND_AUDIENCE_ID="" diff --git a/ai/analyze.ts b/ai/analyze.ts index b64b93b..b2861c7 100644 --- a/ai/analyze.ts +++ b/ai/analyze.ts @@ -4,12 +4,17 @@ import { ActionState } from "@/lib/actions" import OpenAI from "openai" import { AnalyzeAttachment } from "./attachments" +export type AnalysisResult = { + output: Record + tokensUsed: number +} + export async function analyzeTransaction( prompt: string, schema: Record, attachments: AnalyzeAttachment[], apiKey: string -): Promise>> { +): Promise> { const openai = new OpenAI({ apiKey, }) @@ -19,7 +24,7 @@ export async function analyzeTransaction( try { const response = await openai.responses.create({ - model: "gpt-4o-mini-2024-07-18", + model: "gpt-4o-mini", input: [ { role: "user", @@ -48,7 +53,7 @@ export async function analyzeTransaction( console.log("ChatGPT tokens used:", response.usage) const result = JSON.parse(response.output_text) - return { success: true, data: result } + return { success: true, data: { output: result, tokensUsed: response.usage?.total_tokens || 0 } } } catch (error) { console.error("AI Analysis error:", error) return { diff --git a/app/(app)/files/actions.ts b/app/(app)/files/actions.ts index 1d8d2e8..2d4a4f9 100644 --- a/app/(app)/files/actions.ts +++ b/app/(app)/files/actions.ts @@ -2,7 +2,7 @@ import { ActionState } from "@/lib/actions" import { getCurrentUser } from "@/lib/auth" -import { getDirectorySize, getUserUploadsDirectory, unsortedFilePath } from "@/lib/files" +import { getDirectorySize, getUserUploadsDirectory, isEnoughStorageToUploadFile, unsortedFilePath } from "@/lib/files" import { createFile } from "@/models/files" import { updateUser } from "@/models/users" import { randomUUID } from "crypto" @@ -24,6 +24,10 @@ export async function uploadFilesAction(formData: FormData): Promise acc + file.size, 0) + if (!isEnoughStorageToUploadFile(user, totalFileSize)) { + return { success: false, error: `Insufficient storage to upload new files` } + } + const fileRecords = await Promise.all( files.map(async (file) => { const fileUuid = randomUUID() diff --git a/app/(app)/unsorted/actions.ts b/app/(app)/unsorted/actions.ts index e306c8b..6ae9204 100644 --- a/app/(app)/unsorted/actions.ts +++ b/app/(app)/unsorted/actions.ts @@ -1,6 +1,6 @@ "use server" -import { analyzeTransaction } from "@/ai/analyze" +import { AnalysisResult, analyzeTransaction } from "@/ai/analyze" import { AnalyzeAttachment, loadAttachmentsForAI } from "@/ai/attachments" import { buildLLMPrompt } from "@/ai/prompt" import { fieldsToJsonSchema } from "@/ai/schema" @@ -12,6 +12,7 @@ import { getTransactionFileUploadPath, getUserUploadsDirectory } from "@/lib/fil import { DEFAULT_PROMPT_ANALYSE_NEW_FILE } from "@/models/defaults" import { deleteFile, getFileById, updateFile } from "@/models/files" import { createTransaction, updateTransactionFiles } from "@/models/transactions" +import { updateUser } from "@/models/users" import { Category, Field, File, Project, Transaction } from "@prisma/client" import { mkdir, rename } from "fs/promises" import { revalidatePath } from "next/cache" @@ -23,7 +24,7 @@ export async function analyzeFileAction( fields: Field[], categories: Category[], projects: Project[] -): Promise>> { +): Promise> { const user = await getCurrentUser() if (!file || file.userId !== user.id) { @@ -35,6 +36,10 @@ export async function analyzeFileAction( return { success: false, error: "OpenAI API key is not set" } } + if (!config.selfHosted.isEnabled && user.tokenBalance < 0) { + return { success: false, error: "You used all your AI tokens, please upgrade your account" } + } + let attachments: AnalyzeAttachment[] = [] try { attachments = await loadAttachmentsForAI(user, file) @@ -56,6 +61,10 @@ export async function analyzeFileAction( console.log("Analysis results:", results) + if (results.data?.tokensUsed && results.data.tokensUsed > 0) { + await updateUser(user.id, { tokenBalance: { decrement: results.data.tokensUsed } }) + } + return results } diff --git a/app/global-error.tsx b/app/global-error.tsx index cc65b72..2f511fd 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -1,7 +1,9 @@ "use client" +import { Button } from "@/components/ui/button" import * as Sentry from "@sentry/nextjs" -import NextError from "next/error" +import { Angry } from "lucide-react" +import Link from "next/link" import { useEffect } from "react" export default function GlobalError({ error }: { error: Error }) { @@ -12,11 +14,20 @@ export default function GlobalError({ error }: { error: Error }) { return ( - {/* `NextError` is the default Next.js error page component. Its type - definition requires a `statusCode` prop. However, since the App Router - does not expose status codes for errors, we simply pass 0 to render a - generic error message. */} - +
+
+ +

Oops! Something went wrong

+

+ We apologize for the inconvenience. Our team has been notified and is working to fix the issue. +

+
+ +
+
+
) diff --git a/components/forms/select-currency.tsx b/components/forms/select-currency.tsx index 32a9740..7bfe99e 100644 --- a/components/forms/select-currency.tsx +++ b/components/forms/select-currency.tsx @@ -17,7 +17,12 @@ export const FormSelectCurrency = ({ hideIfEmpty?: boolean } & SelectProps) => { const items = useMemo( - () => currencies.map((currency) => ({ code: currency.code, name: `${currency.code} - ${currency.name}` })), + () => + currencies.map((currency) => ({ + code: currency.code, + name: `${currency.code}`, + badge: currency.name, + })), [currencies] ) return ( diff --git a/components/forms/select-type.tsx b/components/forms/select-type.tsx index 310e1f5..7dc01d4 100644 --- a/components/forms/select-type.tsx +++ b/components/forms/select-type.tsx @@ -9,10 +9,10 @@ export const FormSelectType = ({ ...props }: { title: string; emptyValue?: string; placeholder?: string; hideIfEmpty?: boolean } & SelectProps) => { const items = [ - { code: "expense", name: "Expense" }, - { code: "income", name: "Income" }, - { code: "pending", name: "Pending" }, - { code: "other", name: "Other" }, + { code: "expense", name: "Expense", badge: "↓" }, + { code: "income", name: "Income", badge: "↑" }, + { code: "pending", name: "Pending", badge: "⏲︎" }, + { code: "other", name: "Other", badge: "?" }, ] return ( diff --git a/components/forms/simple.tsx b/components/forms/simple.tsx index 0a9d7e3..b3ff2ca 100644 --- a/components/forms/simple.tsx +++ b/components/forms/simple.tsx @@ -1,5 +1,6 @@ "use client" +import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Calendar } from "@/components/ui/calendar" import { Input } from "@/components/ui/input" @@ -57,7 +58,7 @@ export const FormSelect = ({ ...props }: { title: string - items: Array<{ code: string; name: string; color?: string }> + items: Array<{ code: string; name: string; color?: string; badge?: string }> emptyValue?: string placeholder?: string hideIfEmpty?: boolean @@ -78,7 +79,10 @@ export const FormSelect = ({ {items.map((item) => (
- {item.color &&
} + {item.badge && {item.badge}} + {!item.badge && item.color && ( +
+ )} {item.name}
diff --git a/components/settings/profile-settings-form copy.tsx b/components/settings/profile-settings-form copy.tsx deleted file mode 100644 index 8e12831..0000000 --- a/components/settings/profile-settings-form copy.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client" - -import { saveProfileAction } from "@/app/(app)/settings/actions" -import { FormError } from "@/components/forms/error" -import { FormInput } from "@/components/forms/simple" -import { Button } from "@/components/ui/button" -import { User } from "@prisma/client" -import { CircleCheckBig } from "lucide-react" -import { useActionState } from "react" - -export default function ProfileSettingsForm({ user }: { user: User }) { - const [saveState, saveAction, pending] = useActionState(saveProfileAction, null) - - return ( -
- - -
- - {saveState?.success && ( -

- - Saved! -

- )} -
- - {saveState?.error && {saveState.error}} - - ) -} diff --git a/components/settings/profile-settings-form.tsx b/components/settings/profile-settings-form.tsx new file mode 100644 index 0000000..9f91fdf --- /dev/null +++ b/components/settings/profile-settings-form.tsx @@ -0,0 +1,44 @@ +"use client" + +import { saveProfileAction } from "@/app/(app)/settings/actions" +import { FormError } from "@/components/forms/error" +import { FormInput } from "@/components/forms/simple" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { formatBytes, formatNumber } from "@/lib/utils" +import { User } from "@prisma/client" +import { CircleCheckBig } from "lucide-react" +import { useActionState } from "react" + +export default function ProfileSettingsForm({ user }: { user: User }) { + const [saveState, saveAction, pending] = useActionState(saveProfileAction, null) + + return ( +
+
+ + +
+ + {saveState?.success && ( +

+ + Saved! +

+ )} +
+ + {saveState?.error && {saveState.error}} + + +

+ Storage: {user.storageUsed ? formatBytes(user.storageUsed) : "N/A"} /{" "} + {user.storageLimit && user.storageLimit > 0 ? formatBytes(user.storageLimit) : "Unlimited"} +

+

Tokens Balance: {user.tokenBalance ? formatNumber(user.tokenBalance) : "N/A"}

+
+
+ ) +} diff --git a/components/unsorted/analyze-form.tsx b/components/unsorted/analyze-form.tsx index d0e08a8..5d265e3 100644 --- a/components/unsorted/analyze-form.tsx +++ b/components/unsorted/analyze-form.tsx @@ -106,7 +106,7 @@ export default function AnalyzeForm({ setAnalyzeError(results.error ? results.error : "Something went wrong...") } else { const nonEmptyFields = Object.fromEntries( - Object.entries(results.data || {}).filter( + Object.entries(results.data?.output || {}).filter( ([_, value]) => value !== null && value !== undefined && value !== "" ) ) diff --git a/lib/auth-client.ts b/lib/auth-client.ts index 739478b..18531d3 100644 --- a/lib/auth-client.ts +++ b/lib/auth-client.ts @@ -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, + }), + ], }) diff --git a/lib/auth.ts b/lib/auth.ts index 400089e..eda418a 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -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 ], }) diff --git a/lib/config.ts b/lib/config.ts index 2c26be3..5cb83e7 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -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 "), 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, diff --git a/lib/files.ts b/lib/files.ts index 0a6e235..26ee831 100644 --- a/lib/files.ts +++ b/lib/files.ts @@ -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 +} diff --git a/lib/stripe.ts b/lib/stripe.ts new file mode 100644 index 0000000..65e620d --- /dev/null +++ b/lib/stripe.ts @@ -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 +} diff --git a/lib/utils.ts b/lib/utils.ts index 346e6a4..915effa 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -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: "_", diff --git a/package-lock.json b/package-lock.json index 763d41d..2e4333b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "taxhacker", "version": "0.5.0", "dependencies": { + "@better-auth/stripe": "^1.2.5", "@fast-csv/format": "^5.0.2", "@fast-csv/parse": "^5.0.2", "@prisma/client": "^6.6.0", @@ -42,6 +43,7 @@ "sharp": "^0.33.5", "slugify": "^1.6.6", "sonner": "^2.0.1", + "stripe": "^18.0.0", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.2" @@ -347,6 +349,16 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/stripe": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@better-auth/stripe/-/stripe-1.2.5.tgz", + "integrity": "sha512-+87qnc4rtDJxzdCswJQOHTopRRcVw+93cSNz8O1TP3GcBEooEjAspHHAxSmutPm7pluLrHIX5g0uFE2MIOUbmQ==", + "license": "MIT", + "dependencies": { + "better-auth": "^1.2.5", + "zod": "^3.24.1" + } + }, "node_modules/@better-auth/utils": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.2.4.tgz", @@ -5685,7 +5697,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -8857,7 +8868,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9554,6 +9564,21 @@ "node": ">=6.0.0" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -10278,7 +10303,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10298,7 +10322,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10315,7 +10338,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10334,7 +10356,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -10688,6 +10709,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stripe": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.0.0.tgz", + "integrity": "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA==", + "license": "MIT", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", diff --git a/package.json b/package.json index 51f9f87..dd1b401 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "lint": "next lint" }, "dependencies": { + "@better-auth/stripe": "^1.2.5", "@fast-csv/format": "^5.0.2", "@fast-csv/parse": "^5.0.2", "@prisma/client": "^6.6.0", @@ -44,6 +45,7 @@ "sharp": "^0.33.5", "slugify": "^1.6.6", "sonner": "^2.0.1", + "stripe": "^18.0.0", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.2" diff --git a/prisma/migrations/20250421102306_token_limit/migration.sql b/prisma/migrations/20250421102306_token_limit/migration.sql new file mode 100644 index 0000000..914b786 --- /dev/null +++ b/prisma/migrations/20250421102306_token_limit/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "storage_limit" INTEGER DEFAULT -1; diff --git a/prisma/migrations/20250421113343_limits_not_null/migration.sql b/prisma/migrations/20250421113343_limits_not_null/migration.sql new file mode 100644 index 0000000..40c8e17 --- /dev/null +++ b/prisma/migrations/20250421113343_limits_not_null/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Made the column `storage_used` on table `users` required. This step will fail if there are existing NULL values in that column. + - Made the column `token_balance` on table `users` required. This step will fail if there are existing NULL values in that column. + - Made the column `storage_limit` on table `users` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "users" ALTER COLUMN "storage_used" SET NOT NULL, +ALTER COLUMN "token_balance" SET NOT NULL, +ALTER COLUMN "storage_limit" SET NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0531aac..60277de 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -27,8 +27,9 @@ model User { membershipPlan String? @map("membership_plan") membershipExpiresAt DateTime? @map("membership_expires_at") emailVerified Boolean @default(false) @map("is_email_verified") - storageUsed Int? @default(0) @map("storage_used") - tokenBalance Int? @default(0) @map("token_balance") + storageUsed Int @default(0) @map("storage_used") + storageLimit Int @default(-1) @map("storage_limit") + tokenBalance Int @default(0) @map("token_balance") accounts Account[] sessions Session[]