mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-21 21:09:49 +08:00
https://www.loom.com/share/2767f799df9d48519c737a1d082fc3f4?sid=967802e9-5bfb-438d-96cd-2f6fcbd2f69b <!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- RECURSEML_SUMMARY:START --> ## High-level PR Summary This PR adds a "Grant Product" feature to the dashboard's customer page, allowing administrators to manually grant products to users, teams, or custom customers. The UI has been updated to rename "Items" to "Customers" in the navigation, and the page now includes a dialog for selecting a product and quantity (for stackable products) to grant. Additionally, the backend payment logic has been enhanced to properly set `currentPeriodEnd` and `cancelAtPeriodEnd` when canceling conflicting subscriptions during product grants. ⏱️ Estimated Review Time: 15-30 minutes <details> <summary>💡 Review Order Suggestion</summary> | Order | File Path | |-------|-----------| | 1 | `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx` | | 2 | `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/customers/page.tsx` | | 3 | `apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/customers/page-client.tsx` | | 4 | `apps/backend/src/lib/payments.tsx` | | 5 | `packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts` | </details> <details> <summary>⚠️ Inconsistent Changes Detected</summary> | File Path | Warning | |-----------|---------| | `packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts` | This appears to be a simple variable rename (cache to itemsCache) that seems unrelated to adding grant product functionality to the dashboard | </details> [](https://discord.gg/n3SsVDAW6U) [ <!-- RECURSEML_SUMMARY:END --> <!-- ELLIPSIS_HIDDEN --> ---- > [!IMPORTANT] > Adds "Grant Product" feature to dashboard, enabling admins to grant products to customers, with new API endpoints, UI updates, and backend logic enhancements. > > - **Behavior**: > - Adds "Grant Product" feature to dashboard's customer page, allowing admins to grant products to users, teams, or custom customers. > - Updates UI to rename "Items" to "Customers" in navigation and adds dialogs for product selection and quantity. > - Enhances backend payment logic to set `currentPeriodEnd` and `cancelAtPeriodEnd` when canceling conflicting subscriptions. > - **API**: > - New endpoints in `route.ts` for listing customer products and granting products. > - Implements `grantProductToCustomer()` in `payments.tsx` to handle product grants. > - **SDK**: > - Adds `grantProduct` (server) and `listProducts` (client/server) methods. > - Updates `client-app-impl.ts` and `server-app-impl.ts` to support new product functionalities. > - **Models**: > - Adds `CustomerProduct` and `CustomerProductsList` types in `customers/index.ts`. > - **Misc**: > - Introduces `PRODUCT_ALREADY_GRANTED` error in `known-errors.tsx`. > - Updates tests in `products.test.ts` and other test files to cover new product grant scenarios. > > <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup> forf0d112f578. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed.</sup> ---- <!-- ELLIPSIS_HIDDEN --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Dashboard: New "Customers" page to view customers, grant products, and manage items with dialogs and selectors for users, teams, or custom IDs. - API/SDK: Endpoints and client/server SDK methods to list a customer’s products (paginated) and to grant products. - **Improvements** - Error responses for already-owned non-stackable products are now structured with clear codes and headers. - Product payloads include server_only, included_items, and new stackable support. - **UI** - Team search table and clickable team rows for faster navigation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
926 lines
26 KiB
Plaintext
926 lines
26 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
previewFeatures = ["driverAdapters", "relationJoins"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("STACK_DATABASE_CONNECTION_STRING")
|
|
directUrl = env("STACK_DIRECT_DATABASE_CONNECTION_STRING")
|
|
}
|
|
|
|
model Project {
|
|
// Note that the project with ID `internal` is handled as a special case. All other project IDs are UUIDs.
|
|
id String @id
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
displayName String
|
|
description String @default("")
|
|
isProductionMode Boolean
|
|
ownerTeamId String? @db.Uuid
|
|
logoUrl String?
|
|
fullLogoUrl String?
|
|
|
|
projectConfigOverride Json?
|
|
stripeAccountId String?
|
|
|
|
apiKeySets ApiKeySet[]
|
|
projectUsers ProjectUser[]
|
|
provisionedProject ProvisionedProject?
|
|
tenancies Tenancy[]
|
|
environmentConfigOverrides EnvironmentConfigOverride[]
|
|
}
|
|
|
|
model Tenancy {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
branchId String
|
|
|
|
// If organizationId is NULL, hasNoOrganization must be TRUE. If organizationId is not NULL, hasNoOrganization must be NULL.
|
|
organizationId String? @db.Uuid
|
|
hasNoOrganization BooleanTrue?
|
|
|
|
@@unique([projectId, branchId, organizationId])
|
|
@@unique([projectId, branchId, hasNoOrganization])
|
|
}
|
|
|
|
model EnvironmentConfigOverride {
|
|
projectId String
|
|
branchId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
config Json
|
|
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([projectId, branchId])
|
|
}
|
|
|
|
model Team {
|
|
tenancyId String @db.Uuid
|
|
teamId String @default(uuid()) @db.Uuid
|
|
|
|
// Team IDs must be unique across all organizations (but not necessarily across all branches).
|
|
// To model this in the DB, we add two columns that are always equal to tenancy.projectId and tenancy.branchId.
|
|
mirroredProjectId String
|
|
mirroredBranchId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
displayName String
|
|
clientMetadata Json?
|
|
clientReadOnlyMetadata Json?
|
|
serverMetadata Json?
|
|
profileImageUrl String?
|
|
|
|
teamMembers TeamMember[]
|
|
projectApiKey ProjectApiKey[]
|
|
|
|
@@id([tenancyId, teamId])
|
|
@@unique([mirroredProjectId, mirroredBranchId, teamId])
|
|
}
|
|
|
|
// This is used for fields that are boolean but only the true value is part of a unique constraint.
|
|
// For example if you want to allow only one selected team per user, you can make an optional field with this type and add a unique constraint.
|
|
// Only the true value is considered for the unique constraint, the null value is not.
|
|
enum BooleanTrue {
|
|
TRUE
|
|
}
|
|
|
|
model TeamMember {
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
teamId String @db.Uuid
|
|
|
|
// This will override the displayName of the user in this team.
|
|
displayName String?
|
|
profileImageUrl String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
team Team @relation(fields: [tenancyId, teamId], references: [tenancyId, teamId], onDelete: Cascade)
|
|
isSelected BooleanTrue?
|
|
teamMemberDirectPermissions TeamMemberDirectPermission[]
|
|
|
|
@@id([tenancyId, projectUserId, teamId])
|
|
@@unique([tenancyId, projectUserId, isSelected])
|
|
}
|
|
|
|
model ProjectUserDirectPermission {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
permissionId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@unique([tenancyId, projectUserId, permissionId])
|
|
}
|
|
|
|
model TeamMemberDirectPermission {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
teamId String @db.Uuid
|
|
permissionId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
teamMember TeamMember @relation(fields: [tenancyId, projectUserId, teamId], references: [tenancyId, projectUserId, teamId], onDelete: Cascade)
|
|
|
|
@@unique([tenancyId, projectUserId, teamId, permissionId])
|
|
}
|
|
|
|
model ProjectUser {
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @default(uuid()) @db.Uuid
|
|
|
|
// User IDs must be unique across all organizations (but not necessarily across all branches).
|
|
// To model this in the DB, we add two columns that are always equal to tenancy.projectId and tenancy.branchId.
|
|
mirroredProjectId String
|
|
mirroredBranchId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
displayName String?
|
|
serverMetadata Json?
|
|
clientReadOnlyMetadata Json?
|
|
clientMetadata Json?
|
|
profileImageUrl String?
|
|
requiresTotpMfa Boolean @default(false)
|
|
totpSecret Bytes?
|
|
isAnonymous Boolean @default(false)
|
|
|
|
projectUserOAuthAccounts ProjectUserOAuthAccount[]
|
|
teamMembers TeamMember[]
|
|
contactChannels ContactChannel[]
|
|
authMethods AuthMethod[]
|
|
|
|
// some backlinks for the unique constraints on some auth methods
|
|
passwordAuthMethod PasswordAuthMethod[]
|
|
passkeyAuthMethod PasskeyAuthMethod[]
|
|
otpAuthMethod OtpAuthMethod[]
|
|
oauthAuthMethod OAuthAuthMethod[]
|
|
SentEmail SentEmail[]
|
|
projectApiKey ProjectApiKey[]
|
|
directPermissions ProjectUserDirectPermission[]
|
|
Project Project? @relation(fields: [projectId], references: [id])
|
|
projectId String?
|
|
userNotificationPreference UserNotificationPreference[]
|
|
|
|
@@id([tenancyId, projectUserId])
|
|
@@unique([mirroredProjectId, mirroredBranchId, projectUserId])
|
|
// indices for sorting and filtering
|
|
@@index([tenancyId, displayName(sort: Asc)], name: "ProjectUser_displayName_asc")
|
|
@@index([tenancyId, displayName(sort: Desc)], name: "ProjectUser_displayName_desc")
|
|
@@index([tenancyId, createdAt(sort: Asc)], name: "ProjectUser_createdAt_asc")
|
|
@@index([tenancyId, createdAt(sort: Desc)], name: "ProjectUser_createdAt_desc")
|
|
}
|
|
|
|
// This should be renamed to "OAuthAccount" as it is not always bound to a user
|
|
// When ever a user goes through the OAuth flow and gets an account ID from the OAuth provider, we store that here.
|
|
model ProjectUserOAuthAccount {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String? @db.Uuid
|
|
configOAuthProviderId String
|
|
providerAccountId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// This is used for the user to distinguish between multiple accounts from the same provider.
|
|
// we might want to add more user info here later
|
|
email String?
|
|
|
|
// Before the OAuth account is connected to a user (for example, in the link oauth process), the projectUser is null.
|
|
projectUser ProjectUser? @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
oauthTokens OAuthToken[]
|
|
oauthAccessToken OAuthAccessToken[]
|
|
|
|
// if allowSignIn is true, oauthAuthMethod must be set
|
|
oauthAuthMethod OAuthAuthMethod?
|
|
allowConnectedAccounts Boolean @default(true)
|
|
allowSignIn Boolean @default(true)
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, configOAuthProviderId, projectUserId, providerAccountId])
|
|
@@index([tenancyId, projectUserId])
|
|
}
|
|
|
|
enum ContactChannelType {
|
|
EMAIL
|
|
// PHONE
|
|
}
|
|
|
|
model ContactChannel {
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
type ContactChannelType
|
|
isPrimary BooleanTrue?
|
|
usedForAuth BooleanTrue?
|
|
isVerified Boolean
|
|
value String
|
|
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, projectUserId, id])
|
|
// each user has at most one primary contact channel of each type
|
|
@@unique([tenancyId, projectUserId, type, isPrimary])
|
|
// value must be unique per user per type
|
|
@@unique([tenancyId, projectUserId, type, value])
|
|
// only one contact channel per project with the same value and type can be used for auth
|
|
@@unique([tenancyId, type, value, usedForAuth])
|
|
}
|
|
|
|
model AuthMethod {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// exactly one of the xyzAuthMethods should be set
|
|
otpAuthMethod OtpAuthMethod?
|
|
passwordAuthMethod PasswordAuthMethod?
|
|
passkeyAuthMethod PasskeyAuthMethod?
|
|
oauthAuthMethod OAuthAuthMethod?
|
|
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, id])
|
|
@@index([tenancyId, projectUserId])
|
|
}
|
|
|
|
model OtpAuthMethod {
|
|
tenancyId String @db.Uuid
|
|
authMethodId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade)
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, authMethodId])
|
|
// a user can only have one OTP auth method
|
|
@@unique([tenancyId, projectUserId])
|
|
}
|
|
|
|
model PasswordAuthMethod {
|
|
tenancyId String @db.Uuid
|
|
authMethodId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
passwordHash String
|
|
|
|
authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade)
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, authMethodId])
|
|
// a user can only have one password auth method
|
|
@@unique([tenancyId, projectUserId])
|
|
}
|
|
|
|
model PasskeyAuthMethod {
|
|
tenancyId String @db.Uuid
|
|
authMethodId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
credentialId String
|
|
publicKey String
|
|
userHandle String
|
|
transports String[]
|
|
credentialDeviceType String
|
|
counter Int
|
|
|
|
authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade)
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, authMethodId])
|
|
// a user can only have one password auth method
|
|
@@unique([tenancyId, projectUserId])
|
|
}
|
|
|
|
// This connects to projectUserOauthAccount, which might be shared between auth method and connected account.
|
|
model OAuthAuthMethod {
|
|
tenancyId String @db.Uuid
|
|
authMethodId String @db.Uuid
|
|
configOAuthProviderId String
|
|
providerAccountId String
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
authMethod AuthMethod @relation(fields: [tenancyId, authMethodId], references: [tenancyId, id], onDelete: Cascade)
|
|
oauthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, configOAuthProviderId, projectUserId, providerAccountId], references: [tenancyId, configOAuthProviderId, projectUserId, providerAccountId])
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, authMethodId])
|
|
@@unique([tenancyId, configOAuthProviderId, providerAccountId])
|
|
@@unique([tenancyId, projectUserId, configOAuthProviderId])
|
|
@@unique([tenancyId, configOAuthProviderId, projectUserId, providerAccountId])
|
|
}
|
|
|
|
enum StandardOAuthProviderType {
|
|
GITHUB
|
|
FACEBOOK
|
|
GOOGLE
|
|
MICROSOFT
|
|
SPOTIFY
|
|
DISCORD
|
|
GITLAB
|
|
BITBUCKET
|
|
LINKEDIN
|
|
APPLE
|
|
X
|
|
TWITCH
|
|
}
|
|
|
|
model OAuthToken {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
tenancyId String @db.Uuid
|
|
oauthAccountId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, oauthAccountId], references: [tenancyId, id], onDelete: Cascade)
|
|
|
|
refreshToken String
|
|
scopes String[]
|
|
isValid Boolean @default(true)
|
|
}
|
|
|
|
model OAuthAccessToken {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
tenancyId String @db.Uuid
|
|
oauthAccountId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
projectUserOAuthAccount ProjectUserOAuthAccount @relation(fields: [tenancyId, oauthAccountId], references: [tenancyId, id], onDelete: Cascade)
|
|
|
|
accessToken String
|
|
scopes String[]
|
|
expiresAt DateTime
|
|
isValid Boolean @default(true)
|
|
}
|
|
|
|
model OAuthOuterInfo {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
info Json
|
|
innerState String @unique
|
|
expiresAt DateTime
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model ProjectUserRefreshToken {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
refreshToken String @unique
|
|
expiresAt DateTime?
|
|
isImpersonation Boolean @default(false)
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model ProjectUserAuthorizationCode {
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
authorizationCode String @unique
|
|
redirectUri String
|
|
expiresAt DateTime
|
|
|
|
codeChallenge String
|
|
codeChallengeMethod String
|
|
|
|
newUser Boolean
|
|
afterCallbackRedirectUrl String?
|
|
|
|
@@id([tenancyId, authorizationCode])
|
|
}
|
|
|
|
model VerificationCode {
|
|
projectId String
|
|
branchId String
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
type VerificationCodeType
|
|
code String
|
|
expiresAt DateTime
|
|
usedAt DateTime?
|
|
redirectUrl String?
|
|
method Json @default("null")
|
|
data Json
|
|
attemptCount Int @default(0)
|
|
|
|
@@id([projectId, branchId, id])
|
|
@@unique([projectId, branchId, code])
|
|
@@index([data(ops: JsonbPathOps)], type: Gin)
|
|
}
|
|
|
|
enum VerificationCodeType {
|
|
ONE_TIME_PASSWORD
|
|
PASSWORD_RESET
|
|
CONTACT_CHANNEL_VERIFICATION
|
|
TEAM_INVITATION
|
|
MFA_ATTEMPT
|
|
PASSKEY_REGISTRATION_CHALLENGE
|
|
PASSKEY_AUTHENTICATION_CHALLENGE
|
|
INTEGRATION_PROJECT_TRANSFER
|
|
PURCHASE_URL
|
|
}
|
|
|
|
//#region API keys
|
|
// Internal API keys
|
|
model ApiKeySet {
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
id String @default(uuid()) @db.Uuid
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
description String
|
|
expiresAt DateTime
|
|
manuallyRevokedAt DateTime?
|
|
publishableClientKey String? @unique
|
|
secretServerKey String? @unique
|
|
superSecretAdminKey String? @unique
|
|
|
|
@@id([projectId, id])
|
|
}
|
|
|
|
//#endregion
|
|
|
|
model ProjectApiKey {
|
|
tenancyId String @db.Uuid
|
|
|
|
id String @default(uuid()) @db.Uuid
|
|
secretApiKey String @unique
|
|
|
|
// Validity and revocation
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
expiresAt DateTime?
|
|
manuallyRevokedAt DateTime?
|
|
description String
|
|
isPublic Boolean
|
|
|
|
// exactly one of [teamId] or [projectUserId] must be set
|
|
teamId String? @db.Uuid
|
|
projectUserId String? @db.Uuid
|
|
|
|
projectUser ProjectUser? @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
team Team? @relation(fields: [tenancyId, teamId], references: [tenancyId, teamId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
enum EmailTemplateType {
|
|
EMAIL_VERIFICATION
|
|
PASSWORD_RESET
|
|
MAGIC_LINK
|
|
TEAM_INVITATION
|
|
SIGN_IN_INVITATION
|
|
}
|
|
|
|
model EmailTemplate {
|
|
projectId String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
content Json
|
|
type EmailTemplateType
|
|
subject String
|
|
|
|
@@id([projectId, type])
|
|
}
|
|
|
|
//#region IdP
|
|
model IdPAccountToCdfcResultMapping {
|
|
idpId String
|
|
id String
|
|
|
|
idpAccountId String @unique @db.Uuid
|
|
cdfcResult Json
|
|
|
|
@@id([idpId, id])
|
|
}
|
|
|
|
model ProjectWrapperCodes {
|
|
idpId String
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
interactionUid String
|
|
authorizationCode String @unique
|
|
|
|
cdfcResult Json
|
|
|
|
@@id([idpId, id])
|
|
}
|
|
|
|
model IdPAdapterData {
|
|
idpId String
|
|
model String
|
|
id String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
payload Json
|
|
expiresAt DateTime
|
|
|
|
@@id([idpId, model, id])
|
|
@@index([payload(ops: JsonbPathOps)], type: Gin)
|
|
@@index([expiresAt])
|
|
}
|
|
|
|
//#endregion
|
|
|
|
model ProvisionedProject {
|
|
projectId String @id
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
clientId String
|
|
}
|
|
|
|
//#region Events
|
|
|
|
model Event {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// if isWide == false, then eventEndedAt is always equal to eventStartedAt
|
|
isWide Boolean
|
|
eventStartedAt DateTime
|
|
eventEndedAt DateTime
|
|
|
|
// TODO: add event_type, and at least one of either system_event_type or event_type is always set
|
|
systemEventTypeIds String[]
|
|
data Json
|
|
|
|
// ============================== BEGIN END USER PROPERTIES ==============================
|
|
// Below are properties describing the end user that caused this event to be logged
|
|
// This is different from a request IP. See: apps/backend/src/lib/end-users.tsx
|
|
|
|
// Note that the IP may have been spoofed, unless isEndUserIpInfoGuessTrusted is true
|
|
endUserIpInfoGuessId String? @db.Uuid
|
|
endUserIpInfoGuess EventIpInfo? @relation("EventIpInfo", fields: [endUserIpInfoGuessId], references: [id])
|
|
// If true, then endUserIpInfoGuess is not spoofed (might still be behind VPNs/proxies). If false, then the values may be spoofed.
|
|
isEndUserIpInfoGuessTrusted Boolean @default(false)
|
|
// =============================== END END USER PROPERTIES ===============================
|
|
|
|
@@index([data(ops: JsonbPathOps)], type: Gin)
|
|
}
|
|
|
|
// An IP address that was seen in an event. Use the location fields instead of refetching the location from the ip, as the real-world geoip data may have changed since the event was logged.
|
|
model EventIpInfo {
|
|
id String @id @default(uuid()) @db.Uuid
|
|
|
|
ip String
|
|
|
|
countryCode String?
|
|
regionCode String?
|
|
cityName String?
|
|
latitude Float?
|
|
longitude Float?
|
|
tzIdentifier String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
events Event[] @relation("EventIpInfo")
|
|
}
|
|
|
|
//#endregion
|
|
|
|
model SentEmail {
|
|
tenancyId String @db.Uuid
|
|
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
userId String? @db.Uuid
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
senderConfig Json
|
|
to String[]
|
|
subject String
|
|
html String?
|
|
text String?
|
|
|
|
error Json?
|
|
user ProjectUser? @relation(fields: [tenancyId, userId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model EmailDraft {
|
|
tenancyId String @db.Uuid
|
|
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
displayName String
|
|
themeMode DraftThemeMode @default(PROJECT_DEFAULT)
|
|
themeId String?
|
|
tsxSource String
|
|
sentAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
enum DraftThemeMode {
|
|
PROJECT_DEFAULT
|
|
NONE
|
|
CUSTOM
|
|
}
|
|
|
|
model CliAuthAttempt {
|
|
tenancyId String @db.Uuid
|
|
|
|
id String @default(uuid()) @db.Uuid
|
|
pollingCode String @unique
|
|
loginCode String @unique
|
|
refreshToken String?
|
|
expiresAt DateTime
|
|
usedAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model UserNotificationPreference {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
projectUserId String @db.Uuid
|
|
notificationCategoryId String @db.Uuid
|
|
|
|
enabled Boolean
|
|
projectUser ProjectUser @relation(fields: [tenancyId, projectUserId], references: [tenancyId, projectUserId], onDelete: Cascade)
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, projectUserId, notificationCategoryId])
|
|
}
|
|
|
|
model ThreadMessage {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
threadId String @db.Uuid
|
|
|
|
content Json
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
enum CustomerType {
|
|
USER
|
|
TEAM
|
|
CUSTOM
|
|
}
|
|
|
|
enum SubscriptionStatus {
|
|
active
|
|
trialing
|
|
canceled
|
|
paused
|
|
incomplete
|
|
incomplete_expired
|
|
past_due
|
|
unpaid
|
|
}
|
|
|
|
enum PurchaseCreationSource {
|
|
PURCHASE_PAGE
|
|
TEST_MODE
|
|
API_GRANT
|
|
}
|
|
|
|
model Subscription {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
customerId String
|
|
customerType CustomerType
|
|
productId String?
|
|
priceId String?
|
|
product Json
|
|
quantity Int @default(1)
|
|
|
|
stripeSubscriptionId String?
|
|
status SubscriptionStatus
|
|
currentPeriodEnd DateTime
|
|
currentPeriodStart DateTime
|
|
cancelAtPeriodEnd Boolean
|
|
|
|
creationSource PurchaseCreationSource
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, stripeSubscriptionId])
|
|
}
|
|
|
|
model ItemQuantityChange {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
customerId String
|
|
customerType CustomerType
|
|
itemId String
|
|
quantity Int
|
|
description String?
|
|
expiresAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@id([tenancyId, id])
|
|
@@index([tenancyId, customerId, expiresAt])
|
|
}
|
|
|
|
model OneTimePurchase {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
customerId String
|
|
customerType CustomerType
|
|
productId String?
|
|
priceId String?
|
|
product Json
|
|
quantity Int
|
|
stripePaymentIntentId String?
|
|
createdAt DateTime @default(now())
|
|
creationSource PurchaseCreationSource
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, stripePaymentIntentId])
|
|
}
|
|
|
|
model DataVaultEntry {
|
|
id String @default(uuid()) @db.Uuid
|
|
tenancyId String @db.Uuid
|
|
storeId String
|
|
hashedKey String
|
|
encrypted Json // Contains { edkBase64, ciphertextBase64 } from encryptWithKms()
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, storeId, hashedKey])
|
|
@@index([tenancyId, storeId])
|
|
}
|
|
|
|
model WorkflowTriggerToken {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
tokenHash String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
expiresAt DateTime
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, tokenHash])
|
|
}
|
|
|
|
model WorkflowTrigger {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
executionId String @db.Uuid
|
|
|
|
triggerData Json
|
|
|
|
// the following fields determine the state of the trigger:
|
|
// - scheduledAt && !compiledWorkflowId && !output && !error: the trigger is scheduled to be executed
|
|
// - !scheduledAt && !compiledWorkflowId: the trigger was scheduled, but its workflow subsequently deleted. The trigger never ran
|
|
// - !scheduledAt && compiledWorkflowId && !output && !error: the trigger is currently executing
|
|
// - !scheduledAt && compiledWorkflowId && output && !error: the trigger has successfully completed execution
|
|
// - !scheduledAt && compiledWorkflowId && !output && error: the trigger has failed execution
|
|
// All other combinations are invalid.
|
|
scheduledAt DateTime?
|
|
output Json?
|
|
error Json?
|
|
compiledWorkflowId String? @db.Uuid
|
|
compiledWorkflow CompiledWorkflow? @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id])
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id])
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model WorkflowExecution {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
|
|
workflowId String
|
|
|
|
triggerIds String[]
|
|
triggers WorkflowTrigger[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, id])
|
|
}
|
|
|
|
model CurrentlyCompilingWorkflow {
|
|
tenancyId String @db.Uuid
|
|
workflowId String
|
|
compilationVersion Int
|
|
sourceHash String
|
|
|
|
startedCompilingAt DateTime @default(now())
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@id([tenancyId, workflowId, compilationVersion, sourceHash])
|
|
}
|
|
|
|
model CompiledWorkflow {
|
|
tenancyId String @db.Uuid
|
|
id String @default(uuid()) @db.Uuid
|
|
workflowId String // note: The workflow with this ID may have been edited or deleted in the meantime, so there may be multiple CompiledWorkflows with the same workflowId
|
|
compilationVersion Int
|
|
sourceHash String
|
|
|
|
// exactly one of [compiledCode, compileError] must be set
|
|
compiledCode String?
|
|
compileError String?
|
|
|
|
compiledAt DateTime @default(now())
|
|
registeredTriggers String[]
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
workflowTriggers WorkflowTrigger[]
|
|
|
|
@@id([tenancyId, id])
|
|
@@unique([tenancyId, workflowId, compilationVersion, sourceHash])
|
|
}
|