diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/member-section.tsx index 9fc57621..ddaac734 100644 --- a/packages/console/app/src/routes/workspace/member-section.tsx +++ b/packages/console/app/src/routes/workspace/member-section.tsx @@ -56,25 +56,36 @@ const removeMember = action(async (form: FormData) => { ) }, "member.remove") -const updateMemberRole = action(async (form: FormData) => { +const updateMember = action(async (form: FormData) => { "use server" + console.log("!@#!@ Form data entries:") + for (const [key, value] of form.entries()) { + console.log(`!@#!@ ${key}:`, value) + } + const id = form.get("id")?.toString() if (!id) return { error: "ID is required" } const workspaceID = form.get("workspaceID")?.toString() if (!workspaceID) return { error: "Workspace ID is required" } const role = form.get("role")?.toString() as (typeof UserRole)[number] if (!role) return { error: "Role is required" } + const limit = form.get("limit")?.toString() + const monthlyLimit = limit && limit.trim() !== "" ? parseInt(limit) : null + if (monthlyLimit !== null && monthlyLimit < 0) return { error: "Set a valid monthly limit" } + + console.log({ id, role, monthlyLimit, limit }) + return json( await withActor( () => - User.updateRole({ id, role }) + User.update({ id, role, monthlyLimit }) .then((data) => ({ error: undefined, data })) .catch((e) => ({ error: e.message as string })), workspaceID, ), { revalidate: listMembers.key }, ) -}, "member.updateRole") +}, "member.update") export function MemberCreateForm() { const params = useParams() @@ -155,7 +166,7 @@ export function MemberCreateForm() { function MemberRow(props: { member: any; workspaceID: string; currentUserID: string | null }) { const [editing, setEditing] = createSignal(false) - const submission = useSubmission(updateMemberRole) + const submission = useSubmission(updateMember) const isCurrentUser = () => props.currentUserID === props.member.id createEffect(() => { @@ -164,6 +175,29 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str } }) + function getUsageDisplay() { + const currentUsage = (() => { + const dateLastUsed = props.member.timeMonthlyUsageUpdated + if (!dateLastUsed) return 0 + + const current = new Date().toLocaleDateString("en-US", { + year: "numeric", + month: "long", + timeZone: "UTC", + }) + const lastUsed = dateLastUsed.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + timeZone: "UTC", + }) + if (current !== lastUsed) return 0 + return ((props.member.monthlyUsage ?? 0) / 100000000).toFixed(2) + })() + + const limit = props.member.monthlyLimit ? `$${props.member.monthlyLimit}` : "no limit" + return `$${currentUsage} / ${limit}` + } + return ( {props.member.accountEmail ?? props.member.email} {props.member.role} + {getUsageDisplay()} }> invited @@ -190,12 +225,21 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str } > - -
+ +
{props.member.accountEmail ?? props.member.email}
- Role: {props.member.role}}> + + +
Role: {props.member.role}
+ + + } + >
+ +
+ +
+ {(err) =>
{err()}
}
+
- - - +
@@ -258,6 +316,7 @@ export function MemberSection() { Email Role + Usage diff --git a/packages/console/app/src/routes/zen/handler.ts b/packages/console/app/src/routes/zen/handler.ts index 9d08ccdf..feb0c9c3 100644 --- a/packages/console/app/src/routes/zen/handler.ts +++ b/packages/console/app/src/routes/zen/handler.ts @@ -11,6 +11,7 @@ import { Billing } from "../../../../core/src/billing" import { Actor } from "@opencode-ai/console-core/actor.js" import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js" import { ZenModel } from "@opencode-ai/console-core/model.js" +import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js" export async function handler( input: APIEvent, @@ -33,6 +34,7 @@ export async function handler( class AuthError extends Error {} class CreditsError extends Error {} class MonthlyLimitError extends Error {} + class UserLimitError extends Error {} class ModelError extends Error {} type Model = z.infer @@ -181,6 +183,7 @@ export async function handler( error instanceof AuthError || error instanceof CreditsError || error instanceof MonthlyLimitError || + error instanceof UserLimitError || error instanceof ModelError ) return new Response( @@ -243,10 +246,15 @@ export async function handler( monthlyLimit: BillingTable.monthlyLimit, monthlyUsage: BillingTable.monthlyUsage, timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated, + userID: UserTable.id, + userMonthlyLimit: UserTable.monthlyLimit, + userMonthlyUsage: UserTable.monthlyUsage, + timeUserMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated, }) .from(KeyTable) .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, KeyTable.workspaceID)) .innerJoin(BillingTable, eq(BillingTable.workspaceID, KeyTable.workspaceID)) + .innerJoin(UserTable, and(eq(UserTable.workspaceID, KeyTable.workspaceID), eq(UserTable.id, KeyTable.userID))) .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .then((rows) => rows[0]), ) @@ -269,6 +277,12 @@ export async function handler( monthlyUsage: data.monthlyUsage, timeMonthlyUsageUpdated: data.timeMonthlyUsageUpdated, }, + user: { + id: data.userID, + monthlyLimit: data.userMonthlyLimit, + monthlyUsage: data.userMonthlyUsage, + timeMonthlyUsageUpdated: data.timeUserMonthlyUsageUpdated, + }, isFree, } } @@ -280,19 +294,34 @@ export async function handler( const billing = authInfo.billing if (!billing.paymentMethodID) throw new CreditsError("No payment method") if (billing.balance <= 0) throw new CreditsError("Insufficient balance") + + const now = new Date() + const currentYear = now.getUTCFullYear() + const currentMonth = now.getUTCMonth() if ( billing.monthlyLimit && billing.monthlyUsage && billing.timeMonthlyUsageUpdated && billing.monthlyUsage >= centsToMicroCents(billing.monthlyLimit * 100) ) { - const now = new Date() - const currentYear = now.getUTCFullYear() - const currentMonth = now.getUTCMonth() const dateYear = billing.timeMonthlyUsageUpdated.getUTCFullYear() const dateMonth = billing.timeMonthlyUsageUpdated.getUTCMonth() if (currentYear === dateYear && currentMonth === dateMonth) - throw new MonthlyLimitError(`You have reached your monthly spending limit of $${billing.monthlyLimit}.`) + throw new MonthlyLimitError( + `Your workspace has reached its monthly spending limit of $${billing.monthlyLimit}.`, + ) + } + + if ( + authInfo.user.monthlyLimit && + authInfo.user.monthlyUsage && + authInfo.user.timeMonthlyUsageUpdated && + authInfo.user.monthlyUsage >= centsToMicroCents(authInfo.user.monthlyLimit * 100) + ) { + const dateYear = authInfo.user.timeMonthlyUsageUpdated.getUTCFullYear() + const dateMonth = authInfo.user.timeMonthlyUsageUpdated.getUTCMonth() + if (currentYear === dateYear && currentMonth === dateMonth) + throw new UserLimitError(`You have reached your monthly spending limit of $${authInfo.user.monthlyLimit}.`) } } @@ -386,6 +415,18 @@ export async function handler( timeMonthlyUsageUpdated: sql`now()`, }) .where(eq(BillingTable.workspaceID, authInfo.workspaceID)) + await tx + .update(UserTable) + .set({ + monthlyUsage: sql` + CASE + WHEN MONTH(${UserTable.timeMonthlyUsageUpdated}) = MONTH(now()) AND YEAR(${UserTable.timeMonthlyUsageUpdated}) = YEAR(now()) THEN ${UserTable.monthlyUsage} + ${cost} + ELSE ${cost} + END + `, + timeMonthlyUsageUpdated: sql`now()`, + }) + .where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))) }) await Database.use((tx) => diff --git a/packages/console/core/migrations/0029_panoramic_harrier.sql b/packages/console/core/migrations/0029_panoramic_harrier.sql new file mode 100644 index 00000000..5a7bbc3b --- /dev/null +++ b/packages/console/core/migrations/0029_panoramic_harrier.sql @@ -0,0 +1,3 @@ +ALTER TABLE `user` ADD `monthly_limit` int;--> statement-breakpoint +ALTER TABLE `user` ADD `monthly_usage` bigint;--> statement-breakpoint +ALTER TABLE `user` ADD `time_monthly_usage_updated` timestamp(3); \ No newline at end of file diff --git a/packages/console/core/migrations/meta/0029_snapshot.json b/packages/console/core/migrations/meta/0029_snapshot.json new file mode 100644 index 00000000..959004f3 --- /dev/null +++ b/packages/console/core/migrations/meta/0029_snapshot.json @@ -0,0 +1,730 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "33551b4c-fc2e-4753-8d9d-0971f333e65d", + "prevId": "a331e38c-c2e3-406d-a1ff-b0af7229cd85", + "tables": { + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "email": { + "name": "email", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "billing": { + "name": "billing", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_id": { + "name": "payment_method_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_method_last4": { + "name": "payment_method_last4", + "type": "varchar(4)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "balance": { + "name": "balance", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload": { + "name": "reload", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reload_error": { + "name": "reload_error", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_error": { + "name": "time_reload_error", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_reload_locked_till": { + "name": "time_reload_locked_till", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_customer_id": { + "name": "global_customer_id", + "columns": [ + "customer_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_workspace_id_id_pk": { + "name": "billing_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "payment": { + "name": "payment", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payment_id": { + "name": "payment_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_refunded": { + "name": "time_refunded", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "payment_workspace_id_id_pk": { + "name": "payment_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "usage": { + "name": "usage", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "output_tokens": { + "name": "output_tokens", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_read_tokens": { + "name": "cache_read_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_5m_tokens": { + "name": "cache_write_5m_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cache_write_1h_tokens": { + "name": "cache_write_1h_tokens", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cost": { + "name": "cost", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "usage_workspace_id_id_pk": { + "name": "usage_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "key": { + "name": "key", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_used": { + "name": "time_used", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "global_key": { + "name": "global_key", + "columns": [ + "key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "key_workspace_id_id_pk": { + "name": "key_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_seen": { + "name": "time_seen", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('admin','member')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "monthly_usage": { + "name": "monthly_usage", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_monthly_usage_updated": { + "name": "time_monthly_usage_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "user_account_id": { + "name": "user_account_id", + "columns": [ + "workspace_id", + "account_id" + ], + "isUnique": true + }, + "user_email": { + "name": "user_email", + "columns": [ + "workspace_id", + "email" + ], + "isUnique": true + }, + "global_account_id": { + "name": "global_account_id", + "columns": [ + "account_id" + ], + "isUnique": false + }, + "global_email": { + "name": "global_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "user_workspace_id_id_pk": { + "name": "user_workspace_id_id_pk", + "columns": [ + "workspace_id", + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "workspace": { + "name": "workspace", + "columns": { + "id": { + "name": "id", + "type": "varchar(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_created": { + "name": "time_created", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "time_updated": { + "name": "time_updated", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)" + }, + "time_deleted": { + "name": "time_deleted", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "slug": { + "name": "slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "workspace_id": { + "name": "workspace_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index eaa28ddf..22171825 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -204,6 +204,13 @@ "when": 1759805025276, "tag": "0028_careful_cerise", "breakpoints": true + }, + { + "idx": 29, + "version": "5", + "when": 1759811835558, + "tag": "0029_panoramic_harrier", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index 644e3bd8..0b77e4c3 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -1,5 +1,5 @@ import { Stripe } from "stripe" -import { and, Database, eq, sql } from "./drizzle" +import { Database, eq, sql } from "./drizzle" import { BillingTable, PaymentTable, UsageTable } from "./schema/billing.sql" import { Actor } from "./actor" import { fn } from "./util/fn" diff --git a/packages/console/core/src/schema/user.sql.ts b/packages/console/core/src/schema/user.sql.ts index 121199f7..7fd7f5e1 100644 --- a/packages/console/core/src/schema/user.sql.ts +++ b/packages/console/core/src/schema/user.sql.ts @@ -1,4 +1,4 @@ -import { mysqlTable, uniqueIndex, varchar, int, mysqlEnum, index } from "drizzle-orm/mysql-core" +import { mysqlTable, uniqueIndex, varchar, int, mysqlEnum, index, bigint } from "drizzle-orm/mysql-core" import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" @@ -15,6 +15,9 @@ export const UserTable = mysqlTable( timeSeen: utc("time_seen"), color: int("color"), role: mysqlEnum("role", UserRole).notNull(), + monthlyLimit: int("monthly_limit"), + monthlyUsage: bigint("monthly_usage", { mode: "number" }), + timeMonthlyUsageUpdated: utc("time_monthly_usage_updated"), }, (table) => [ ...workspaceIndexes(table), diff --git a/packages/console/core/src/user.ts b/packages/console/core/src/user.ts index d4a0da0f..5e7605e9 100644 --- a/packages/console/core/src/user.ts +++ b/packages/console/core/src/user.ts @@ -174,18 +174,19 @@ export namespace User { ) }) - export const updateRole = fn( + export const update = fn( z.object({ id: z.string(), role: z.enum(UserRole), + monthlyLimit: z.number().nullable(), }), - async ({ id, role }) => { + async ({ id, role, monthlyLimit }) => { await assertAdmin() if (role === "member") assertNotSelf(id) return await Database.use((tx) => tx .update(UserTable) - .set({ role }) + .set({ role, monthlyLimit }) .where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, Actor.workspace()))), ) },