diff options
| author | Frank <[email protected]> | 2025-10-01 19:34:37 -0400 |
|---|---|---|
| committer | Frank <[email protected]> | 2025-10-01 19:34:37 -0400 |
| commit | 70da3a9399d3385b53f7831beb08f716b972860d (patch) | |
| tree | 6934fafb8558d426df88ca83d043eb3ac5910dcd /packages | |
| parent | 1024537b471c341581826b39c360d34d6c32404f (diff) | |
| download | opencode-70da3a9399d3385b53f7831beb08f716b972860d.tar.gz opencode-70da3a9399d3385b53f7831beb08f716b972860d.zip | |
wip: zen
Diffstat (limited to 'packages')
30 files changed, 1427 insertions, 94 deletions
diff --git a/packages/console/app/package.json b/packages/console/app/package.json index 5c43c3bbe..18a2f5e23 100644 --- a/packages/console/app/package.json +++ b/packages/console/app/package.json @@ -11,8 +11,10 @@ }, "dependencies": { "@ibm/plex": "6.4.1", + "@jsx-email/render": "1.1.1", "@openauthjs/openauth": "0.0.0-20250322224806", "@opencode/console-core": "workspace:*", + "@opencode/console-mail": "workspace:*", "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "^1.1.0", diff --git a/packages/console/app/public/email b/packages/console/app/public/email new file mode 120000 index 000000000..0df016d01 --- /dev/null +++ b/packages/console/app/public/email @@ -0,0 +1 @@ +../../mail/emails/templates/static
\ No newline at end of file diff --git a/packages/console/app/src/context/auth.ts b/packages/console/app/src/context/auth.ts index 585cac3c6..7097787fb 100644 --- a/packages/console/app/src/context/auth.ts +++ b/packages/console/app/src/context/auth.ts @@ -1,5 +1,5 @@ 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 { UserTable } from "@opencode/console-core/schema/user.sql.js" 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 ?? {}) if (accounts.length) { - const result = await Database.transaction(async (tx) => { - return await tx + const result = await Database.use((tx) => + tx .select({ 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))) .limit(1) .execute() - .then((x) => x[0]) - }) + .then((x) => x[0]), + ) if (result) { + await Database.use((tx) => + tx + .update(UserTable) + .set({ timeSeen: sql`now()` }) + .where(eq(UserTable.id, result.user.id)), + ) return { type: "user", properties: { diff --git a/packages/console/app/src/routes/workspace/[id].tsx b/packages/console/app/src/routes/workspace/[id].tsx index 5257a8e97..df05b14b1 100644 --- a/packages/console/app/src/routes/workspace/[id].tsx +++ b/packages/console/app/src/routes/workspace/[id].tsx @@ -7,10 +7,33 @@ import { UsageSection } from "./usage-section" import { KeySection } from "./key-section" import { MemberSection } from "./member-section" 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 () { const params = useParams() + const data = createAsync(() => getUser(params.id)) return ( <div data-page="workspace-[id]"> <section data-component="title-section"> @@ -27,13 +50,17 @@ export default function () { <div data-slot="sections"> <NewUserSection /> <KeySection /> - <Show when={isBeta(params.id)}> - <MemberSection /> + <Show when={data()?.isAdmin}> + <Show when={isBeta(params.id)}> + <MemberSection /> + </Show> + <BillingSection /> + <MonthlyLimitSection /> </Show> - <BillingSection /> - <MonthlyLimitSection /> <UsageSection /> - <PaymentSection /> + <Show when={data()?.isAdmin}> + <PaymentSection /> + </Show> </div> </div> ) @@ -43,6 +70,6 @@ export function isBeta(workspaceID: string) { return [ "wrk_01K46JDFR0E75SG2Q8K172KF3Y", // production "wrk_01K4NFRR5P7FSYWH88307B4DDS", // dev - "wrk_01K68M8J1KK0PJ39H59B1EGHP6", // frank + "wrk_01K6G7HBZ7C046A4XK01CVD0NS", // frank ].includes(workspaceID) } diff --git a/packages/console/app/src/routes/workspace/member-section.tsx b/packages/console/app/src/routes/workspace/member-section.tsx index d0afd7eb9..0e3a101fd 100644 --- a/packages/console/app/src/routes/workspace/member-section.tsx +++ b/packages/console/app/src/routes/workspace/member-section.tsx @@ -2,11 +2,99 @@ import { json, query, action, useParams, createAsync, useSubmission } from "@sol import { createEffect, createSignal, For, Show } from "solid-js" import { withActor } from "~/context/auth.withActor" import { createStore } from "solid-js/store" -import { formatDateUTC, formatDateForTable } from "./common" 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 { Identifier } from "@opencode/console-core/identifier.js" +import { Actor } from "@opencode/console-core/actor.js" +import { AWS } from "@opencode/console-core/aws.js" + +const assertAdmin = async (workspaceID: string) => { + const actor = Actor.use() + if (actor.type !== "user") throw new Error(`Expected admin user, got ${actor.type}`) + const user = await Database.use((tx) => + tx + .select() + .from(UserTable) + .where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.id, actor.properties.userID))), + ).then((x) => x[0]) + if (user?.role !== "admin") throw new Error(`Expected admin user, got ${user?.role}`) + return actor +} + +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) => { + "use server" + const email = form.get("email")?.toString().trim() + if (!email) return { error: "Email 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) + return Database.use((tx) => + tx + .insert(UserTable) + .values({ + id: Identifier.create("user"), + name: "", + email, + workspaceID, + role, + }) + .then((data) => ({ error: undefined, data })) + .then(async (data) => { + const { render } = await import("@jsx-email/render") + 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 }, + ) +}, "member.create") const removeMember = action(async (form: FormData) => { "use server" @@ -15,57 +103,57 @@ const removeMember = action(async (form: FormData) => { const workspaceID = form.get("workspaceID")?.toString() if (!workspaceID) return { error: "Workspace ID is required" } return json( - await withActor( - () => - Database.use((tx) => - tx - .update(UserTable) - .set({ timeDeleted: sql`now()` }) - .where(and(eq(UserTable.id, id), eq(UserTable.workspaceID, workspaceID))), - ), - workspaceID, - ), + 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.remove") -const inviteMember = action(async (form: FormData) => { +const updateMemberRole = action(async (form: FormData) => { "use server" - const email = form.get("email")?.toString().trim() - if (!email) return { error: "Email is required" } + 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( - () => - Database.use((tx) => - tx - .insert(UserTable) - .values({ - id: Identifier.create("user"), - name: "", - email, - workspaceID, - role, - }) - .then((data) => ({ error: undefined, data })) - .catch((e) => ({ error: e.message as string })), - ), - workspaceID, - ), + 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.create") - -const listMembers = query(async (workspaceID: string) => { - "use server" - return withActor( - () => Database.use((tx) => tx.select().from(UserTable).where(eq(UserTable.workspaceID, workspaceID))), - workspaceID, - ) -}, "member.list") +}, "member.updateRole") export function MemberCreateForm() { 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() { const params = useParams() - const members = createAsync(() => listMembers(params.id)) + const data = createAsync(() => listMembers(params.id)) return ( <section class={styles.root}> @@ -157,7 +325,7 @@ export function MemberSection() { <MemberCreateForm /> <div data-slot="members-table"> <Show - when={members()?.length} + when={data()?.members.length} fallback={ <div data-component="empty-state"> <p>Invite a member to your workspace</p> @@ -169,32 +337,15 @@ export function MemberSection() { <tr> <th>Email</th> <th>Role</th> - <th>Joined</th> + <th></th> <th></th> </tr> </thead> <tbody> - <For each={members()!}> - {(member) => { - return ( - <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 each={data()!.members}> + {(member) => ( + <MemberRow member={member} workspaceID={params.id} currentUserID={data()!.currentUserID} /> + )} </For> </tbody> </table> diff --git a/packages/console/core/migrations/0021_flawless_clea.sql b/packages/console/core/migrations/0021_flawless_clea.sql new file mode 100644 index 000000000..8c4489c2f --- /dev/null +++ b/packages/console/core/migrations/0021_flawless_clea.sql @@ -0,0 +1,2 @@ +ALTER TABLE `user` MODIFY COLUMN `email` varchar(255);--> statement-breakpoint +ALTER TABLE `user` ADD `old_email` varchar(255);
\ No newline at end of file diff --git a/packages/console/core/migrations/meta/0021_snapshot.json b/packages/console/core/migrations/meta/0021_snapshot.json new file mode 100644 index 000000000..b285e34fa --- /dev/null +++ b/packages/console/core/migrations/meta/0021_snapshot.json @@ -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": {} + } +}
\ No newline at end of file diff --git a/packages/console/core/migrations/meta/_journal.json b/packages/console/core/migrations/meta/_journal.json index 5b45082fd..6879a3b3f 100644 --- a/packages/console/core/migrations/meta/_journal.json +++ b/packages/console/core/migrations/meta/_journal.json @@ -148,6 +148,13 @@ "when": 1759169697658, "tag": "0020_supreme_jack_power", "breakpoints": true + }, + { + "idx": 21, + "version": "5", + "when": 1759186023755, + "tag": "0021_flawless_clea", + "breakpoints": true } ] }
\ No newline at end of file diff --git a/packages/console/core/package.json b/packages/console/core/package.json index a1d0681cf..acbef22ee 100644 --- a/packages/console/core/package.json +++ b/packages/console/core/package.json @@ -8,6 +8,7 @@ "@aws-sdk/client-sts": "3.782.0", "@opencode/console-resource": "workspace:*", "@planetscale/database": "1.19.0", + "aws4fetch": "1.0.20", "drizzle-orm": "0.41.0", "postgres": "3.4.7", "stripe": "18.0.0", diff --git a/packages/console/core/src/account.ts b/packages/console/core/src/account.ts index cb123e048..3bed2bef1 100644 --- a/packages/console/core/src/account.ts +++ b/packages/console/core/src/account.ts @@ -54,13 +54,7 @@ export namespace Account { .select(getTableColumns(WorkspaceTable)) .from(WorkspaceTable) .innerJoin(UserTable, eq(UserTable.workspaceID, WorkspaceTable.id)) - .where( - and( - eq(UserTable.email, actor.properties.email), - isNull(UserTable.timeDeleted), - isNull(WorkspaceTable.timeDeleted), - ), - ) + .where(and(eq(UserTable.email, actor.properties.email), isNull(WorkspaceTable.timeDeleted))) .execute(), ) } diff --git a/packages/console/core/src/actor.ts b/packages/console/core/src/actor.ts index f9db01293..0d13f7216 100644 --- a/packages/console/core/src/actor.ts +++ b/packages/console/core/src/actor.ts @@ -1,5 +1,4 @@ import { Context } from "./context" -import { UserRole } from "./schema/user.sql" import { Log } from "./util/log" export namespace Actor { @@ -21,7 +20,6 @@ export namespace Actor { properties: { userID: string workspaceID: string - role: (typeof UserRole)[number] } } diff --git a/packages/console/core/src/aws.ts b/packages/console/core/src/aws.ts new file mode 100644 index 000000000..200e29e4a --- /dev/null +++ b/packages/console/core/src/aws.ts @@ -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 <[email protected]>`, + 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}`) + } + }, + ) +} diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index a87498a33..9c683a359 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -206,7 +206,7 @@ export namespace Billing { }, } : { - customer_email: user.email, + customer_email: user.email!, customer_creation: "always", }), currency: "usd", diff --git a/packages/console/core/src/schema/user.sql.ts b/packages/console/core/src/schema/user.sql.ts index 34939474e..eaadb06d5 100644 --- a/packages/console/core/src/schema/user.sql.ts +++ b/packages/console/core/src/schema/user.sql.ts @@ -9,7 +9,8 @@ export const UserTable = mysqlTable( { ...workspaceColumns, ...timestamps, - email: varchar("email", { length: 255 }).notNull(), + email: varchar("email", { length: 255 }), + oldEmail: varchar("old_email", { length: 255 }), name: varchar("name", { length: 255 }).notNull(), timeSeen: utc("time_seen"), color: int("color"), diff --git a/packages/console/core/src/workspace.ts b/packages/console/core/src/workspace.ts index 0ff3a1532..d6eeb80cf 100644 --- a/packages/console/core/src/workspace.ts +++ b/packages/console/core/src/workspace.ts @@ -21,7 +21,6 @@ export namespace Workspace { id: Identifier.create("user"), email: account.properties.email, name: "", - timeSeen: sql`now()`, role: "admin", }) await tx.insert(BillingTable).values({ diff --git a/packages/console/function/src/auth.ts b/packages/console/function/src/auth.ts index 5dc799683..77199fef5 100644 --- a/packages/console/function/src/auth.ts +++ b/packages/console/function/src/auth.ts @@ -111,11 +111,7 @@ export default { } else if (response.provider === "google") { if (!response.id.email_verified) throw new Error("Google email not verified") email = response.id.email as string - } - //if (response.provider === "email") { - // email = response.claims.email - //} - else throw new Error("Unsupported provider") + } else throw new Error("Unsupported provider") if (!email) throw new Error("No email found") diff --git a/packages/console/function/sst-env.d.ts b/packages/console/function/sst-env.d.ts index 0cd862dff..6a5d2bbf4 100644 --- a/packages/console/function/sst-env.d.ts +++ b/packages/console/function/sst-env.d.ts @@ -10,6 +10,14 @@ declare module "sst" { "type": "sst.sst.Linkable" "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": { "type": "sst.cloudflare.SolidStart" "url": string diff --git a/packages/console/mail/emails/components.tsx b/packages/console/mail/emails/components.tsx new file mode 100644 index 000000000..d030b6cbf --- /dev/null +++ b/packages/console/mail/emails/components.tsx @@ -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}</> +} diff --git a/packages/console/mail/emails/styles.ts b/packages/console/mail/emails/styles.ts new file mode 100644 index 000000000..f9b62a7cd --- /dev/null +++ b/packages/console/mail/emails/styles.ts @@ -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", +}; diff --git a/packages/console/mail/emails/templates/InviteEmail.tsx b/packages/console/mail/emails/templates/InviteEmail.tsx new file mode 100644 index 000000000..978080a9c --- /dev/null +++ b/packages/console/mail/emails/templates/InviteEmail.tsx @@ -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 diff --git a/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-400.woff2 b/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-400.woff2 Binary files differnew file mode 100644 index 000000000..89713fa9a --- /dev/null +++ b/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-400.woff2 diff --git a/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-500.woff2 b/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-500.woff2 Binary files differnew file mode 100644 index 000000000..21413e73f --- /dev/null +++ b/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-500.woff2 diff --git a/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-600.woff2 b/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-600.woff2 Binary files differnew file mode 100644 index 000000000..4a2ebd962 --- /dev/null +++ b/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-600.woff2 diff --git a/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-700.woff2 b/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-700.woff2 Binary files differnew file mode 100644 index 000000000..624783be5 --- /dev/null +++ b/packages/console/mail/emails/templates/static/ibm-plex-mono-latin-700.woff2 diff --git a/packages/console/mail/emails/templates/static/rubik-latin.woff2 b/packages/console/mail/emails/templates/static/rubik-latin.woff2 Binary files differnew file mode 100644 index 000000000..c533eec64 --- /dev/null +++ b/packages/console/mail/emails/templates/static/rubik-latin.woff2 diff --git a/packages/console/mail/emails/templates/static/zen-logo.png b/packages/console/mail/emails/templates/static/zen-logo.png Binary files differnew file mode 100644 index 000000000..382223627 --- /dev/null +++ b/packages/console/mail/emails/templates/static/zen-logo.png diff --git a/packages/console/mail/package.json b/packages/console/mail/package.json new file mode 100644 index 000000000..8ceb170e6 --- /dev/null +++ b/packages/console/mail/package.json @@ -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" + } +} diff --git a/packages/console/mail/sst-env.d.ts b/packages/console/mail/sst-env.d.ts new file mode 100644 index 000000000..9b9de7327 --- /dev/null +++ b/packages/console/mail/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 {}
\ No newline at end of file diff --git a/packages/console/resource/sst-env.d.ts b/packages/console/resource/sst-env.d.ts index 0cd862dff..6a5d2bbf4 100644 --- a/packages/console/resource/sst-env.d.ts +++ b/packages/console/resource/sst-env.d.ts @@ -10,6 +10,14 @@ declare module "sst" { "type": "sst.sst.Linkable" "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": { "type": "sst.cloudflare.SolidStart" "url": string diff --git a/packages/function/sst-env.d.ts b/packages/function/sst-env.d.ts index 0cd862dff..6a5d2bbf4 100644 --- a/packages/function/sst-env.d.ts +++ b/packages/function/sst-env.d.ts @@ -10,6 +10,14 @@ declare module "sst" { "type": "sst.sst.Linkable" "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": { "type": "sst.cloudflare.SolidStart" "url": string |
