mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-23 10:44:21 +01:00
wip: zen
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user