mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-19 16:54:22 +01:00
wip: zen
This commit is contained in:
@@ -110,6 +110,9 @@ const STRIPE_WEBHOOK_SECRET = new sst.Linkable("STRIPE_WEBHOOK_SECRET", {
|
|||||||
// CONSOLE
|
// CONSOLE
|
||||||
////////////////
|
////////////////
|
||||||
|
|
||||||
|
const AWS_SES_ACCESS_KEY_ID = new sst.Secret("AWS_SES_ACCESS_KEY_ID")
|
||||||
|
const AWS_SES_SECRET_ACCESS_KEY = new sst.Secret("AWS_SES_SECRET_ACCESS_KEY")
|
||||||
|
|
||||||
let logProcessor
|
let logProcessor
|
||||||
if ($app.stage === "production" || $app.stage === "frank") {
|
if ($app.stage === "production" || $app.stage === "frank") {
|
||||||
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
|
const HONEYCOMB_API_KEY = new sst.Secret("HONEYCOMB_API_KEY")
|
||||||
@@ -122,7 +125,16 @@ if ($app.stage === "production" || $app.stage === "frank") {
|
|||||||
new sst.cloudflare.x.SolidStart("Console", {
|
new sst.cloudflare.x.SolidStart("Console", {
|
||||||
domain,
|
domain,
|
||||||
path: "packages/console/app",
|
path: "packages/console/app",
|
||||||
link: [database, AUTH_API_URL, STRIPE_WEBHOOK_SECRET, STRIPE_SECRET_KEY, ZEN_MODELS, EMAILOCTOPUS_API_KEY],
|
link: [
|
||||||
|
database,
|
||||||
|
AUTH_API_URL,
|
||||||
|
STRIPE_WEBHOOK_SECRET,
|
||||||
|
STRIPE_SECRET_KEY,
|
||||||
|
ZEN_MODELS,
|
||||||
|
EMAILOCTOPUS_API_KEY,
|
||||||
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
|
AWS_SES_SECRET_ACCESS_KEY,
|
||||||
|
],
|
||||||
environment: {
|
environment: {
|
||||||
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
//VITE_DOCS_URL: web.url.apply((url) => url!),
|
||||||
//VITE_API_URL: gateway.url.apply((url) => url!),
|
//VITE_API_URL: gateway.url.apply((url) => url!),
|
||||||
|
|||||||
@@ -11,8 +11,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ibm/plex": "6.4.1",
|
"@ibm/plex": "6.4.1",
|
||||||
|
"@jsx-email/render": "1.1.1",
|
||||||
"@openauthjs/openauth": "0.0.0-20250322224806",
|
"@openauthjs/openauth": "0.0.0-20250322224806",
|
||||||
"@opencode/console-core": "workspace:*",
|
"@opencode/console-core": "workspace:*",
|
||||||
|
"@opencode/console-mail": "workspace:*",
|
||||||
"@solidjs/meta": "^0.29.4",
|
"@solidjs/meta": "^0.29.4",
|
||||||
"@solidjs/router": "^0.15.0",
|
"@solidjs/router": "^0.15.0",
|
||||||
"@solidjs/start": "^1.1.0",
|
"@solidjs/start": "^1.1.0",
|
||||||
|
|||||||
1
packages/console/app/public/email
Symbolic link
1
packages/console/app/public/email
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../mail/emails/templates/static
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getRequestEvent } from "solid-js/web"
|
import { getRequestEvent } from "solid-js/web"
|
||||||
import { and, Database, eq, inArray } from "@opencode/console-core/drizzle/index.js"
|
import { and, Database, eq, inArray, sql } from "@opencode/console-core/drizzle/index.js"
|
||||||
import { WorkspaceTable } from "@opencode/console-core/schema/workspace.sql.js"
|
import { WorkspaceTable } from "@opencode/console-core/schema/workspace.sql.js"
|
||||||
import { UserTable } from "@opencode/console-core/schema/user.sql.js"
|
import { UserTable } from "@opencode/console-core/schema/user.sql.js"
|
||||||
import { redirect } from "@solidjs/router"
|
import { redirect } from "@solidjs/router"
|
||||||
@@ -54,8 +54,8 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
|||||||
}
|
}
|
||||||
const accounts = Object.keys(auth.data.account ?? {})
|
const accounts = Object.keys(auth.data.account ?? {})
|
||||||
if (accounts.length) {
|
if (accounts.length) {
|
||||||
const result = await Database.transaction(async (tx) => {
|
const result = await Database.use((tx) =>
|
||||||
return await tx
|
tx
|
||||||
.select({
|
.select({
|
||||||
user: UserTable,
|
user: UserTable,
|
||||||
})
|
})
|
||||||
@@ -65,9 +65,15 @@ export const getActor = async (workspace?: string): Promise<Actor.Info> => {
|
|||||||
.where(and(inArray(AccountTable.id, accounts), eq(WorkspaceTable.id, workspace)))
|
.where(and(inArray(AccountTable.id, accounts), eq(WorkspaceTable.id, workspace)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.execute()
|
.execute()
|
||||||
.then((x) => x[0])
|
.then((x) => x[0]),
|
||||||
})
|
)
|
||||||
if (result) {
|
if (result) {
|
||||||
|
await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.update(UserTable)
|
||||||
|
.set({ timeSeen: sql`now()` })
|
||||||
|
.where(eq(UserTable.id, result.user.id)),
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
type: "user",
|
type: "user",
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
@@ -7,10 +7,33 @@ import { UsageSection } from "./usage-section"
|
|||||||
import { KeySection } from "./key-section"
|
import { KeySection } from "./key-section"
|
||||||
import { MemberSection } from "./member-section"
|
import { MemberSection } from "./member-section"
|
||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import { useParams } from "@solidjs/router"
|
import { createAsync, query, useParams } from "@solidjs/router"
|
||||||
|
import { Actor } from "@opencode/console-core/actor.js"
|
||||||
|
import { withActor } from "~/context/auth.withActor"
|
||||||
|
import { and, Database, eq } from "@opencode/console-core/drizzle/index.js"
|
||||||
|
import { UserTable } from "@opencode/console-core/schema/user.sql.js"
|
||||||
|
|
||||||
|
const getUser = query(async (workspaceID: string) => {
|
||||||
|
"use server"
|
||||||
|
return withActor(async () => {
|
||||||
|
const actor = Actor.use()
|
||||||
|
const isAdmin = await (async () => {
|
||||||
|
if (actor.type !== "user") return false
|
||||||
|
const role = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({ role: UserTable.role })
|
||||||
|
.from(UserTable)
|
||||||
|
.where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, actor.properties.userID))),
|
||||||
|
).then((x) => x[0]?.role)
|
||||||
|
return role === "admin"
|
||||||
|
})()
|
||||||
|
return { isAdmin }
|
||||||
|
}, workspaceID)
|
||||||
|
}, "user.get")
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
|
const data = createAsync(() => getUser(params.id))
|
||||||
return (
|
return (
|
||||||
<div data-page="workspace-[id]">
|
<div data-page="workspace-[id]">
|
||||||
<section data-component="title-section">
|
<section data-component="title-section">
|
||||||
@@ -27,13 +50,17 @@ export default function () {
|
|||||||
<div data-slot="sections">
|
<div data-slot="sections">
|
||||||
<NewUserSection />
|
<NewUserSection />
|
||||||
<KeySection />
|
<KeySection />
|
||||||
|
<Show when={data()?.isAdmin}>
|
||||||
<Show when={isBeta(params.id)}>
|
<Show when={isBeta(params.id)}>
|
||||||
<MemberSection />
|
<MemberSection />
|
||||||
</Show>
|
</Show>
|
||||||
<BillingSection />
|
<BillingSection />
|
||||||
<MonthlyLimitSection />
|
<MonthlyLimitSection />
|
||||||
|
</Show>
|
||||||
<UsageSection />
|
<UsageSection />
|
||||||
|
<Show when={data()?.isAdmin}>
|
||||||
<PaymentSection />
|
<PaymentSection />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -43,6 +70,6 @@ export function isBeta(workspaceID: string) {
|
|||||||
return [
|
return [
|
||||||
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // production
|
"wrk_01K46JDFR0E75SG2Q8K172KF3Y", // production
|
||||||
"wrk_01K4NFRR5P7FSYWH88307B4DDS", // dev
|
"wrk_01K4NFRR5P7FSYWH88307B4DDS", // dev
|
||||||
"wrk_01K68M8J1KK0PJ39H59B1EGHP6", // frank
|
"wrk_01K6G7HBZ7C046A4XK01CVD0NS", // frank
|
||||||
].includes(workspaceID)
|
].includes(workspaceID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,49 @@ import { json, query, action, useParams, createAsync, useSubmission } from "@sol
|
|||||||
import { createEffect, createSignal, For, Show } from "solid-js"
|
import { createEffect, createSignal, For, Show } from "solid-js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
import { formatDateUTC, formatDateForTable } from "./common"
|
|
||||||
import styles from "./member-section.module.css"
|
import styles from "./member-section.module.css"
|
||||||
import { and, Database, eq, sql } from "@opencode/console-core/drizzle/index.js"
|
import { and, Database, eq, isNull, sql } from "@opencode/console-core/drizzle/index.js"
|
||||||
import { UserTable, UserRole } from "@opencode/console-core/schema/user.sql.js"
|
import { UserTable, UserRole } from "@opencode/console-core/schema/user.sql.js"
|
||||||
import { Identifier } from "@opencode/console-core/identifier.js"
|
import { Identifier } from "@opencode/console-core/identifier.js"
|
||||||
|
import { Actor } from "@opencode/console-core/actor.js"
|
||||||
|
import { AWS } from "@opencode/console-core/aws.js"
|
||||||
|
|
||||||
const removeMember = action(async (form: FormData) => {
|
const assertAdmin = async (workspaceID: string) => {
|
||||||
"use server"
|
const actor = Actor.use()
|
||||||
const id = form.get("id")?.toString()
|
if (actor.type !== "user") throw new Error(`Expected admin user, got ${actor.type}`)
|
||||||
if (!id) return { error: "ID is required" }
|
const user = await Database.use((tx) =>
|
||||||
const workspaceID = form.get("workspaceID")?.toString()
|
|
||||||
if (!workspaceID) return { error: "Workspace ID is required" }
|
|
||||||
return json(
|
|
||||||
await withActor(
|
|
||||||
() =>
|
|
||||||
Database.use((tx) =>
|
|
||||||
tx
|
tx
|
||||||
.update(UserTable)
|
.select()
|
||||||
.set({ timeDeleted: sql`now()` })
|
.from(UserTable)
|
||||||
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID))),
|
.where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, actor.properties.userID))),
|
||||||
),
|
).then((x) => x[0])
|
||||||
workspaceID,
|
if (user?.role !== "admin") throw new Error(`Expected admin user, got ${user?.role}`)
|
||||||
),
|
return actor
|
||||||
{ revalidate: listMembers.key },
|
}
|
||||||
)
|
|
||||||
}, "member.remove")
|
const assertNotSelf = (id: string) => {
|
||||||
|
const actor = Actor.use()
|
||||||
|
if (actor.type === "user" && actor.properties.userID === id) {
|
||||||
|
throw new Error(`Expected not self actor, got self actor`)
|
||||||
|
}
|
||||||
|
return actor
|
||||||
|
}
|
||||||
|
|
||||||
|
const listMembers = query(async (workspaceID: string) => {
|
||||||
|
"use server"
|
||||||
|
return withActor(async () => {
|
||||||
|
const actor = await assertAdmin(workspaceID)
|
||||||
|
return Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select()
|
||||||
|
.from(UserTable)
|
||||||
|
.where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
|
||||||
|
).then((members) => ({
|
||||||
|
members,
|
||||||
|
currentUserID: actor.properties.userID,
|
||||||
|
}))
|
||||||
|
}, workspaceID)
|
||||||
|
}, "member.list")
|
||||||
|
|
||||||
const inviteMember = action(async (form: FormData) => {
|
const inviteMember = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
@@ -38,9 +55,9 @@ const inviteMember = action(async (form: FormData) => {
|
|||||||
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
const role = form.get("role")?.toString() as (typeof UserRole)[number]
|
||||||
if (!role) return { error: "Role is required" }
|
if (!role) return { error: "Role is required" }
|
||||||
return json(
|
return json(
|
||||||
await withActor(
|
await withActor(async () => {
|
||||||
() =>
|
await assertAdmin(workspaceID)
|
||||||
Database.use((tx) =>
|
return Database.use((tx) =>
|
||||||
tx
|
tx
|
||||||
.insert(UserTable)
|
.insert(UserTable)
|
||||||
.values({
|
.values({
|
||||||
@@ -51,21 +68,92 @@ const inviteMember = action(async (form: FormData) => {
|
|||||||
role,
|
role,
|
||||||
})
|
})
|
||||||
.then((data) => ({ error: undefined, data }))
|
.then((data) => ({ error: undefined, data }))
|
||||||
.catch((e) => ({ error: e.message as string })),
|
.then(async (data) => {
|
||||||
),
|
const { render } = await import("@jsx-email/render")
|
||||||
workspaceID,
|
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`,
|
||||||
|
body: render(
|
||||||
|
// @ts-ignore
|
||||||
|
InviteEmail({
|
||||||
|
assetsUrl: `https://opencode.ai/email`,
|
||||||
|
workspace: workspaceID,
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
let error = e.message
|
||||||
|
if (error.match(/Duplicate entry '.*' for key 'user.user_email'/))
|
||||||
|
error = "A user with this email has already been invited."
|
||||||
|
return { error }
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}, workspaceID),
|
||||||
{ revalidate: listMembers.key },
|
{ revalidate: listMembers.key },
|
||||||
)
|
)
|
||||||
}, "member.create")
|
}, "member.create")
|
||||||
|
|
||||||
const listMembers = query(async (workspaceID: string) => {
|
const removeMember = action(async (form: FormData) => {
|
||||||
"use server"
|
"use server"
|
||||||
return withActor(
|
const id = form.get("id")?.toString()
|
||||||
() => Database.use((tx) => tx.select().from(UserTable).where(eq(UserTable.workspaceID, workspaceID))),
|
if (!id) return { error: "ID is required" }
|
||||||
workspaceID,
|
const workspaceID = form.get("workspaceID")?.toString()
|
||||||
|
if (!workspaceID) return { error: "Workspace ID is required" }
|
||||||
|
return json(
|
||||||
|
await withActor(async () => {
|
||||||
|
await assertAdmin(workspaceID)
|
||||||
|
assertNotSelf(id)
|
||||||
|
return Database.transaction(async (tx) => {
|
||||||
|
const email = await tx
|
||||||
|
.select({ email: UserTable.email })
|
||||||
|
.from(UserTable)
|
||||||
|
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID)))
|
||||||
|
.execute()
|
||||||
|
.then((rows) => rows[0].email)
|
||||||
|
if (!email) return { error: "User not found" }
|
||||||
|
await tx
|
||||||
|
.update(UserTable)
|
||||||
|
.set({
|
||||||
|
oldEmail: email,
|
||||||
|
email: null,
|
||||||
|
timeDeleted: sql`now()`,
|
||||||
|
})
|
||||||
|
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID)))
|
||||||
|
})
|
||||||
|
.then(() => ({ error: undefined }))
|
||||||
|
.catch((e) => ({ error: e.message as string }))
|
||||||
|
}, workspaceID),
|
||||||
|
{ revalidate: listMembers.key },
|
||||||
)
|
)
|
||||||
}, "member.list")
|
}, "member.remove")
|
||||||
|
|
||||||
|
const updateMemberRole = action(async (form: FormData) => {
|
||||||
|
"use server"
|
||||||
|
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" }
|
||||||
|
return json(
|
||||||
|
await withActor(async () => {
|
||||||
|
await assertAdmin(workspaceID)
|
||||||
|
if (role === "member") assertNotSelf(id)
|
||||||
|
return Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.update(UserTable)
|
||||||
|
.set({ role })
|
||||||
|
.where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID)))
|
||||||
|
.then((data) => ({ error: undefined, data }))
|
||||||
|
.catch((e) => ({ error: e.message as string })),
|
||||||
|
)
|
||||||
|
}, workspaceID),
|
||||||
|
{ revalidate: listMembers.key },
|
||||||
|
)
|
||||||
|
}, "member.updateRole")
|
||||||
|
|
||||||
export function MemberCreateForm() {
|
export function MemberCreateForm() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -144,9 +232,89 @@ export function MemberCreateForm() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MemberRow(props: { member: any; workspaceID: string; currentUserID: string | null }) {
|
||||||
|
const [editing, setEditing] = createSignal(false)
|
||||||
|
const submission = useSubmission(updateMemberRole)
|
||||||
|
const isCurrentUser = () => props.currentUserID === props.member.id
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!submission.pending && submission.result && !submission.result.error) {
|
||||||
|
setEditing(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show
|
||||||
|
when={editing()}
|
||||||
|
fallback={
|
||||||
|
<tr>
|
||||||
|
<td data-slot="member-email">{props.member.email}</td>
|
||||||
|
<td data-slot="member-role">{props.member.role}</td>
|
||||||
|
<Show when={!props.member.timeSeen} fallback={<td data-slot="member-joined"></td>}>
|
||||||
|
<td data-slot="member-joined">invited</td>
|
||||||
|
</Show>
|
||||||
|
<td data-slot="member-actions">
|
||||||
|
<button data-color="ghost" onClick={() => setEditing(true)}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<Show when={!isCurrentUser()}>
|
||||||
|
<form action={removeMember} method="post">
|
||||||
|
<input type="hidden" name="id" value={props.member.id} />
|
||||||
|
<input type="hidden" name="workspaceID" value={props.workspaceID} />
|
||||||
|
<button data-color="ghost">Delete</button>
|
||||||
|
</form>
|
||||||
|
</Show>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">
|
||||||
|
<form action={updateMemberRole} method="post">
|
||||||
|
<div data-slot="edit-member-email">{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>}>
|
||||||
|
<div data-slot="role-selector">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="role" value="admin" checked={props.member.role === "admin"} />
|
||||||
|
<div>
|
||||||
|
<strong>Admin</strong>
|
||||||
|
<p>Can manage models, members, and billing</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="role" value="member" checked={props.member.role === "member"} />
|
||||||
|
<div>
|
||||||
|
<strong>Member</strong>
|
||||||
|
<p>Can only generate API keys for themselves</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function MemberSection() {
|
export function MemberSection() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const members = createAsync(() => listMembers(params.id))
|
const data = createAsync(() => listMembers(params.id))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section class={styles.root}>
|
<section class={styles.root}>
|
||||||
@@ -157,7 +325,7 @@ export function MemberSection() {
|
|||||||
<MemberCreateForm />
|
<MemberCreateForm />
|
||||||
<div data-slot="members-table">
|
<div data-slot="members-table">
|
||||||
<Show
|
<Show
|
||||||
when={members()?.length}
|
when={data()?.members.length}
|
||||||
fallback={
|
fallback={
|
||||||
<div data-component="empty-state">
|
<div data-component="empty-state">
|
||||||
<p>Invite a member to your workspace</p>
|
<p>Invite a member to your workspace</p>
|
||||||
@@ -169,32 +337,15 @@ export function MemberSection() {
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
<th>Joined</th>
|
<th></th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={members()!}>
|
<For each={data()!.members}>
|
||||||
{(member) => {
|
{(member) => (
|
||||||
return (
|
<MemberRow member={member} workspaceID={params.id} currentUserID={data()!.currentUserID} />
|
||||||
<tr>
|
)}
|
||||||
<td data-slot="member-email">{member.email}</td>
|
|
||||||
<td data-slot="member-role">{member.role}</td>
|
|
||||||
<Show when={member.timeSeen} fallback={<td data-slot="member-joined">invited</td>}>
|
|
||||||
<td data-slot="member-joined" title={formatDateUTC(member.timeSeen!)}>
|
|
||||||
{formatDateForTable(member.timeSeen!)}
|
|
||||||
</td>
|
|
||||||
</Show>
|
|
||||||
<td data-slot="member-actions">
|
|
||||||
<form action={removeMember} method="post">
|
|
||||||
<input type="hidden" name="id" value={member.id} />
|
|
||||||
<input type="hidden" name="workspaceID" value={params.id} />
|
|
||||||
<button data-color="ghost">Delete</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
2
packages/console/core/migrations/0021_flawless_clea.sql
Normal file
2
packages/console/core/migrations/0021_flawless_clea.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `user` MODIFY COLUMN `email` varchar(255);--> statement-breakpoint
|
||||||
|
ALTER TABLE `user` ADD `old_email` varchar(255);
|
||||||
702
packages/console/core/migrations/meta/0021_snapshot.json
Normal file
702
packages/console/core/migrations/meta/0021_snapshot.json
Normal file
@@ -0,0 +1,702 @@
|
|||||||
|
{
|
||||||
|
"version": "5",
|
||||||
|
"dialect": "mysql",
|
||||||
|
"id": "14616ba2-c21e-4787-a289-f2a3eb6de04f",
|
||||||
|
"prevId": "908437f9-54ed-4c83-b555-614926e326f8",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"actor": {
|
||||||
|
"name": "actor",
|
||||||
|
"type": "json",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"old_name": {
|
||||||
|
"name": "old_name",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"name": "key",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"old_email": {
|
||||||
|
"name": "old_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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_email": {
|
||||||
|
"name": "user_email",
|
||||||
|
"columns": [
|
||||||
|
"workspace_id",
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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": false,
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -148,6 +148,13 @@
|
|||||||
"when": 1759169697658,
|
"when": 1759169697658,
|
||||||
"tag": "0020_supreme_jack_power",
|
"tag": "0020_supreme_jack_power",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1759186023755,
|
||||||
|
"tag": "0021_flawless_clea",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"@aws-sdk/client-sts": "3.782.0",
|
"@aws-sdk/client-sts": "3.782.0",
|
||||||
"@opencode/console-resource": "workspace:*",
|
"@opencode/console-resource": "workspace:*",
|
||||||
"@planetscale/database": "1.19.0",
|
"@planetscale/database": "1.19.0",
|
||||||
|
"aws4fetch": "1.0.20",
|
||||||
"drizzle-orm": "0.41.0",
|
"drizzle-orm": "0.41.0",
|
||||||
"postgres": "3.4.7",
|
"postgres": "3.4.7",
|
||||||
"stripe": "18.0.0",
|
"stripe": "18.0.0",
|
||||||
|
|||||||
@@ -54,13 +54,7 @@ export namespace Account {
|
|||||||
.select(getTableColumns(WorkspaceTable))
|
.select(getTableColumns(WorkspaceTable))
|
||||||
.from(WorkspaceTable)
|
.from(WorkspaceTable)
|
||||||
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
.innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id))
|
||||||
.where(
|
.where(and(eq(UserTable.email, actor.properties.email), isNull(WorkspaceTable.timeDeleted)))
|
||||||
and(
|
|
||||||
eq(UserTable.email, actor.properties.email),
|
|
||||||
isNull(UserTable.timeDeleted),
|
|
||||||
isNull(WorkspaceTable.timeDeleted),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.execute(),
|
.execute(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Context } from "./context"
|
import { Context } from "./context"
|
||||||
import { UserRole } from "./schema/user.sql"
|
|
||||||
import { Log } from "./util/log"
|
import { Log } from "./util/log"
|
||||||
|
|
||||||
export namespace Actor {
|
export namespace Actor {
|
||||||
@@ -21,7 +20,6 @@ export namespace Actor {
|
|||||||
properties: {
|
properties: {
|
||||||
userID: string
|
userID: string
|
||||||
workspaceID: string
|
workspaceID: string
|
||||||
role: (typeof UserRole)[number]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
packages/console/core/src/aws.ts
Normal file
63
packages/console/core/src/aws.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
import { Resource } from "@opencode/console-resource"
|
||||||
|
import { AwsClient } from "aws4fetch"
|
||||||
|
import { fn } from "./util/fn"
|
||||||
|
|
||||||
|
export namespace AWS {
|
||||||
|
let client: AwsClient
|
||||||
|
|
||||||
|
const createClient = () => {
|
||||||
|
if (!client) {
|
||||||
|
client = new AwsClient({
|
||||||
|
accessKeyId: Resource.AWS_SES_ACCESS_KEY_ID.value,
|
||||||
|
secretAccessKey: Resource.AWS_SES_SECRET_ACCESS_KEY.value,
|
||||||
|
region: "us-east-1",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sendEmail = fn(
|
||||||
|
z.object({
|
||||||
|
to: z.string(),
|
||||||
|
subject: z.string(),
|
||||||
|
body: z.string(),
|
||||||
|
}),
|
||||||
|
async (input) => {
|
||||||
|
const res = await createClient().fetch("https://email.us-east-1.amazonaws.com/v2/email/outbound-emails", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-Amz-Target": "SES.SendEmail",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
FromEmailAddress: `OpenCode Zen <contact@anoma.ly>`,
|
||||||
|
Destination: {
|
||||||
|
ToAddresses: [input.to],
|
||||||
|
},
|
||||||
|
Content: {
|
||||||
|
Simple: {
|
||||||
|
Subject: {
|
||||||
|
Charset: "UTF-8",
|
||||||
|
Data: input.subject,
|
||||||
|
},
|
||||||
|
Body: {
|
||||||
|
Text: {
|
||||||
|
Charset: "UTF-8",
|
||||||
|
Data: input.body,
|
||||||
|
},
|
||||||
|
Html: {
|
||||||
|
Charset: "UTF-8",
|
||||||
|
Data: input.body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Failed to send email: ${res.statusText}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -206,7 +206,7 @@ export namespace Billing {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
customer_email: user.email,
|
customer_email: user.email!,
|
||||||
customer_creation: "always",
|
customer_creation: "always",
|
||||||
}),
|
}),
|
||||||
currency: "usd",
|
currency: "usd",
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ export const UserTable = mysqlTable(
|
|||||||
{
|
{
|
||||||
...workspaceColumns,
|
...workspaceColumns,
|
||||||
...timestamps,
|
...timestamps,
|
||||||
email: varchar("email", { length: 255 }).notNull(),
|
email: varchar("email", { length: 255 }),
|
||||||
|
oldEmail: varchar("old_email", { length: 255 }),
|
||||||
name: varchar("name", { length: 255 }).notNull(),
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
timeSeen: utc("time_seen"),
|
timeSeen: utc("time_seen"),
|
||||||
color: int("color"),
|
color: int("color"),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export namespace Workspace {
|
|||||||
id: Identifier.create("user"),
|
id: Identifier.create("user"),
|
||||||
email: account.properties.email,
|
email: account.properties.email,
|
||||||
name: "",
|
name: "",
|
||||||
timeSeen: sql`now()`,
|
|
||||||
role: "admin",
|
role: "admin",
|
||||||
})
|
})
|
||||||
await tx.insert(BillingTable).values({
|
await tx.insert(BillingTable).values({
|
||||||
|
|||||||
@@ -111,11 +111,7 @@ export default {
|
|||||||
} else if (response.provider === "google") {
|
} else if (response.provider === "google") {
|
||||||
if (!response.id.email_verified) throw new Error("Google email not verified")
|
if (!response.id.email_verified) throw new Error("Google email not verified")
|
||||||
email = response.id.email as string
|
email = response.id.email as string
|
||||||
}
|
} else throw new Error("Unsupported provider")
|
||||||
//if (response.provider === "email") {
|
|
||||||
// email = response.claims.email
|
|
||||||
//}
|
|
||||||
else throw new Error("Unsupported provider")
|
|
||||||
|
|
||||||
if (!email) throw new Error("No email found")
|
if (!email) throw new Error("No email found")
|
||||||
|
|
||||||
|
|||||||
8
packages/console/function/sst-env.d.ts
vendored
8
packages/console/function/sst-env.d.ts
vendored
@@ -10,6 +10,14 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Linkable"
|
"type": "sst.sst.Linkable"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"AWS_SES_ACCESS_KEY_ID": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Console": {
|
"Console": {
|
||||||
"type": "sst.cloudflare.SolidStart"
|
"type": "sst.cloudflare.SolidStart"
|
||||||
"url": string
|
"url": string
|
||||||
|
|||||||
108
packages/console/mail/emails/components.tsx
Normal file
108
packages/console/mail/emails/components.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from "react"
|
||||||
|
import { Font, Hr as JEHr, Text as JEText, type HrProps, type TextProps } from "@jsx-email/all"
|
||||||
|
import { DIVIDER_COLOR, SURFACE_DIVIDER_COLOR, textColor } from "./styles"
|
||||||
|
|
||||||
|
export function Text(props: TextProps) {
|
||||||
|
return <JEText {...props} style={{ ...textColor, ...props.style }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Hr(props: HrProps) {
|
||||||
|
return <JEHr {...props} style={{ borderTop: `1px solid ${DIVIDER_COLOR}`, ...props.style }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SurfaceHr(props: HrProps) {
|
||||||
|
return (
|
||||||
|
<JEHr
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
borderTop: `1px solid ${SURFACE_DIVIDER_COLOR}`,
|
||||||
|
...props.style,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Title({ children }: TitleProps) {
|
||||||
|
return React.createElement("title", null, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function A({ children, ...props }: AProps) {
|
||||||
|
return React.createElement("a", props, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Span({ children, ...props }: SpanProps) {
|
||||||
|
return React.createElement("span", props, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Wbr({ children, ...props }: WbrProps) {
|
||||||
|
return React.createElement("wbr", props, children)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Fonts({ assetsUrl }: { assetsUrl: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Font
|
||||||
|
fontFamily="IBM Plex Mono"
|
||||||
|
fallbackFontFamily="monospace"
|
||||||
|
webFont={{
|
||||||
|
url: `${assetsUrl}/ibm-plex-mono-latin-400.woff2`,
|
||||||
|
format: "woff2",
|
||||||
|
}}
|
||||||
|
fontWeight="400"
|
||||||
|
fontStyle="normal"
|
||||||
|
/>
|
||||||
|
<Font
|
||||||
|
fontFamily="IBM Plex Mono"
|
||||||
|
fallbackFontFamily="monospace"
|
||||||
|
webFont={{
|
||||||
|
url: `${assetsUrl}/ibm-plex-mono-latin-500.woff2`,
|
||||||
|
format: "woff2",
|
||||||
|
}}
|
||||||
|
fontWeight="500"
|
||||||
|
fontStyle="normal"
|
||||||
|
/>
|
||||||
|
<Font
|
||||||
|
fontFamily="IBM Plex Mono"
|
||||||
|
fallbackFontFamily="monospace"
|
||||||
|
webFont={{
|
||||||
|
url: `${assetsUrl}/ibm-plex-mono-latin-600.woff2`,
|
||||||
|
format: "woff2",
|
||||||
|
}}
|
||||||
|
fontWeight="600"
|
||||||
|
fontStyle="normal"
|
||||||
|
/>
|
||||||
|
<Font
|
||||||
|
fontFamily="IBM Plex Mono"
|
||||||
|
fallbackFontFamily="monospace"
|
||||||
|
webFont={{
|
||||||
|
url: `${assetsUrl}/ibm-plex-mono-latin-700.woff2`,
|
||||||
|
format: "woff2",
|
||||||
|
}}
|
||||||
|
fontWeight="700"
|
||||||
|
fontStyle="normal"
|
||||||
|
/>
|
||||||
|
<Font
|
||||||
|
fontFamily="Rubik"
|
||||||
|
fallbackFontFamily={["Helvetica", "Arial", "sans-serif"]}
|
||||||
|
webFont={{
|
||||||
|
url: `${assetsUrl}/rubik-latin.woff2`,
|
||||||
|
format: "woff2",
|
||||||
|
}}
|
||||||
|
fontWeight="400 500 600 700"
|
||||||
|
fontStyle="normal"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SplitString({ text, split }: { text: string; split: number }) {
|
||||||
|
const segments: JSX.Element[] = []
|
||||||
|
for (let i = 0; i < text.length; i += split) {
|
||||||
|
segments.push(<>{text.slice(i, i + split)}</>)
|
||||||
|
if (i + split < text.length) {
|
||||||
|
segments.push(<Wbr key={`${i}wbr`} />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <>{segments}</>
|
||||||
|
}
|
||||||
110
packages/console/mail/emails/styles.ts
Normal file
110
packages/console/mail/emails/styles.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
export const unit = 16;
|
||||||
|
|
||||||
|
export const GREY_COLOR = [
|
||||||
|
"#1A1A2E", //0
|
||||||
|
"#2F2F41", //1
|
||||||
|
"#444454", //2
|
||||||
|
"#585867", //3
|
||||||
|
"#6D6D7A", //4
|
||||||
|
"#82828D", //5
|
||||||
|
"#9797A0", //6
|
||||||
|
"#ACACB3", //7
|
||||||
|
"#C1C1C6", //8
|
||||||
|
"#D5D5D9", //9
|
||||||
|
"#EAEAEC", //10
|
||||||
|
"#FFFFFF", //11
|
||||||
|
];
|
||||||
|
|
||||||
|
export const BLUE_COLOR = "#395C6B";
|
||||||
|
export const DANGER_COLOR = "#ED322C";
|
||||||
|
export const TEXT_COLOR = GREY_COLOR[0];
|
||||||
|
export const SECONDARY_COLOR = GREY_COLOR[5];
|
||||||
|
export const DIMMED_COLOR = GREY_COLOR[7];
|
||||||
|
export const DIVIDER_COLOR = GREY_COLOR[10];
|
||||||
|
export const BACKGROUND_COLOR = "#F0F0F1";
|
||||||
|
export const SURFACE_COLOR = DIVIDER_COLOR;
|
||||||
|
export const SURFACE_DIVIDER_COLOR = GREY_COLOR[9];
|
||||||
|
|
||||||
|
export const body = {
|
||||||
|
background: BACKGROUND_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const container = {
|
||||||
|
minWidth: "600px",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const medium = {
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const danger = {
|
||||||
|
color: DANGER_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const frame = {
|
||||||
|
padding: `${unit * 1.5}px`,
|
||||||
|
border: `1px solid ${SURFACE_DIVIDER_COLOR}`,
|
||||||
|
background: "#FFF",
|
||||||
|
borderRadius: "6px",
|
||||||
|
boxShadow: `0 1px 2px rgba(0,0,0,0.03),
|
||||||
|
0 2px 4px rgba(0,0,0,0.03),
|
||||||
|
0 2px 6px rgba(0,0,0,0.03)`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const textColor = {
|
||||||
|
color: TEXT_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const code = {
|
||||||
|
fontFamily: "IBM Plex Mono, monospace",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const headingHr = {
|
||||||
|
margin: `${unit}px 0`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buttonPrimary = {
|
||||||
|
...code,
|
||||||
|
padding: "12px 18px",
|
||||||
|
color: "#FFF",
|
||||||
|
borderRadius: "4px",
|
||||||
|
background: BLUE_COLOR,
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compactText = {
|
||||||
|
margin: "0 0 2px",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const breadcrumb = {
|
||||||
|
fontSize: "14px",
|
||||||
|
color: SECONDARY_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const breadcrumbColonSeparator = {
|
||||||
|
padding: " 0 4px",
|
||||||
|
color: DIMMED_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const breadcrumbSeparator = {
|
||||||
|
color: DIVIDER_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const heading = {
|
||||||
|
fontSize: "22px",
|
||||||
|
fontWeight: 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sectionLabel = {
|
||||||
|
...code,
|
||||||
|
...compactText,
|
||||||
|
letterSpacing: "0.5px",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: DIMMED_COLOR,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const footerLink = {
|
||||||
|
fontSize: "14px",
|
||||||
|
};
|
||||||
113
packages/console/mail/emails/templates/InviteEmail.tsx
Normal file
113
packages/console/mail/emails/templates/InviteEmail.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import React from "react"
|
||||||
|
import { Img, Row, Html, Link, Body, Head, Button, Column, Preview, Section, Container } from "@jsx-email/all"
|
||||||
|
import { Hr, Text, Fonts, SplitString, Title, A, Span } from "../components"
|
||||||
|
import {
|
||||||
|
unit,
|
||||||
|
body,
|
||||||
|
code,
|
||||||
|
frame,
|
||||||
|
medium,
|
||||||
|
heading,
|
||||||
|
container,
|
||||||
|
headingHr,
|
||||||
|
footerLink,
|
||||||
|
breadcrumb,
|
||||||
|
compactText,
|
||||||
|
buttonPrimary,
|
||||||
|
breadcrumbColonSeparator,
|
||||||
|
} from "../styles"
|
||||||
|
|
||||||
|
const LOCAL_ASSETS_URL = "/static"
|
||||||
|
const CONSOLE_URL = "https://opencode.ai/"
|
||||||
|
const DOC_URL = "https://opencode.ai/docs/zen"
|
||||||
|
|
||||||
|
interface InviteEmailProps {
|
||||||
|
workspace: string
|
||||||
|
assetsUrl: string
|
||||||
|
}
|
||||||
|
export const InviteEmail = ({ workspace, assetsUrl = LOCAL_ASSETS_URL }: InviteEmailProps) => {
|
||||||
|
const subject = `Join the ${workspace} workspace`
|
||||||
|
const messagePlain = `You've been invited to join the ${workspace} workspace in the OpenCode Zen Console.`
|
||||||
|
const url = `${CONSOLE_URL}workspace/${workspace}`
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head>
|
||||||
|
<Title>{`OpenCode Zen — ${messagePlain}`}</Title>
|
||||||
|
</Head>
|
||||||
|
<Fonts assetsUrl={assetsUrl} />
|
||||||
|
<Preview>{messagePlain}</Preview>
|
||||||
|
<Body style={body} id={Math.random().toString()}>
|
||||||
|
<Container style={container}>
|
||||||
|
<Section style={frame}>
|
||||||
|
<Row>
|
||||||
|
<Column>
|
||||||
|
<A href={CONSOLE_URL}>
|
||||||
|
<Img height="32" alt="OpenCode Zen Logo" src={`${assetsUrl}/zen-logo.png`} />
|
||||||
|
</A>
|
||||||
|
</Column>
|
||||||
|
<Column align="right">
|
||||||
|
<Button style={buttonPrimary} href={url}>
|
||||||
|
<Span style={code}>Join Workspace</Span>
|
||||||
|
</Button>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row style={headingHr}>
|
||||||
|
<Column>
|
||||||
|
<Hr />
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Text style={{ ...compactText, ...breadcrumb }}>
|
||||||
|
<Span>OpenCode Zen</Span>
|
||||||
|
<Span style={{ ...code, ...breadcrumbColonSeparator }}>:</Span>
|
||||||
|
<Span>{workspace}</Span>
|
||||||
|
</Text>
|
||||||
|
<Text style={{ ...heading, ...compactText }}>
|
||||||
|
<Link href={url}>
|
||||||
|
<SplitString text={subject} split={40} />
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
<Section style={{ padding: `${unit}px 0 0 0` }}>
|
||||||
|
<Text style={{ ...compactText }}>
|
||||||
|
You've been invited to join the{" "}
|
||||||
|
<Link style={medium} href={url}>
|
||||||
|
{workspace}
|
||||||
|
</Link>{" "}
|
||||||
|
workspace in the{" "}
|
||||||
|
<Link style={medium} href={CONSOLE_URL}>
|
||||||
|
OpenCode Zen Console
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Row style={headingHr}>
|
||||||
|
<Column>
|
||||||
|
<Hr />
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Column>
|
||||||
|
<Link href={CONSOLE_URL} style={footerLink}>
|
||||||
|
Console
|
||||||
|
</Link>
|
||||||
|
</Column>
|
||||||
|
<Column align="right">
|
||||||
|
<Link style={footerLink} href={DOC_URL}>
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
</Column>
|
||||||
|
</Row>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InviteEmail
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
packages/console/mail/emails/templates/static/rubik-latin.woff2
Normal file
BIN
packages/console/mail/emails/templates/static/rubik-latin.woff2
Normal file
Binary file not shown.
BIN
packages/console/mail/emails/templates/static/zen-logo.png
Normal file
BIN
packages/console/mail/emails/templates/static/zen-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
19
packages/console/mail/package.json
Normal file
19
packages/console/mail/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@opencode/console-mail",
|
||||||
|
"version": "0.13.5",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@jsx-email/all": "2.2.3",
|
||||||
|
"@jsx-email/cli": "1.4.3",
|
||||||
|
"@types/react": "18.0.25",
|
||||||
|
"react": "18.2.0"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./*": "./emails/templates/*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "email preview emails/templates"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/console/mail/sst-env.d.ts
vendored
Normal file
9
packages/console/mail/sst-env.d.ts
vendored
Normal file
@@ -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 {}
|
||||||
8
packages/console/resource/sst-env.d.ts
vendored
8
packages/console/resource/sst-env.d.ts
vendored
@@ -10,6 +10,14 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Linkable"
|
"type": "sst.sst.Linkable"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"AWS_SES_ACCESS_KEY_ID": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Console": {
|
"Console": {
|
||||||
"type": "sst.cloudflare.SolidStart"
|
"type": "sst.cloudflare.SolidStart"
|
||||||
"url": string
|
"url": string
|
||||||
|
|||||||
8
packages/function/sst-env.d.ts
vendored
8
packages/function/sst-env.d.ts
vendored
@@ -10,6 +10,14 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Linkable"
|
"type": "sst.sst.Linkable"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"AWS_SES_ACCESS_KEY_ID": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Console": {
|
"Console": {
|
||||||
"type": "sst.cloudflare.SolidStart"
|
"type": "sst.cloudflare.SolidStart"
|
||||||
"url": string
|
"url": string
|
||||||
|
|||||||
8
sst-env.d.ts
vendored
8
sst-env.d.ts
vendored
@@ -9,6 +9,14 @@ declare module "sst" {
|
|||||||
"type": "sst.sst.Linkable"
|
"type": "sst.sst.Linkable"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"AWS_SES_ACCESS_KEY_ID": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
|
"AWS_SES_SECRET_ACCESS_KEY": {
|
||||||
|
"type": "sst.sst.Secret"
|
||||||
|
"value": string
|
||||||
|
}
|
||||||
"Api": {
|
"Api": {
|
||||||
"type": "sst.cloudflare.Worker"
|
"type": "sst.cloudflare.Worker"
|
||||||
"url": string
|
"url": string
|
||||||
|
|||||||
Reference in New Issue
Block a user