Frank 4 месяцев назад
Родитель
Сommit
4159db4549

+ 1 - 1
packages/console/app/src/entry-server.tsx

@@ -9,7 +9,7 @@ export default createHandler(
           <head>
             <meta charset="utf-8" />
             <meta name="viewport" content="width=device-width, initial-scale=1" />
-            <link rel="icon" href="/favicon-zen.svg" />
+            <link rel="icon" href="/favicon.svg" />
             <meta property="og:image" content="/social-share.png" />
             <meta property="twitter:image" content="/social-share.png" />
             {assets}

+ 6 - 10
packages/console/app/src/routes/workspace/[id].tsx

@@ -11,13 +11,17 @@ import { createAsync, query, useParams } from "@solidjs/router"
 import { Actor } from "@opencode/console-core/actor.js"
 import { withActor } from "~/context/auth.withActor"
 import { User } from "@opencode/console-core/user.js"
+import { Resource } from "@opencode/console-resource"
 
 const getUser = query(async (workspaceID: string) => {
   "use server"
   return withActor(async () => {
     const actor = Actor.assert("user")
     const user = await User.fromID(actor.properties.userID)
-    return { isAdmin: user?.role === "admin" }
+    return {
+      isAdmin: user?.role === "admin",
+      isBeta: Resource.App.stage === "production" ? workspaceID === "wrk_01K46JDFR0E75SG2Q8K172KF3Y" : true,
+    }
   }, workspaceID)
 }, "user.get")
 
@@ -41,7 +45,7 @@ export default function () {
         <NewUserSection />
         <KeySection />
         <Show when={data()?.isAdmin}>
-          <Show when={isBeta(params.id)}>
+          <Show when={data()?.isBeta}>
             <MemberSection />
           </Show>
           <BillingSection />
@@ -55,11 +59,3 @@ export default function () {
     </div>
   )
 }
-
-export function isBeta(workspaceID: string) {
-  return [
-    "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // production
-    "wrk_01K4NFRR5P7FSYWH88307B4DDS", // dev
-    "wrk_01K6G7HBZ7C046A4XK01CVD0NS", // frank
-  ].includes(workspaceID)
-}

+ 81 - 50
packages/console/core/src/user.ts

@@ -1,15 +1,15 @@
 import { z } from "zod"
-import { and, eq, getTableColumns, isNull, sql } from "drizzle-orm"
+import { and, eq, getTableColumns, inArray, isNull, or, sql } from "drizzle-orm"
 import { fn } from "./util/fn"
 import { Database } from "./drizzle"
 import { UserRole, UserTable } from "./schema/user.sql"
 import { Actor } from "./actor"
 import { Identifier } from "./identifier"
 import { render } from "@jsx-email/render"
-import { InviteEmail } from "@opencode/console-mail/InviteEmail.jsx"
 import { AWS } from "./aws"
 import { Account } from "./account"
 import { AccountTable } from "./schema/account.sql"
+import { Key } from "./key"
 
 export namespace User {
   const assertAdmin = async () => {
@@ -74,71 +74,66 @@ export namespace User {
       const workspaceID = Actor.workspace()
       await Database.transaction(async (tx) => {
         const account = await Account.fromEmail(email)
-        const members = await tx.select().from(UserTable).where(eq(UserTable.workspaceID, Actor.workspace()))
-
-        await (async () => {
-          if (account) {
-            // case: account previously invited and removed
-            if (members.some((m) => m.oldAccountID === account.id)) {
-              await tx
-                .update(UserTable)
-                .set({
-                  timeDeleted: null,
-                  oldAccountID: null,
-                  accountID: account.id,
-                })
-                .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.accountID, account.id)))
-              return
-            }
-            // case: account previously not invited
-            await tx
-              .insert(UserTable)
-              .values({
-                id: Identifier.create("user"),
-                name: "",
-                accountID: account.id,
-                workspaceID,
-                role,
-              })
-              .catch((e: any) => {
-                if (e.message.match(/Duplicate entry '.*' for key 'user.user_account_id'/))
-                  throw new Error("A user with this email has already been invited.")
-                throw e
-              })
-            return
-          }
-          // case: email previously invited and removed
-          if (members.some((m) => m.oldEmail === email)) {
-            await tx
-              .update(UserTable)
-              .set({
-                timeDeleted: null,
-                oldEmail: null,
-                email,
-              })
-              .where(and(eq(UserTable.workspaceID, Actor.workspace()), eq(UserTable.email, email)))
-            return
-          }
-          // case: email previously not invited
+        const existing = await tx
+          .select()
+          .from(UserTable)
+          .where(
+            and(
+              eq(UserTable.workspaceID, Actor.workspace()),
+              account ? eq(UserTable.oldAccountID, account.id) : eq(UserTable.oldEmail, email),
+            ),
+          )
+          .then((rows) => rows[0])
+
+        // case: previously invited and removed
+        if (existing) {
+          await tx
+            .update(UserTable)
+            .set({
+              role,
+              timeDeleted: null,
+              ...(account
+                ? {
+                    oldAccountID: null,
+                    accountID: account.id,
+                  }
+                : {
+                    oldEmail: null,
+                    email,
+                  }),
+            })
+            .where(and(eq(UserTable.workspaceID, existing.workspaceID), eq(UserTable.id, existing.id)))
+        }
+        // case: account previously not invited
+        else {
           await tx
             .insert(UserTable)
             .values({
               id: Identifier.create("user"),
               name: "",
-              email,
+              ...(account
+                ? {
+                    accountID: account.id,
+                  }
+                : {
+                    email,
+                  }),
               workspaceID,
               role,
             })
             .catch((e: any) => {
+              if (e.message.match(/Duplicate entry '.*' for key 'user.user_account_id'/))
+                throw new Error("A user with this email has already been invited.")
               if (e.message.match(/Duplicate entry '.*' for key 'user.user_email'/))
                 throw new Error("A user with this email has already been invited.")
               throw e
             })
-        })()
+        }
       })
 
       // send email, ignore errors
       try {
+        const { InviteEmail } = await import("@opencode/console-mail/InviteEmail.jsx")
         await AWS.sendEmail({
           to: email,
           subject: `You've been invited to join the ${workspaceID} workspace on OpenCode Zen`,
@@ -156,6 +151,42 @@ export namespace User {
     },
   )
 
+  export const joinInvitedWorkspaces = fn(z.void(), async () => {
+    const account = Actor.assert("account")
+    const invitations = await Database.use(async (tx) => {
+      const invitations = await tx
+        .select({
+          id: UserTable.id,
+          workspaceID: UserTable.workspaceID,
+        })
+        .from(UserTable)
+        .where(eq(UserTable.email, account.properties.email))
+
+      await tx
+        .update(UserTable)
+        .set({
+          accountID: account.properties.accountID,
+          email: null,
+        })
+        .where(eq(UserTable.email, account.properties.email))
+      return invitations
+    })
+
+    await Promise.all(
+      invitations.map((invite) =>
+        Actor.provide(
+          "system",
+          {
+            workspaceID: invite.workspaceID,
+          },
+          () => Key.create({ name: "Default API Key" }),
+        ),
+      ),
+    )
+
+    return invitations.length
+  })
+
   export const updateRole = fn(
     z.object({
       id: z.string(),

+ 3 - 3
packages/console/function/src/auth.ts

@@ -10,7 +10,7 @@ import { Account } from "@opencode/console-core/account.js"
 import { Workspace } from "@opencode/console-core/workspace.js"
 import { Actor } from "@opencode/console-core/actor.js"
 import { Resource } from "@opencode/console-resource"
-import { Database } from "@opencode/console-core/drizzle/index.js"
+import { User } from "@opencode/console-core/user.js"
 
 type Env = {
   AuthStorage: KVNamespace
@@ -123,8 +123,8 @@ export default {
           })
         }
         await Actor.provide("account", { accountID, email }, async () => {
-          const workspaces = await Account.workspaces()
-          if (workspaces.length === 0) {
+          const workspaceCount = await User.joinInvitedWorkspaces()
+          if (workspaceCount === 0) {
             await Workspace.create()
           }
         })

+ 2 - 0
packages/console/function/tsconfig.json

@@ -4,6 +4,8 @@
   "compilerOptions": {
     "module": "ESNext",
     "moduleResolution": "bundler",
+    "jsx": "preserve",
+    "jsxImportSource": "react",
     "types": ["@cloudflare/workers-types", "node"]
   }
 }