feat(console): add support actions (#33492)
This commit is contained in:
parent
633fc6fc03
commit
ad3651d8f1
@ -256,6 +256,7 @@ new sst.cloudflare.x.SolidStart("Console", {
|
||||
SECRET.UpstashRedisRestToken,
|
||||
AUTH_API_URL,
|
||||
STRIPE_WEBHOOK_SECRET,
|
||||
SECRET.SupportApiKey,
|
||||
DISCORD_INCIDENT_WEBHOOK_URL,
|
||||
SECRET.HoneycombWebhookSecret,
|
||||
STRIPE_SECRET_KEY,
|
||||
|
||||
@ -7,6 +7,7 @@ new sst.cloudflare.x.SolidStart("Teams", {
|
||||
domain: shortDomain,
|
||||
path: "packages/enterprise",
|
||||
buildCommand: "bun run build:cloudflare",
|
||||
link: [SECRET.SupportApiKey],
|
||||
environment: {
|
||||
OPENCODE_STORAGE_ADAPTER: "r2",
|
||||
OPENCODE_STORAGE_ACCOUNT_ID: sst.cloudflare.DEFAULT_ACCOUNT_ID,
|
||||
|
||||
@ -9,6 +9,7 @@ export const SECRET = {
|
||||
R2SecretKey: new sst.Secret("R2SecretKey", "unknown"),
|
||||
HoneycombApiKey: new sst.Secret("HONEYCOMB_API_KEY"),
|
||||
HoneycombWebhookSecret: new random.RandomPassword("HoneycombWebhookSecret", { length: 24 }),
|
||||
SupportApiKey: new sst.Secret("SUPPORT_API_KEY"),
|
||||
UpstashRedisRestUrl: new sst.Secret("UpstashRedisRestUrl"),
|
||||
UpstashRedisRestToken: new sst.Secret("UpstashRedisRestToken"),
|
||||
}
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Referral } from "@opencode-ai/console-core/referral.js"
|
||||
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import z from "zod"
|
||||
|
||||
const Body = z.object({
|
||||
inviterWorkspaceID: z.string().startsWith("wrk_"),
|
||||
inviteeWorkspaceID: z.string().startsWith("wrk_"),
|
||||
})
|
||||
|
||||
export async function POST(event: APIEvent) {
|
||||
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = Body.safeParse(await event.request.json().catch(() => undefined))
|
||||
if (!body.success) {
|
||||
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
|
||||
}
|
||||
return Referral.create(body.data)
|
||||
.then((result) => Response.json({ success: true, message: "Referral created", result }))
|
||||
.catch((error) =>
|
||||
Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }),
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
import { Account } from "@opencode-ai/console-core/account.js"
|
||||
import { safeEqual } from "@opencode-ai/console-core/util/crypto.js"
|
||||
import { Resource } from "@opencode-ai/console-resource"
|
||||
import z from "zod"
|
||||
|
||||
const Body = z.object({ email: z.email() })
|
||||
|
||||
export async function DELETE(event: APIEvent) {
|
||||
if (!safeEqual(event.request.headers.get("authorization") ?? "", `Bearer ${Resource.SUPPORT_API_KEY.value}`)) {
|
||||
return Response.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = Body.safeParse(await event.request.json().catch(() => undefined))
|
||||
if (!body.success) {
|
||||
return Response.json({ error: "Invalid request", issues: body.error.issues }, { status: 400 })
|
||||
}
|
||||
return Account.remove(body.data.email)
|
||||
.then(() => Response.json({ success: true, message: "Account deleted" }))
|
||||
.catch((error) =>
|
||||
Response.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }),
|
||||
)
|
||||
}
|
||||
@ -37,7 +37,6 @@
|
||||
"update-limits": "script/update-limits.ts",
|
||||
"promote-limits-to-dev": "script/promote-limits.ts dev",
|
||||
"promote-limits-to-prod": "script/promote-limits.ts production",
|
||||
"referral-backfill": "script/referral-backfill.ts",
|
||||
"typecheck": "tsgo --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
import { and, Database, eq, inArray, isNull } from "../src/drizzle/index.js"
|
||||
import { Identifier } from "../src/identifier.js"
|
||||
import { Referral } from "../src/referral.js"
|
||||
import { LiteTable } from "../src/schema/billing.sql.js"
|
||||
import { ReferralRewardTable, ReferralTable } from "../src/schema/referral.sql.js"
|
||||
import { UserTable } from "../src/schema/user.sql.js"
|
||||
import { WorkspaceTable } from "../src/schema/workspace.sql.js"
|
||||
|
||||
const backfills = [
|
||||
{
|
||||
inviterWorkspaceID: "wrk_00000000000000000000000000",
|
||||
inviteeWorkspaceID: "wrk_00000000000000000000000000",
|
||||
inviteeAccountID: "acc_00000000000000000000000000",
|
||||
},
|
||||
]
|
||||
|
||||
console.log(`Backfilling ${backfills.length} referrals`)
|
||||
|
||||
for (const [index, backfill] of backfills.entries()) {
|
||||
console.log(`[${index + 1}/${backfills.length}] ${backfill.inviterWorkspaceID} -> ${backfill.inviteeWorkspaceID}`)
|
||||
console.log(` invitee account: ${backfill.inviteeAccountID}`)
|
||||
|
||||
const result = await Database.transaction(async (tx) => {
|
||||
if (backfill.inviterWorkspaceID === backfill.inviteeWorkspaceID) throw new Error("Self-referral workspace mismatch")
|
||||
|
||||
const inviterWorkspace = await tx
|
||||
.select({ id: WorkspaceTable.id })
|
||||
.from(WorkspaceTable)
|
||||
.where(and(eq(WorkspaceTable.id, backfill.inviterWorkspaceID), isNull(WorkspaceTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!inviterWorkspace) throw new Error(`Inviter workspace not found: ${backfill.inviterWorkspaceID}`)
|
||||
|
||||
const inviteeWorkspace = await tx
|
||||
.select({ id: WorkspaceTable.id })
|
||||
.from(WorkspaceTable)
|
||||
.where(and(eq(WorkspaceTable.id, backfill.inviteeWorkspaceID), isNull(WorkspaceTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!inviteeWorkspace) throw new Error(`Invitee workspace not found: ${backfill.inviteeWorkspaceID}`)
|
||||
|
||||
const inviteeUser = await tx
|
||||
.select({ id: UserTable.id })
|
||||
.from(UserTable)
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.workspaceID, backfill.inviteeWorkspaceID),
|
||||
eq(UserTable.accountID, backfill.inviteeAccountID),
|
||||
eq(UserTable.role, "admin"),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (!inviteeUser) throw new Error(`Invitee workspace owner not found: ${backfill.inviteeAccountID}`)
|
||||
|
||||
const inviterUser = await tx
|
||||
.select({ id: UserTable.id })
|
||||
.from(UserTable)
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.workspaceID, backfill.inviterWorkspaceID),
|
||||
eq(UserTable.accountID, backfill.inviteeAccountID),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (inviterUser) throw new Error(`Self-referral is not allowed: ${backfill.inviteeAccountID}`)
|
||||
|
||||
const lite = await tx
|
||||
.select({ id: LiteTable.id })
|
||||
.from(LiteTable)
|
||||
.where(
|
||||
and(
|
||||
eq(LiteTable.workspaceID, backfill.inviteeWorkspaceID),
|
||||
eq(LiteTable.userID, inviteeUser.id),
|
||||
isNull(LiteTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (!lite) throw new Error(`Invitee Lite subscription not found: ${backfill.inviteeWorkspaceID}`)
|
||||
|
||||
const existingReferral = await tx
|
||||
.select({ id: ReferralTable.id, workspaceID: ReferralTable.workspaceID })
|
||||
.from(ReferralTable)
|
||||
.where(and(eq(ReferralTable.inviteeAccountID, backfill.inviteeAccountID), isNull(ReferralTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (existingReferral && existingReferral.workspaceID !== backfill.inviterWorkspaceID) {
|
||||
throw new Error(`Referral already belongs to ${existingReferral.workspaceID}: ${existingReferral.id}`)
|
||||
}
|
||||
|
||||
const referralID = existingReferral?.id ?? Identifier.create("referral")
|
||||
if (!existingReferral) {
|
||||
await tx.insert(ReferralTable).ignore().values({
|
||||
workspaceID: backfill.inviterWorkspaceID,
|
||||
id: referralID,
|
||||
inviteeAccountID: backfill.inviteeAccountID,
|
||||
})
|
||||
|
||||
const referral = await tx
|
||||
.select({ id: ReferralTable.id })
|
||||
.from(ReferralTable)
|
||||
.where(and(eq(ReferralTable.inviteeAccountID, backfill.inviteeAccountID), isNull(ReferralTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!referral) throw new Error(`Referral not created: ${backfill.inviteeAccountID}`)
|
||||
if (referral.id !== referralID) throw new Error(`Referral already redeemed: ${referral.id}`)
|
||||
}
|
||||
|
||||
const rewardInsert = await tx
|
||||
.insert(ReferralRewardTable)
|
||||
.ignore()
|
||||
.values([
|
||||
{
|
||||
workspaceID: backfill.inviterWorkspaceID,
|
||||
referralID,
|
||||
amount: Referral.REWARD_AMOUNT,
|
||||
},
|
||||
{
|
||||
workspaceID: backfill.inviteeWorkspaceID,
|
||||
referralID,
|
||||
amount: Referral.REWARD_AMOUNT,
|
||||
},
|
||||
])
|
||||
|
||||
const rewards = await tx
|
||||
.select({ workspaceID: ReferralRewardTable.workspaceID, amount: ReferralRewardTable.amount })
|
||||
.from(ReferralRewardTable)
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralRewardTable.referralID, referralID),
|
||||
inArray(ReferralRewardTable.workspaceID, [backfill.inviterWorkspaceID, backfill.inviteeWorkspaceID]),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
if (rewards.length !== 2) throw new Error(`Referral rewards not created: ${referralID}`)
|
||||
if (rewards.some((reward) => reward.amount !== Referral.REWARD_AMOUNT)) {
|
||||
throw new Error(`Referral reward amount mismatch: ${referralID}`)
|
||||
}
|
||||
|
||||
return {
|
||||
referralID,
|
||||
createdReferral: !existingReferral,
|
||||
createdRewards: rewardInsert.rowsAffected,
|
||||
inviteeUserID: inviteeUser.id,
|
||||
liteID: lite.id,
|
||||
rewardWorkspaces: rewards.map((reward) => reward.workspaceID),
|
||||
}
|
||||
})
|
||||
|
||||
console.log(` invitee user: ${result.inviteeUserID}`)
|
||||
console.log(` lite: ${result.liteID}`)
|
||||
console.log(` referral: ${result.referralID} (${result.createdReferral ? "created" : "existing"})`)
|
||||
console.log(` rewards: ${result.rewardWorkspaces.join(", ")} (${result.createdRewards} inserted)`)
|
||||
}
|
||||
|
||||
console.log("Referral backfill complete")
|
||||
@ -1,9 +1,13 @@
|
||||
import { z } from "zod"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and, eq, inArray, sql } from "drizzle-orm"
|
||||
import { fn } from "./util/fn"
|
||||
import { Database } from "./drizzle"
|
||||
import { Identifier } from "./identifier"
|
||||
import { AccountTable } from "./schema/account.sql"
|
||||
import { AuthTable } from "./schema/auth.sql"
|
||||
import { UserTable } from "./schema/user.sql"
|
||||
import { KeyTable } from "./schema/key.sql"
|
||||
import { CouponTable } from "./schema/billing.sql"
|
||||
|
||||
export namespace Account {
|
||||
export const create = fn(
|
||||
@ -20,6 +24,42 @@ export namespace Account {
|
||||
}),
|
||||
)
|
||||
|
||||
export const remove = fn(z.email(), async (email) => {
|
||||
await Database.transaction(async (tx) => {
|
||||
const account = await tx
|
||||
.select({ id: AccountTable.id })
|
||||
.from(AuthTable)
|
||||
.innerJoin(AccountTable, eq(AccountTable.id, AuthTable.accountID))
|
||||
.where(and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, email)))
|
||||
.then((rows) => rows[0])
|
||||
if (!account) throw new Error("Account not found")
|
||||
|
||||
const emails = await tx
|
||||
.select({ email: AuthTable.subject })
|
||||
.from(AuthTable)
|
||||
.where(and(eq(AuthTable.accountID, account.id), eq(AuthTable.provider, "email")))
|
||||
const users = await tx
|
||||
.select({ id: UserTable.id })
|
||||
.from(UserTable)
|
||||
.where(eq(UserTable.accountID, account.id))
|
||||
if (users.length > 0) {
|
||||
await tx
|
||||
.update(KeyTable)
|
||||
.set({ timeDeleted: sql`now()` })
|
||||
.where(inArray(KeyTable.userID, users.map((user) => user.id)))
|
||||
}
|
||||
await tx
|
||||
.update(UserTable)
|
||||
.set({ accountID: null, email: null, name: "", timeDeleted: sql`now()` })
|
||||
.where(eq(UserTable.accountID, account.id))
|
||||
if (emails.length > 0) {
|
||||
await tx.delete(CouponTable).where(inArray(CouponTable.email, emails.map((row) => row.email)))
|
||||
}
|
||||
await tx.delete(AuthTable).where(eq(AuthTable.accountID, account.id))
|
||||
await tx.update(AccountTable).set({ timeDeleted: sql`now()` }).where(eq(AccountTable.id, account.id))
|
||||
})
|
||||
})
|
||||
|
||||
export const fromID = fn(z.string(), async (id) =>
|
||||
Database.use((tx) =>
|
||||
tx
|
||||
|
||||
@ -362,6 +362,119 @@ export namespace Referral {
|
||||
})
|
||||
}
|
||||
|
||||
export async function create(input: { inviterWorkspaceID: string; inviteeWorkspaceID: string }) {
|
||||
return Database.transaction(async (tx) => {
|
||||
if (input.inviterWorkspaceID === input.inviteeWorkspaceID) throw new Error("Self-referral workspace mismatch")
|
||||
|
||||
const inviterWorkspace = await tx
|
||||
.select({ id: WorkspaceTable.id })
|
||||
.from(WorkspaceTable)
|
||||
.where(and(eq(WorkspaceTable.id, input.inviterWorkspaceID), isNull(WorkspaceTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!inviterWorkspace) throw new Error(`Inviter workspace not found: ${input.inviterWorkspaceID}`)
|
||||
|
||||
const inviteeWorkspace = await tx
|
||||
.select({ id: WorkspaceTable.id })
|
||||
.from(WorkspaceTable)
|
||||
.where(and(eq(WorkspaceTable.id, input.inviteeWorkspaceID), isNull(WorkspaceTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!inviteeWorkspace) throw new Error(`Invitee workspace not found: ${input.inviteeWorkspaceID}`)
|
||||
|
||||
const invitee = await tx
|
||||
.select({ accountID: UserTable.accountID, userID: UserTable.id, liteID: LiteTable.id })
|
||||
.from(UserTable)
|
||||
.innerJoin(
|
||||
LiteTable,
|
||||
and(
|
||||
eq(LiteTable.workspaceID, UserTable.workspaceID),
|
||||
eq(LiteTable.userID, UserTable.id),
|
||||
isNull(LiteTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.workspaceID, input.inviteeWorkspaceID),
|
||||
eq(UserTable.role, "admin"),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (!invitee?.accountID) throw new Error(`Invitee Lite workspace owner not found: ${input.inviteeWorkspaceID}`)
|
||||
|
||||
const inviterUser = await tx
|
||||
.select({ id: UserTable.id })
|
||||
.from(UserTable)
|
||||
.where(
|
||||
and(
|
||||
eq(UserTable.workspaceID, input.inviterWorkspaceID),
|
||||
eq(UserTable.accountID, invitee.accountID),
|
||||
isNull(UserTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
.then((rows) => rows[0])
|
||||
if (inviterUser) throw new Error(`Self-referral is not allowed: ${invitee.accountID}`)
|
||||
|
||||
const existingReferral = await tx
|
||||
.select({ id: ReferralTable.id, workspaceID: ReferralTable.workspaceID })
|
||||
.from(ReferralTable)
|
||||
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (existingReferral && existingReferral.workspaceID !== input.inviterWorkspaceID) {
|
||||
throw new Error(`Referral already belongs to ${existingReferral.workspaceID}: ${existingReferral.id}`)
|
||||
}
|
||||
|
||||
const referralID = existingReferral?.id ?? Identifier.create("referral")
|
||||
if (!existingReferral) {
|
||||
await tx.insert(ReferralTable).ignore().values({
|
||||
workspaceID: input.inviterWorkspaceID,
|
||||
id: referralID,
|
||||
inviteeAccountID: invitee.accountID,
|
||||
})
|
||||
|
||||
const referral = await tx
|
||||
.select({ id: ReferralTable.id })
|
||||
.from(ReferralTable)
|
||||
.where(and(eq(ReferralTable.inviteeAccountID, invitee.accountID), isNull(ReferralTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!referral) throw new Error(`Referral not created: ${invitee.accountID}`)
|
||||
if (referral.id !== referralID) throw new Error(`Referral already redeemed: ${referral.id}`)
|
||||
}
|
||||
|
||||
const rewardInsert = await tx
|
||||
.insert(ReferralRewardTable)
|
||||
.ignore()
|
||||
.values([
|
||||
{ workspaceID: input.inviterWorkspaceID, referralID, amount: REWARD_AMOUNT },
|
||||
{ workspaceID: input.inviteeWorkspaceID, referralID, amount: REWARD_AMOUNT },
|
||||
])
|
||||
|
||||
const rewards = await tx
|
||||
.select({ workspaceID: ReferralRewardTable.workspaceID, amount: ReferralRewardTable.amount })
|
||||
.from(ReferralRewardTable)
|
||||
.where(
|
||||
and(
|
||||
eq(ReferralRewardTable.referralID, referralID),
|
||||
inArray(ReferralRewardTable.workspaceID, [input.inviterWorkspaceID, input.inviteeWorkspaceID]),
|
||||
isNull(ReferralRewardTable.timeDeleted),
|
||||
),
|
||||
)
|
||||
if (rewards.length !== 2) throw new Error(`Referral rewards not created: ${referralID}`)
|
||||
if (rewards.some((reward) => reward.amount !== REWARD_AMOUNT)) {
|
||||
throw new Error(`Referral reward amount mismatch: ${referralID}`)
|
||||
}
|
||||
|
||||
return {
|
||||
referralID,
|
||||
createdReferral: !existingReferral,
|
||||
createdRewards: rewardInsert.rowsAffected,
|
||||
inviteeAccountID: invitee.accountID,
|
||||
inviteeUserID: invitee.userID,
|
||||
liteID: invitee.liteID,
|
||||
rewardWorkspaces: rewards.map((reward) => reward.workspaceID),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function completeFromLiteSubscription(input: { workspaceID: string; userID: string }) {
|
||||
return Database.transaction(async (tx) => {
|
||||
const invitee = await tx
|
||||
|
||||
@ -11,6 +11,7 @@ import { Key } from "./key"
|
||||
import { KeyTable } from "./schema/key.sql"
|
||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
import { AuthTable } from "./schema/auth.sql"
|
||||
import { AccountTable } from "./schema/account.sql"
|
||||
|
||||
export namespace User {
|
||||
const assertNotSelf = (id: string) => {
|
||||
@ -161,6 +162,13 @@ export namespace User {
|
||||
export const joinInvitedWorkspaces = fn(z.void(), async () => {
|
||||
const account = Actor.assert("account")
|
||||
const invitations = await Database.use(async (tx) => {
|
||||
const active = await tx
|
||||
.select({ id: AccountTable.id })
|
||||
.from(AccountTable)
|
||||
.where(and(eq(AccountTable.id, account.properties.accountID), isNull(AccountTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!active) throw new Error("Account is not active")
|
||||
|
||||
const invitations = await tx
|
||||
.select({
|
||||
id: UserTable.id,
|
||||
|
||||
@ -6,8 +6,9 @@ import { Identifier } from "./identifier"
|
||||
import { UserTable } from "./schema/user.sql"
|
||||
import { BillingTable } from "./schema/billing.sql"
|
||||
import { WorkspaceTable } from "./schema/workspace.sql"
|
||||
import { AccountTable } from "./schema/account.sql"
|
||||
import { Key } from "./key"
|
||||
import { eq, sql } from "drizzle-orm"
|
||||
import { and, eq, isNull, sql } from "drizzle-orm"
|
||||
|
||||
export namespace Workspace {
|
||||
export const create = fn(
|
||||
@ -19,6 +20,13 @@ export namespace Workspace {
|
||||
const workspaceID = Identifier.create("workspace")
|
||||
const userID = Identifier.create("user")
|
||||
await Database.transaction(async (tx) => {
|
||||
const active = await tx
|
||||
.select({ id: AccountTable.id })
|
||||
.from(AccountTable)
|
||||
.where(and(eq(AccountTable.id, account.properties.accountID), isNull(AccountTable.timeDeleted)))
|
||||
.then((rows) => rows[0])
|
||||
if (!active) throw new Error("Account is not active")
|
||||
|
||||
await tx.insert(WorkspaceTable).values({
|
||||
id: workspaceID,
|
||||
name,
|
||||
|
||||
@ -147,6 +147,12 @@ export namespace Share {
|
||||
}
|
||||
})
|
||||
|
||||
export const removeAdmin = fn(Info.pick({ id: true }), async (body) => {
|
||||
const share = await get(body.id)
|
||||
if (!share) throw new Errors.NotFound(body.id)
|
||||
await remove({ id: share.id, secret: share.secret })
|
||||
})
|
||||
|
||||
export const sync = fn(
|
||||
z.object({
|
||||
share: Info.pick({ id: true, secret: true }),
|
||||
|
||||
@ -5,6 +5,8 @@ import { validator } from "hono-openapi"
|
||||
import z from "zod"
|
||||
import { cors } from "hono/cors"
|
||||
import { Share } from "~/core/share"
|
||||
import { Resource } from "sst"
|
||||
import { timingSafeEqual } from "node:crypto"
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
@ -137,6 +139,22 @@ app
|
||||
return c.json({})
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/support/actions/remove-share",
|
||||
async (c) => {
|
||||
const authorization = c.req.header("authorization")
|
||||
const expected = `Bearer ${(Resource as unknown as Record<string, { value: string }>).SUPPORT_API_KEY.value}`
|
||||
const actual = Buffer.from(authorization ?? "")
|
||||
const secret = Buffer.from(expected)
|
||||
if (actual.length !== secret.length || !timingSafeEqual(actual, secret)) return c.json({ error: "Unauthorized" }, 401)
|
||||
|
||||
const body = z.object({ shareID: z.string().min(1) }).safeParse(await c.req.json().catch(() => undefined))
|
||||
if (!body.success) return c.json({ error: "Invalid request", issues: body.error.issues }, 400)
|
||||
return Share.removeAdmin({ id: body.data.shareID })
|
||||
.then(() => c.json({ success: true, message: "Share removed" }))
|
||||
.catch((error) => c.json({ error: error instanceof Error ? error.message : String(error) }, 400))
|
||||
},
|
||||
)
|
||||
|
||||
export function GET(event: APIEvent) {
|
||||
return app.fetch(event.request)
|
||||
|
||||
@ -14,6 +14,14 @@ describe.concurrent("core.share", () => {
|
||||
await Share.remove({ id: share.id, secret: share.secret })
|
||||
})
|
||||
|
||||
test("should remove a share as admin", async () => {
|
||||
const share = await Share.create({ sessionID: Identifier.descending() })
|
||||
|
||||
await Share.removeAdmin({ id: share.id })
|
||||
|
||||
expect(await Share.get(share.id)).toBeUndefined()
|
||||
})
|
||||
|
||||
test("should sync data to a share", async () => {
|
||||
const sessionID = Identifier.descending()
|
||||
const share = await Share.create({ sessionID })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user