Skip to content

Commit f067803

Browse files
committed
feat: implement managed storage plans and provider settings
- Added new UI schema for managing storage plans, including catalog, pricing, and product configurations. - Introduced StoragePlanService to handle storage plan operations and integrate with existing billing services. - Updated SuperAdmin settings to include managed storage provider configurations. - Enhanced localization files with new keys for storage plan management. - Implemented API endpoints for fetching and updating storage plans. Signed-off-by: Innei <[email protected]>
1 parent 9143428 commit f067803

37 files changed

+3532
-166
lines changed

be/apps/core/src/locales/ui-schema/en.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,37 @@ const enUiSchema = {
298298
},
299299
},
300300
},
301+
'storage-plans': {
302+
title: 'Storage plans',
303+
description: 'Managed storage catalog, pricing, and Creem products for storage subscriptions.',
304+
fields: {
305+
catalog: {
306+
title: 'Plan catalog',
307+
description:
308+
'Manage storage plans for managed B2 space. Use the dashboard editor; JSON is no longer required.',
309+
placeholder: 'Configured via dashboard',
310+
helper: 'Plans include name/description/capacity and active flag.',
311+
},
312+
pricing: {
313+
title: 'Storage pricing',
314+
description: 'Monthly price and currency per storage plan.',
315+
placeholder: 'Configured via dashboard',
316+
helper: 'Blank values fall back to defaults or hide pricing.',
317+
},
318+
products: {
319+
title: 'Creem products',
320+
description: 'Creem product per storage plan for checkout and portal.',
321+
placeholder: 'Configured via dashboard',
322+
helper: 'Blank values hide the upgrade entry for that plan.',
323+
},
324+
'managed-provider': {
325+
title: 'Managed provider key',
326+
description: 'Provider ID from storage providers list that backs managed storage plans (e.g., b2-managed).',
327+
placeholder: 'b2-managed',
328+
helper: 'Used by backend to issue upload/read credentials for managed tenants.',
329+
},
330+
},
331+
},
301332
oauth: {
302333
title: 'OAuth providers',
303334
description: 'Configure shared third-party login providers for all tenants.',

be/apps/core/src/locales/ui-schema/zh-CN.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,36 @@ const zhCnUiSchema = {
296296
},
297297
},
298298
},
299+
'storage-plans': {
300+
title: '存储计划',
301+
description: '管理托管存储方案的目录、定价以及 Creem 商品映射。',
302+
fields: {
303+
catalog: {
304+
title: '计划目录',
305+
description: '存储计划的名称/描述/容量与启用状态,建议在控制台中编辑,无需手填 JSON。',
306+
placeholder: '通过控制台编辑',
307+
helper: '包含 plan id、name、description、capacityBytes、isActive 等字段。',
308+
},
309+
pricing: {
310+
title: '存储定价',
311+
description: '每个存储计划的月费与币种。',
312+
placeholder: '通过控制台编辑',
313+
helper: '留空回退到默认值或隐藏价格。',
314+
},
315+
products: {
316+
title: 'Creem 商品',
317+
description: '为存储计划绑定 Creem 商品 ID,用于结算与用户门户。',
318+
placeholder: '通过控制台编辑',
319+
helper: '留空则该计划不会展示升级入口。',
320+
},
321+
'managed-provider': {
322+
title: '托管存储 Provider Key',
323+
description: '与存储提供商列表中的配置项对应(例如 b2-managed)。',
324+
placeholder: 'b2-managed',
325+
helper: '后端将用该 Provider 为托管存储租户发放上传/读取权限。',
326+
},
327+
},
328+
},
299329
oauth: {
300330
title: 'OAuth 登录渠道',
301331
description: '统一配置所有租户可用的第三方登录渠道。',

be/apps/core/src/modules/configuration/system-setting/system-setting.constants.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import {
77
BILLING_PLAN_PRODUCTS_SETTING_KEY,
88
} from 'core/modules/platform/billing/billing-plan.constants'
99
import type { BillingPlanId, BillingPlanQuota } from 'core/modules/platform/billing/billing-plan.types'
10+
import {
11+
DEFAULT_STORAGE_PLAN_CATALOG,
12+
STORAGE_PLAN_CATALOG_SETTING_KEY,
13+
STORAGE_PLAN_PRICING_SETTING_KEY,
14+
STORAGE_PLAN_PRODUCTS_SETTING_KEY,
15+
} from 'core/modules/platform/billing/storage-plan.constants'
16+
import type { StoragePlanCatalog } from 'core/modules/platform/billing/storage-plan.types'
1017
import { z } from 'zod'
1118

1219
const nonEmptyString = z.string().trim().min(1)
@@ -114,6 +121,36 @@ export const SYSTEM_SETTING_DEFINITIONS = {
114121
defaultValue: {},
115122
isSensitive: false,
116123
},
124+
storagePlanCatalog: {
125+
key: STORAGE_PLAN_CATALOG_SETTING_KEY,
126+
schema: z.record(z.string(), z.any()),
127+
defaultValue: DEFAULT_STORAGE_PLAN_CATALOG satisfies StoragePlanCatalog,
128+
isSensitive: false,
129+
},
130+
storagePlanProducts: {
131+
key: STORAGE_PLAN_PRODUCTS_SETTING_KEY,
132+
schema: z.record(z.string(), z.any()),
133+
defaultValue: {},
134+
isSensitive: false,
135+
},
136+
storagePlanPricing: {
137+
key: STORAGE_PLAN_PRICING_SETTING_KEY,
138+
schema: z.record(z.string(), z.any()),
139+
defaultValue: {},
140+
isSensitive: false,
141+
},
142+
managedStorageProvider: {
143+
key: 'system.storage.managed.provider',
144+
schema: z.string().trim().min(1).nullable(),
145+
defaultValue: null as string | null,
146+
isSensitive: false,
147+
},
148+
managedStorageProviders: {
149+
key: 'system.storage.managed.providers',
150+
schema: z.string(),
151+
defaultValue: '[]',
152+
isSensitive: true,
153+
},
117154
} as const
118155

