mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-19 16:54:22 +01:00
wip: zen
This commit is contained in:
@@ -73,6 +73,7 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
|||||||
properties: {
|
properties: {
|
||||||
userID: user.id,
|
userID: user.id,
|
||||||
workspaceID: user.workspaceID,
|
workspaceID: user.workspaceID,
|
||||||
|
accountID: user.accountID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
packages/console/app/src/lib/beta.ts
Normal file
7
packages/console/app/src/lib/beta.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { query } from "@solidjs/router"
|
||||||
|
import { Resource } from "@opencode/console-resource"
|
||||||
|
|
||||||
|
export const beta = query(async (workspaceID?: string) => {
|
||||||
|
"use server"
|
||||||
|
return Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true
|
||||||
|
}, "beta")
|
||||||
@@ -1,11 +1,29 @@
|
|||||||
import { Account } from "@opencode-ai/console-core/account.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
|
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
|
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||||
import { redirect } from "@solidjs/router"
|
import { redirect } from "@solidjs/router"
|
||||||
import type { APIEvent } from "@solidjs/start/server"
|
import type { APIEvent } from "@solidjs/start/server"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
|
|
||||||
export async function GET(input: APIEvent) {
|
export async function GET(input: APIEvent) {
|
||||||
try {
|
try {
|
||||||
const workspaces = await withActor(async () => Account.workspaces())
|
const workspaces = await withActor(async () => {
|
||||||
|
const actor = Actor.assert("account")
|
||||||
|
return Database.transaction(async (tx) =>
|
||||||
|
tx
|
||||||
|
.select({ id: WorkspaceTable.id })
|
||||||
|
.from(UserTable)
|
||||||
|
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(UserTable.accountID, actor.properties.accountID),
|
||||||
|
isNull(UserTable.timeDeleted),
|
||||||
|
isNull(WorkspaceTable.timeDeleted),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
return redirect(`/workspace/${workspaces[0].id}`)
|
return redirect(`/workspace/${workspaces[0].id}`)
|
||||||
} catch {
|
} catch {
|
||||||
return redirect("/auth/authorize")
|
return redirect("/auth/authorize")
|
||||||
|
|||||||
184
packages/console/app/src/routes/workspace-picker.css
Normal file
184
packages/console/app/src/routes/workspace-picker.css
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
[data-component="workspace-picker"] {
|
||||||
|
position: relative;
|
||||||
|
/* Override blue accent colors with neutral colors */
|
||||||
|
--color-accent: var(--color-border);
|
||||||
|
--color-accent-hover: var(--color-border);
|
||||||
|
--color-accent-active: var(--color-border);
|
||||||
|
--color-primary: var(--color-border);
|
||||||
|
--color-primary-hover: var(--color-border);
|
||||||
|
--color-primary-active: var(--color-border);
|
||||||
|
--color-primary-alpha-20: transparent;
|
||||||
|
|
||||||
|
[data-slot="trigger"] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 200px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="chevron"] {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="dropdown"] button {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure text inside buttons has no underline */
|
||||||
|
[data-slot="dropdown"] button * {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="dropdown"] {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
margin-top: var(--space-1);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="option"],
|
||||||
|
[data-slot="create-option"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-2-5) var(--space-3);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: var(--border-radius-sm);
|
||||||
|
border-top-right-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-left-radius: var(--border-radius-sm);
|
||||||
|
border-bottom-right-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="option"][data-selected="true"] {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="create-option"] {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="create-form"] {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="create-input-group"] {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot="create-input"] {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-2-5) var(--space-3);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-border);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button[type="submit"],
|
||||||
|
button[type="button"] {
|
||||||
|
padding: var(--space-2-5) var(--space-4);
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-color="primary"] {
|
||||||
|
background-color: var(--color-text-secondary);
|
||||||
|
border-color: var(--color-text-secondary);
|
||||||
|
color: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 30rem) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
packages/console/app/src/routes/workspace-picker.tsx
Normal file
144
packages/console/app/src/routes/workspace-picker.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { query, useParams, action, createAsync, redirect } from "@solidjs/router"
|
||||||
|
import { For, Show, createEffect, onCleanup } from "solid-js"
|
||||||
|
import { createStore } from "solid-js/store"
|
||||||
|
import { withActor } from "~/context/auth.withActor"
|
||||||
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
|
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||||
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
|
import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
||||||
|
import "./workspace-picker.css"
|
||||||
|
|
||||||
|
const getWorkspaces = query(async () => {
|
||||||
|
"use server"
|
||||||
|
return withActor(async () => {
|
||||||
|
return Database.transaction((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: WorkspaceTable.id,
|
||||||
|
name: WorkspaceTable.name,
|
||||||
|
slug: WorkspaceTable.slug,
|
||||||
|
})
|
||||||
|
.from(UserTable)
|
||||||
|
.innerJoin(WorkspaceTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
|
.where(and(eq(UserTable.accountID, Actor.account()), isNull(WorkspaceTable.timeDeleted))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, "workspaces")
|
||||||
|
|
||||||
|
const createWorkspace = action(async (form: FormData) => {
|
||||||
|
"use server"
|
||||||
|
const name = form.get("workspaceName") as string
|
||||||
|
if (name?.trim()) {
|
||||||
|
return withActor(async () => {
|
||||||
|
const workspaceID = await Workspace.create({ name: name.trim() })
|
||||||
|
return redirect(`/workspace/${workspaceID}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, "createWorkspace")
|
||||||
|
|
||||||
|
export function WorkspacePicker() {
|
||||||
|
const params = useParams()
|
||||||
|
const workspaces = createAsync(() => getWorkspaces())
|
||||||
|
const [store, setStore] = createStore({
|
||||||
|
showForm: false,
|
||||||
|
showDropdown: false,
|
||||||
|
})
|
||||||
|
let dropdownRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const currentWorkspace = () => {
|
||||||
|
const ws = workspaces()?.find((w) => w.id === params.id)
|
||||||
|
return ws ? ws.name : "Select workspace"
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWorkspaceNew = () => {
|
||||||
|
setStore({ showForm: true, showDropdown: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectWorkspace = (workspaceID: string) => {
|
||||||
|
if (workspaceID === params.id) {
|
||||||
|
setStore("showDropdown", false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.href = `/workspace/${workspaceID}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset signals when workspace ID changes
|
||||||
|
createEffect(() => {
|
||||||
|
params.id
|
||||||
|
setStore("showForm", false)
|
||||||
|
setStore("showDropdown", false)
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef && !dropdownRef.contains(event.target as Node)) {
|
||||||
|
setStore("showDropdown", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("click", handleClickOutside)
|
||||||
|
onCleanup(() => document.removeEventListener("click", handleClickOutside))
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-component="workspace-picker">
|
||||||
|
<div ref={dropdownRef}>
|
||||||
|
<div data-slot="trigger" onClick={() => setStore("showDropdown", !store.showDropdown)}>
|
||||||
|
<span>{currentWorkspace()}</span>
|
||||||
|
<svg data-slot="chevron" width="12" height="8" viewBox="0 0 12 8" fill="none">
|
||||||
|
<path
|
||||||
|
d="M1 1L6 6L11 1"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={store.showDropdown}>
|
||||||
|
<div data-slot="dropdown">
|
||||||
|
<For each={workspaces()}>
|
||||||
|
{(workspace) => (
|
||||||
|
<button
|
||||||
|
data-slot="option"
|
||||||
|
data-selected={workspace.id === params.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSelectWorkspace(workspace.id)}
|
||||||
|
>
|
||||||
|
{workspace.name || workspace.slug}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<button data-slot="create-option" type="button" onClick={() => handleWorkspaceNew()}>
|
||||||
|
+ Create New Workspace
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={store.showForm}>
|
||||||
|
<form data-slot="create-form" action={createWorkspace} method="post">
|
||||||
|
<div data-slot="create-input-group">
|
||||||
|
<input
|
||||||
|
data-slot="create-input"
|
||||||
|
type="text"
|
||||||
|
name="workspaceName"
|
||||||
|
placeholder="Enter workspace name"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<button type="submit" data-color="primary">
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setStore("showForm", false)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
import { Show } from "solid-js"
|
||||||
|
import { getRequestEvent } from "solid-js/web"
|
||||||
|
import { query, action, redirect, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
|
||||||
import "./workspace.css"
|
import "./workspace.css"
|
||||||
import { useAuthSession } from "~/context/auth.session"
|
import { useAuthSession } from "~/context/auth.session"
|
||||||
import { IconLogo } from "../component/icon"
|
import { IconLogo } from "../component/icon"
|
||||||
|
import { WorkspacePicker } from "./workspace-picker"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { query, action, redirect, createAsync, RouteSectionProps, useParams, A } from "@solidjs/router"
|
|
||||||
import { User } from "@opencode-ai/console-core/user.js"
|
import { User } from "@opencode-ai/console-core/user.js"
|
||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { getRequestEvent } from "solid-js/web"
|
import { beta } from "~/lib/beta"
|
||||||
|
|
||||||
const getUserInfo = query(async (workspaceID: string) => {
|
const getUserInfo = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
@@ -35,6 +38,7 @@ const logout = action(async () => {
|
|||||||
export default function WorkspaceLayout(props: RouteSectionProps) {
|
export default function WorkspaceLayout(props: RouteSectionProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const userInfo = createAsync(() => getUserInfo(params.id))
|
const userInfo = createAsync(() => getUserInfo(params.id))
|
||||||
|
const isBeta = createAsync(() => beta(params.id))
|
||||||
return (
|
return (
|
||||||
<main data-page="workspace">
|
<main data-page="workspace">
|
||||||
<header data-component="workspace-header">
|
<header data-component="workspace-header">
|
||||||
@@ -44,6 +48,9 @@ export default function WorkspaceLayout(props: RouteSectionProps) {
|
|||||||
</A>
|
</A>
|
||||||
</div>
|
</div>
|
||||||
<div data-slot="header-actions">
|
<div data-slot="header-actions">
|
||||||
|
<Show when={isBeta()}>
|
||||||
|
<WorkspacePicker />
|
||||||
|
</Show>
|
||||||
<span data-slot="user">{userInfo()?.email}</span>
|
<span data-slot="user">{userInfo()?.email}</span>
|
||||||
<form action={logout} method="post">
|
<form action={logout} method="post">
|
||||||
<button type="submit" formaction={logout}>
|
<button type="submit" formaction={logout}>
|
||||||
|
|||||||
@@ -11,23 +11,23 @@ import { createAsync, query, useParams } from "@solidjs/router"
|
|||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { User } from "@opencode-ai/console-core/user.js"
|
import { User } from "@opencode-ai/console-core/user.js"
|
||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { beta } from "~/lib/beta"
|
||||||
|
|
||||||
const getUser = query(async (workspaceID: string) => {
|
const getUserInfo = query(async (workspaceID: string) => {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(async () => {
|
return withActor(async () => {
|
||||||
const actor = Actor.assert("user")
|
const actor = Actor.assert("user")
|
||||||
const user = await User.fromID(actor.properties.userID)
|
const user = await User.fromID(actor.properties.userID)
|
||||||
return {
|
return {
|
||||||
isAdmin: user?.role === "admin",
|
isAdmin: user?.role === "admin",
|
||||||
isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
|
|
||||||
}
|
}
|
||||||
}, workspaceID)
|
}, workspaceID)
|
||||||
}, "user.get")
|
}, "user.get")
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const data = createAsync(() => getUser(params.id))
|
const userInfo = createAsync(() => getUserInfo(params.id))
|
||||||
|
const isBeta = createAsync(() => beta(params.id))
|
||||||
return (
|
return (
|
||||||
<div data-page="workspace-[id]">
|
<div data-page="workspace-[id]">
|
||||||
<section data-component="title-section">
|
<section data-component="title-section">
|
||||||
@@ -44,15 +44,15 @@ export default function () {
|
|||||||
<div data-slot="sections">
|
<div data-slot="sections">
|
||||||
<NewUserSection />
|
<NewUserSection />
|
||||||
<KeySection />
|
<KeySection />
|
||||||
<Show when={data()?.isAdmin}>
|
<Show when={userInfo()?.isAdmin}>
|
||||||
<Show when={data()?.isBeta}>
|
<Show when={isBeta()}>
|
||||||
<MemberSection />
|
<MemberSection />
|
||||||
</Show>
|
</Show>
|
||||||
<BillingSection />
|
<BillingSection />
|
||||||
<MonthlyLimitSection />
|
<MonthlyLimitSection />
|
||||||
</Show>
|
</Show>
|
||||||
<UsageSection />
|
<UsageSection />
|
||||||
<Show when={data()?.isAdmin}>
|
<Show when={userInfo()?.isAdmin}>
|
||||||
<PaymentSection />
|
<PaymentSection />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { and, eq, getTableColumns, isNull } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
import { fn } from "./util/fn"
|
import { fn } from "./util/fn"
|
||||||
import { Database } from "./drizzle"
|
import { Database } from "./drizzle"
|
||||||
import { Identifier } from "./identifier"
|
import { Identifier } from "./identifier"
|
||||||
import { AccountTable } from "./schema/account.sql"
|
import { AccountTable } from "./schema/account.sql"
|
||||||
import { Actor } from "./actor"
|
|
||||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
|
||||||
import { UserTable } from "./schema/user.sql"
|
|
||||||
|
|
||||||
export namespace Account {
|
export namespace Account {
|
||||||
export const create = fn(
|
export const create = fn(
|
||||||
@@ -46,16 +43,4 @@ export namespace Account {
|
|||||||
.then((rows) => rows[0])
|
.then((rows) => rows[0])
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const workspaces = async () => {
|
|
||||||
const actor = Actor.assert("account")
|
|
||||||
return Database.transaction(async (tx) =>
|
|
||||||
tx
|
|
||||||
.select(getTableColumns(WorkspaceTable))
|
|
||||||
.from(WorkspaceTable)
|
|
||||||
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
|
||||||
.where(and(eq(UserTable.accountID, actor.properties.accountID), isNull(WorkspaceTable.timeDeleted)))
|
|
||||||
.execute(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export namespace Actor {
|
|||||||
properties: {
|
properties: {
|
||||||
userID: string
|
userID: string
|
||||||
workspaceID: string
|
workspaceID: string
|
||||||
|
accountID: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,4 +72,12 @@ export namespace Actor {
|
|||||||
}
|
}
|
||||||
throw new Error(`actor of type "${actor.type}" is not associated with a workspace`)
|
throw new Error(`actor of type "${actor.type}" is not associated with a workspace`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function account() {
|
||||||
|
const actor = use()
|
||||||
|
if ("accountID" in actor.properties) {
|
||||||
|
return actor.properties.accountID
|
||||||
|
}
|
||||||
|
throw new Error(`actor of type "${actor.type}" is not associated with an account`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { primaryKey, mysqlTable, uniqueIndex, varchar, boolean } from "drizzle-orm/mysql-core"
|
import { primaryKey, mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core"
|
||||||
import { timestamps, ulid } from "../drizzle/types"
|
import { timestamps, ulid } from "../drizzle/types"
|
||||||
|
|
||||||
export const WorkspaceTable = mysqlTable(
|
export const WorkspaceTable = mysqlTable(
|
||||||
|
|||||||
@@ -172,8 +172,6 @@ export namespace User {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return invitations.length
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateRole = fn(
|
export const updateRole = fn(
|
||||||
|
|||||||
@@ -9,13 +9,18 @@ import { WorkspaceTable } from "./schema/workspace.sql"
|
|||||||
import { Key } from "./key"
|
import { Key } from "./key"
|
||||||
|
|
||||||
export namespace Workspace {
|
export namespace Workspace {
|
||||||
export const create = fn(z.void(), async () => {
|
export const create = fn(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
}),
|
||||||
|
async ({ name }) => {
|
||||||
const account = Actor.assert("account")
|
const account = Actor.assert("account")
|
||||||
const workspaceID = Identifier.create("workspace")
|
const workspaceID = Identifier.create("workspace")
|
||||||
const userID = Identifier.create("user")
|
const userID = Identifier.create("user")
|
||||||
await Database.transaction(async (tx) => {
|
await Database.transaction(async (tx) => {
|
||||||
await tx.insert(WorkspaceTable).values({
|
await tx.insert(WorkspaceTable).values({
|
||||||
id: workspaceID,
|
id: workspaceID,
|
||||||
|
name,
|
||||||
})
|
})
|
||||||
await tx.insert(UserTable).values({
|
await tx.insert(UserTable).values({
|
||||||
workspaceID,
|
workspaceID,
|
||||||
@@ -38,5 +43,6 @@ export namespace Workspace {
|
|||||||
() => Key.create({ userID, name: "Default API Key" }),
|
() => Key.create({ userID, name: "Default API Key" }),
|
||||||
)
|
)
|
||||||
return workspaceID
|
return workspaceID
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import { Workspace } from "@opencode-ai/console-core/workspace.js"
|
|||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
import { User } from "@opencode-ai/console-core/user.js"
|
import { User } from "@opencode-ai/console-core/user.js"
|
||||||
|
import { and, Database, eq, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
|
import { WorkspaceTable } from "@opencode-ai/console-core/schema/workspace.sql.js"
|
||||||
|
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||||
|
|
||||||
type Env = {
|
type Env = {
|
||||||
AuthStorage: KVNamespace
|
AuthStorage: KVNamespace
|
||||||
@@ -123,9 +126,22 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
await Actor.provide("account", { accountID, email }, async () => {
|
await Actor.provide("account", { accountID, email }, async () => {
|
||||||
const workspaceCount = await User.joinInvitedWorkspaces()
|
await User.joinInvitedWorkspaces()
|
||||||
if (workspaceCount === 0) {
|
const workspaces = await Database.transaction(async (tx) =>
|
||||||
await Workspace.create()
|
tx
|
||||||
|
.select({ id: WorkspaceTable.id })
|
||||||
|
.from(WorkspaceTable)
|
||||||
|
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(UserTable.accountID, accountID),
|
||||||
|
isNull(UserTable.timeDeleted),
|
||||||
|
isNull(WorkspaceTable.timeDeleted),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (workspaces.length === 0) {
|
||||||
|
await Workspace.create({ name: "Default" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return ctx.subject("account", accountID, { accountID, email })
|
return ctx.subject("account", accountID, { accountID, email })
|
||||||
|
|||||||
Reference in New Issue
Block a user