summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFrank <[email protected]>2025-10-07 09:17:05 -0400
committerFrank <[email protected]>2025-10-07 09:17:08 -0400
commit6c99b833e443ba1531845a7fcf4f74247a0837bc (patch)
tree8ad2e4c1bc3b67f44019d5dcbccc54b3fe2d0959
parentcd3780b7f5b46f03b121dff6172adb445bd748e5 (diff)
downloadopencode-6c99b833e443ba1531845a7fcf4f74247a0837bc.tar.gz
opencode-6c99b833e443ba1531845a7fcf4f74247a0837bc.zip
wip: zen
-rw-r--r--packages/console/app/src/routes/workspace/member-section.tsx83
-rw-r--r--packages/console/app/src/routes/zen/handler.ts49
-rw-r--r--packages/console/core/migrations/0029_panoramic_harrier.sql3
-rw-r--r--packages/console/core/migrations/meta/0029_snapshot.json730
-rw-r--r--packages/console/core/migrations/meta/_journal.json7
-rw-r--r--packages/console/core/src/billing.ts2
-rw-r--r--packages/console/core/src/schema/user.sql.ts5
-rw-r--r--packages/console/core/src/user.ts7
8 files changed, 865 insertions, 21 deletions
diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/member-section.tsx
index 9fc57621c..ddaac7348 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 (
<Show
when={editing()}
@@ -171,6 +205,7 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str
<tr>
<td data-slot="member-email">{props.member.accountEmail ?? props.member.email}</td>
<td data-slot="member-role">{props.member.role}</td>
+ <td data-slot="member-usage">{getUsageDisplay()}</td>
<Show when={!props.member.timeSeen} fallback={<td data-slot="member-joined"></td>}>
<td data-slot="member-joined">invited</td>
</Show>
@@ -190,12 +225,21 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str
}
>
<tr>
- <td colspan="4">
- <form action={updateMemberRole} method="post">
+ <td colspan="5">
+ <form action={updateMember} method="post">
<div data-slot="edit-member-email">{props.member.accountEmail ?? props.member.email}</div>
<input type="hidden" name="id" value={props.member.id} />
<input type="hidden" name="workspaceID" value={props.workspaceID} />
- <Show when={!isCurrentUser()} fallback={<div data-slot="current-user-role">Role: {props.member.role}</div>}>
+
+ <Show
+ when={!isCurrentUser()}
+ fallback={
+ <>
+ <div data-slot="current-user-role">Role: {props.member.role}</div>
+ <input type="hidden" name="role" value={props.member.role} />
+ </>
+ }
+ >
<div data-slot="role-selector">
<label>
<input type="radio" name="role" value="admin" checked={props.member.role === "admin"} />
@@ -213,18 +257,32 @@ function MemberRow(props: { member: any; workspaceID: string; currentUserID: str
</label>
</div>
</Show>
+
+ <div data-slot="limit-selector">
+ <label>
+ <strong>Monthly Limit</strong>
+ <input
+ type="number"
+ name="limit"
+ value={props.member.monthlyLimit ?? ""}
+ placeholder="No limit"
+ min="0"
+ />
+ <p>Set a monthly spending limit for this user</p>
+ </label>
+ </div>
+
<Show when={submission.result && submission.result.error}>
{(err) => <div data-slot="form-error">{err()}</div>}
</Show>
+
<div data-slot="form-actions">
<button type="button" data-color="ghost" onClick={() => setEditing(false)}>
Cancel
</button>
- <Show when={!isCurrentUser()}>
- <button type="submit" data-color="primary" disabled={submission.pending}>
- {submission.pending ? "Saving..." : "Save"}
- </button>
- </Show>
+ <button type="submit" data-color="primary" disabled={submission.pending}>
+ {submission.pending ? "Saving..." : "Save"}
+ </button>
</div>
</form>
</td>
@@ -258,6 +316,7 @@ export function MemberSection() {
<tr>
<th>Email</th>
<th>Role</th>
+ <th>Usage</th>
<th></th>
<th></th>
</tr>
diff --git a/packages/console/app/src/routes/zen/handler.ts b/packages/console/app/src/routes/zen/handler.ts
index 9d08ccdf9..feb0c9c3e 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<typeof ZenModel.ModelSchema>
@@ -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 000000000..5a7bbc3be
--- /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 000000000..959004f33
--- /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 eaa28ddf6..221718250 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 644e3bd83..0b77e4c30 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 121199f72..7fd7f5e1e 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 d4a0da0f8..5e7605e94 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()))),
)
},