Bladeren bron

ignore: cloud stuff

Dax Raad 6 maanden geleden
bovenliggende
commit
522bed6b7d

+ 1 - 0
bun.lock

@@ -16,6 +16,7 @@
       "dependencies": {
         "@ibm/plex": "6.4.1",
         "@openauthjs/openauth": "0.0.0-20250322224806",
+        "@opencode/cloud-core": "workspace:*",
         "@solidjs/meta": "^0.29.4",
         "@solidjs/router": "^0.15.0",
         "@solidjs/start": "^1.1.0",

+ 2 - 1
cloud/app/package.json

@@ -14,7 +14,8 @@
     "@solidjs/router": "^0.15.0",
     "@solidjs/start": "^1.1.0",
     "solid-js": "^1.9.5",
-    "vinxi": "^0.5.7"
+    "vinxi": "^0.5.7",
+    "@opencode/cloud-core": "workspace:*"
   },
   "engines": {
     "node": ">=22"

+ 4 - 2
cloud/app/src/app.tsx

@@ -1,7 +1,7 @@
 import { MetaProvider, Title } from "@solidjs/meta";
 import { Router } from "@solidjs/router";
 import { FileRoutes } from "@solidjs/start/router";
-import { Suspense } from "solid-js";
+import { ErrorBoundary, Suspense } from "solid-js";
 import "@ibm/plex/css/ibm-plex.css";
 import "./app.css";
 
@@ -11,7 +11,9 @@ export default function App() {
       root={props => (
         <MetaProvider>
           <Title>SolidStart - Basic</Title>
-          <Suspense>{props.children}</Suspense>
+          <ErrorBoundary fallback={<div>Something went wrong</div>}>
+            <Suspense>{props.children}</Suspense>
+          </ErrorBoundary>
         </MetaProvider>
       )}
     >

+ 83 - 2
cloud/app/src/context/auth.tsx

@@ -1,9 +1,90 @@
+
+
 import { useSession } from "vinxi/http"
 import { createClient } from "@openauthjs/openauth/client"
+import { getRequestEvent } from "solid-js/web"
+import { and, Database, eq, inArray } from "@opencode/cloud-core/drizzle/index.js"
+import { WorkspaceTable } from "@opencode/cloud-core/schema/workspace.sql.js"
+import { UserTable } from "@opencode/cloud-core/schema/user.sql.js"
+import { query, redirect } from "@solidjs/router"
+import { AccountTable } from "@opencode/cloud-core/schema/account.sql.js"
+import { Actor } from "@opencode/cloud-core/actor.js"
+
+export async function withActor<T>(fn: () => T) {
+  const actor = await getActor()
+  return Actor.provide(actor.type, actor.properties, fn)
+}
+
+export const getActor = query(async (): Promise<Actor.Info> => {
+  "use server"
+  const evt = getRequestEvent()
+  const url = new URL(evt!.request.headers.get("referer") ?? evt!.request.url)
+  const auth = await useAuthSession()
+  const [workspaceHint] = url.pathname.split("/").filter((x) => x.length > 0)
+  if (!workspaceHint) {
+    if (auth.data.current) {
+      const current = auth.data.account[auth.data.current]
+      return {
+        type: "account",
+        properties: {
+          email: current.email,
+          accountID: current.id,
+        },
+      }
+    }
+    if (Object.keys(auth.data.account).length > 0) {
+      const current = Object.values(auth.data.account)[0]
+      await auth.update(val => ({
+        ...val,
+        current: current.id,
+      }))
+      return {
+        type: "account",
+        properties: {
+          email: current.email,
+          accountID: current.id,
+        },
+      }
+    }
+    return {
+      type: "public",
+      properties: {},
+    }
+  }
+  const accounts = Object.keys(auth.data.account)
+  const result = await Database.transaction(async (tx) => {
+    return await tx.select({
+      user: UserTable
+    })
+      .from(AccountTable)
+      .innerJoin(UserTable, and(eq(UserTable.email, AccountTable.email)))
+      .innerJoin(WorkspaceTable, eq(WorkspaceTable.id, UserTable.workspaceID))
+      .where(
+        and(
+          inArray(AccountTable.id, accounts),
+          eq(WorkspaceTable.id, workspaceHint),
+        )
+      )
+      .limit(1)
+      .execute()
+      .then((x) => x[0])
+  })
+  if (result) {
+    return {
+      type: "user",
+      properties: {
+        userID: result.user.id,
+        workspaceID: result.user.workspaceID,
+      },
+    }
+  }
+  throw redirect("/auth/authorize")
+}, "actor")
+
 
 export const AuthClient = createClient({
   clientID: "app",
-  issuer: "https://auth.dev.opencode.ai",
+  issuer: import.meta.env.VITE_AUTH_URL,
 })
 
 export interface AuthSession {
@@ -15,7 +96,6 @@ export interface AuthSession {
 }
 
 export function useAuthSession() {
-  "use server"
 
   return useSession<AuthSession>({
     password: "0".repeat(32),
@@ -26,3 +106,4 @@ export function useAuthSession() {
 
 export function AuthProvider() {
 }
+

+ 15 - 0
cloud/app/src/routes/[workspaceID].tsx

@@ -0,0 +1,15 @@
+import { createAsync, query } from "@solidjs/router"
+import { getActor, withActor } from "~/context/auth"
+
+const getPosts = query(async () => {
+  "use server"
+  return withActor(() => {
+    return "ok"
+  })
+}, "posts")
+
+
+export default function () {
+  const actor = createAsync(async () => getActor())
+  return <div>{JSON.stringify(actor())}</div>
+}

+ 0 - 5
cloud/app/src/routes/auth/callback.ts

@@ -5,11 +5,6 @@ export async function GET(input: APIEvent) {
   const url = new URL(input.request.url)
   const code = url.searchParams.get("code")
   if (!code) throw new Error("No code found")
-  const redirectURI = `${url.origin}${url.pathname}`
-  console.log({
-    redirectURI,
-    code,
-  })
   const result = await AuthClient.exchange(code, `${url.origin}${url.pathname}`)
   if (result.err) {
     throw new Error(result.err.message)

+ 18 - 0
cloud/app/src/routes/index.tsx

@@ -6,6 +6,9 @@ import IMG_SPLASH from "../asset/screenshot-splash.webp"
 import IMG_VSCODE from "../asset/screenshot-vscode.webp"
 import IMG_GITHUB from "../asset/screenshot-github.webp"
 import { IconCopy, IconCheck } from "../component/icon"
+import { createAsync, query, redirect, RouteDefinition } from "@solidjs/router"
+import { getActor, withActor } from "~/context/auth"
+import { Account } from "@opencode/cloud-core/account.js"
 
 function CopyStatus() {
   return (
@@ -16,7 +19,22 @@ function CopyStatus() {
   )
 }
 
+const isLoggedIn = query(async () => {
+  "use server"
+  const actor = await getActor()
+  if (actor.type === "account") {
+    const workspaces = await withActor(() => Account.workspaces())
+    throw redirect("/" + workspaces[0].id)
+  }
+  return
+}, "isLoggedIn")
+
+
+
 export default function Home() {
+  createAsync(() => isLoggedIn(), {
+    deferStream: true,
+  })
   onMount(() => {
     const commands = document.querySelectorAll("[data-copy]")
     for (const button of commands) {

+ 9 - 0
cloud/app/sst-env.d.ts

@@ -0,0 +1,9 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+/// <reference path="../../sst-env.d.ts" />
+
+import "sst"
+export {}

+ 0 - 1
cloud/core/src/actor.ts

@@ -20,7 +20,6 @@ export namespace Actor {
     properties: {
       userID: string
       workspaceID: string
-      email: string
     }
   }
 

+ 3 - 2
cloud/core/src/drizzle/index.ts

@@ -3,7 +3,7 @@ import { Resource } from "sst"
 export * from "drizzle-orm"
 import postgres from "postgres"
 
-function createClient() {
+const createClient = memo(() => {
   const client = postgres({
     idle_timeout: 30000,
     connect_timeout: 30000,
@@ -19,12 +19,13 @@ function createClient() {
   })
 
   return drizzle(client, {})
-}
+})
 
 import { PgTransaction, type PgTransactionConfig } from "drizzle-orm/pg-core"
 import type { ExtractTablesWithRelations } from "drizzle-orm"
 import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js"
 import { Context } from "../context"
+import { memo } from "../util/memo"
 
 export namespace Database {
   export type Transaction = PgTransaction<

+ 11 - 0
cloud/core/src/util/memo.ts

@@ -0,0 +1,11 @@
+export function memo<T>(fn: () => T) {
+  let value: T | undefined
+  let loaded = false
+
+  return (): T => {
+    if (loaded) return value as T
+    loaded = true
+    value = fn()
+    return value as T
+  }
+}

+ 8 - 1
cloud/function/src/auth.ts

@@ -2,11 +2,12 @@ import { Resource } from "sst"
 import { z } from "zod"
 import { issuer } from "@openauthjs/openauth"
 import { createSubjects } from "@openauthjs/openauth/subject"
-import { CodeProvider } from "@openauthjs/openauth/provider/code"
 import { GithubProvider } from "@openauthjs/openauth/provider/github"
 import { GoogleOidcProvider } from "@openauthjs/openauth/provider/google"
 import { CloudflareStorage } from "@openauthjs/openauth/storage/cloudflare"
 import { Account } from "@opencode/cloud-core/account.js"
+import { Workspace } from "@opencode/cloud-core/workspace.js"
+import { Actor } from "@opencode/cloud-core/actor.js"
 
 type Env = {
   AuthStorage: KVNamespace
@@ -117,6 +118,12 @@ export default {
             email: email!,
           })
         }
+        await Actor.provide("account", { accountID, email }, async () => {
+          const workspaces = await Account.workspaces()
+          if (workspaces.length === 0) {
+            await Workspace.create()
+          }
+        })
         return ctx.subject("account", accountID, { accountID, email })
       },
     }).fetch(request, env, ctx)

+ 0 - 4
cloud/function/sst-env.d.ts

@@ -14,10 +14,6 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "value": string
     }
-    "Console": {
-      "type": "sst.cloudflare.StaticSite"
-      "url": string
-    }
     "DATABASE_PASSWORD": {
       "type": "sst.sst.Secret"
       "value": string

+ 9 - 0
github/sst-env.d.ts

@@ -0,0 +1,9 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+
+/// <reference path="../sst-env.d.ts" />
+
+import "sst"
+export {}

+ 3 - 3
infra/app.ts

@@ -25,9 +25,9 @@ export const api = new sst.cloudflare.Worker("Api", {
       ])
       args.migrations = {
         // Note: when releasing the next tag, make sure all stages use tag v2
-        oldTag: $app.stage === "production" ? "" : "v1",
-        newTag: $app.stage === "production" ? "" : "v1",
-        //newSqliteClasses: ["SyncServer"],
+        // oldTag: $app.stage === "production" ? "" : "v1",
+        // newTag: $app.stage === "production" ? "" : "v1",
+        newSqliteClasses: ["SyncServer"],
       }
     },
   },

+ 14 - 1
infra/cloud.ts

@@ -10,7 +10,7 @@ const DATABASE_USERNAME = new sst.Secret("DATABASE_USERNAME")
 const DATABASE_PASSWORD = new sst.Secret("DATABASE_PASSWORD")
 export const database = new sst.Linkable("Database", {
   properties: {
-    host: "aws-us-east-2-1.pg.psdb.cloud",
+    host: `aws-us-east-2-${$app.stage === "thdxr" ? "2" : "1"}.pg.psdb.cloud`,
     database: "postgres",
     username: DATABASE_USERNAME.value,
     password: DATABASE_PASSWORD.value,
@@ -106,6 +106,7 @@ export const gateway = new sst.cloudflare.Worker("GatewayApi", {
 // CONSOLE
 ////////////////
 
+/*
 export const console = new sst.cloudflare.x.StaticSite("Console", {
   domain: `console.${domain}`,
   path: "cloud/web",
@@ -119,3 +120,15 @@ export const console = new sst.cloudflare.x.StaticSite("Console", {
     VITE_AUTH_URL: auth.url.apply((url) => url!),
   },
 })
+*/
+
+new sst.x.DevCommand("Solid", {
+  link: [database],
+  dev: {
+    directory: "cloud/app",
+    command: "bun dev",
+  },
+  environment: {
+    VITE_AUTH_URL: auth.url.apply((url) => url!),
+  },
+})

+ 0 - 4
packages/function/sst-env.d.ts

@@ -14,10 +14,6 @@ declare module "sst" {
       "type": "sst.sst.Linkable"
       "value": string
     }
-    "Console": {
-      "type": "sst.cloudflare.StaticSite"
-      "url": string
-    }
     "DATABASE_PASSWORD": {
       "type": "sst.sst.Secret"
       "value": string

+ 0 - 4
sst-env.d.ts

@@ -27,10 +27,6 @@ declare module "sst" {
     "Bucket": {
       "type": "sst.cloudflare.Bucket"
     }
-    "Console": {
-      "type": "sst.cloudflare.StaticSite"
-      "url": string
-    }
     "DATABASE_PASSWORD": {
       "type": "sst.sst.Secret"
       "value": string