feat(console): add support actions (#33492)
This commit is contained in:
parent
633fc6fc03
commit
ad3651d8f1
14 changed files with 255 additions and 156 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue