mirror of
https://github.com/Afilmory/afilmory
synced 2026-04-25 07:15:36 +00:00
feat(billing): enhance billing plan management with product and pricing configurations
- Added support for billing plan products and pricing settings, allowing for detailed configuration of subscription plans. - Introduced new constants and types for managing billing plan overrides, products, and pricing. - Updated the SystemSettingService to handle new billing-related settings and integrated them into the admin interface. - Enhanced the BillingPlanService to retrieve and apply product and pricing information for tenant plans. - Updated UI components to reflect the new billing plan configurations and ensure proper display of pricing details. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
"@afilmory/task-queue": "workspace:*",
|
"@afilmory/task-queue": "workspace:*",
|
||||||
"@afilmory/utils": "workspace:*",
|
"@afilmory/utils": "workspace:*",
|
||||||
"@aws-sdk/client-s3": "3.929.0",
|
"@aws-sdk/client-s3": "3.929.0",
|
||||||
|
"@creem_io/better-auth": "0.0.8",
|
||||||
"@hono/node-server": "^1.19.6",
|
"@hono/node-server": "^1.19.6",
|
||||||
"@resvg/resvg-js": "2.6.2",
|
"@resvg/resvg-js": "2.6.2",
|
||||||
"better-auth": "1.3.34",
|
"better-auth": "1.3.34",
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { DEFAULT_BASE_DOMAIN } from '@afilmory/utils'
|
import { DEFAULT_BASE_DOMAIN } from '@afilmory/utils'
|
||||||
|
import {
|
||||||
|
BILLING_PLAN_DEFINITIONS,
|
||||||
|
BILLING_PLAN_IDS,
|
||||||
|
BILLING_PLAN_OVERRIDES_SETTING_KEY,
|
||||||
|
BILLING_PLAN_PRICING_SETTING_KEY,
|
||||||
|
BILLING_PLAN_PRODUCTS_SETTING_KEY,
|
||||||
|
} from 'core/modules/platform/billing/billing-plan.constants'
|
||||||
|
import type { BillingPlanId, BillingPlanQuota } from 'core/modules/platform/billing/billing-plan.types'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
const nonEmptyString = z.string().trim().min(1)
|
const nonEmptyString = z.string().trim().min(1)
|
||||||
@@ -88,9 +96,74 @@ export const SYSTEM_SETTING_DEFINITIONS = {
|
|||||||
defaultValue: null as string | null,
|
defaultValue: null as string | null,
|
||||||
isSensitive: true,
|
isSensitive: true,
|
||||||
},
|
},
|
||||||
|
billingPlanOverrides: {
|
||||||
|
key: BILLING_PLAN_OVERRIDES_SETTING_KEY,
|
||||||
|
schema: z.record(z.string(), z.any()),
|
||||||
|
defaultValue: {},
|
||||||
|
isSensitive: false,
|
||||||
|
},
|
||||||
|
billingPlanProducts: {
|
||||||
|
key: BILLING_PLAN_PRODUCTS_SETTING_KEY,
|
||||||
|
schema: z.record(z.string(), z.any()),
|
||||||
|
defaultValue: {},
|
||||||
|
isSensitive: false,
|
||||||
|
},
|
||||||
|
billingPlanPricing: {
|
||||||
|
key: BILLING_PLAN_PRICING_SETTING_KEY,
|
||||||
|
schema: z.record(z.string(), z.any()),
|
||||||
|
defaultValue: {},
|
||||||
|
isSensitive: false,
|
||||||
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type SystemSettingField = keyof typeof SYSTEM_SETTING_DEFINITIONS
|
const BILLING_PLAN_QUOTA_KEYS = [
|
||||||
export type SystemSettingKey = (typeof SYSTEM_SETTING_DEFINITIONS)[SystemSettingField]['key']
|
'monthlyAssetProcessLimit',
|
||||||
|
'libraryItemLimit',
|
||||||
|
'maxUploadSizeMb',
|
||||||
|
'maxSyncObjectSizeMb',
|
||||||
|
] as const
|
||||||
|
export type BillingPlanQuotaFieldKey = (typeof BILLING_PLAN_QUOTA_KEYS)[number]
|
||||||
|
|
||||||
|
const BILLING_PLAN_PRICING_KEYS = ['monthlyPrice', 'currency'] as const
|
||||||
|
export type BillingPlanPricingFieldKey = (typeof BILLING_PLAN_PRICING_KEYS)[number]
|
||||||
|
|
||||||
|
const BILLING_PLAN_PAYMENT_KEYS = ['creemProductId'] as const
|
||||||
|
export type BillingPlanPaymentFieldKey = (typeof BILLING_PLAN_PAYMENT_KEYS)[number]
|
||||||
|
|
||||||
|
export type BillingPlanQuotaField = `billingPlan.${BillingPlanId}.quota.${BillingPlanQuotaFieldKey}`
|
||||||
|
export type BillingPlanPricingField = `billingPlan.${BillingPlanId}.pricing.${BillingPlanPricingFieldKey}`
|
||||||
|
export type BillingPlanPaymentField = `billingPlan.${BillingPlanId}.payment.${BillingPlanPaymentFieldKey}`
|
||||||
|
|
||||||
|
export type BillingPlanSettingField = BillingPlanQuotaField | BillingPlanPricingField | BillingPlanPaymentField
|
||||||
|
|
||||||
|
export type SystemSettingDbField = keyof typeof SYSTEM_SETTING_DEFINITIONS
|
||||||
|
|
||||||
|
export type SystemSettingField = SystemSettingDbField | BillingPlanSettingField
|
||||||
|
export type SystemSettingKey = (typeof SYSTEM_SETTING_DEFINITIONS)[SystemSettingDbField]['key']
|
||||||
|
|
||||||
|
export const BILLING_PLAN_FIELD_DESCRIPTORS = {
|
||||||
|
quotas: BILLING_PLAN_IDS.flatMap((planId) =>
|
||||||
|
BILLING_PLAN_QUOTA_KEYS.map((key) => ({
|
||||||
|
planId,
|
||||||
|
key,
|
||||||
|
field: `billingPlan.${planId}.quota.${key}` as BillingPlanQuotaField,
|
||||||
|
defaultValue: BILLING_PLAN_DEFINITIONS[planId].quotas[key as keyof BillingPlanQuota],
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
pricing: BILLING_PLAN_IDS.flatMap((planId) =>
|
||||||
|
BILLING_PLAN_PRICING_KEYS.map((key) => ({
|
||||||
|
planId,
|
||||||
|
key,
|
||||||
|
field: `billingPlan.${planId}.pricing.${key}` as BillingPlanPricingField,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
payment: BILLING_PLAN_IDS.flatMap((planId) =>
|
||||||
|
BILLING_PLAN_PAYMENT_KEYS.map((key) => ({
|
||||||
|
planId,
|
||||||
|
key,
|
||||||
|
field: `billingPlan.${planId}.payment.${key}` as BillingPlanPaymentField,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
} as const
|
||||||
|
|
||||||
export const SYSTEM_SETTING_KEYS = Object.values(SYSTEM_SETTING_DEFINITIONS).map((definition) => definition.key)
|
export const SYSTEM_SETTING_KEYS = Object.values(SYSTEM_SETTING_DEFINITIONS).map((definition) => definition.key)
|
||||||
|
|||||||
@@ -2,20 +2,37 @@ import { authUsers } from '@afilmory/db'
|
|||||||
import { DbAccessor } from 'core/database/database.provider'
|
import { DbAccessor } from 'core/database/database.provider'
|
||||||
import { BizException, ErrorCode } from 'core/errors'
|
import { BizException, ErrorCode } from 'core/errors'
|
||||||
import type { SocialProvidersConfig } from 'core/modules/platform/auth/auth.config'
|
import type { SocialProvidersConfig } from 'core/modules/platform/auth/auth.config'
|
||||||
import { BILLING_PLAN_OVERRIDES_SETTING_KEY } from 'core/modules/platform/billing/billing-plan.constants'
|
import {
|
||||||
import type { BillingPlanOverrides } from 'core/modules/platform/billing/billing-plan.types'
|
BILLING_PLAN_OVERRIDES_SETTING_KEY,
|
||||||
|
BILLING_PLAN_PRICING_SETTING_KEY,
|
||||||
|
BILLING_PLAN_PRODUCTS_SETTING_KEY,
|
||||||
|
} from 'core/modules/platform/billing/billing-plan.constants'
|
||||||
|
import type {
|
||||||
|
BillingPlanId,
|
||||||
|
BillingPlanOverrides,
|
||||||
|
BillingPlanPaymentInfo,
|
||||||
|
BillingPlanPricing,
|
||||||
|
BillingPlanPricingConfigs,
|
||||||
|
BillingPlanProductConfigs,
|
||||||
|
BillingPlanQuota,
|
||||||
|
} from 'core/modules/platform/billing/billing-plan.types'
|
||||||
import { sql } from 'drizzle-orm'
|
import { sql } from 'drizzle-orm'
|
||||||
import { injectable } from 'tsyringe'
|
import { injectable } from 'tsyringe'
|
||||||
import type {ZodType} from 'zod';
|
import type { ZodType } from 'zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
import { SYSTEM_SETTING_DEFINITIONS, SYSTEM_SETTING_KEYS } from './system-setting.constants'
|
import type { SystemSettingDbField } from './system-setting.constants'
|
||||||
|
import {
|
||||||
|
BILLING_PLAN_FIELD_DESCRIPTORS,
|
||||||
|
SYSTEM_SETTING_DEFINITIONS,
|
||||||
|
SYSTEM_SETTING_KEYS,
|
||||||
|
} from './system-setting.constants'
|
||||||
import { SystemSettingStore } from './system-setting.store.service'
|
import { SystemSettingStore } from './system-setting.store.service'
|
||||||
import type {
|
import type {
|
||||||
SystemSettingField,
|
|
||||||
SystemSettingOverview,
|
SystemSettingOverview,
|
||||||
SystemSettings,
|
SystemSettings,
|
||||||
SystemSettingStats,
|
SystemSettingStats,
|
||||||
|
SystemSettingValueMap,
|
||||||
UpdateSystemSettingsInput,
|
UpdateSystemSettingsInput,
|
||||||
} from './system-setting.types'
|
} from './system-setting.types'
|
||||||
import { SYSTEM_SETTING_UI_SCHEMA } from './system-setting.ui-schema'
|
import { SYSTEM_SETTING_UI_SCHEMA } from './system-setting.ui-schema'
|
||||||
@@ -41,21 +58,6 @@ export class SystemSettingService {
|
|||||||
SYSTEM_SETTING_DEFINITIONS.maxRegistrableUsers.schema,
|
SYSTEM_SETTING_DEFINITIONS.maxRegistrableUsers.schema,
|
||||||
SYSTEM_SETTING_DEFINITIONS.maxRegistrableUsers.defaultValue,
|
SYSTEM_SETTING_DEFINITIONS.maxRegistrableUsers.defaultValue,
|
||||||
)
|
)
|
||||||
const maxPhotoUploadSizeMb = this.parseSetting(
|
|
||||||
rawValues[SYSTEM_SETTING_DEFINITIONS.maxPhotoUploadSizeMb.key],
|
|
||||||
SYSTEM_SETTING_DEFINITIONS.maxPhotoUploadSizeMb.schema,
|
|
||||||
SYSTEM_SETTING_DEFINITIONS.maxPhotoUploadSizeMb.defaultValue,
|
|
||||||
)
|
|
||||||
const maxDataSyncObjectSizeMb = this.parseSetting(
|
|
||||||
rawValues[SYSTEM_SETTING_DEFINITIONS.maxDataSyncObjectSizeMb.key],
|
|
||||||
SYSTEM_SETTING_DEFINITIONS.maxDataSyncObjectSizeMb.schema,
|
|
||||||
SYSTEM_SETTING_DEFINITIONS.maxDataSyncObjectSizeMb.defaultValue,
|
|
||||||
)
|
|
||||||
const maxPhotoLibraryItems = this.parseSetting(
|
|
||||||
rawValues[SYSTEM_SETTING_DEFINITIONS.maxPhotoLibraryItems.key],
|
|
||||||
SYSTEM_SETTING_DEFINITIONS.maxPhotoLibraryItems.schema,
|
|
||||||
SYSTEM_SETTING_DEFINITIONS.maxPhotoLibraryItems.defaultValue,
|
|
||||||
)
|
|
||||||
|
|
||||||
const localProviderEnabled = this.parseSetting(
|
const localProviderEnabled = this.parseSetting(
|
||||||
rawValues[SYSTEM_SETTING_DEFINITIONS.localProviderEnabled.key],
|
rawValues[SYSTEM_SETTING_DEFINITIONS.localProviderEnabled.key],
|
||||||
@@ -99,12 +101,24 @@ export class SystemSettingService {
|
|||||||
SYSTEM_SETTING_DEFINITIONS.oauthGithubClientSecret.schema,
|
SYSTEM_SETTING_DEFINITIONS.oauthGithubClientSecret.schema,
|
||||||
SYSTEM_SETTING_DEFINITIONS.oauthGithubClientSecret.defaultValue,
|
SYSTEM_SETTING_DEFINITIONS.oauthGithubClientSecret.defaultValue,
|
||||||
)
|
)
|
||||||
|
const billingPlanOverrides = this.parseSetting(
|
||||||
|
rawValues[SYSTEM_SETTING_DEFINITIONS.billingPlanOverrides.key],
|
||||||
|
BILLING_PLAN_OVERRIDES_SCHEMA,
|
||||||
|
{},
|
||||||
|
) as BillingPlanOverrides
|
||||||
|
const billingPlanProducts = this.parseSetting(
|
||||||
|
rawValues[SYSTEM_SETTING_DEFINITIONS.billingPlanProducts.key],
|
||||||
|
BILLING_PLAN_PRODUCTS_SCHEMA,
|
||||||
|
{},
|
||||||
|
) as BillingPlanProductConfigs
|
||||||
|
const billingPlanPricing = this.parseSetting(
|
||||||
|
rawValues[SYSTEM_SETTING_DEFINITIONS.billingPlanPricing.key],
|
||||||
|
BILLING_PLAN_PRICING_SCHEMA,
|
||||||
|
{},
|
||||||
|
) as BillingPlanPricingConfigs
|
||||||
return {
|
return {
|
||||||
allowRegistration,
|
allowRegistration,
|
||||||
maxRegistrableUsers,
|
maxRegistrableUsers,
|
||||||
maxPhotoUploadSizeMb,
|
|
||||||
maxDataSyncObjectSizeMb,
|
|
||||||
maxPhotoLibraryItems,
|
|
||||||
localProviderEnabled,
|
localProviderEnabled,
|
||||||
baseDomain,
|
baseDomain,
|
||||||
oauthGatewayUrl,
|
oauthGatewayUrl,
|
||||||
@@ -112,28 +126,34 @@ export class SystemSettingService {
|
|||||||
oauthGoogleClientSecret,
|
oauthGoogleClientSecret,
|
||||||
oauthGithubClientId,
|
oauthGithubClientId,
|
||||||
oauthGithubClientSecret,
|
oauthGithubClientSecret,
|
||||||
|
billingPlanOverrides,
|
||||||
|
billingPlanProducts,
|
||||||
|
billingPlanPricing,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBillingPlanOverrides(): Promise<BillingPlanOverrides> {
|
async getBillingPlanOverrides(): Promise<BillingPlanOverrides> {
|
||||||
const raw = await this.systemSettingStore.getRaw(BILLING_PLAN_OVERRIDES_SETTING_KEY)
|
const settings = await this.getSettings()
|
||||||
const parsed = BILLING_PLAN_OVERRIDES_SCHEMA.safeParse(raw)
|
return settings.billingPlanOverrides ?? {}
|
||||||
return parsed.success ? (parsed.data as BillingPlanOverrides) : {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStats(): Promise<SystemSettingStats> {
|
async getBillingPlanProducts(): Promise<BillingPlanProductConfigs> {
|
||||||
const settings = await this.getSettings()
|
const settings = await this.getSettings()
|
||||||
const totalUsers = await this.getTotalUserCount()
|
return settings.billingPlanProducts ?? {}
|
||||||
return this.buildStats(settings, totalUsers)
|
}
|
||||||
|
|
||||||
|
async getBillingPlanPricing(): Promise<BillingPlanPricingConfigs> {
|
||||||
|
const settings = await this.getSettings()
|
||||||
|
return settings.billingPlanPricing ?? {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOverview(): Promise<SystemSettingOverview> {
|
async getOverview(): Promise<SystemSettingOverview> {
|
||||||
const values = await this.getSettings()
|
const settings = await this.getSettings()
|
||||||
const totalUsers = await this.getTotalUserCount()
|
const totalUsers = await this.getTotalUserCount()
|
||||||
const stats = this.buildStats(values, totalUsers)
|
const stats = this.buildStats(settings, totalUsers)
|
||||||
return {
|
return {
|
||||||
schema: SYSTEM_SETTING_UI_SCHEMA,
|
schema: SYSTEM_SETTING_UI_SCHEMA,
|
||||||
values,
|
values: this.buildValueMap(settings),
|
||||||
stats,
|
stats,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,11 +164,16 @@ export class SystemSettingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const current = await this.getSettings()
|
const current = await this.getSettings()
|
||||||
const updates: Array<{ field: SystemSettingField; value: SystemSettings[SystemSettingField] }> = []
|
const planFieldUpdates = this.extractPlanFieldUpdates(patch)
|
||||||
|
if (planFieldUpdates.hasUpdates) {
|
||||||
|
await this.applyPlanFieldUpdates(current, planFieldUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
const enqueueUpdate = <K extends SystemSettingField>(field: K, value: SystemSettings[K]) => {
|
const updates: Array<{ field: SystemSettingDbField; value: unknown }> = []
|
||||||
|
|
||||||
|
const enqueueUpdate = <K extends SystemSettingDbField>(field: K, value: unknown) => {
|
||||||
updates.push({ field, value })
|
updates.push({ field, value })
|
||||||
current[field] = value
|
;(current as unknown as Record<string, unknown>)[field] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patch.allowRegistration !== undefined && patch.allowRegistration !== current.allowRegistration) {
|
if (patch.allowRegistration !== undefined && patch.allowRegistration !== current.allowRegistration) {
|
||||||
@@ -175,30 +200,6 @@ export class SystemSettingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patch.maxPhotoUploadSizeMb !== undefined) {
|
|
||||||
const normalized =
|
|
||||||
patch.maxPhotoUploadSizeMb === null ? null : Math.max(1, Math.trunc(patch.maxPhotoUploadSizeMb))
|
|
||||||
if (normalized !== current.maxPhotoUploadSizeMb) {
|
|
||||||
enqueueUpdate('maxPhotoUploadSizeMb', normalized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patch.maxDataSyncObjectSizeMb !== undefined) {
|
|
||||||
const normalized =
|
|
||||||
patch.maxDataSyncObjectSizeMb === null ? null : Math.max(1, Math.trunc(patch.maxDataSyncObjectSizeMb))
|
|
||||||
if (normalized !== current.maxDataSyncObjectSizeMb) {
|
|
||||||
enqueueUpdate('maxDataSyncObjectSizeMb', normalized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patch.maxPhotoLibraryItems !== undefined) {
|
|
||||||
const normalized =
|
|
||||||
patch.maxPhotoLibraryItems === null ? null : Math.max(0, Math.trunc(patch.maxPhotoLibraryItems))
|
|
||||||
if (normalized !== current.maxPhotoLibraryItems) {
|
|
||||||
enqueueUpdate('maxPhotoLibraryItems', normalized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patch.baseDomain !== undefined) {
|
if (patch.baseDomain !== undefined) {
|
||||||
const sanitized = patch.baseDomain === null ? null : String(patch.baseDomain).trim().toLowerCase()
|
const sanitized = patch.baseDomain === null ? null : String(patch.baseDomain).trim().toLowerCase()
|
||||||
if (!sanitized) {
|
if (!sanitized) {
|
||||||
@@ -251,7 +252,7 @@ export class SystemSettingService {
|
|||||||
const definition = SYSTEM_SETTING_DEFINITIONS[entry.field]
|
const definition = SYSTEM_SETTING_DEFINITIONS[entry.field]
|
||||||
return {
|
return {
|
||||||
key: definition.key,
|
key: definition.key,
|
||||||
value: (entry.value ?? null) as SystemSettings[typeof entry.field] | null,
|
value: (entry.value ?? null) as unknown,
|
||||||
options: { isSensitive: definition.isSensitive ?? false },
|
options: { isSensitive: definition.isSensitive ?? false },
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -260,6 +261,174 @@ export class SystemSettingService {
|
|||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildValueMap(settings: SystemSettings): SystemSettingValueMap {
|
||||||
|
const map = {} as SystemSettingValueMap
|
||||||
|
|
||||||
|
;(Object.keys(SYSTEM_SETTING_DEFINITIONS) as SystemSettingDbField[]).forEach((field) => {
|
||||||
|
;(map as Record<string, unknown>)[field] = settings[field]
|
||||||
|
})
|
||||||
|
|
||||||
|
const overrides = settings.billingPlanOverrides ?? {}
|
||||||
|
BILLING_PLAN_FIELD_DESCRIPTORS.quotas.forEach((descriptor) => {
|
||||||
|
const planOverrides = overrides[descriptor.planId]
|
||||||
|
if (planOverrides && descriptor.key in (planOverrides as object)) {
|
||||||
|
;(map as Record<string, unknown>)[descriptor.field] =
|
||||||
|
(planOverrides as BillingPlanQuota)[descriptor.key as keyof BillingPlanQuota] ?? null
|
||||||
|
} else {
|
||||||
|
;(map as Record<string, unknown>)[descriptor.field] = descriptor.defaultValue ?? null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const pricing = settings.billingPlanPricing ?? {}
|
||||||
|
BILLING_PLAN_FIELD_DESCRIPTORS.pricing.forEach((descriptor) => {
|
||||||
|
const entry = pricing[descriptor.planId]
|
||||||
|
if (descriptor.key === 'currency') {
|
||||||
|
;(map as Record<string, unknown>)[descriptor.field] = entry?.currency ?? null
|
||||||
|
} else if (descriptor.key === 'monthlyPrice') {
|
||||||
|
;(map as Record<string, unknown>)[descriptor.field] = entry?.monthlyPrice ?? null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const products = settings.billingPlanProducts ?? {}
|
||||||
|
BILLING_PLAN_FIELD_DESCRIPTORS.payment.forEach((descriptor) => {
|
||||||
|
const entry = products[descriptor.planId]
|
||||||
|
;(map as Record<string, unknown>)[descriptor.field] = entry?.creemProductId ?? null
|
||||||
|
})
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractPlanFieldUpdates(patch: UpdateSystemSettingsInput): PlanFieldUpdateSummary {
|
||||||
|
const summary: PlanFieldUpdateSummary = {
|
||||||
|
hasUpdates: false,
|
||||||
|
quotas: {},
|
||||||
|
pricing: {},
|
||||||
|
products: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const descriptor of BILLING_PLAN_FIELD_DESCRIPTORS.quotas) {
|
||||||
|
if (!(descriptor.field in patch)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
summary.hasUpdates = true
|
||||||
|
const raw = patch[descriptor.field]
|
||||||
|
delete (patch as Record<string, unknown>)[descriptor.field]
|
||||||
|
const numericValue = raw === null || raw === undefined ? null : typeof raw === 'number' ? raw : Number(raw)
|
||||||
|
|
||||||
|
const planPatch = summary.quotas[descriptor.planId] ?? {}
|
||||||
|
planPatch[descriptor.key as keyof BillingPlanQuota] =
|
||||||
|
numericValue === null || Number.isNaN(numericValue) ? null : numericValue
|
||||||
|
summary.quotas[descriptor.planId] = planPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const descriptor of BILLING_PLAN_FIELD_DESCRIPTORS.pricing) {
|
||||||
|
if (!(descriptor.field in patch)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
summary.hasUpdates = true
|
||||||
|
const raw = patch[descriptor.field]
|
||||||
|
delete (patch as Record<string, unknown>)[descriptor.field]
|
||||||
|
const planPatch = summary.pricing[descriptor.planId] ?? {}
|
||||||
|
|
||||||
|
if (descriptor.key === 'currency') {
|
||||||
|
const normalized =
|
||||||
|
raw === null || raw === undefined
|
||||||
|
? null
|
||||||
|
: typeof raw === 'string'
|
||||||
|
? this.normalizeNullableString(raw)
|
||||||
|
: this.normalizeNullableString(String(raw))
|
||||||
|
planPatch.currency = normalized
|
||||||
|
} else if (descriptor.key === 'monthlyPrice') {
|
||||||
|
const numericValue = raw === null || raw === undefined ? null : typeof raw === 'number' ? raw : Number(raw)
|
||||||
|
planPatch.monthlyPrice = numericValue === null || Number.isNaN(numericValue) ? null : numericValue
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.pricing[descriptor.planId] = planPatch
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const descriptor of BILLING_PLAN_FIELD_DESCRIPTORS.payment) {
|
||||||
|
if (!(descriptor.field in patch)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
summary.hasUpdates = true
|
||||||
|
const raw = patch[descriptor.field]
|
||||||
|
delete (patch as Record<string, unknown>)[descriptor.field]
|
||||||
|
const normalized =
|
||||||
|
raw === null || raw === undefined
|
||||||
|
? null
|
||||||
|
: typeof raw === 'string'
|
||||||
|
? this.normalizeNullableString(raw)
|
||||||
|
: this.normalizeNullableString(String(raw))
|
||||||
|
summary.products[descriptor.planId] = { creemProductId: normalized }
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applyPlanFieldUpdates(current: SystemSettings, updates: PlanFieldUpdateSummary): Promise<void> {
|
||||||
|
if (!updates.hasUpdates) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updates.quotas).length > 0) {
|
||||||
|
const nextOverrides: BillingPlanOverrides = structuredClone(current.billingPlanOverrides ?? {})
|
||||||
|
for (const [planId, quotaPatch] of Object.entries(updates.quotas) as Array<[
|
||||||
|
BillingPlanId,
|
||||||
|
Partial<BillingPlanQuota>,
|
||||||
|
]>) {
|
||||||
|
const existing = { ...nextOverrides[planId] }
|
||||||
|
for (const [quotaKey, value] of Object.entries(quotaPatch) as Array<[keyof BillingPlanQuota, number | null]>) {
|
||||||
|
if (value === null || value === undefined || Number.isNaN(value)) {
|
||||||
|
delete existing[quotaKey]
|
||||||
|
} else {
|
||||||
|
existing[quotaKey] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(existing).length === 0) {
|
||||||
|
delete nextOverrides[planId]
|
||||||
|
} else {
|
||||||
|
nextOverrides[planId] = existing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.systemSettingStore.set(BILLING_PLAN_OVERRIDES_SETTING_KEY, nextOverrides)
|
||||||
|
current.billingPlanOverrides = nextOverrides
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updates.pricing).length > 0) {
|
||||||
|
const nextPricing: BillingPlanPricingConfigs = structuredClone(current.billingPlanPricing ?? {})
|
||||||
|
for (const [planId, pricingPatch] of Object.entries(updates.pricing) as Array<[
|
||||||
|
BillingPlanId,
|
||||||
|
Partial<BillingPlanPricing>,
|
||||||
|
]>) {
|
||||||
|
const entry: BillingPlanPricing = {
|
||||||
|
monthlyPrice: pricingPatch.monthlyPrice ?? null,
|
||||||
|
currency: pricingPatch.currency ?? null,
|
||||||
|
}
|
||||||
|
if (entry.monthlyPrice === null && !entry.currency) {
|
||||||
|
delete nextPricing[planId]
|
||||||
|
} else {
|
||||||
|
nextPricing[planId] = entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.systemSettingStore.set(BILLING_PLAN_PRICING_SETTING_KEY, nextPricing)
|
||||||
|
current.billingPlanPricing = nextPricing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updates.products).length > 0) {
|
||||||
|
const nextProducts: BillingPlanProductConfigs = structuredClone(current.billingPlanProducts ?? {})
|
||||||
|
for (const [planId, product] of Object.entries(updates.products) as Array<[BillingPlanId, BillingPlanPaymentInfo]>) {
|
||||||
|
const normalized = this.normalizeNullableString(product.creemProductId)
|
||||||
|
if (!normalized) {
|
||||||
|
delete nextProducts[planId]
|
||||||
|
} else {
|
||||||
|
nextProducts[planId] = { creemProductId: normalized }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.systemSettingStore.set(BILLING_PLAN_PRODUCTS_SETTING_KEY, nextProducts)
|
||||||
|
current.billingPlanProducts = nextProducts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async ensureRegistrationAllowed(): Promise<void> {
|
async ensureRegistrationAllowed(): Promise<void> {
|
||||||
const settings = await this.getSettings()
|
const settings = await this.getSettings()
|
||||||
|
|
||||||
@@ -381,3 +550,27 @@ const PLAN_OVERRIDE_ENTRY_SCHEMA = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const BILLING_PLAN_OVERRIDES_SCHEMA = z.record(z.string(), PLAN_OVERRIDE_ENTRY_SCHEMA).default({})
|
const BILLING_PLAN_OVERRIDES_SCHEMA = z.record(z.string(), PLAN_OVERRIDE_ENTRY_SCHEMA).default({})
|
||||||
|
|
||||||
|
const PLAN_PRODUCT_ENTRY_SCHEMA = z.object({
|
||||||
|
creemProductId: z.string().trim().min(1).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const BILLING_PLAN_PRODUCTS_SCHEMA = z.record(z.string(), PLAN_PRODUCT_ENTRY_SCHEMA).default({})
|
||||||
|
|
||||||
|
const PLAN_PRICING_ENTRY_SCHEMA = z.object({
|
||||||
|
monthlyPrice: z.number().min(0).nullable().optional(),
|
||||||
|
currency: z.string().trim().min(1).nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const BILLING_PLAN_PRICING_SCHEMA = z.record(z.string(), PLAN_PRICING_ENTRY_SCHEMA).default({})
|
||||||
|
|
||||||
|
type PlanQuotaUpdateMap = Partial<Record<BillingPlanId, Partial<BillingPlanQuota>>>
|
||||||
|
type PlanPricingUpdateMap = Partial<Record<BillingPlanId, Partial<BillingPlanPricing>>>
|
||||||
|
type PlanProductUpdateMap = Partial<Record<BillingPlanId, BillingPlanPaymentInfo>>
|
||||||
|
|
||||||
|
interface PlanFieldUpdateSummary {
|
||||||
|
hasUpdates: boolean
|
||||||
|
quotas: PlanQuotaUpdateMap
|
||||||
|
pricing: PlanPricingUpdateMap
|
||||||
|
products: PlanProductUpdateMap
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import type {
|
||||||
|
BillingPlanOverrides,
|
||||||
|
BillingPlanPricingConfigs,
|
||||||
|
BillingPlanProductConfigs,
|
||||||
|
} from 'core/modules/platform/billing/billing-plan.types'
|
||||||
import type { UiSchema } from 'core/modules/ui/ui-schema/ui-schema.type'
|
import type { UiSchema } from 'core/modules/ui/ui-schema/ui-schema.type'
|
||||||
|
|
||||||
import type { SystemSettingField } from './system-setting.constants'
|
import type { BillingPlanSettingField, SystemSettingDbField, SystemSettingField } from './system-setting.constants'
|
||||||
|
|
||||||
export interface SystemSettings {
|
export interface SystemSettings {
|
||||||
allowRegistration: boolean
|
allowRegistration: boolean
|
||||||
maxRegistrableUsers: number | null
|
maxRegistrableUsers: number | null
|
||||||
maxPhotoUploadSizeMb: number | null
|
|
||||||
maxDataSyncObjectSizeMb: number | null
|
|
||||||
maxPhotoLibraryItems: number | null
|
|
||||||
localProviderEnabled: boolean
|
localProviderEnabled: boolean
|
||||||
baseDomain: string
|
baseDomain: string
|
||||||
oauthGatewayUrl: string | null
|
oauthGatewayUrl: string | null
|
||||||
@@ -15,11 +17,14 @@ export interface SystemSettings {
|
|||||||
oauthGoogleClientSecret: string | null
|
oauthGoogleClientSecret: string | null
|
||||||
oauthGithubClientId: string | null
|
oauthGithubClientId: string | null
|
||||||
oauthGithubClientSecret: string | null
|
oauthGithubClientSecret: string | null
|
||||||
|
billingPlanOverrides: BillingPlanOverrides
|
||||||
|
billingPlanProducts: BillingPlanProductConfigs
|
||||||
|
billingPlanPricing: BillingPlanPricingConfigs
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SystemSettingValueMap = {
|
export type SystemSettingValueMap = {
|
||||||
[K in SystemSettingField]: SystemSettings[K]
|
[K in SystemSettingDbField]: SystemSettings[K]
|
||||||
}
|
} & Partial<Record<BillingPlanSettingField, string | number | boolean | null>>
|
||||||
|
|
||||||
export interface SystemSettingStats {
|
export interface SystemSettingStats {
|
||||||
totalUsers: number
|
totalUsers: number
|
||||||
@@ -32,6 +37,7 @@ export interface SystemSettingOverview {
|
|||||||
stats: SystemSettingStats
|
stats: SystemSettingStats
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateSystemSettingsInput = Partial<SystemSettings>
|
export type UpdateSystemSettingsInput = Partial<SystemSettings> &
|
||||||
|
Partial<Record<BillingPlanSettingField, string | number | boolean | null | undefined>>
|
||||||
|
|
||||||
export { type SystemSettingField } from './system-setting.constants'
|
export { type SystemSettingField } from './system-setting.constants'
|
||||||
|
|||||||
@@ -1,9 +1,113 @@
|
|||||||
|
import { BILLING_PLAN_DEFINITIONS, BILLING_PLAN_IDS } from 'core/modules/platform/billing/billing-plan.constants'
|
||||||
import type { UiNode, UiSchema } from 'core/modules/ui/ui-schema/ui-schema.type'
|
import type { UiNode, UiSchema } from 'core/modules/ui/ui-schema/ui-schema.type'
|
||||||
|
|
||||||
import type { SystemSettingField } from './system-setting.constants'
|
import type { SystemSettingField } from './system-setting.constants'
|
||||||
|
|
||||||
export const SYSTEM_SETTING_UI_SCHEMA_VERSION = '1.4.0'
|
export const SYSTEM_SETTING_UI_SCHEMA_VERSION = '1.4.0'
|
||||||
|
|
||||||
|
const PLAN_QUOTA_FIELDS = [
|
||||||
|
{
|
||||||
|
suffix: 'monthlyAssetProcessLimit',
|
||||||
|
title: '每月可新增照片 (张)',
|
||||||
|
description: '达到上限后将阻止新增照片。留空表示回退到默认值或不限。',
|
||||||
|
placeholder: '例如 300',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
suffix: 'libraryItemLimit',
|
||||||
|
title: '图库容量限制 (张)',
|
||||||
|
description: '限制单个租户可管理的照片数量,0 表示完全禁止新增。',
|
||||||
|
placeholder: '例如 500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
suffix: 'maxUploadSizeMb',
|
||||||
|
title: '后台上传大小上限 (MB)',
|
||||||
|
description: '单次上传的最大文件体积,留空表示默认值或无限制。',
|
||||||
|
placeholder: '例如 20',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
suffix: 'maxSyncObjectSizeMb',
|
||||||
|
title: '同步素材大小上限 (MB)',
|
||||||
|
description: 'Data Sync 导入时允许的最大文件尺寸。',
|
||||||
|
placeholder: '例如 50',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const PLAN_PRICING_FIELDS = [
|
||||||
|
{
|
||||||
|
suffix: 'monthlyPrice',
|
||||||
|
title: '月度定价',
|
||||||
|
description: '用于展示的价格或 Creem 产品价格,留空保留默认值。',
|
||||||
|
placeholder: '例如 49',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
suffix: 'currency',
|
||||||
|
title: '币种',
|
||||||
|
description: 'ISO 货币代码,如 CNY、USD 等。',
|
||||||
|
placeholder: 'CNY',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const PLAN_PAYMENT_FIELDS = [
|
||||||
|
{
|
||||||
|
suffix: 'creemProductId',
|
||||||
|
title: 'Creem Product ID',
|
||||||
|
description: '用于创建结算会话的 Creem 商品 ID。留空表示该计划不会显示升级入口。',
|
||||||
|
placeholder: 'prod_xxx',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const BILLING_PLAN_GROUPS: ReadonlyArray<UiNode<SystemSettingField>> = BILLING_PLAN_IDS.map((planId) => {
|
||||||
|
const metadata = BILLING_PLAN_DEFINITIONS[planId]
|
||||||
|
const quotaFields = PLAN_QUOTA_FIELDS.map((field) => ({
|
||||||
|
type: 'field' as const,
|
||||||
|
id: `${planId}-${field.suffix}`,
|
||||||
|
title: field.title,
|
||||||
|
description: field.description,
|
||||||
|
helperText: '留空表示遵循默认或不限,填写数字后将覆盖对应计划。',
|
||||||
|
key: `billingPlan.${planId}.quota.${field.suffix}` as SystemSettingField,
|
||||||
|
component: {
|
||||||
|
type: 'text' as const,
|
||||||
|
inputType: 'number' as const,
|
||||||
|
placeholder: field.placeholder,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const pricingFields = PLAN_PRICING_FIELDS.map((field) => ({
|
||||||
|
type: 'field' as const,
|
||||||
|
id: `${planId}-pricing-${field.suffix}`,
|
||||||
|
title: field.title,
|
||||||
|
description: field.description,
|
||||||
|
helperText: field.suffix === 'monthlyPrice' ? '留空表示暂不展示定价信息。' : '留空表示使用默认币种或不展示。',
|
||||||
|
key: `billingPlan.${planId}.pricing.${field.suffix}` as SystemSettingField,
|
||||||
|
component: {
|
||||||
|
type: 'text' as const,
|
||||||
|
inputType: field.suffix === 'monthlyPrice' ? ('number' as const) : ('text' as const),
|
||||||
|
placeholder: field.placeholder,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const paymentFields = PLAN_PAYMENT_FIELDS.map((field) => ({
|
||||||
|
type: 'field' as const,
|
||||||
|
id: `${planId}-payment-${field.suffix}`,
|
||||||
|
title: field.title,
|
||||||
|
description: field.description,
|
||||||
|
helperText: '为空将隐藏升级入口。',
|
||||||
|
key: `billingPlan.${planId}.payment.${field.suffix}` as SystemSettingField,
|
||||||
|
component: {
|
||||||
|
type: 'text' as const,
|
||||||
|
placeholder: field.placeholder,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'group' as const,
|
||||||
|
id: `billing-plan-${planId}`,
|
||||||
|
title: `${metadata.name} (${planId})`,
|
||||||
|
description: metadata.description,
|
||||||
|
children: [...quotaFields, ...pricingFields, ...paymentFields],
|
||||||
|
} satisfies UiNode<SystemSettingField>
|
||||||
|
})
|
||||||
|
|
||||||
export const SYSTEM_SETTING_UI_SCHEMA: UiSchema<SystemSettingField> = {
|
export const SYSTEM_SETTING_UI_SCHEMA: UiSchema<SystemSettingField> = {
|
||||||
version: SYSTEM_SETTING_UI_SCHEMA_VERSION,
|
version: SYSTEM_SETTING_UI_SCHEMA_VERSION,
|
||||||
title: '系统设置',
|
title: '系统设置',
|
||||||
@@ -56,7 +160,7 @@ export const SYSTEM_SETTING_UI_SCHEMA: UiSchema<SystemSettingField> = {
|
|||||||
helperText: '设置为 0 时将立即阻止新的用户注册。',
|
helperText: '设置为 0 时将立即阻止新的用户注册。',
|
||||||
key: 'maxRegistrableUsers',
|
key: 'maxRegistrableUsers',
|
||||||
component: {
|
component: {
|
||||||
type: 'text',
|
type: 'text' as const,
|
||||||
inputType: 'number',
|
inputType: 'number',
|
||||||
placeholder: '无限制',
|
placeholder: '无限制',
|
||||||
},
|
},
|
||||||
@@ -65,51 +169,11 @@ export const SYSTEM_SETTING_UI_SCHEMA: UiSchema<SystemSettingField> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
id: 'photo-constraints',
|
id: 'billing-plan-settings',
|
||||||
title: '照片库资源限制',
|
title: '订阅计划配置',
|
||||||
description: '统一设置照片上传、同步及照片总量的上限,确保资源消耗在可控范围内。',
|
description: '为每个订阅计划定义资源限制、展示价格以及 Creem 商品映射。',
|
||||||
icon: 'image-up',
|
icon: 'badge-dollar-sign',
|
||||||
children: [
|
children: BILLING_PLAN_GROUPS,
|
||||||
{
|
|
||||||
type: 'field',
|
|
||||||
id: 'photo-upload-max-size',
|
|
||||||
title: '单张上传大小上限 (MB)',
|
|
||||||
description: '限制用户通过后台上传的照片文件体积,超出限制将被拒绝。',
|
|
||||||
helperText: '留空表示不限制,最小值 1 MB。',
|
|
||||||
key: 'maxPhotoUploadSizeMb',
|
|
||||||
component: {
|
|
||||||
type: 'text',
|
|
||||||
inputType: 'number',
|
|
||||||
placeholder: '无限制',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'field',
|
|
||||||
id: 'photo-sync-max-size',
|
|
||||||
title: 'Data Sync 单文件上限 (MB)',
|
|
||||||
description: '控制数据同步时允许导入的存储文件大小,避免超大素材拖慢同步。',
|
|
||||||
helperText: '留空表示不限制,最小值 1 MB。',
|
|
||||||
key: 'maxDataSyncObjectSizeMb',
|
|
||||||
component: {
|
|
||||||
type: 'text',
|
|
||||||
inputType: 'number',
|
|
||||||
placeholder: '无限制',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'field',
|
|
||||||
id: 'photo-library-max-items',
|
|
||||||
title: '单租户可管理照片数量',
|
|
||||||
description: '达到上限后用户无法再新增图片,可留空表示不限制。',
|
|
||||||
helperText: '设置为 0 将阻止任何新增图片。',
|
|
||||||
key: 'maxPhotoLibraryItems',
|
|
||||||
component: {
|
|
||||||
type: 'text',
|
|
||||||
inputType: 'number',
|
|
||||||
placeholder: '无限制',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'section',
|
type: 'section',
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { CURRENT_PHOTO_MANIFEST_VERSION, DATABASE_ONLY_PROVIDER, photoAssets } f
|
|||||||
import { EventEmitterService } from '@afilmory/framework'
|
import { EventEmitterService } from '@afilmory/framework'
|
||||||
import { DbAccessor } from 'core/database/database.provider'
|
import { DbAccessor } from 'core/database/database.provider'
|
||||||
import { BizException, ErrorCode } from 'core/errors'
|
import { BizException, ErrorCode } from 'core/errors'
|
||||||
import { SystemSettingService } from 'core/modules/configuration/system-setting/system-setting.service'
|
|
||||||
import { runWithBuilderLogRelay } from 'core/modules/infrastructure/data-sync/builder-log-relay'
|
import { runWithBuilderLogRelay } from 'core/modules/infrastructure/data-sync/builder-log-relay'
|
||||||
import type {
|
import type {
|
||||||
DataSyncAction,
|
DataSyncAction,
|
||||||
@@ -94,7 +93,6 @@ export class PhotoAssetService {
|
|||||||
private readonly dbAccessor: DbAccessor,
|
private readonly dbAccessor: DbAccessor,
|
||||||
private readonly photoBuilderService: PhotoBuilderService,
|
private readonly photoBuilderService: PhotoBuilderService,
|
||||||
private readonly photoStorageService: PhotoStorageService,
|
private readonly photoStorageService: PhotoStorageService,
|
||||||
private readonly systemSettingService: SystemSettingService,
|
|
||||||
private readonly billingPlanService: BillingPlanService,
|
private readonly billingPlanService: BillingPlanService,
|
||||||
private readonly billingUsageService: BillingUsageService,
|
private readonly billingUsageService: BillingUsageService,
|
||||||
) {}
|
) {}
|
||||||
@@ -264,9 +262,8 @@ export class PhotoAssetService {
|
|||||||
|
|
||||||
const tenant = requireTenantContext()
|
const tenant = requireTenantContext()
|
||||||
const db = this.dbAccessor.get()
|
const db = this.dbAccessor.get()
|
||||||
const systemSettings = await this.systemSettingService.getSettings()
|
|
||||||
const planQuota = await this.billingPlanService.getQuotaForTenant(tenant.tenant.id)
|
const planQuota = await this.billingPlanService.getQuotaForTenant(tenant.tenant.id)
|
||||||
const uploadSizeLimit = planQuota.maxUploadSizeMb ?? systemSettings.maxPhotoUploadSizeMb
|
const uploadSizeLimit = planQuota.maxUploadSizeMb
|
||||||
this.enforceUploadSizeLimit(inputs, uploadSizeLimit)
|
this.enforceUploadSizeLimit(inputs, uploadSizeLimit)
|
||||||
const { builderConfig, storageConfig } = await this.photoStorageService.resolveConfigForTenant(tenant.tenant.id)
|
const { builderConfig, storageConfig } = await this.photoStorageService.resolveConfigForTenant(tenant.tenant.id)
|
||||||
|
|
||||||
@@ -333,7 +330,7 @@ export class PhotoAssetService {
|
|||||||
|
|
||||||
const pendingPhotoPlans = photoPlans.filter((plan) => !existingPhotoKeySet.has(plan.storageKey))
|
const pendingPhotoPlans = photoPlans.filter((plan) => !existingPhotoKeySet.has(plan.storageKey))
|
||||||
await this.billingPlanService.ensurePhotoProcessingAllowance(tenant.tenant.id, pendingPhotoPlans.length)
|
await this.billingPlanService.ensurePhotoProcessingAllowance(tenant.tenant.id, pendingPhotoPlans.length)
|
||||||
const libraryLimit = planQuota.libraryItemLimit ?? systemSettings.maxPhotoLibraryItems
|
const libraryLimit = planQuota.libraryItemLimit
|
||||||
await this.ensurePhotoLibraryCapacity(tenant.tenant.id, db, pendingPhotoPlans.length, libraryLimit)
|
await this.ensurePhotoLibraryCapacity(tenant.tenant.id, db, pendingPhotoPlans.length, libraryLimit)
|
||||||
throwIfAborted()
|
throwIfAborted()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { CURRENT_PHOTO_MANIFEST_VERSION, DATABASE_ONLY_PROVIDER, photoAssets, ph
|
|||||||
import { createLogger, EventEmitterService } from '@afilmory/framework'
|
import { createLogger, EventEmitterService } from '@afilmory/framework'
|
||||||
import { DbAccessor } from 'core/database/database.provider'
|
import { DbAccessor } from 'core/database/database.provider'
|
||||||
import { BizException, ErrorCode } from 'core/errors'
|
import { BizException, ErrorCode } from 'core/errors'
|
||||||
import { SystemSettingService } from 'core/modules/configuration/system-setting/system-setting.service'
|
|
||||||
import { PhotoBuilderService } from 'core/modules/content/photo/builder/photo-builder.service'
|
import { PhotoBuilderService } from 'core/modules/content/photo/builder/photo-builder.service'
|
||||||
import { PhotoStorageService } from 'core/modules/content/photo/storage/photo-storage.service'
|
import { PhotoStorageService } from 'core/modules/content/photo/storage/photo-storage.service'
|
||||||
import { BILLING_USAGE_EVENT } from 'core/modules/platform/billing/billing.constants'
|
import { BILLING_USAGE_EVENT } from 'core/modules/platform/billing/billing.constants'
|
||||||
@@ -78,7 +77,6 @@ export class DataSyncService {
|
|||||||
private readonly dbAccessor: DbAccessor,
|
private readonly dbAccessor: DbAccessor,
|
||||||
private readonly photoBuilderService: PhotoBuilderService,
|
private readonly photoBuilderService: PhotoBuilderService,
|
||||||
private readonly photoStorageService: PhotoStorageService,
|
private readonly photoStorageService: PhotoStorageService,
|
||||||
private readonly systemSettingService: SystemSettingService,
|
|
||||||
private readonly billingPlanService: BillingPlanService,
|
private readonly billingPlanService: BillingPlanService,
|
||||||
private readonly billingUsageService: BillingUsageService,
|
private readonly billingUsageService: BillingUsageService,
|
||||||
) {}
|
) {}
|
||||||
@@ -90,13 +88,12 @@ export class DataSyncService {
|
|||||||
async runSync(options: DataSyncOptions, onProgress?: DataSyncProgressEmitter): Promise<DataSyncResult> {
|
async runSync(options: DataSyncOptions, onProgress?: DataSyncProgressEmitter): Promise<DataSyncResult> {
|
||||||
const tenant = requireTenantContext()
|
const tenant = requireTenantContext()
|
||||||
const runStartedAt = new Date()
|
const runStartedAt = new Date()
|
||||||
const systemSettings = await this.systemSettingService.getSettings()
|
|
||||||
const planQuota = await this.billingPlanService.getQuotaForTenant(tenant.tenant.id)
|
const planQuota = await this.billingPlanService.getQuotaForTenant(tenant.tenant.id)
|
||||||
const effectiveMaxObjectMb = planQuota.maxSyncObjectSizeMb ?? systemSettings.maxDataSyncObjectSizeMb
|
const effectiveMaxObjectMb = planQuota.maxSyncObjectSizeMb
|
||||||
const syncLimits = {
|
const syncLimits = {
|
||||||
maxObjectBytes: this.convertMbToBytes(effectiveMaxObjectMb),
|
maxObjectBytes: this.convertMbToBytes(effectiveMaxObjectMb),
|
||||||
maxObjectSizeMb: effectiveMaxObjectMb,
|
maxObjectSizeMb: effectiveMaxObjectMb,
|
||||||
libraryLimit: planQuota.libraryItemLimit ?? systemSettings.maxPhotoLibraryItems,
|
libraryLimit: planQuota.libraryItemLimit,
|
||||||
}
|
}
|
||||||
const { builderConfig, storageConfig } = await this.resolveBuilderConfigForTenant(tenant.tenant.id, options)
|
const { builderConfig, storageConfig } = await this.resolveBuilderConfigForTenant(tenant.tenant.id, options)
|
||||||
const context = await this.prepareSyncContext(tenant.tenant.id, builderConfig, storageConfig)
|
const context = await this.prepareSyncContext(tenant.tenant.id, builderConfig, storageConfig)
|
||||||
|
|||||||
@@ -149,7 +149,11 @@ export class AuthController {
|
|||||||
return {
|
return {
|
||||||
user: authContext.user,
|
user: authContext.user,
|
||||||
session: authContext.session,
|
session: authContext.session,
|
||||||
tenant: tenantContext,
|
tenant: {
|
||||||
|
isPlaceholder: tenantContext.isPlaceholder,
|
||||||
|
requestedSlug: tenantContext.requestedSlug,
|
||||||
|
...tenantContext.tenant,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
|
|
||||||
import { authAccounts, authSessions, authUsers, authVerifications, generateId } from '@afilmory/db'
|
import { authAccounts, authSessions, authUsers, authVerifications, generateId } from '@afilmory/db'
|
||||||
|
import { env } from '@afilmory/env'
|
||||||
import type { OnModuleInit } from '@afilmory/framework'
|
import type { OnModuleInit } from '@afilmory/framework'
|
||||||
import { createLogger, HttpContext } from '@afilmory/framework'
|
import { createLogger, HttpContext } from '@afilmory/framework'
|
||||||
|
import { creem } from '@creem_io/better-auth'
|
||||||
import { betterAuth } from 'better-auth'
|
import { betterAuth } from 'better-auth'
|
||||||
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
|
||||||
import { APIError, createAuthMiddleware } from 'better-auth/api'
|
import { APIError, createAuthMiddleware } from 'better-auth/api'
|
||||||
@@ -10,6 +12,9 @@ import { admin } from 'better-auth/plugins'
|
|||||||
import { DrizzleProvider } from 'core/database/database.provider'
|
import { DrizzleProvider } from 'core/database/database.provider'
|
||||||
import { BizException } from 'core/errors'
|
import { BizException } from 'core/errors'
|
||||||
import { SystemSettingService } from 'core/modules/configuration/system-setting/system-setting.service'
|
import { SystemSettingService } from 'core/modules/configuration/system-setting/system-setting.service'
|
||||||
|
import { BILLING_PLAN_IDS } from 'core/modules/platform/billing/billing-plan.constants'
|
||||||
|
import { BillingPlanService } from 'core/modules/platform/billing/billing-plan.service'
|
||||||
|
import type { BillingPlanId } from 'core/modules/platform/billing/billing-plan.types'
|
||||||
import type { Context } from 'hono'
|
import type { Context } from 'hono'
|
||||||
import { injectable } from 'tsyringe'
|
import { injectable } from 'tsyringe'
|
||||||
|
|
||||||
@@ -33,6 +38,7 @@ export class AuthProvider implements OnModuleInit {
|
|||||||
private readonly drizzleProvider: DrizzleProvider,
|
private readonly drizzleProvider: DrizzleProvider,
|
||||||
private readonly systemSettings: SystemSettingService,
|
private readonly systemSettings: SystemSettingService,
|
||||||
private readonly tenantService: TenantService,
|
private readonly tenantService: TenantService,
|
||||||
|
private readonly billingPlanService: BillingPlanService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onModuleInit(): Promise<void> {
|
async onModuleInit(): Promise<void> {
|
||||||
@@ -294,6 +300,70 @@ export class AuthProvider implements OnModuleInit {
|
|||||||
defaultRole: 'user',
|
defaultRole: 'user',
|
||||||
defaultBanReason: 'Spamming',
|
defaultBanReason: 'Spamming',
|
||||||
}),
|
}),
|
||||||
|
creem({
|
||||||
|
apiKey: env.CREEM_API_KEY,
|
||||||
|
webhookSecret: env.CREEM_WEBHOOK_SECRET,
|
||||||
|
persistSubscriptions: true,
|
||||||
|
schema: {
|
||||||
|
user: {
|
||||||
|
modelName: 'auth_user',
|
||||||
|
fields: {
|
||||||
|
creemCustomerId: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'creem_customer_id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subscription: {
|
||||||
|
modelName: 'creem_subscription',
|
||||||
|
fields: {
|
||||||
|
productId: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'product_id',
|
||||||
|
},
|
||||||
|
referenceId: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'reference_id',
|
||||||
|
},
|
||||||
|
creemCustomerId: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'creem_customer_id',
|
||||||
|
},
|
||||||
|
creemSubscriptionId: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'creem_subscription_id',
|
||||||
|
},
|
||||||
|
creemOrderId: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'creem_order_id',
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'status',
|
||||||
|
},
|
||||||
|
periodStart: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'period_start',
|
||||||
|
},
|
||||||
|
periodEnd: {
|
||||||
|
type: 'string',
|
||||||
|
fieldName: 'period_end',
|
||||||
|
},
|
||||||
|
cancelAtPeriodEnd: {
|
||||||
|
type: 'boolean',
|
||||||
|
fieldName: 'cancel_at_period_end',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testMode: env.NODE_ENV !== 'production',
|
||||||
|
onGrantAccess: async ({ metadata }) => {
|
||||||
|
await this.handleCreemGrant(metadata)
|
||||||
|
},
|
||||||
|
onRevokeAccess: async ({ metadata }) => {
|
||||||
|
await this.handleCreemRevoke(metadata)
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
before: createAuthMiddleware(async (ctx) => {
|
before: createAuthMiddleware(async (ctx) => {
|
||||||
@@ -367,6 +437,61 @@ export class AuthProvider implements OnModuleInit {
|
|||||||
return hash.digest('hex')
|
return hash.digest('hex')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async handleCreemGrant(metadata?: Record<string, unknown>): Promise<void> {
|
||||||
|
const tenantId = this.extractMetadataValue(metadata, 'tenantId')
|
||||||
|
const planId = this.extractPlanIdFromMetadata(metadata)
|
||||||
|
|
||||||
|
if (!tenantId || !planId) {
|
||||||
|
logger.warn('[AuthProvider] Creem grant event missing tenantId or planId metadata')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.billingPlanService.updateTenantPlan(tenantId, planId)
|
||||||
|
logger.info(`[AuthProvider] Tenant ${tenantId} upgraded to ${planId} via Creem`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[AuthProvider] Failed to update tenant ${tenantId} plan from Creem grant`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleCreemRevoke(metadata?: Record<string, unknown>): Promise<void> {
|
||||||
|
const tenantId = this.extractMetadataValue(metadata, 'tenantId')
|
||||||
|
if (!tenantId) {
|
||||||
|
logger.warn('[AuthProvider] Creem revoke event missing tenantId metadata')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.billingPlanService.updateTenantPlan(tenantId, 'free')
|
||||||
|
logger.info(`[AuthProvider] Tenant ${tenantId} downgraded to free via Creem revoke`)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[AuthProvider] Failed to downgrade tenant ${tenantId} after Creem revoke`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractPlanIdFromMetadata(metadata?: Record<string, unknown>): BillingPlanId | null {
|
||||||
|
const planId = this.extractMetadataValue(metadata, 'planId')
|
||||||
|
if (!planId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (BILLING_PLAN_IDS.includes(planId as BillingPlanId)) {
|
||||||
|
return planId as BillingPlanId
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractMetadataValue(metadata: Record<string, unknown> | undefined, key: string): string | null {
|
||||||
|
if (!metadata) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const raw = metadata[key]
|
||||||
|
if (typeof raw !== 'string') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const trimmed = raw.trim()
|
||||||
|
return trimmed.length > 0 ? trimmed : null
|
||||||
|
}
|
||||||
|
|
||||||
async handler(context: Context): Promise<Response> {
|
async handler(context: Context): Promise<Response> {
|
||||||
const requestPath = typeof context.req.path === 'string' ? context.req.path : new URL(context.req.url).pathname
|
const requestPath = typeof context.req.path === 'string' ? context.req.path : new URL(context.req.url).pathname
|
||||||
if (requestPath.startsWith('/api/auth/error')) {
|
if (requestPath.startsWith('/api/auth/error')) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { BillingPlanDefinition, BillingPlanId } from './billing-plan.types'
|
import type { BillingPlanDefinition, BillingPlanId } from './billing-plan.types'
|
||||||
|
|
||||||
export const BILLING_PLAN_IDS: readonly BillingPlanId[] = ['free', 'pro', 'friend']
|
export const BILLING_PLAN_IDS: readonly BillingPlanId[] = ['free', 'pro', 'friend']
|
||||||
export const PUBLIC_PLAN_IDS: readonly BillingPlanId[] = ['free']
|
|
||||||
|
|
||||||
export const BILLING_PLAN_DEFINITIONS: Record<BillingPlanId, BillingPlanDefinition> = {
|
export const BILLING_PLAN_DEFINITIONS: Record<BillingPlanId, BillingPlanDefinition> = {
|
||||||
free: {
|
free: {
|
||||||
@@ -28,7 +27,7 @@ export const BILLING_PLAN_DEFINITIONS: Record<BillingPlanId, BillingPlanDefiniti
|
|||||||
},
|
},
|
||||||
friend: {
|
friend: {
|
||||||
id: 'friend',
|
id: 'friend',
|
||||||
name: 'Friend (Internal)',
|
name: 'Friend',
|
||||||
description: '内部使用的好友方案,没有任何限制,仅超级管理员可设置。',
|
description: '内部使用的好友方案,没有任何限制,仅超级管理员可设置。',
|
||||||
quotas: {
|
quotas: {
|
||||||
monthlyAssetProcessLimit: null,
|
monthlyAssetProcessLimit: null,
|
||||||
@@ -40,3 +39,5 @@ export const BILLING_PLAN_DEFINITIONS: Record<BillingPlanId, BillingPlanDefiniti
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BILLING_PLAN_OVERRIDES_SETTING_KEY = 'system.billing.planOverrides'
|
export const BILLING_PLAN_OVERRIDES_SETTING_KEY = 'system.billing.planOverrides'
|
||||||
|
export const BILLING_PLAN_PRODUCTS_SETTING_KEY = 'system.billing.planProducts'
|
||||||
|
export const BILLING_PLAN_PRICING_SETTING_KEY = 'system.billing.planPricing'
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ import { eq } from 'drizzle-orm'
|
|||||||
import { injectable } from 'tsyringe'
|
import { injectable } from 'tsyringe'
|
||||||
|
|
||||||
import { BILLING_USAGE_EVENT } from './billing.constants'
|
import { BILLING_USAGE_EVENT } from './billing.constants'
|
||||||
import { BILLING_PLAN_DEFINITIONS, BILLING_PLAN_IDS, PUBLIC_PLAN_IDS } from './billing-plan.constants'
|
import { BILLING_PLAN_DEFINITIONS, BILLING_PLAN_IDS } from './billing-plan.constants'
|
||||||
import type {
|
import type {
|
||||||
BillingPlanDefinition,
|
BillingPlanDefinition,
|
||||||
BillingPlanId,
|
BillingPlanId,
|
||||||
BillingPlanOverrides,
|
BillingPlanOverrides,
|
||||||
|
BillingPlanPaymentInfo,
|
||||||
|
BillingPlanPricing,
|
||||||
|
BillingPlanPricingConfigs,
|
||||||
|
BillingPlanProductConfigs,
|
||||||
BillingPlanQuota,
|
BillingPlanQuota,
|
||||||
BillingPlanQuotaOverride,
|
BillingPlanQuotaOverride,
|
||||||
} from './billing-plan.types'
|
} from './billing-plan.types'
|
||||||
@@ -62,29 +66,42 @@ export class BillingPlanService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPlanSummaryForTenant(tenantId: string): Promise<BillingPlanSummary> {
|
async getPlanSummaryForTenant(tenantId: string): Promise<BillingPlanSummary> {
|
||||||
const planId = await this.resolvePlanIdForTenant(tenantId)
|
const [planId, overrides, productConfigs, pricingConfigs] = await Promise.all([
|
||||||
|
this.resolvePlanIdForTenant(tenantId),
|
||||||
|
this.getPlanOverrides(),
|
||||||
|
this.getPlanProducts(),
|
||||||
|
this.getPlanPricing(),
|
||||||
|
])
|
||||||
|
|
||||||
const definition = BILLING_PLAN_DEFINITIONS[planId]
|
const definition = BILLING_PLAN_DEFINITIONS[planId]
|
||||||
const overrides = await this.getPlanOverrides()
|
|
||||||
const quotas = this.applyOverrides(definition.quotas, overrides[planId])
|
const quotas = this.applyOverrides(definition.quotas, overrides[planId])
|
||||||
return {
|
return {
|
||||||
planId,
|
planId,
|
||||||
name: definition.name,
|
name: definition.name,
|
||||||
description: definition.description,
|
description: definition.description,
|
||||||
quotas,
|
quotas,
|
||||||
|
payment: this.buildPaymentInfo(productConfigs[planId]),
|
||||||
|
pricing: this.buildPricingInfo(pricingConfigs[planId]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPublicPlanSummaries(): Promise<BillingPlanSummary[]> {
|
async getPublicPlanSummaries(): Promise<BillingPlanSummary[]> {
|
||||||
const overrides = await this.getPlanOverrides()
|
const [overrides, productConfigs, pricingConfigs] = await Promise.all([
|
||||||
return PUBLIC_PLAN_IDS.map((id) => {
|
this.getPlanOverrides(),
|
||||||
|
this.getPlanProducts(),
|
||||||
|
this.getPlanPricing(),
|
||||||
|
])
|
||||||
|
return BILLING_PLAN_IDS.map((id) => {
|
||||||
const definition = BILLING_PLAN_DEFINITIONS[id]
|
const definition = BILLING_PLAN_DEFINITIONS[id]
|
||||||
return {
|
return {
|
||||||
planId: id,
|
planId: id,
|
||||||
name: definition.name,
|
name: definition.name,
|
||||||
description: definition.description,
|
description: definition.description,
|
||||||
quotas: this.applyOverrides(definition.quotas, overrides[id]),
|
quotas: this.applyOverrides(definition.quotas, overrides[id]),
|
||||||
|
payment: this.buildPaymentInfo(productConfigs[id]),
|
||||||
|
pricing: this.buildPricingInfo(pricingConfigs[id]),
|
||||||
}
|
}
|
||||||
})
|
}).filter((plan) => this.shouldExposePlan(plan.planId, plan.payment))
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensurePhotoProcessingAllowance(tenantId: string, incomingItems: number): Promise<void> {
|
async ensurePhotoProcessingAllowance(tenantId: string, incomingItems: number): Promise<void> {
|
||||||
@@ -129,6 +146,14 @@ export class BillingPlanService {
|
|||||||
return await this.systemSettingService.getBillingPlanOverrides()
|
return await this.systemSettingService.getBillingPlanOverrides()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getPlanProducts(): Promise<BillingPlanProductConfigs> {
|
||||||
|
return await this.systemSettingService.getBillingPlanProducts()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getPlanPricing(): Promise<BillingPlanPricingConfigs> {
|
||||||
|
return await this.systemSettingService.getBillingPlanPricing()
|
||||||
|
}
|
||||||
|
|
||||||
private applyOverrides(base: BillingPlanQuota, override?: BillingPlanQuotaOverride): BillingPlanQuota {
|
private applyOverrides(base: BillingPlanQuota, override?: BillingPlanQuotaOverride): BillingPlanQuota {
|
||||||
if (!override) {
|
if (!override) {
|
||||||
return { ...base }
|
return { ...base }
|
||||||
@@ -144,6 +169,48 @@ export class BillingPlanService {
|
|||||||
override.maxSyncObjectSizeMb !== undefined ? override.maxSyncObjectSizeMb : base.maxSyncObjectSizeMb,
|
override.maxSyncObjectSizeMb !== undefined ? override.maxSyncObjectSizeMb : base.maxSyncObjectSizeMb,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildPaymentInfo(entry?: BillingPlanPaymentInfo): BillingPlanPaymentInfo | undefined {
|
||||||
|
if (!entry) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const creemProductId = this.normalizeString(entry.creemProductId)
|
||||||
|
if (!creemProductId) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return { creemProductId }
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldExposePlan(planId: BillingPlanId, payment?: BillingPlanPaymentInfo): boolean {
|
||||||
|
if (planId === 'free') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(payment?.creemProductId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildPricingInfo(entry?: BillingPlanPricing): BillingPlanPricing | undefined {
|
||||||
|
if (!entry) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const hasPrice = entry.monthlyPrice !== null && !Number.isNaN(entry.monthlyPrice ?? undefined)
|
||||||
|
const hasCurrency = !!entry.currency
|
||||||
|
if (!hasPrice && !hasCurrency) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
monthlyPrice: hasPrice ? entry.monthlyPrice : null,
|
||||||
|
currency: entry.currency ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeString(value?: string | null): string | null {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const trimmed = value.trim()
|
||||||
|
return trimmed.length > 0 ? trimmed : null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BillingPlanSummary {
|
export interface BillingPlanSummary {
|
||||||
@@ -151,4 +218,6 @@ export interface BillingPlanSummary {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
quotas: BillingPlanQuota
|
quotas: BillingPlanQuota
|
||||||
|
pricing?: BillingPlanPricing
|
||||||
|
payment?: BillingPlanPaymentInfo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,3 +17,16 @@ export interface BillingPlanDefinition {
|
|||||||
export type BillingPlanQuotaOverride = Partial<BillingPlanQuota>
|
export type BillingPlanQuotaOverride = Partial<BillingPlanQuota>
|
||||||
|
|
||||||
export type BillingPlanOverrides = Record<BillingPlanId | string, BillingPlanQuotaOverride>
|
export type BillingPlanOverrides = Record<BillingPlanId | string, BillingPlanQuotaOverride>
|
||||||
|
|
||||||
|
export interface BillingPlanPaymentInfo {
|
||||||
|
creemProductId?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BillingPlanProductConfigs = Record<BillingPlanId | string, BillingPlanPaymentInfo | undefined>
|
||||||
|
|
||||||
|
export interface BillingPlanPricing {
|
||||||
|
monthlyPrice: number | null
|
||||||
|
currency: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BillingPlanPricingConfigs = Record<BillingPlanId | string, BillingPlanPricing | undefined>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { BillingUsageOverview } from './billing-usage.service'
|
|||||||
import { BillingUsageService } from './billing-usage.service'
|
import { BillingUsageService } from './billing-usage.service'
|
||||||
|
|
||||||
const usageQuerySchema = z.object({
|
const usageQuerySchema = z.object({
|
||||||
limit: z.number().optional(),
|
limit: z.coerce.number().positive().int().optional().default(10),
|
||||||
})
|
})
|
||||||
class UsageQueryDto extends createZodSchemaDto(usageQuerySchema) {}
|
class UsageQueryDto extends createZodSchemaDto(usageQuerySchema) {}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,38 @@ import { BILLING_PLAN_IDS } from 'core/modules/platform/billing/billing-plan.con
|
|||||||
import type { BillingPlanId } from 'core/modules/platform/billing/billing-plan.types'
|
import type { BillingPlanId } from 'core/modules/platform/billing/billing-plan.types'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const planQuotaFields = (() => {
|
||||||
|
const fields: Record<string, z.ZodTypeAny> = {}
|
||||||
|
for (const planId of BILLING_PLAN_IDS) {
|
||||||
|
fields[`billingPlan.${planId}.quota.monthlyAssetProcessLimit`] = z.number().int().min(0).nullable().optional()
|
||||||
|
fields[`billingPlan.${planId}.quota.libraryItemLimit`] = z.number().int().min(0).nullable().optional()
|
||||||
|
fields[`billingPlan.${planId}.quota.maxUploadSizeMb`] = z.number().int().min(1).nullable().optional()
|
||||||
|
fields[`billingPlan.${planId}.quota.maxSyncObjectSizeMb`] = z.number().int().min(1).nullable().optional()
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
})()
|
||||||
|
|
||||||
|
const planPricingFields = (() => {
|
||||||
|
const fields: Record<string, z.ZodTypeAny> = {}
|
||||||
|
for (const planId of BILLING_PLAN_IDS) {
|
||||||
|
fields[`billingPlan.${planId}.pricing.monthlyPrice`] = z.number().min(0).nullable().optional()
|
||||||
|
fields[`billingPlan.${planId}.pricing.currency`] = z.string().trim().min(1).nullable().optional()
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
})()
|
||||||
|
|
||||||
|
const planProductFields = (() => {
|
||||||
|
const fields: Record<string, z.ZodTypeAny> = {}
|
||||||
|
for (const planId of BILLING_PLAN_IDS) {
|
||||||
|
fields[`billingPlan.${planId}.payment.creemProductId`] = z.string().trim().min(1).nullable().optional()
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
})()
|
||||||
|
|
||||||
const updateSuperAdminSettingsSchema = z
|
const updateSuperAdminSettingsSchema = z
|
||||||
.object({
|
.object({
|
||||||
allowRegistration: z.boolean().optional(),
|
allowRegistration: z.boolean().optional(),
|
||||||
maxRegistrableUsers: z.number().int().min(0).nullable().optional(),
|
maxRegistrableUsers: z.number().int().min(0).nullable().optional(),
|
||||||
maxPhotoUploadSizeMb: z.number().int().positive().nullable().optional(),
|
|
||||||
maxDataSyncObjectSizeMb: z.number().int().positive().nullable().optional(),
|
|
||||||
maxPhotoLibraryItems: z.number().int().min(0).nullable().optional(),
|
|
||||||
localProviderEnabled: z.boolean().optional(),
|
localProviderEnabled: z.boolean().optional(),
|
||||||
baseDomain: z
|
baseDomain: z
|
||||||
.string()
|
.string()
|
||||||
@@ -30,6 +55,9 @@ const updateSuperAdminSettingsSchema = z
|
|||||||
oauthGoogleClientSecret: z.string().trim().min(1).nullable().optional(),
|
oauthGoogleClientSecret: z.string().trim().min(1).nullable().optional(),
|
||||||
oauthGithubClientId: z.string().trim().min(1).nullable().optional(),
|
oauthGithubClientId: z.string().trim().min(1).nullable().optional(),
|
||||||
oauthGithubClientSecret: z.string().trim().min(1).nullable().optional(),
|
oauthGithubClientSecret: z.string().trim().min(1).nullable().optional(),
|
||||||
|
...planQuotaFields,
|
||||||
|
...planPricingFields,
|
||||||
|
...planProductFields,
|
||||||
})
|
})
|
||||||
.refine((value) => Object.values(value).some((entry) => entry !== undefined), {
|
.refine((value) => Object.values(value).some((entry) => entry !== undefined), {
|
||||||
message: '至少需要更新一项设置',
|
message: '至少需要更新一项设置',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@afilmory/hooks": "workspace:*",
|
"@afilmory/hooks": "workspace:*",
|
||||||
"@afilmory/ui": "workspace:*",
|
"@afilmory/ui": "workspace:*",
|
||||||
"@afilmory/utils": "workspace:*",
|
"@afilmory/utils": "workspace:*",
|
||||||
|
"@creem_io/better-auth": "0.0.8",
|
||||||
"@essentials/request-timeout": "1.3.0",
|
"@essentials/request-timeout": "1.3.0",
|
||||||
"@headlessui/react": "2.2.9",
|
"@headlessui/react": "2.2.9",
|
||||||
"@pastel-palette/tailwindcss": "1.0.0-canary.3",
|
"@pastel-palette/tailwindcss": "1.0.0-canary.3",
|
||||||
|
|||||||
100
be/apps/dashboard/public/creem-redirect.html
Normal file
100
be/apps/dashboard/public/creem-redirect.html
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Finishing checkout…</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
color: #0f172a;
|
||||||
|
background: #f8fafc;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 420px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 2rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: white;
|
||||||
|
box-shadow:
|
||||||
|
0 10px 15px rgba(15, 23, 42, 0.08),
|
||||||
|
0 4px 6px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #2563eb;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Finishing checkout</h1>
|
||||||
|
<p data-status>Redirecting you back to your workspace…</p>
|
||||||
|
<p data-manual hidden>
|
||||||
|
If this page does not close automatically,
|
||||||
|
<a data-link href="/">click here to continue</a>.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
;(() => {
|
||||||
|
const statusEl = document.querySelector('[data-status]')
|
||||||
|
const manualEl = document.querySelector('[data-manual]')
|
||||||
|
const linkEl = document.querySelector('[data-link]')
|
||||||
|
|
||||||
|
if (!statusEl || !manualEl || !linkEl) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
const redirectParam = params.get('redirect')
|
||||||
|
|
||||||
|
if (!redirectParam) {
|
||||||
|
statusEl.textContent = 'Missing redirect target. You can safely close this tab.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let target
|
||||||
|
try {
|
||||||
|
target = new URL(redirectParam)
|
||||||
|
} catch {
|
||||||
|
statusEl.textContent = 'Invalid redirect target received.'
|
||||||
|
manualEl.hidden = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = target.hostname.toLowerCase()
|
||||||
|
const allowed =
|
||||||
|
host === 'localhost' ||
|
||||||
|
host.endsWith('.localhost') ||
|
||||||
|
host === window.location.hostname.toLowerCase()
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
statusEl.textContent = 'Blocked an unexpected redirect destination.'
|
||||||
|
linkEl.href = target.toString()
|
||||||
|
linkEl.textContent = target.toString()
|
||||||
|
manualEl.hidden = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
manualEl.hidden = true
|
||||||
|
statusEl.textContent = 'Redirecting you back to your workspace…'
|
||||||
|
window.setTimeout(() => {
|
||||||
|
window.location.replace(target.toString())
|
||||||
|
}, 500)
|
||||||
|
})()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,9 +4,10 @@ import { camelCaseKeys } from '~/lib/case'
|
|||||||
import type { BetterAuthSession, BetterAuthUser } from '../types'
|
import type { BetterAuthSession, BetterAuthUser } from '../types'
|
||||||
|
|
||||||
export interface SessionTenant {
|
export interface SessionTenant {
|
||||||
|
isPlaceholder: boolean
|
||||||
|
requestedSlug: string | null
|
||||||
id: string
|
id: string
|
||||||
slug: string | null
|
slug: string | null
|
||||||
isPlaceholder: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionResponse = {
|
export type SessionResponse = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createAuthClient } from 'better-auth/react'
|
import { creemClient } from '@creem_io/better-auth/client'
|
||||||
|
import { createCreemAuthClient } from '@creem_io/better-auth/create-creem-auth-client'
|
||||||
import { FetchError } from 'ofetch'
|
import { FetchError } from 'ofetch'
|
||||||
|
|
||||||
const apiBase = import.meta.env.VITE_APP_API_BASE?.replace(/\/$/, '') || '/api'
|
const apiBase = import.meta.env.VITE_APP_API_BASE?.replace(/\/$/, '') || '/api'
|
||||||
@@ -11,8 +12,9 @@ const commonOptions = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createCreemAuthClient({
|
||||||
baseURL: authBase,
|
baseURL: authBase,
|
||||||
|
plugins: [creemClient()],
|
||||||
...commonOptions,
|
...commonOptions,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ export interface BillingPlanSummary {
|
|||||||
name: string
|
name: string
|
||||||
description: string
|
description: string
|
||||||
quotas: BillingPlanQuota
|
quotas: BillingPlanQuota
|
||||||
|
pricing?: {
|
||||||
|
monthlyPrice: number | null
|
||||||
|
currency: string | null
|
||||||
|
}
|
||||||
|
payment?: {
|
||||||
|
creemProductId?: string | null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BillingPlanResponse {
|
export interface BillingPlanResponse {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { startTransition, useCallback, useEffect, useMemo, useRef, useState } fr
|
|||||||
import { LinearBorderPanel } from '~/components/common/GlassPanel'
|
import { LinearBorderPanel } from '~/components/common/GlassPanel'
|
||||||
|
|
||||||
import { SchemaFormRenderer } from '../../schema-form/SchemaFormRenderer'
|
import { SchemaFormRenderer } from '../../schema-form/SchemaFormRenderer'
|
||||||
import type { SchemaFormState, SchemaFormValue } from '../../schema-form/types'
|
import type { SchemaFormState, SchemaFormValue, UiNode } from '../../schema-form/types'
|
||||||
import { useSuperAdminSettingsQuery, useUpdateSuperAdminSettingsMutation } from '../hooks'
|
import { useSuperAdminSettingsQuery, useUpdateSuperAdminSettingsMutation } from '../hooks'
|
||||||
import type { SuperAdminSettingsResponse, UpdateSuperAdminSettingsPayload } from '../types'
|
import type { SuperAdminSettingsResponse, UpdateSuperAdminSettingsPayload } from '../types'
|
||||||
import type { SuperAdminFieldMap } from '../utils/schema-form-adapter'
|
import type { SuperAdminFieldMap } from '../utils/schema-form-adapter'
|
||||||
@@ -45,7 +45,11 @@ function extractRawSettings(payload: SuperAdminSettingsResponse): Record<string,
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SuperAdminSettingsForm() {
|
interface SuperAdminSettingsFormProps {
|
||||||
|
visibleSectionIds?: readonly string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SuperAdminSettingsForm({ visibleSectionIds }: SuperAdminSettingsFormProps = {}) {
|
||||||
const { data, isLoading, isError, error } = useSuperAdminSettingsQuery()
|
const { data, isLoading, isError, error } = useSuperAdminSettingsQuery()
|
||||||
const [fieldMap, setFieldMap] = useState<SuperAdminFieldMap>(() => new Map())
|
const [fieldMap, setFieldMap] = useState<SuperAdminFieldMap>(() => new Map())
|
||||||
const [formState, setFormState] = useState<FormState | null>(null)
|
const [formState, setFormState] = useState<FormState | null>(null)
|
||||||
@@ -145,6 +149,19 @@ export function SuperAdminSettingsForm() {
|
|||||||
return '所有设置已同步'
|
return '所有设置已同步'
|
||||||
}, [hasChanges, updateMutation.error, updateMutation.isError, updateMutation.isPending, updateMutation.isSuccess])
|
}, [hasChanges, updateMutation.error, updateMutation.isError, updateMutation.isPending, updateMutation.isSuccess])
|
||||||
|
|
||||||
|
const shouldRenderNode = useMemo(() => {
|
||||||
|
if (!visibleSectionIds || visibleSectionIds.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const allowed = new Set(visibleSectionIds)
|
||||||
|
return (node: UiNode<string>) => {
|
||||||
|
if (node.type === 'section') {
|
||||||
|
return allowed.has(node.id)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}, [visibleSectionIds])
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<LinearBorderPanel className="p-6">
|
<LinearBorderPanel className="p-6">
|
||||||
@@ -190,7 +207,12 @@ export function SuperAdminSettingsForm() {
|
|||||||
transition={Spring.presets.smooth}
|
transition={Spring.presets.smooth}
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
>
|
>
|
||||||
<SchemaFormRenderer schema={data.schema} values={formState} onChange={handleChange} />
|
<SchemaFormRenderer
|
||||||
|
schema={data.schema}
|
||||||
|
values={formState}
|
||||||
|
onChange={handleChange}
|
||||||
|
shouldRenderNode={shouldRenderNode}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<LinearBorderPanel className="p-6">
|
<LinearBorderPanel className="p-6">
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Button } from '@afilmory/ui'
|
import { Button } from '@afilmory/ui'
|
||||||
import { Spring } from '@afilmory/utils'
|
import { clsxm } from '@afilmory/utils'
|
||||||
import { m } from 'motion/react'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
|
||||||
|
import { LinearBorderPanel } from '~/components/common/GlassPanel'
|
||||||
import { MainPageLayout } from '~/components/layouts/MainPageLayout'
|
import { MainPageLayout } from '~/components/layouts/MainPageLayout'
|
||||||
|
import type { SessionResponse } from '~/modules/auth/api/session'
|
||||||
|
import { AUTH_SESSION_QUERY_KEY } from '~/modules/auth/api/session'
|
||||||
|
import { authClient } from '~/modules/auth/auth-client'
|
||||||
import type { BillingPlanSummary } from '~/modules/billing'
|
import type { BillingPlanSummary } from '~/modules/billing'
|
||||||
import { useTenantPlanQuery } from '~/modules/billing'
|
import { useTenantPlanQuery } from '~/modules/billing'
|
||||||
|
|
||||||
@@ -22,12 +28,30 @@ const QUOTA_UNITS: Record<string, string> = {
|
|||||||
|
|
||||||
export function Component() {
|
export function Component() {
|
||||||
const planQuery = useTenantPlanQuery()
|
const planQuery = useTenantPlanQuery()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const session = (queryClient.getQueryData<SessionResponse | null>(AUTH_SESSION_QUERY_KEY) ??
|
||||||
|
null) as SessionResponse | null
|
||||||
|
|
||||||
|
const tenantId = session?.tenant?.id ?? null
|
||||||
|
const tenantSlug = session?.tenant?.slug ?? null
|
||||||
|
|
||||||
const plan = planQuery.data?.plan ?? null
|
const plan = planQuery.data?.plan ?? null
|
||||||
const availablePlans = planQuery.data?.availablePlans ?? []
|
const availablePlans = planQuery.data?.availablePlans ?? []
|
||||||
|
const plans = useMemo(() => {
|
||||||
|
if (!plan) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const merged = new Map<string, BillingPlanSummary>()
|
||||||
|
for (const candidate of [plan, ...availablePlans]) {
|
||||||
|
if (candidate && !merged.has(candidate.planId)) {
|
||||||
|
merged.set(candidate.planId, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(merged.values())
|
||||||
|
}, [availablePlans, plan])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainPageLayout title="订阅计划" description="查看当前订阅状态与资源限制,未来版本将在此处开放升级入口。">
|
<MainPageLayout title="订阅计划" description="查看当前订阅状态与资源限制,并在此处发起升级或管理订阅。">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{planQuery.isError && (
|
{planQuery.isError && (
|
||||||
<div className="text-red text-sm">
|
<div className="text-red text-sm">
|
||||||
@@ -38,37 +62,133 @@ export function Component() {
|
|||||||
{planQuery.isLoading || !plan ? (
|
{planQuery.isLoading || !plan ? (
|
||||||
<PlanSkeleton />
|
<PlanSkeleton />
|
||||||
) : (
|
) : (
|
||||||
<PlanList currentPlanId={plan.planId} plans={availablePlans.length > 0 ? availablePlans : [plan]} />
|
<PlanList currentPlanId={plan.planId} plans={plans} tenantId={tenantId} tenantSlug={tenantSlug} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</MainPageLayout>
|
</MainPageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlanList({ currentPlanId, plans }: { currentPlanId: string; plans: BillingPlanSummary[] }) {
|
function PlanList({
|
||||||
|
currentPlanId,
|
||||||
|
plans,
|
||||||
|
tenantId,
|
||||||
|
tenantSlug,
|
||||||
|
}: {
|
||||||
|
currentPlanId: string
|
||||||
|
plans: BillingPlanSummary[]
|
||||||
|
tenantId: string | null
|
||||||
|
tenantSlug: string | null
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
{plans.map((plan) => (
|
{plans.map((plan) => (
|
||||||
<PlanCard key={plan.planId} plan={plan} isCurrent={plan.planId === currentPlanId} />
|
<PlanCard
|
||||||
|
key={plan.planId}
|
||||||
|
plan={plan}
|
||||||
|
isCurrent={plan.planId === currentPlanId}
|
||||||
|
tenantId={tenantId}
|
||||||
|
tenantSlug={tenantSlug}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlanCard({ plan, isCurrent }: { plan: BillingPlanSummary; isCurrent: boolean }) {
|
function PlanCard({
|
||||||
|
plan,
|
||||||
|
isCurrent,
|
||||||
|
tenantId,
|
||||||
|
tenantSlug,
|
||||||
|
}: {
|
||||||
|
plan: BillingPlanSummary
|
||||||
|
isCurrent: boolean
|
||||||
|
tenantId: string | null
|
||||||
|
tenantSlug: string | null
|
||||||
|
}) {
|
||||||
|
const [checkoutLoading, setCheckoutLoading] = useState(false)
|
||||||
|
const [portalLoading, setPortalLoading] = useState(false)
|
||||||
|
const productId = plan.payment?.creemProductId ?? null
|
||||||
|
|
||||||
|
const canCheckout = Boolean(!isCurrent && tenantId && productId)
|
||||||
|
|
||||||
|
const showPortalButton = isCurrent && plan.planId !== 'free' && Boolean(productId)
|
||||||
|
|
||||||
|
const handleCheckout = async () => {
|
||||||
|
if (!canCheckout || !tenantId || !productId) {
|
||||||
|
toast.error('该方案暂未开放,请稍后再试。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setCheckoutLoading(true)
|
||||||
|
const successUrl = buildCheckoutSuccessUrl(tenantSlug)
|
||||||
|
const metadata: Record<string, string> = {
|
||||||
|
tenantId,
|
||||||
|
planId: plan.planId,
|
||||||
|
}
|
||||||
|
if (tenantSlug) {
|
||||||
|
metadata.tenantSlug = tenantSlug
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { data, error } = await authClient.creem.createCheckout({
|
||||||
|
productId,
|
||||||
|
successUrl,
|
||||||
|
metadata,
|
||||||
|
})
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message ?? 'Creem 返回了未知错误')
|
||||||
|
}
|
||||||
|
if (data?.url) {
|
||||||
|
window.location.href = data.url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.error('Creem 未返回有效的结算链接,请稍后再试。')
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : '无法创建订阅结算会话')
|
||||||
|
} finally {
|
||||||
|
setCheckoutLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePortal = async () => {
|
||||||
|
if (!showPortalButton) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setPortalLoading(true)
|
||||||
|
try {
|
||||||
|
const { data, error } = await authClient.creem.createPortal()
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message ?? '无法打开订阅管理')
|
||||||
|
}
|
||||||
|
if (data?.url) {
|
||||||
|
window.location.href = data.url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toast.error('Creem 未返回订阅管理地址,请稍后再试。')
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : '无法打开订阅管理')
|
||||||
|
} finally {
|
||||||
|
setPortalLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<m.div
|
<LinearBorderPanel className="bg-background-secondary/70 p-5">
|
||||||
initial={{ opacity: 0, y: 8 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={Spring.presets.smooth}
|
|
||||||
className="rounded-2xl border border-border/40 bg-background-secondary/70 p-5"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-text">{plan.name}</h2>
|
<h2 className="text-lg font-semibold text-text">{plan.name}</h2>
|
||||||
<p className="text-text-secondary text-sm">{plan.description}</p>
|
<p className="text-text-secondary text-sm">{plan.description}</p>
|
||||||
|
{plan.pricing && plan.pricing.monthlyPrice !== null && plan.pricing.monthlyPrice !== undefined && (
|
||||||
|
<p
|
||||||
|
className={clsxm(
|
||||||
|
'text-text absolute right-0 top-0 mt-1 text-sm font-semibold',
|
||||||
|
isCurrent && 'translate-y-6',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatPrice(plan.pricing.monthlyPrice, plan.pricing.currency)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isCurrent && <CurrentBadge />}
|
{isCurrent && <CurrentBadge planId={plan.planId} />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="mt-6 space-y-2">
|
<ul className="mt-6 space-y-2">
|
||||||
@@ -81,16 +201,54 @@ function PlanCard({ plan, isCurrent }: { plan: BillingPlanSummary; isCurrent: bo
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{!isCurrent && (
|
{!isCurrent && (
|
||||||
<Button type="button" disabled className="mt-4 w-full" size="sm">
|
<Button
|
||||||
即将开放
|
type="button"
|
||||||
|
className="mt-4 w-full"
|
||||||
|
size="sm"
|
||||||
|
disabled={!canCheckout || checkoutLoading}
|
||||||
|
onClick={handleCheckout}
|
||||||
|
>
|
||||||
|
{checkoutLoading ? '请稍候…' : canCheckout ? '升级此方案' : '敬请期待'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</m.div>
|
|
||||||
|
{showPortalButton && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
className="mt-4 w-full"
|
||||||
|
size="sm"
|
||||||
|
disabled={portalLoading}
|
||||||
|
onClick={handlePortal}
|
||||||
|
>
|
||||||
|
{portalLoading ? '打开中…' : '管理订阅'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</LinearBorderPanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CurrentBadge() {
|
function buildCheckoutSuccessUrl(tenantSlug: string | null): string {
|
||||||
return <span className="bg-accent/10 text-accent rounded-full px-2 py-0.5 text-xs font-semibold">当前方案</span>
|
const { origin, pathname, search, hash, protocol, hostname, port } = window.location
|
||||||
|
const defaultUrl = `${origin}${pathname}${search}${hash}`
|
||||||
|
const isLocalSubdomain = hostname !== 'localhost' && hostname.endsWith('.localhost')
|
||||||
|
|
||||||
|
if (!isLocalSubdomain) {
|
||||||
|
return defaultUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectOrigin = `${protocol}//localhost${port ? `:${port}` : ''}`
|
||||||
|
const redirectUrl = new URL('/creem-redirect.html', redirectOrigin)
|
||||||
|
redirectUrl.searchParams.set('redirect', defaultUrl)
|
||||||
|
if (tenantSlug) {
|
||||||
|
redirectUrl.searchParams.set('tenant', tenantSlug)
|
||||||
|
}
|
||||||
|
return redirectUrl.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function CurrentBadge({ planId }: { planId: string }) {
|
||||||
|
const label = planId === 'friend' ? '内部方案' : '当前方案'
|
||||||
|
return <span className="bg-accent/10 text-accent rounded-full px-2 py-0.5 text-xs font-semibold">{label}</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderQuotaValue(value: number | null, unit?: string): string {
|
function renderQuotaValue(value: number | null, unit?: string): string {
|
||||||
@@ -101,6 +259,12 @@ function renderQuotaValue(value: number | null, unit?: string): string {
|
|||||||
return unit ? `${numeral}${unit}` : numeral
|
return unit ? `${numeral}${unit}` : numeral
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatPrice(value: number, currency: string | null | undefined): string {
|
||||||
|
const normalizedCurrency = currency?.toUpperCase() ?? ''
|
||||||
|
const formatted = value.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
|
||||||
|
return normalizedCurrency ? `${normalizedCurrency} ${formatted}` : formatted
|
||||||
|
}
|
||||||
|
|
||||||
function PlanSkeleton() {
|
function PlanSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
import { Spring } from '@afilmory/utils'
|
|
||||||
import { m } from 'motion/react'
|
|
||||||
|
|
||||||
|
import { MainPageLayout } from '~/components/layouts/MainPageLayout'
|
||||||
import { BuilderSettingsForm } from '~/modules/builder-settings'
|
import { BuilderSettingsForm } from '~/modules/builder-settings'
|
||||||
|
|
||||||
export function Component() {
|
export function Component() {
|
||||||
return (
|
return (
|
||||||
<m.div
|
<MainPageLayout title="构建器设置">
|
||||||
initial={{ opacity: 0, y: 8 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={Spring.presets.smooth}
|
|
||||||
className="space-y-6"
|
|
||||||
>
|
|
||||||
<header className="space-y-2">
|
|
||||||
<h1 className="text-text text-2xl font-semibold">构建器设置</h1>
|
|
||||||
<p className="text-text-secondary text-sm">调整照片构建任务的并发、日志输出与仓库同步策略。</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<BuilderSettingsForm />
|
<BuilderSettingsForm />
|
||||||
</m.div>
|
</MainPageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ export function Component() {
|
|||||||
const isSuperAdmin = useIsSuperAdmin()
|
const isSuperAdmin = useIsSuperAdmin()
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ to: '/superadmin/settings', label: '系统设置', end: true },
|
{ to: '/superadmin/settings', label: '系统设置', end: true },
|
||||||
|
{ to: '/superadmin/plans', label: '订阅计划', end: true },
|
||||||
{ to: '/superadmin/tenants', label: '租户管理', end: true },
|
{ to: '/superadmin/tenants', label: '租户管理', end: true },
|
||||||
{
|
{
|
||||||
label: '构建器',
|
label: '构建器',
|
||||||
to: '/settings/builder',
|
to: '/superadmin/builder',
|
||||||
end: true,
|
end: true,
|
||||||
},
|
},
|
||||||
{ to: '/superadmin/debug', label: 'Builder 调试', end: false },
|
{ to: '/superadmin/debug', label: 'Builder 调试', end: false },
|
||||||
|
|||||||
26
be/apps/dashboard/src/pages/superadmin/plans.tsx
Normal file
26
be/apps/dashboard/src/pages/superadmin/plans.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Spring } from '@afilmory/utils'
|
||||||
|
import { m } from 'motion/react'
|
||||||
|
|
||||||
|
import { SuperAdminSettingsForm } from '~/modules/super-admin'
|
||||||
|
|
||||||
|
const PLAN_SECTION_IDS = ['billing-plan-settings'] as const
|
||||||
|
|
||||||
|
export function Component() {
|
||||||
|
return (
|
||||||
|
<m.div
|
||||||
|
initial={{ opacity: 0, y: 8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={Spring.presets.smooth}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<header className="space-y-2">
|
||||||
|
<h1 className="text-text text-2xl font-semibold">订阅计划配置</h1>
|
||||||
|
<p className="text-text-secondary text-sm">
|
||||||
|
管理各个订阅计划的资源配额、定价信息与 Creem Product 连接,仅超级管理员可编辑。
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<SuperAdminSettingsForm visibleSectionIds={PLAN_SECTION_IDS} />
|
||||||
|
</m.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ export function Component() {
|
|||||||
<p className="text-text-secondary text-sm">管理整个平台的注册策略与本地登录渠道,由超级管理员统一维护。</p>
|
<p className="text-text-secondary text-sm">管理整个平台的注册策略与本地登录渠道,由超级管理员统一维护。</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<SuperAdminSettingsForm />
|
<SuperAdminSettingsForm visibleSectionIds={['registration-control', 'oauth-providers']} />
|
||||||
</m.div>
|
</m.div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
16
be/packages/db/migrations/0004_aromatic_bishop.sql
Normal file
16
be/packages/db/migrations/0004_aromatic_bishop.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE "creem_subscription" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"product_id" text NOT NULL,
|
||||||
|
"reference_id" text NOT NULL,
|
||||||
|
"creem_customer_id" text,
|
||||||
|
"creem_subscription_id" text,
|
||||||
|
"creem_order_id" text,
|
||||||
|
"status" text DEFAULT 'pending' NOT NULL,
|
||||||
|
"period_start" timestamp,
|
||||||
|
"period_end" timestamp,
|
||||||
|
"cancel_at_period_end" boolean DEFAULT false NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_user" ADD COLUMN "creem_customer_id" text;
|
||||||
1485
be/packages/db/migrations/meta/0004_snapshot.json
Normal file
1485
be/packages/db/migrations/meta/0004_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,13 @@
|
|||||||
"when": 1763292246073,
|
"when": 1763292246073,
|
||||||
"tag": "0003_natural_ultimates",
|
"tag": "0003_natural_ultimates",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1763315183401,
|
||||||
|
"tag": "0004_aromatic_bishop",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export const authUsers = pgTable('auth_user', {
|
|||||||
email: text('email').notNull().unique(),
|
email: text('email').notNull().unique(),
|
||||||
emailVerified: boolean('email_verified').default(false).notNull(),
|
emailVerified: boolean('email_verified').default(false).notNull(),
|
||||||
image: text('image'),
|
image: text('image'),
|
||||||
|
creemCustomerId: text('creem_customer_id'),
|
||||||
role: userRoleEnum('role').notNull().default('user'),
|
role: userRoleEnum('role').notNull().default('user'),
|
||||||
tenantId: text('tenant_id').references(() => tenants.id, { onDelete: 'set null' }),
|
tenantId: text('tenant_id').references(() => tenants.id, { onDelete: 'set null' }),
|
||||||
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||||
@@ -131,6 +132,21 @@ export const authVerifications = pgTable('auth_verification', {
|
|||||||
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const creemSubscriptions = pgTable('creem_subscription', {
|
||||||
|
id: text('id').primaryKey(),
|
||||||
|
productId: text('product_id').notNull(),
|
||||||
|
referenceId: text('reference_id').notNull(),
|
||||||
|
creemCustomerId: text('creem_customer_id'),
|
||||||
|
creemSubscriptionId: text('creem_subscription_id'),
|
||||||
|
creemOrderId: text('creem_order_id'),
|
||||||
|
status: text('status').notNull().default('pending'),
|
||||||
|
periodStart: timestamp('period_start', { mode: 'string' }),
|
||||||
|
periodEnd: timestamp('period_end', { mode: 'string' }),
|
||||||
|
cancelAtPeriodEnd: boolean('cancel_at_period_end').default(false).notNull(),
|
||||||
|
createdAt: timestamp('created_at', { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at', { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
export const tenantAuthUsers = pgTable(
|
export const tenantAuthUsers = pgTable(
|
||||||
'tenant_auth_user',
|
'tenant_auth_user',
|
||||||
{
|
{
|
||||||
|
|||||||
4
be/packages/env/src/index.ts
vendored
4
be/packages/env/src/index.ts
vendored
@@ -23,6 +23,10 @@ export const env = createEnv({
|
|||||||
|
|
||||||
CONFIG_ENCRYPTION_KEY: z.string().min(1),
|
CONFIG_ENCRYPTION_KEY: z.string().min(1),
|
||||||
|
|
||||||
|
// Payment
|
||||||
|
CREEM_API_KEY: z.string().min(1),
|
||||||
|
CREEM_WEBHOOK_SECRET: z.string().min(1),
|
||||||
|
|
||||||
DEFAULT_SUPERADMIN_EMAIL: z.email().default('root@local.host'),
|
DEFAULT_SUPERADMIN_EMAIL: z.email().default('root@local.host'),
|
||||||
DEFAULT_SUPERADMIN_USERNAME: z
|
DEFAULT_SUPERADMIN_USERNAME: z
|
||||||
.string()
|
.string()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ This document tracks the current subscription plans, quota knobs, and the design
|
|||||||
|
|
||||||
1. **Plan definitions** live in `billing-plan.constants.ts`. Each entry carries human-friendly metadata for the super-admin dashboard plus a `quotas` object.
|
1. **Plan definitions** live in `billing-plan.constants.ts`. Each entry carries human-friendly metadata for the super-admin dashboard plus a `quotas` object.
|
||||||
2. **Overrides** are stored under `system.billing.planOverrides`. This is a JSON blob keyed by plan id. It is parsed through zod (`SystemSettingService.getBillingPlanOverrides`) and merged in the billing plan service.
|
2. **Overrides** are stored under `system.billing.planOverrides`. This is a JSON blob keyed by plan id. It is parsed through zod (`SystemSettingService.getBillingPlanOverrides`) and merged in the billing plan service.
|
||||||
|
3. **Payment product mapping** lives in `system.billing.planProducts`. Each plan id can map to provider specific identifiers (e.g. `creemProductId`). Plans that require checkout (like `pro`) stay hidden until a product id is configured, which prevents exposing upgrade buttons in environments that are not ready.
|
||||||
3. **Tenant assignment** is tracked via `tenant.plan_id` and can only be changed by superadmins (see `/super-admin/tenants` backend+dashboard). The Friend plan is intentionally absent from any public selector.
|
3. **Tenant assignment** is tracked via `tenant.plan_id` and can only be changed by superadmins (see `/super-admin/tenants` backend+dashboard). The Friend plan is intentionally absent from any public selector.
|
||||||
4. **Quota enforcement** is performed in:
|
4. **Quota enforcement** is performed in:
|
||||||
- `PhotoAssetService` (manual upload size + library limit + monthly process allowance)
|
- `PhotoAssetService` (manual upload size + library limit + monthly process allowance)
|
||||||
|
|||||||
187
pnpm-lock.yaml
generated
187
pnpm-lock.yaml
generated
@@ -98,7 +98,7 @@ importers:
|
|||||||
version: 9.39.1(jiti@2.6.1)
|
version: 9.39.1(jiti@2.6.1)
|
||||||
eslint-config-hyoban:
|
eslint-config-hyoban:
|
||||||
specifier: 4.0.10
|
specifier: 4.0.10
|
||||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||||
fast-glob:
|
fast-glob:
|
||||||
specifier: 3.3.3
|
specifier: 3.3.3
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
@@ -345,7 +345,7 @@ importers:
|
|||||||
version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: 16.0.1
|
specifier: 16.0.1
|
||||||
version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: 0.4.6
|
specifier: 0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -578,7 +578,7 @@ importers:
|
|||||||
version: 0.31.6
|
version: 0.31.6
|
||||||
next:
|
next:
|
||||||
specifier: 16.0.1
|
specifier: 16.0.1
|
||||||
version: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
postcss:
|
postcss:
|
||||||
specifier: 8.5.6
|
specifier: 8.5.6
|
||||||
version: 8.5.6
|
version: 8.5.6
|
||||||
@@ -626,7 +626,7 @@ importers:
|
|||||||
version: 2.2.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 2.2.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@lobehub/fluent-emoji':
|
'@lobehub/fluent-emoji':
|
||||||
specifier: 2.0.0
|
specifier: 2.0.0
|
||||||
version: 2.0.0(@babel/core@7.28.4)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 2.0.0(@babel/core@7.28.5)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@maplibre/maplibre-gl-geocoder':
|
'@maplibre/maplibre-gl-geocoder':
|
||||||
specifier: ^1.9.1
|
specifier: ^1.9.1
|
||||||
version: 1.9.1(maplibre-gl@5.12.0)
|
version: 1.9.1(maplibre-gl@5.12.0)
|
||||||
@@ -689,7 +689,7 @@ importers:
|
|||||||
version: 10.2.0
|
version: 10.2.0
|
||||||
jotai:
|
jotai:
|
||||||
specifier: 2.15.1
|
specifier: 2.15.1
|
||||||
version: 2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.3)(react@19.2.0)
|
version: 2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.3)(react@19.2.0)
|
||||||
maplibre-gl:
|
maplibre-gl:
|
||||||
specifier: ^5.12.0
|
specifier: ^5.12.0
|
||||||
version: 5.12.0
|
version: 5.12.0
|
||||||
@@ -737,7 +737,7 @@ importers:
|
|||||||
version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react-scan:
|
react-scan:
|
||||||
specifier: 0.4.3
|
specifier: 0.4.3
|
||||||
version: 0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2)
|
version: 0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2)
|
||||||
react-use-measure:
|
react-use-measure:
|
||||||
specifier: 2.1.7
|
specifier: 2.1.7
|
||||||
version: 2.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 2.1.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -926,6 +926,9 @@ importers:
|
|||||||
'@aws-sdk/client-s3':
|
'@aws-sdk/client-s3':
|
||||||
specifier: 3.929.0
|
specifier: 3.929.0
|
||||||
version: 3.929.0
|
version: 3.929.0
|
||||||
|
'@creem_io/better-auth':
|
||||||
|
specifier: 0.0.8
|
||||||
|
version: 0.0.8(better-auth@1.3.34)(creem@0.4.0)(zod@4.1.12)
|
||||||
'@hono/node-server':
|
'@hono/node-server':
|
||||||
specifier: ^1.19.6
|
specifier: ^1.19.6
|
||||||
version: 1.19.6(hono@4.10.5)
|
version: 1.19.6(hono@4.10.5)
|
||||||
@@ -934,7 +937,7 @@ importers:
|
|||||||
version: 2.6.2
|
version: 2.6.2
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.3.34
|
specifier: 1.3.34
|
||||||
version: 1.3.34(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 1.3.34(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.44.7
|
specifier: ^0.44.7
|
||||||
version: 0.44.7(@types/pg@8.15.6)(@vercel/postgres@0.10.0)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7)
|
version: 0.44.7(@types/pg@8.15.6)(@vercel/postgres@0.10.0)(kysely@0.28.8)(pg@8.16.3)(postgres@3.4.7)
|
||||||
@@ -1008,6 +1011,9 @@ importers:
|
|||||||
'@afilmory/utils':
|
'@afilmory/utils':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../packages/utils
|
version: link:../../../packages/utils
|
||||||
|
'@creem_io/better-auth':
|
||||||
|
specifier: 0.0.8
|
||||||
|
version: 0.0.8(better-auth@1.3.34)(creem@0.4.0)(zod@4.1.12)
|
||||||
'@essentials/request-timeout':
|
'@essentials/request-timeout':
|
||||||
specifier: 1.3.0
|
specifier: 1.3.0
|
||||||
version: 1.3.0
|
version: 1.3.0
|
||||||
@@ -1061,7 +1067,7 @@ importers:
|
|||||||
version: 5.90.8(react@19.2.0)
|
version: 5.90.8(react@19.2.0)
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.3.34
|
specifier: 1.3.34
|
||||||
version: 1.3.34(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 1.3.34(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: 0.7.1
|
specifier: 0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -1079,7 +1085,7 @@ importers:
|
|||||||
version: 10.2.0
|
version: 10.2.0
|
||||||
jotai:
|
jotai:
|
||||||
specifier: 2.15.1
|
specifier: 2.15.1
|
||||||
version: 2.15.1(@babel/core@7.28.5)(@babel/template@7.27.2)(@types/react@19.2.3)(react@19.2.0)
|
version: 2.15.1(@babel/core@7.28.4)(@babel/template@7.27.2)(@types/react@19.2.3)(react@19.2.0)
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: 0.553.0
|
specifier: 0.553.0
|
||||||
version: 0.553.0(react@19.2.0)
|
version: 0.553.0(react@19.2.0)
|
||||||
@@ -1106,7 +1112,7 @@ importers:
|
|||||||
version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react-scan:
|
react-scan:
|
||||||
specifier: 0.4.3
|
specifier: 0.4.3
|
||||||
version: 0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.53.2)
|
version: 0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.53.2)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: 2.0.7
|
specifier: 2.0.7
|
||||||
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -1164,7 +1170,7 @@ importers:
|
|||||||
version: 9.39.1(jiti@2.6.1)
|
version: 9.39.1(jiti@2.6.1)
|
||||||
eslint-config-hyoban:
|
eslint-config-hyoban:
|
||||||
specifier: 4.0.10
|
specifier: 4.0.10
|
||||||
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
|
||||||
lint-staged:
|
lint-staged:
|
||||||
specifier: 16.2.6
|
specifier: 16.2.6
|
||||||
version: 16.2.6
|
version: 16.2.6
|
||||||
@@ -1548,7 +1554,7 @@ importers:
|
|||||||
version: 0.16.3(ms@2.1.3)(synckit@0.11.11)(typescript@5.9.3)
|
version: 0.16.3(ms@2.1.3)(synckit@0.11.11)(typescript@5.9.3)
|
||||||
unplugin-dts:
|
unplugin-dts:
|
||||||
specifier: 1.0.0-beta.6
|
specifier: 1.0.0-beta.6
|
||||||
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.25.12)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
|
version: 1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.25.12)(rolldown@1.0.0-beta.49)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))
|
||||||
vite:
|
vite:
|
||||||
specifier: 7.2.2
|
specifier: 7.2.2
|
||||||
version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||||
@@ -2408,6 +2414,13 @@ packages:
|
|||||||
conventional-commits-parser:
|
conventional-commits-parser:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@creem_io/better-auth@0.0.8':
|
||||||
|
resolution: {integrity: sha512-tTDsYqbyRgryk5jSBg+i8/eI05JLRLNOEF5aYsnKVDrEHerVMHz6Lp6059zewQ/bhcwKdn9Bwm5F2iFoL426tw==}
|
||||||
|
peerDependencies:
|
||||||
|
better-auth: ^1.3.34
|
||||||
|
creem: ^0.4.0
|
||||||
|
zod: ^3.23.8 || ^4
|
||||||
|
|
||||||
'@discoveryjs/json-ext@0.5.7':
|
'@discoveryjs/json-ext@0.5.7':
|
||||||
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
|
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -7004,6 +7017,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
|
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
creem@0.4.0:
|
||||||
|
resolution: {integrity: sha512-1RENze+vqUSq478DzisYaFuHmHt24QKg2zASuThQ9GqXi1XnW1KQa6ekuO0rA2RGqiDSYyfR6+DxaGJBYuVghg==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@modelcontextprotocol/sdk': '>=1.5.0 <1.10.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@modelcontextprotocol/sdk':
|
||||||
|
optional: true
|
||||||
|
|
||||||
cross-env@10.1.0:
|
cross-env@10.1.0:
|
||||||
resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
|
resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
@@ -12896,9 +12918,9 @@ snapshots:
|
|||||||
regexpu-core: 6.4.0
|
regexpu-core: 6.4.0
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
|
||||||
'@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.28.4)':
|
'@babel/helper-define-polyfill-provider@0.6.4(@babel/core@7.28.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-compilation-targets': 7.27.2
|
'@babel/helper-compilation-targets': 7.27.2
|
||||||
'@babel/helper-plugin-utils': 7.27.1
|
'@babel/helper-plugin-utils': 7.27.1
|
||||||
debug: 4.4.3(supports-color@5.5.0)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
@@ -13363,14 +13385,14 @@ snapshots:
|
|||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-plugin-utils': 7.27.1
|
'@babel/helper-plugin-utils': 7.27.1
|
||||||
|
|
||||||
'@babel/plugin-transform-runtime@7.27.4(@babel/core@7.28.4)':
|
'@babel/plugin-transform-runtime@7.27.4(@babel/core@7.28.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-module-imports': 7.27.1
|
'@babel/helper-module-imports': 7.27.1
|
||||||
'@babel/helper-plugin-utils': 7.27.1
|
'@babel/helper-plugin-utils': 7.27.1
|
||||||
babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.28.4)
|
babel-plugin-polyfill-corejs2: 0.4.13(@babel/core@7.28.5)
|
||||||
babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.28.4)
|
babel-plugin-polyfill-corejs3: 0.11.1(@babel/core@7.28.5)
|
||||||
babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.28.4)
|
babel-plugin-polyfill-regenerator: 0.6.4(@babel/core@7.28.5)
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -13663,6 +13685,12 @@ snapshots:
|
|||||||
conventional-commits-filter: 5.0.0
|
conventional-commits-filter: 5.0.0
|
||||||
conventional-commits-parser: 6.2.1
|
conventional-commits-parser: 6.2.1
|
||||||
|
|
||||||
|
'@creem_io/better-auth@0.0.8(better-auth@1.3.34)(creem@0.4.0)(zod@4.1.12)':
|
||||||
|
dependencies:
|
||||||
|
better-auth: 1.3.34(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
creem: 0.4.0
|
||||||
|
zod: 4.1.12
|
||||||
|
|
||||||
'@discoveryjs/json-ext@0.5.7': {}
|
'@discoveryjs/json-ext@0.5.7': {}
|
||||||
|
|
||||||
'@dnd-kit/accessibility@3.1.1(react@19.2.0)':
|
'@dnd-kit/accessibility@3.1.1(react@19.2.0)':
|
||||||
@@ -14608,10 +14636,10 @@ snapshots:
|
|||||||
|
|
||||||
'@lobehub/emojilib@1.0.0': {}
|
'@lobehub/emojilib@1.0.0': {}
|
||||||
|
|
||||||
'@lobehub/fluent-emoji@2.0.0(@babel/core@7.28.4)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
'@lobehub/fluent-emoji@2.0.0(@babel/core@7.28.5)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lobehub/emojilib': 1.0.0
|
'@lobehub/emojilib': 1.0.0
|
||||||
'@lobehub/ui': 2.7.3(@babel/core@7.28.4)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
'@lobehub/ui': 2.7.3(@babel/core@7.28.5)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
antd: 5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
antd: 5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
antd-style: 3.7.1(@types/react@19.2.3)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
antd-style: 3.7.1(@types/react@19.2.3)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
emoji-regex: 10.4.0
|
emoji-regex: 10.4.0
|
||||||
@@ -14628,9 +14656,9 @@ snapshots:
|
|||||||
- framer-motion
|
- framer-motion
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@lobehub/icons@2.7.0(@babel/core@7.28.4)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
'@lobehub/icons@2.7.0(@babel/core@7.28.5)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lobehub/ui': 2.7.3(@babel/core@7.28.4)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
'@lobehub/ui': 2.7.3(@babel/core@7.28.5)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
antd: 5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
antd: 5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
antd-style: 3.7.1(@types/react@19.2.3)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
antd-style: 3.7.1(@types/react@19.2.3)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
lucide-react: 0.469.0(react@19.2.0)
|
lucide-react: 0.469.0(react@19.2.0)
|
||||||
@@ -14645,7 +14673,7 @@ snapshots:
|
|||||||
- framer-motion
|
- framer-motion
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@lobehub/ui@2.7.3(@babel/core@7.28.4)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
'@lobehub/ui@2.7.3(@babel/core@7.28.5)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ant-design/cssinjs': 1.23.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
'@ant-design/cssinjs': 1.23.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@dnd-kit/core': 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
'@dnd-kit/core': 6.3.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -14656,8 +14684,8 @@ snapshots:
|
|||||||
'@emoji-mart/react': 1.1.1(emoji-mart@5.6.0)(react@19.2.0)
|
'@emoji-mart/react': 1.1.1(emoji-mart@5.6.0)(react@19.2.0)
|
||||||
'@floating-ui/react': 0.27.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
'@floating-ui/react': 0.27.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@giscus/react': 3.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
'@giscus/react': 3.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@lobehub/fluent-emoji': 2.0.0(@babel/core@7.28.4)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
'@lobehub/fluent-emoji': 2.0.0(@babel/core@7.28.5)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@lobehub/icons': 2.7.0(@babel/core@7.28.4)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
'@lobehub/icons': 2.7.0(@babel/core@7.28.5)(@types/react@19.2.3)(acorn@8.15.0)(antd@5.26.2(luxon@3.7.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@mdx-js/mdx': 3.1.0(acorn@8.15.0)
|
'@mdx-js/mdx': 3.1.0(acorn@8.15.0)
|
||||||
'@mdx-js/react': 3.1.1(@types/react@19.2.3)(react@19.2.0)
|
'@mdx-js/react': 3.1.1(@types/react@19.2.3)(react@19.2.0)
|
||||||
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.3)(react@19.2.0)
|
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.3)(react@19.2.0)
|
||||||
@@ -14687,7 +14715,7 @@ snapshots:
|
|||||||
rc-menu: 9.16.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
rc-menu: 9.16.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
re-resizable: 6.11.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
re-resizable: 6.11.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
react-avatar-editor: 13.0.2(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
react-avatar-editor: 13.0.2(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
react-error-boundary: 5.0.0(react@19.2.0)
|
react-error-boundary: 5.0.0(react@19.2.0)
|
||||||
react-hotkeys-hook: 5.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
react-hotkeys-hook: 5.1.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -18158,11 +18186,11 @@ snapshots:
|
|||||||
cosmiconfig: 7.1.0
|
cosmiconfig: 7.1.0
|
||||||
resolve: 1.22.11
|
resolve: 1.22.11
|
||||||
|
|
||||||
babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.28.4):
|
babel-plugin-polyfill-corejs2@0.4.13(@babel/core@7.28.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/compat-data': 7.28.0
|
'@babel/compat-data': 7.28.0
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.4)
|
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.5)
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -18176,10 +18204,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.28.4):
|
babel-plugin-polyfill-corejs3@0.11.1(@babel/core@7.28.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.4)
|
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.5)
|
||||||
core-js-compat: 3.46.0
|
core-js-compat: 3.46.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -18192,10 +18220,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.28.4):
|
babel-plugin-polyfill-regenerator@0.6.4(@babel/core@7.28.5):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.5
|
||||||
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.4)
|
'@babel/helper-define-polyfill-provider': 0.6.4(@babel/core@7.28.5)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -18220,7 +18248,7 @@ snapshots:
|
|||||||
|
|
||||||
batch-cluster@15.0.1: {}
|
batch-cluster@15.0.1: {}
|
||||||
|
|
||||||
better-auth@1.3.34(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
better-auth@1.3.34(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
|
'@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
|
||||||
'@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
|
'@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)
|
||||||
@@ -18237,7 +18265,7 @@ snapshots:
|
|||||||
nanostores: 1.0.1
|
nanostores: 1.0.1
|
||||||
zod: 4.1.12
|
zod: 4.1.12
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
next: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
next: 16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
@@ -18657,6 +18685,10 @@ snapshots:
|
|||||||
path-type: 4.0.0
|
path-type: 4.0.0
|
||||||
yaml: 1.10.2
|
yaml: 1.10.2
|
||||||
|
|
||||||
|
creem@0.4.0:
|
||||||
|
dependencies:
|
||||||
|
zod: 3.25.76
|
||||||
|
|
||||||
cross-env@10.1.0:
|
cross-env@10.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@epic-web/invariant': 1.0.0
|
'@epic-web/invariant': 1.0.0
|
||||||
@@ -21839,7 +21871,7 @@ snapshots:
|
|||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.0.1
|
'@next/env': 16.0.1
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -21857,14 +21889,13 @@ snapshots:
|
|||||||
'@next/swc-linux-x64-musl': 16.0.1
|
'@next/swc-linux-x64-musl': 16.0.1
|
||||||
'@next/swc-win32-arm64-msvc': 16.0.1
|
'@next/swc-win32-arm64-msvc': 16.0.1
|
||||||
'@next/swc-win32-x64-msvc': 16.0.1
|
'@next/swc-win32-x64-msvc': 16.0.1
|
||||||
babel-plugin-react-compiler: 19.1.0-rc.3
|
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.0.1
|
'@next/env': 16.0.1
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -21882,6 +21913,7 @@ snapshots:
|
|||||||
'@next/swc-linux-x64-musl': 16.0.1
|
'@next/swc-linux-x64-musl': 16.0.1
|
||||||
'@next/swc-win32-arm64-msvc': 16.0.1
|
'@next/swc-win32-arm64-msvc': 16.0.1
|
||||||
'@next/swc-win32-x64-msvc': 16.0.1
|
'@next/swc-win32-x64-msvc': 16.0.1
|
||||||
|
babel-plugin-react-compiler: 19.1.0-rc.3
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@@ -22803,9 +22835,9 @@ snapshots:
|
|||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
react-avatar-editor@13.0.2(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
react-avatar-editor@13.0.2(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/plugin-transform-runtime': 7.27.4(@babel/core@7.28.4)
|
'@babel/plugin-transform-runtime': 7.27.4(@babel/core@7.28.5)
|
||||||
'@babel/runtime': 7.28.4
|
'@babel/runtime': 7.28.4
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
react: 19.2.0
|
react: 19.2.0
|
||||||
@@ -23004,38 +23036,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
|
||||||
react-scan@0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
|
react-scan@0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.53.2):
|
||||||
dependencies:
|
|
||||||
'@babel/core': 7.28.4
|
|
||||||
'@babel/generator': 7.28.3
|
|
||||||
'@babel/types': 7.28.4
|
|
||||||
'@clack/core': 0.3.5
|
|
||||||
'@clack/prompts': 0.8.2
|
|
||||||
'@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
|
||||||
'@preact/signals': 1.3.2(preact@10.27.2)
|
|
||||||
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
|
||||||
'@types/node': 20.19.25
|
|
||||||
bippy: 0.3.27(@types/react@19.2.3)(react@19.2.0)
|
|
||||||
esbuild: 0.25.11
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
kleur: 4.1.5
|
|
||||||
mri: 1.2.0
|
|
||||||
playwright: 1.55.0
|
|
||||||
preact: 10.27.2
|
|
||||||
react: 19.2.0
|
|
||||||
react-dom: 19.2.0(react@19.2.0)
|
|
||||||
tsx: 4.20.6
|
|
||||||
optionalDependencies:
|
|
||||||
next: 16.0.1(@babel/core@7.28.4)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
|
||||||
react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
|
||||||
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
|
||||||
unplugin: 2.1.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@types/react'
|
|
||||||
- rollup
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
react-scan@0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@4.53.2):
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.4
|
||||||
'@babel/generator': 7.28.3
|
'@babel/generator': 7.28.3
|
||||||
@@ -23057,7 +23058,38 @@ snapshots:
|
|||||||
react-dom: 19.2.0(react@19.2.0)
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
tsx: 4.20.6
|
tsx: 4.20.6
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
next: 16.0.1(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
next: 16.0.1(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
unplugin: 2.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
- rollup
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
react-scan@0.4.3(@types/react@19.2.3)(next@16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-dom@19.2.0(react@19.2.0))(react-router-dom@6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)(rollup@2.79.2):
|
||||||
|
dependencies:
|
||||||
|
'@babel/core': 7.28.4
|
||||||
|
'@babel/generator': 7.28.3
|
||||||
|
'@babel/types': 7.28.4
|
||||||
|
'@clack/core': 0.3.5
|
||||||
|
'@clack/prompts': 0.8.2
|
||||||
|
'@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
|
'@preact/signals': 1.3.2(preact@10.27.2)
|
||||||
|
'@rollup/pluginutils': 5.3.0(rollup@2.79.2)
|
||||||
|
'@types/node': 20.19.25
|
||||||
|
bippy: 0.3.27(@types/react@19.2.3)(react@19.2.0)
|
||||||
|
esbuild: 0.25.11
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
kleur: 4.1.5
|
||||||
|
mri: 1.2.0
|
||||||
|
playwright: 1.55.0
|
||||||
|
preact: 10.27.2
|
||||||
|
react: 19.2.0
|
||||||
|
react-dom: 19.2.0(react@19.2.0)
|
||||||
|
tsx: 4.20.6
|
||||||
|
optionalDependencies:
|
||||||
|
next: 16.0.1(@babel/core@7.28.5)(babel-plugin-react-compiler@19.1.0-rc.3)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
react-router: 7.9.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
react-router-dom: 6.30.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
unplugin: 2.1.0
|
unplugin: 2.1.0
|
||||||
@@ -24380,7 +24412,7 @@ snapshots:
|
|||||||
magic-string-ast: 1.0.3
|
magic-string-ast: 1.0.3
|
||||||
unplugin: 2.3.10
|
unplugin: 2.3.10
|
||||||
|
|
||||||
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.25.12)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)):
|
unplugin-dts@1.0.0-beta.6(@microsoft/api-extractor@7.52.13(@types/node@24.10.1))(esbuild@0.25.12)(rolldown@1.0.0-beta.49)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.2)
|
'@rollup/pluginutils': 5.3.0(rollup@4.53.2)
|
||||||
'@volar/typescript': 2.4.23
|
'@volar/typescript': 2.4.23
|
||||||
@@ -24394,6 +24426,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@microsoft/api-extractor': 7.52.13(@types/node@24.10.1)
|
'@microsoft/api-extractor': 7.52.13(@types/node@24.10.1)
|
||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
|
rolldown: 1.0.0-beta.49
|
||||||
rollup: 4.53.2
|
rollup: 4.53.2
|
||||||
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user