119156
const BILLING_PLAN_QUOTA_KEYS = [

be/apps/core/src/modules/configuration/system-setting/system-setting.service.ts

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,24 @@ import type {
1616
BillingPlanProductConfigs,
1717
BillingPlanQuota,
1818
} from 'core/modules/platform/billing/billing-plan.types'
19+
import type {
20+
StoragePlanCatalog,
21+
StoragePlanPricingConfigs,
22+
StoragePlanProductConfigs,
23+
} from 'core/modules/platform/billing/storage-plan.types'
1924
import { sql } from 'drizzle-orm'
2025
import { injectable } from 'tsyringe'
2126
import type { ZodType } from 'zod'
2227
import { z } from 'zod'
2328

2429
import { getUiSchemaTranslator } from '../../ui/ui-schema/ui-schema.i18n'
30+
import type { BuilderStorageProvider } from '../setting/storage-provider.utils'
31+
import {
32+
maskStorageProviderSecrets,
33+
mergeStorageProviderSecrets,
34+
parseStorageProviders,
35+
serializeStorageProviders,
36+
} from '../setting/storage-provider.utils'
2537
import type { SystemSettingDbField } from './system-setting.constants'
2638
import {
2739
BILLING_PLAN_FIELD_DESCRIPTORS,
@@ -135,6 +147,29 @@ export class SystemSettingService {
135147
BILLING_PLAN_PRICING_SCHEMA,
136148
{},
137149
) as BillingPlanPricingConfigs
150+
const storagePlanCatalog = this.parseSetting(
151+
rawValues[SYSTEM_SETTING_DEFINITIONS.storagePlanCatalog.key],
152+
STORAGE_PLAN_CATALOG_SCHEMA,
153+
SYSTEM_SETTING_DEFINITIONS.storagePlanCatalog.defaultValue as StoragePlanCatalog,
154+
) as StoragePlanCatalog
155+
const storagePlanProducts = this.parseSetting(
156+
rawValues[SYSTEM_SETTING_DEFINITIONS.storagePlanProducts.key],
157+
STORAGE_PLAN_PRODUCTS_SCHEMA,
158+
{},
159+
) as StoragePlanProductConfigs
160+
const storagePlanPricing = this.parseSetting(
161+
rawValues[SYSTEM_SETTING_DEFINITIONS.storagePlanPricing.key],
162+
STORAGE_PLAN_PRICING_SCHEMA,
163+
{},
164+
) as StoragePlanPricingConfigs
165+
const managedStorageProvider = this.parseSetting(
166+
rawValues[SYSTEM_SETTING_DEFINITIONS.managedStorageProvider.key],
167+
SYSTEM_SETTING_DEFINITIONS.managedStorageProvider.schema,
168+
SYSTEM_SETTING_DEFINITIONS.managedStorageProvider.defaultValue,
169+
)
170+
const managedStorageProviders = this.parseManagedStorageProviders(
171+
rawValues[SYSTEM_SETTING_DEFINITIONS.managedStorageProviders.key],
172+
)
138173
return {
139174
allowRegistration,
140175
maxRegistrableUsers,
@@ -151,6 +186,11 @@ export class SystemSettingService {
151186
billingPlanOverrides,
152187
billingPlanProducts,
153188
billingPlanPricing,
189+
storagePlanCatalog,
190+
storagePlanProducts,
191+
storagePlanPricing,
192+
managedStorageProvider,
193+
managedStorageProviders,
154194
}
155195
}
156196

@@ -169,6 +209,26 @@ export class SystemSettingService {
169209
return settings.billingPlanPricing ?? {}
170210
}
171211

212+
async getStoragePlanCatalog(): Promise<StoragePlanCatalog> {
213+
const settings = await this.getSettings()
214+
return settings.storagePlanCatalog ?? {}
215+
}
216+
217+
async getStoragePlanProducts(): Promise<StoragePlanProductConfigs> {
218+
const settings = await this.getSettings()
219+
return settings.storagePlanProducts ?? {}
220+
}
221+
222+
async getStoragePlanPricing(): Promise<StoragePlanPricingConfigs> {
223+
const settings = await this.getSettings()
224+
return settings.storagePlanPricing ?? {}
225+
}
226+
227+
async getManagedStorageProviderKey(): Promise<string | null> {
228+
const settings = await this.getSettings()
229+
return settings.managedStorageProvider ?? null
230+
}
231+
172232
async getOverview(): Promise<SystemSettingOverview> {
173233
const settings = await this.getSettings()
174234
const totalUsers = await this.getTotalUserCount()
@@ -194,9 +254,17 @@ export class SystemSettingService {
194254

195255
const updates: Array<{ field: SystemSettingDbField; value: unknown }> = []
196256

197-
const enqueueUpdate = <K extends SystemSettingDbField>(field: K, value: unknown) => {
257+
const enqueueUpdate = <K extends SystemSettingDbField>(
258+
field: K,
259+
value: unknown,
260+
currentValue?: SystemSettings[K],
261+
) => {
198262
updates.push({ field, value })
199-
;(current as unknown as Record<string, unknown>)[field] = value
263+
if (currentValue !== undefined) {
264+
;(current as unknown as Record<string, unknown>)[field] = currentValue
265+
} else {
266+
;(current as unknown as Record<string, unknown>)[field] = value
267+
}
200268
}
201269

202270
if (patch.allowRegistration !== undefined && patch.allowRegistration !== current.allowRegistration) {
@@ -287,6 +355,37 @@ export class SystemSettingService {
287355
}
288356
}
289357

358+
if (patch.storagePlanCatalog !== undefined) {
359+
const parsed = STORAGE_PLAN_CATALOG_SCHEMA.parse(patch.storagePlanCatalog)
360+
enqueueUpdate('storagePlanCatalog', parsed)
361+
}
362+
363+
if (patch.storagePlanProducts !== undefined) {
364+
const parsed = STORAGE_PLAN_PRODUCTS_SCHEMA.parse(patch.storagePlanProducts)
365+
enqueueUpdate('storagePlanProducts', parsed)
366+
}
367+
368+
if (patch.storagePlanPricing !== undefined) {
369+
const parsed = STORAGE_PLAN_PRICING_SCHEMA.parse(patch.storagePlanPricing)
370+
enqueueUpdate('storagePlanPricing', parsed)
371+
}
372+
373+
if (patch.managedStorageProvider !== undefined && patch.managedStorageProvider !== current.managedStorageProvider) {
374+
enqueueUpdate('managedStorageProvider', this.normalizeNullableString(patch.managedStorageProvider))
375+
}
376+
377+
if (patch.managedStorageProviders !== undefined) {
378+
const normalizedProviders = this.normalizeManagedStorageProvidersPatch(
379+
patch.managedStorageProviders,
380+
current.managedStorageProviders ?? [],
381+
)
382+
const nextSerialized = serializeStorageProviders(normalizedProviders)
383+
const currentSerialized = serializeStorageProviders(current.managedStorageProviders ?? [])
384+
if (nextSerialized !== currentSerialized) {
385+
enqueueUpdate('managedStorageProviders', nextSerialized, normalizedProviders)
386+
}
387+
}
388+
290389
if (updates.length === 0) {
291390
return current
292391
}
@@ -309,6 +408,10 @@ export class SystemSettingService {
309408
const map = {} as SystemSettingValueMap
310409

311410
;(Object.keys(SYSTEM_SETTING_DEFINITIONS) as SystemSettingDbField[]).forEach((field) => {
411+
if (field === 'managedStorageProviders') {
412+
;(map as Record<string, unknown>)[field] = maskStorageProviderSecrets(settings.managedStorageProviders ?? [])
413+
return
414+
}
312415
;(map as Record<string, unknown>)[field] = settings[field]
313416
})
314417

@@ -342,6 +445,51 @@ export class SystemSettingService {
342445
return map
343446
}
344447

448+
private parseManagedStorageProviders(raw: unknown): BuilderStorageProvider[] {
449+
if (!raw) {
450+
return []
451+
}
452+
453+
if (Array.isArray(raw) || typeof raw === 'object') {
454+
try {
455+
return parseStorageProviders(JSON.stringify(raw))
456+
} catch {
457+
return []
458+
}
459+
}
460+
461+
if (typeof raw === 'string') {
462+
const normalized = raw.trim()
463+
if (!normalized) {
464+
return []
465+
}
466+
return parseStorageProviders(normalized)
467+
}
468+
469+
return []
470+
}
471+
472+
private normalizeManagedStorageProvidersPatch(
473+
patch: unknown,
474+
current: BuilderStorageProvider[],
475+
): BuilderStorageProvider[] {
476+
let normalized: string
477+
if (typeof patch === 'string') {
478+
normalized = patch
479+
} else if (patch == null) {
480+
normalized = '[]'
481+
} else {
482+
try {
483+
normalized = JSON.stringify(patch)
484+
} catch {
485+
normalized = '[]'
486+
}
487+
}
488+
489+
const incoming = parseStorageProviders(normalized)
490+
return mergeStorageProviderSecrets(incoming, current ?? [])
491+
}
492+
345493
private extractPlanFieldUpdates(patch: UpdateSystemSettingsInput): PlanFieldUpdateSummary {
346494
const summary: PlanFieldUpdateSummary = {
347495
hasUpdates: false,
@@ -608,6 +756,17 @@ const PLAN_PRICING_ENTRY_SCHEMA = z.object({
608756

609757
const BILLING_PLAN_PRICING_SCHEMA = z.record(z.string(), PLAN_PRICING_ENTRY_SCHEMA).default({})
610758

759+
const STORAGE_PLAN_CATALOG_ENTRY_SCHEMA = z.object({
760+
name: z.string().trim().min(1),
761+
description: z.string().trim().nullable().optional(),
762+
capacityBytes: z.number().int().min(0).nullable().optional(),
763+
isActive: z.boolean().optional(),
764+
})
765+
766+
const STORAGE_PLAN_CATALOG_SCHEMA = z.record(z.string(), STORAGE_PLAN_CATALOG_ENTRY_SCHEMA).default({})
767+
const STORAGE_PLAN_PRODUCTS_SCHEMA = z.record(z.string(), PLAN_PRODUCT_ENTRY_SCHEMA).default({})
768+
const STORAGE_PLAN_PRICING_SCHEMA = z.record(z.string(), PLAN_PRICING_ENTRY_SCHEMA).default({})
769+
611770
type PlanQuotaUpdateMap = Partial<Record<BillingPlanId, Partial<BillingPlanQuota>>>
612771
type PlanPricingUpdateMap = Partial<Record<BillingPlanId, Partial<BillingPlanPricing>>>
613772
type PlanProductUpdateMap = Partial<Record<BillingPlanId, BillingPlanPaymentInfo>>

be/apps/core/src/modules/configuration/system-setting/system-setting.types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ import type {
33
BillingPlanPricingConfigs,
44
BillingPlanProductConfigs,
55
} from 'core/modules/platform/billing/billing-plan.types'
6+
import type {
7+
StoragePlanCatalog,
8+
StoragePlanPricingConfigs,
9+
StoragePlanProductConfigs,
10+
} from 'core/modules/platform/billing/storage-plan.types'
611
import type { UiSchema } from 'core/modules/ui/ui-schema/ui-schema.type'
712

13+
import type { BuilderStorageProvider } from '../setting/storage-provider.utils'
814
import type { BillingPlanSettingField, SystemSettingDbField, SystemSettingField } from './system-setting.constants'
915

1016
export interface SystemSettings {
@@ -23,6 +29,11 @@ export interface SystemSettings {
2329
billingPlanOverrides: BillingPlanOverrides
2430
billingPlanProducts: BillingPlanProductConfigs
2531
billingPlanPricing: BillingPlanPricingConfigs
32+
storagePlanCatalog: StoragePlanCatalog
33+
storagePlanProducts: StoragePlanProductConfigs
34+
storagePlanPricing: StoragePlanPricingConfigs
35+
managedStorageProvider: string | null
36+
managedStorageProviders: BuilderStorageProvider[]
2637
}
2738

2839
export type SystemSettingValueMap = {

0 commit comments

Comments
 (0)