openxiangda 1.0.84 → 1.0.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/lib/cli.js +214 -0
- package/openxiangda-skills/SKILL.md +4 -1
- package/openxiangda-skills/references/architecture-design.md +38 -4
- package/openxiangda-skills/references/connector-resources.md +3 -0
- package/openxiangda-skills/references/pages/page-sdk.md +39 -0
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +81 -0
- package/package.json +1 -1
- package/packages/sdk/dist/openxiangdaProvider-CaXMpsnK.d.mts +328 -0
- package/packages/sdk/dist/openxiangdaProvider-CaXMpsnK.d.ts +328 -0
- package/packages/sdk/dist/runtime/index.cjs +4250 -3641
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +1 -1
- package/packages/sdk/dist/runtime/index.d.ts +1 -1
- package/packages/sdk/dist/runtime/index.mjs +3845 -3219
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +645 -38
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +2 -162
- package/packages/sdk/dist/runtime/react.d.ts +2 -162
- package/packages/sdk/dist/runtime/react.mjs +667 -39
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/src/app/router.tsx +2 -1
package/README.md
CHANGED
|
@@ -178,6 +178,8 @@ const affiliatedDepartmentExternalId = user?.affiliatedDepartment?.externalId
|
|
|
178
178
|
|
|
179
179
|
后端业务逻辑优先声明为 App Function:源码放在 `src/functions/<functionCode>/index.ts`,资源 manifest 放在 `src/resources/functions/<functionCode>.json`。函数运行在 trusted_node 中,通过 `ctx.form`、`ctx.dataView`、`ctx.connector`、`ctx.notification`、`ctx.platform.api` 等受控 API 访问平台能力;自动化、流程和运行时接口都可以调用同一个 function。JS_CODE V2 仍兼容,但新逻辑建议写成 function,再由 `function_call` 节点、`sdk.function.invoke(code, { input })` 或 `/:appType/v1/functions/:code/invoke.json` 调用。当前 MVP 中直接运行时接口要求调用者具备应用自动化管理权限,自动化/流程内部调用走服务端受控上下文。
|
|
180
180
|
|
|
181
|
+
应用登录能力通过 auth resource 和 runtime SDK 提供:登录配置放在 `src/resources/auth/<code>.json`,默认 React SPA 模板已包含 `/view/:appType/login`,自定义页面可使用 `createAuthClient({ appType, servicePrefix })` 或 `LoginPage` / `useAuth` from `openxiangda/runtime/react`。手机号验证码、CAS/SSO 或其他外部登录可以由 App Function provider 校验外部凭证,但 provider 只能返回 `phone` / `email` / `externalId` / `unionId` 等身份声明;平台后端按 auth resource 策略执行账号匹配、绑定、创建或拒绝,并由平台统一签发 token/cookie。默认注册策略是拒绝,开启自动注册或白名单注册前必须确认身份匹配键、默认角色、验证码 TTL/频率/失败次数、审计字段和错误文案策略。
|
|
182
|
+
|
|
181
183
|
常用数据视图命令:
|
|
182
184
|
|
|
183
185
|
```bash
|
package/lib/cli.js
CHANGED
|
@@ -3374,6 +3374,7 @@ function ensureResourceBuckets(bound) {
|
|
|
3374
3374
|
bound.resources.roles = bound.resources.roles || {};
|
|
3375
3375
|
bound.resources.connectors = bound.resources.connectors || {};
|
|
3376
3376
|
bound.resources.dataViews = bound.resources.dataViews || {};
|
|
3377
|
+
bound.resources.authConfigs = bound.resources.authConfigs || {};
|
|
3377
3378
|
bound.resources.notifications = bound.resources.notifications || {};
|
|
3378
3379
|
bound.resources.notifications.templates = bound.resources.notifications.templates || {};
|
|
3379
3380
|
bound.resources.notifications.typeConfigs = bound.resources.notifications.typeConfigs || {};
|
|
@@ -3584,6 +3585,14 @@ function saveDataViewResource(target, dataViewCode, dataViewId, extra = {}) {
|
|
|
3584
3585
|
}, keys);
|
|
3585
3586
|
}
|
|
3586
3587
|
|
|
3588
|
+
function saveAuthConfigResource(target, authConfigCode, configId, extra = {}) {
|
|
3589
|
+
const keys = ['configId', 'status'];
|
|
3590
|
+
saveStateResource(target, 'authConfigs', authConfigCode, {
|
|
3591
|
+
...pickStateFields(extra, keys),
|
|
3592
|
+
configId,
|
|
3593
|
+
}, keys);
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3587
3596
|
function saveRoleResource(target, roleCode, roleId) {
|
|
3588
3597
|
saveStateResource(target, 'roles', roleCode, { roleId }, ['roleId']);
|
|
3589
3598
|
}
|
|
@@ -3691,6 +3700,7 @@ const RESOURCE_SPECS = [
|
|
|
3691
3700
|
{ key: 'automations', dir: 'automations', topFiles: ['automations.json'], pluralKeys: ['automations'] },
|
|
3692
3701
|
{ key: 'functions', dir: 'functions', topFiles: ['functions.json'], pluralKeys: ['functions'] },
|
|
3693
3702
|
{ key: 'dataViews', dir: 'data-views', topFiles: ['data-views.json'], pluralKeys: ['dataViews', 'data-views'] },
|
|
3703
|
+
{ key: 'authConfigs', dir: 'auth', topFiles: ['auth.json'], pluralKeys: ['authConfigs', 'auth'] },
|
|
3694
3704
|
{
|
|
3695
3705
|
key: 'pagePermissionGroups',
|
|
3696
3706
|
dir: path.join('permissions', 'page-groups'),
|
|
@@ -3890,6 +3900,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3890
3900
|
);
|
|
3891
3901
|
const dataViewCodes = unique((manifest.dataViews || []).map(item => item.code).filter(Boolean));
|
|
3892
3902
|
const functionCodes = unique((manifest.functions || []).map(item => item.code).filter(Boolean));
|
|
3903
|
+
const authConfigCodes = unique((manifest.authConfigs || []).map(item => item.code).filter(Boolean));
|
|
3893
3904
|
const content = [
|
|
3894
3905
|
'/* eslint-disable */',
|
|
3895
3906
|
'// Generated by openxiangda resource typegen. Do not edit manually.',
|
|
@@ -3909,6 +3920,9 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3909
3920
|
`export const functionCodes = ${JSON.stringify(functionCodes, null, 2)} as const`,
|
|
3910
3921
|
'export type FunctionCode = typeof functionCodes[number]',
|
|
3911
3922
|
'',
|
|
3923
|
+
`export const authConfigCodes = ${JSON.stringify(authConfigCodes, null, 2)} as const`,
|
|
3924
|
+
'export type AuthConfigCode = typeof authConfigCodes[number]',
|
|
3925
|
+
'',
|
|
3912
3926
|
`export const runtimeMenus = ${JSON.stringify(menus, null, 2)} as const`,
|
|
3913
3927
|
'',
|
|
3914
3928
|
`export const pagePermissionGroups = ${JSON.stringify(pagePermissionGroups, null, 2)} as const`,
|
|
@@ -3923,6 +3937,7 @@ function generateResourceTypes(manifest, outputFile) {
|
|
|
3923
3937
|
pagePermissionGroups: pagePermissionGroupCodes.length,
|
|
3924
3938
|
dataViews: dataViewCodes.length,
|
|
3925
3939
|
functions: functionCodes.length,
|
|
3940
|
+
authConfigs: authConfigCodes.length,
|
|
3926
3941
|
};
|
|
3927
3942
|
}
|
|
3928
3943
|
|
|
@@ -4001,6 +4016,32 @@ function validateResourceItem(kind, item, errors, warnings) {
|
|
|
4001
4016
|
}
|
|
4002
4017
|
validateDataViewPerformance(label, definition, errors, warnings, storageMode);
|
|
4003
4018
|
}
|
|
4019
|
+
if (kind === 'authConfigs') {
|
|
4020
|
+
const config = item.configJson || item.config || item;
|
|
4021
|
+
if (!Array.isArray(config.methods)) {
|
|
4022
|
+
errors.push(`${label}: 缺少 methods`);
|
|
4023
|
+
} else {
|
|
4024
|
+
config.methods.forEach((method, index) => {
|
|
4025
|
+
if (!method?.type) errors.push(`${label}.methods[${index}]: 缺少 type`);
|
|
4026
|
+
if (
|
|
4027
|
+
method?.type === 'phone_code' &&
|
|
4028
|
+
method.enabled !== false &&
|
|
4029
|
+
!(
|
|
4030
|
+
method.provider?.functionCode ||
|
|
4031
|
+
method.providerFunctionCode ||
|
|
4032
|
+
config.providers?.phone_code?.functionCode ||
|
|
4033
|
+
config.providers?.phoneCode?.functionCode
|
|
4034
|
+
)
|
|
4035
|
+
) {
|
|
4036
|
+
errors.push(`${label}.methods[${index}]: phone_code 缺少 provider.functionCode`);
|
|
4037
|
+
}
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
const registrationMode = String(config.registration?.mode || 'reject');
|
|
4041
|
+
if (registrationMode !== 'reject') {
|
|
4042
|
+
warnings.push(`${label}: 已开启非默认注册策略 ${registrationMode},请确认身份匹配键、默认角色和审计字段`);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4004
4045
|
if (kind === 'pagePermissionGroups' && !item.name) errors.push(`${label}: 缺少 name`);
|
|
4005
4046
|
if (kind === 'formPermissionGroups') {
|
|
4006
4047
|
if (!item.name) errors.push(`${label}: 缺少 name`);
|
|
@@ -4402,6 +4443,7 @@ async function buildResourcePlan(config, target, manifest) {
|
|
|
4402
4443
|
addNotificationPlanActions(target, actions, manifest.notifications || [], existing);
|
|
4403
4444
|
await addWorkflowPlanActions(config, target, actions, manifest.workflows, existing.workflows);
|
|
4404
4445
|
addPlanActions(actions, 'function', manifest.functions, existing.functions, (item, current) => functionEquals(target, item, current));
|
|
4446
|
+
addPlanActions(actions, 'authConfig', manifest.authConfigs, existing.authConfigs, (item, current) => authConfigEquals(item, current));
|
|
4405
4447
|
await addAutomationPlanActions(config, target, actions, manifest.automations, existing.automations);
|
|
4406
4448
|
addPlanActions(actions, 'dataView', manifest.dataViews, existing.dataViews, (item, current) => dataViewEquals(target.bound, item, current));
|
|
4407
4449
|
addPlanActions(actions, 'pagePermissionGroup', manifest.pagePermissionGroups, existing.pagePermissionGroups, (item, current) => pagePermissionGroupEquals(target.bound, item, current));
|
|
@@ -4437,6 +4479,7 @@ async function publishResourceManifest(config, target, manifest, options = {}) {
|
|
|
4437
4479
|
await publishNotificationResources(config, target, manifest.notifications || [], result);
|
|
4438
4480
|
await publishWorkflowResources(config, target, manifest.workflows || [], result);
|
|
4439
4481
|
await publishFunctionResources(config, target, manifest.functions || [], result);
|
|
4482
|
+
await publishAuthConfigResources(config, target, manifest.authConfigs || [], result);
|
|
4440
4483
|
await publishAutomationResources(config, target, manifest.automations || [], result);
|
|
4441
4484
|
await publishDataViewResources(config, target, manifest.dataViews || [], result);
|
|
4442
4485
|
await publishPagePermissionGroupResources(config, target, manifest.pagePermissionGroups || [], result);
|
|
@@ -4460,6 +4503,7 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
4460
4503
|
notificationTypeConfigs: new Map(),
|
|
4461
4504
|
workflows: new Map(),
|
|
4462
4505
|
functions: new Map(),
|
|
4506
|
+
authConfigs: new Map(),
|
|
4463
4507
|
automations: new Map(),
|
|
4464
4508
|
dataViews: new Map(),
|
|
4465
4509
|
pagePermissionGroups: new Map(),
|
|
@@ -4565,6 +4609,31 @@ async function fetchExistingResourceMaps(config, target, manifest) {
|
|
|
4565
4609
|
});
|
|
4566
4610
|
}
|
|
4567
4611
|
}
|
|
4612
|
+
if ((manifest.authConfigs || []).length > 0) {
|
|
4613
|
+
const data = await requestWithAuth(
|
|
4614
|
+
config,
|
|
4615
|
+
target.profileName,
|
|
4616
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
|
|
4617
|
+
page: 1,
|
|
4618
|
+
pageSize: 1000,
|
|
4619
|
+
})
|
|
4620
|
+
);
|
|
4621
|
+
indexByCode(maps.authConfigs, normalizeItems(data), item => item.code || item.resourceCode);
|
|
4622
|
+
for (const item of manifest.authConfigs || []) {
|
|
4623
|
+
const code = item.code || item.resourceCode;
|
|
4624
|
+
const existing = maps.authConfigs.get(code);
|
|
4625
|
+
if (!existing) continue;
|
|
4626
|
+
const detail = await requestOptionalWithAuth(
|
|
4627
|
+
config,
|
|
4628
|
+
target.profileName,
|
|
4629
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
|
|
4630
|
+
);
|
|
4631
|
+
maps.authConfigs.set(code, {
|
|
4632
|
+
...existing,
|
|
4633
|
+
...(detail || {}),
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4568
4637
|
if ((manifest.dataViews || []).length > 0) {
|
|
4569
4638
|
const data = await requestWithAuth(
|
|
4570
4639
|
config,
|
|
@@ -5227,6 +5296,38 @@ async function publishFunctionResources(config, target, functions, result) {
|
|
|
5227
5296
|
}
|
|
5228
5297
|
}
|
|
5229
5298
|
|
|
5299
|
+
async function publishAuthConfigResources(config, target, authConfigs, result) {
|
|
5300
|
+
for (const authItem of authConfigs) {
|
|
5301
|
+
const code = authItem.code || authItem.resourceCode || 'default';
|
|
5302
|
+
const existing = await findExistingAuthConfig(config, target, code);
|
|
5303
|
+
const body = normalizeAuthConfigManifest(authItem);
|
|
5304
|
+
const data = existing
|
|
5305
|
+
? await requestWithAuth(
|
|
5306
|
+
config,
|
|
5307
|
+
target.profileName,
|
|
5308
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`,
|
|
5309
|
+
{ method: 'PUT', body }
|
|
5310
|
+
)
|
|
5311
|
+
: await requestWithAuth(
|
|
5312
|
+
config,
|
|
5313
|
+
target.profileName,
|
|
5314
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`,
|
|
5315
|
+
{ method: 'POST', body }
|
|
5316
|
+
);
|
|
5317
|
+
if (data?.id) {
|
|
5318
|
+
saveAuthConfigResource(target, data.code || code, data.id, {
|
|
5319
|
+
status: data.status,
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
result.published.push({
|
|
5323
|
+
kind: 'authConfig',
|
|
5324
|
+
code,
|
|
5325
|
+
action: existing ? 'update' : 'create',
|
|
5326
|
+
id: data?.id,
|
|
5327
|
+
});
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
|
|
5230
5331
|
async function publishDataViewResources(config, target, dataViews, result) {
|
|
5231
5332
|
for (const dataViewItem of dataViews) {
|
|
5232
5333
|
const existing = await findExistingDataView(config, target, dataViewItem.code);
|
|
@@ -5272,6 +5373,18 @@ async function findExistingFunction(config, target, code) {
|
|
|
5272
5373
|
return null;
|
|
5273
5374
|
}
|
|
5274
5375
|
|
|
5376
|
+
async function findExistingAuthConfig(config, target, code) {
|
|
5377
|
+
const stateId = target.bound.resources?.authConfigs?.[code]?.configId;
|
|
5378
|
+
const byCode = await requestOptionalWithAuth(
|
|
5379
|
+
config,
|
|
5380
|
+
target.profileName,
|
|
5381
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
|
|
5382
|
+
);
|
|
5383
|
+
if (byCode?.id) return byCode;
|
|
5384
|
+
if (stateId) return { id: stateId, code };
|
|
5385
|
+
return null;
|
|
5386
|
+
}
|
|
5387
|
+
|
|
5275
5388
|
async function findExistingDataView(config, target, code) {
|
|
5276
5389
|
const stateId = target.bound.resources?.dataViews?.[code]?.dataViewId;
|
|
5277
5390
|
const byCode = await requestOptionalWithAuth(
|
|
@@ -5422,6 +5535,7 @@ async function pruneResourceManifest(config, target, manifest, result) {
|
|
|
5422
5535
|
await pruneWorkflows(config, target, desiredCodes(manifest.workflows), result);
|
|
5423
5536
|
await pruneAutomations(config, target, desiredCodes(manifest.automations), result);
|
|
5424
5537
|
await pruneFunctions(config, target, desiredCodes(manifest.functions), result);
|
|
5538
|
+
await pruneAuthConfigs(config, target, desiredCodes(manifest.authConfigs), result);
|
|
5425
5539
|
await pruneDataViews(config, target, desiredCodes(manifest.dataViews), result);
|
|
5426
5540
|
await prunePagePermissionGroups(
|
|
5427
5541
|
config,
|
|
@@ -5617,6 +5731,29 @@ async function pruneFunctions(config, target, desired, result) {
|
|
|
5617
5731
|
}
|
|
5618
5732
|
}
|
|
5619
5733
|
|
|
5734
|
+
async function pruneAuthConfigs(config, target, desired, result) {
|
|
5735
|
+
const data = await requestWithAuth(
|
|
5736
|
+
config,
|
|
5737
|
+
target.profileName,
|
|
5738
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
|
|
5739
|
+
page: 1,
|
|
5740
|
+
pageSize: 1000,
|
|
5741
|
+
})
|
|
5742
|
+
);
|
|
5743
|
+
for (const item of normalizeItems(data)) {
|
|
5744
|
+
const code = item.code || item.resourceCode;
|
|
5745
|
+
if (!code || desired.has(code)) continue;
|
|
5746
|
+
await pruneOne(config, target, result, 'authConfig', code, item.id, async () => {
|
|
5747
|
+
await requestWithAuth(
|
|
5748
|
+
config,
|
|
5749
|
+
target.profileName,
|
|
5750
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`,
|
|
5751
|
+
{ method: 'DELETE' }
|
|
5752
|
+
);
|
|
5753
|
+
});
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
|
|
5620
5757
|
async function prunePagePermissionGroups(config, target, desired, result) {
|
|
5621
5758
|
const data = await requestWithAuth(
|
|
5622
5759
|
config,
|
|
@@ -5692,6 +5829,7 @@ function removeStateResource(target, kind, code) {
|
|
|
5692
5829
|
workflow: 'workflows',
|
|
5693
5830
|
automation: 'automations',
|
|
5694
5831
|
function: 'functions',
|
|
5832
|
+
authConfig: 'authConfigs',
|
|
5695
5833
|
dataView: 'dataViews',
|
|
5696
5834
|
pagePermissionGroup: 'pagePermissionGroups',
|
|
5697
5835
|
formPermissionGroup: 'formPermissionGroups',
|
|
@@ -5890,6 +6028,26 @@ async function pullResources(config, target) {
|
|
|
5890
6028
|
written.push(path.relative(process.cwd(), filePath));
|
|
5891
6029
|
}
|
|
5892
6030
|
|
|
6031
|
+
const authConfigs = await requestWithAuth(
|
|
6032
|
+
config,
|
|
6033
|
+
target.profileName,
|
|
6034
|
+
apiPathWithQuery(`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs`, {
|
|
6035
|
+
page: 1,
|
|
6036
|
+
pageSize: 1000,
|
|
6037
|
+
})
|
|
6038
|
+
);
|
|
6039
|
+
for (const item of normalizeItems(authConfigs)) {
|
|
6040
|
+
const code = item.code || item.resourceCode || item.id;
|
|
6041
|
+
const detail = await requestWithAuth(
|
|
6042
|
+
config,
|
|
6043
|
+
target.profileName,
|
|
6044
|
+
`/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/auth/configs/${encodeURIComponent(code)}`
|
|
6045
|
+
);
|
|
6046
|
+
const filePath = path.join(baseDir, 'auth', `${code}.json`);
|
|
6047
|
+
writeResourceJsonFile(filePath, toPulledAuthConfig(detail));
|
|
6048
|
+
written.push(path.relative(process.cwd(), filePath));
|
|
6049
|
+
}
|
|
6050
|
+
|
|
5893
6051
|
const dataViews = await requestWithAuth(
|
|
5894
6052
|
config,
|
|
5895
6053
|
target.profileName,
|
|
@@ -6105,6 +6263,16 @@ function toPulledDataView(dataView, permissionGroups, lookups) {
|
|
|
6105
6263
|
});
|
|
6106
6264
|
}
|
|
6107
6265
|
|
|
6266
|
+
function toPulledAuthConfig(authConfig) {
|
|
6267
|
+
return stripUndefinedValues({
|
|
6268
|
+
code: authConfig.code || authConfig.resourceCode,
|
|
6269
|
+
name: authConfig.name,
|
|
6270
|
+
description: authConfig.description || '',
|
|
6271
|
+
status: authConfig.status || 'active',
|
|
6272
|
+
configJson: authConfig.configJson || authConfig.config || {},
|
|
6273
|
+
});
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6108
6276
|
function rewriteDataViewDefinitionForManifest(definition, lookups) {
|
|
6109
6277
|
rewriteDataViewSourceForManifest(definition.base, lookups);
|
|
6110
6278
|
for (const join of definition.joins || []) {
|
|
@@ -6285,6 +6453,40 @@ function normalizeDataViewManifest(bound, dataView) {
|
|
|
6285
6453
|
});
|
|
6286
6454
|
}
|
|
6287
6455
|
|
|
6456
|
+
function normalizeAuthConfigManifest(authConfig) {
|
|
6457
|
+
const code = authConfig.code || authConfig.resourceCode || 'default';
|
|
6458
|
+
const configJson = clonePlainJson(
|
|
6459
|
+
authConfig.configJson || authConfig.config || withoutAuthConfigManifestMeta(authConfig)
|
|
6460
|
+
);
|
|
6461
|
+
return stripUndefinedValues({
|
|
6462
|
+
code,
|
|
6463
|
+
name: authConfig.name || configJson.name || code,
|
|
6464
|
+
description:
|
|
6465
|
+
authConfig.description !== undefined
|
|
6466
|
+
? authConfig.description
|
|
6467
|
+
: configJson.description || '',
|
|
6468
|
+
status: authConfig.status || configJson.status || 'active',
|
|
6469
|
+
configJson: {
|
|
6470
|
+
...configJson,
|
|
6471
|
+
methods: Array.isArray(configJson.methods) ? configJson.methods : [],
|
|
6472
|
+
registration: configJson.registration || { mode: 'reject' },
|
|
6473
|
+
binding: configJson.binding || { mode: 'auto' },
|
|
6474
|
+
},
|
|
6475
|
+
});
|
|
6476
|
+
}
|
|
6477
|
+
|
|
6478
|
+
function withoutAuthConfigManifestMeta(authConfig) {
|
|
6479
|
+
const next = withoutResourceMeta(authConfig);
|
|
6480
|
+
delete next.code;
|
|
6481
|
+
delete next.resourceCode;
|
|
6482
|
+
delete next.name;
|
|
6483
|
+
delete next.description;
|
|
6484
|
+
delete next.status;
|
|
6485
|
+
delete next.config;
|
|
6486
|
+
delete next.configJson;
|
|
6487
|
+
return next;
|
|
6488
|
+
}
|
|
6489
|
+
|
|
6288
6490
|
function withoutDataViewManifestMeta(dataView) {
|
|
6289
6491
|
const next = withoutResourceMeta(dataView);
|
|
6290
6492
|
delete next.permissionGroups;
|
|
@@ -6754,6 +6956,18 @@ function functionEquals(target, desired, existing) {
|
|
|
6754
6956
|
);
|
|
6755
6957
|
}
|
|
6756
6958
|
|
|
6959
|
+
function authConfigEquals(desired, existing) {
|
|
6960
|
+
if (!existing) return false;
|
|
6961
|
+
const expected = normalizeAuthConfigManifest(desired);
|
|
6962
|
+
return (
|
|
6963
|
+
String(existing.code || existing.resourceCode || '') === String(expected.code || '') &&
|
|
6964
|
+
String(existing.name || '') === String(expected.name || '') &&
|
|
6965
|
+
String(existing.description || '') === String(expected.description || '') &&
|
|
6966
|
+
String(existing.status || 'active') === String(expected.status || 'active') &&
|
|
6967
|
+
jsonEqualsForPlan(existing.configJson || {}, expected.configJson || {}, desired.__dir)
|
|
6968
|
+
);
|
|
6969
|
+
}
|
|
6970
|
+
|
|
6757
6971
|
function dataViewEquals(bound, desired, existing) {
|
|
6758
6972
|
if (!existing) return false;
|
|
6759
6973
|
const expected = normalizeDataViewManifest(bound, desired);
|
|
@@ -28,6 +28,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
28
28
|
| 审批流程 / workflow / 流程节点 / JS_CODE | `openxiangda-workflow-automation` | `openxiangda workflow validate / create / publish` |
|
|
29
29
|
| 自动化 / 定时任务 / 提交触发 / cron | `openxiangda-workflow-automation` | `openxiangda automation validate / create / publish / enable` |
|
|
30
30
|
| App Function / function_call / 可复用后端逻辑 | `openxiangda-workflow-automation` | declare `src/resources/functions/<code>.json` + `src/functions/<code>/index.ts` |
|
|
31
|
+
| 应用登录 / Auth SDK / 手机号验证码 / CAS / 钉钉免登 | `openxiangda-architecture-design` + `openxiangda-page` | design security gate first, then declare `src/resources/auth/<code>.json` and use `createAuthClient` / `LoginPage` |
|
|
31
32
|
| 角色 / 权限组 / 字段权限 / 数据范围 / 公开访问 | `openxiangda-permission-settings` | `openxiangda permission ...` / `openxiangda settings ...` |
|
|
32
33
|
| 看应用结构 / 快照 / 对比 / 诊断 / 排查 / 报错 | `openxiangda-inspect` | `openxiangda app snapshot <APP_XXX> --profile <name> --json` |
|
|
33
34
|
| 登录 / 切换平台 / profile / token / whoami | `openxiangda-core` | `openxiangda env --profile <name>` / `openxiangda auth status` |
|
|
@@ -42,6 +43,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
42
43
|
- ✅ Each profile (dev / prod / ...) has its own `appType` and resource IDs; never copy a `formUuid` / `pageId` / `workflowId` / `automationId` across profiles.
|
|
43
44
|
- ✅ Run `openxiangda update check --json` at the start of substantial work; if `updateAvailable`, run `openxiangda update install` and `openxiangda skill install --force`.
|
|
44
45
|
- ✅ For non-trivial app requirements, run the architecture design gate first: forms, fields, pages, permissions, data views, status/workflow, automation, notifications, and development tasks must be decided before coding.
|
|
46
|
+
- ✅ For app login/auth requirements, run the auth security gate before coding: enabled methods, registration policy, identity matching keys, provider boundary, default roles, rate limits, audit fields, and third-party ownership must be confirmed.
|
|
45
47
|
- ✅ For form-entry UX, pick components in this order: OpenXiangda platform form components → `antd` / `antd-mobile` wrappers → custom component only when neither exists.
|
|
46
48
|
- ✅ List pages, linked options, remote selectors, and report drill-downs must use paginated server-side queries with explicit searchable fields. Do not fetch a large page and filter in local state.
|
|
47
49
|
- ✅ Treat workflow forms as an exception. Ordinary ticket/order/task/asset lifecycles use status fields, state machines, action logs, permissions, and automations; workflows are for real approval tasks.
|
|
@@ -142,6 +144,7 @@ When the user provides a root domain such as `https://yida.wisejob.cn/`, use it
|
|
|
142
144
|
- For repeated read-only multi-form queries or predefined dashboard metrics, create a data view manifest in `src/resources/data-views/`. Use `storageMode: "materialized"` for refreshed lists/reports that tolerate delay; use `storageMode: "live"` for bounded real-time joins where source changes must appear immediately. Use row views plus `sdk.dataView.query` for joined lists/lookups; use `viewType: "aggregate"` plus `sdk.dataView.stats` for count/sum/avg/min/max statistics. Add `indexes` only for materialized views. Do not use data views for single-form CRUD, simple linkedForm selects, writes, write-back, unbounded realtime joins, or ad-hoc BI. Read `references/data-views.md` before designing one.
|
|
143
145
|
- For external APIs, create a connector manifest in `src/resources/connectors/` and call it from pages through `sdk.connector`; never put third-party API keys in page source.
|
|
144
146
|
- For reusable backend logic shared by pages, automations, and workflows, create an App Function in `src/resources/functions/<functionCode>.json` plus `src/functions/<functionCode>/index.ts`. Call it from pages with `sdk.function.invoke`, from automation/workflow graphs with `function_call`, or from the runtime endpoint when the caller has app automation management permission. Current App Function MVP exposes controlled helpers such as `ctx.resources`, `ctx.form`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, and `ctx.platform.api`; it does not expose raw SQL or Redis.
|
|
147
|
+
- For application login, declare auth resources in `src/resources/auth/<code>.json` and publish them with `openxiangda resource publish`. App Function auth providers may validate external credentials and return only an identity assertion; they must never issue tokens, set cookies, or write platform user/binding tables directly. The platform auth service decides create/bind/reject and issues tokens.
|
|
145
148
|
- Before scaffolding a real app, complex page, data management page, portal shell, status lifecycle, role governance, or automation, read `references/best-practices.md` and pick the architecture first. Prefer copying the relevant pattern from `examples/best-practices/` into `src/` instead of generating a single large page file.
|
|
146
149
|
- Before publishing to another platform, verify the workspace is bound for that profile. Resource IDs from one profile must not be reused for another profile.
|
|
147
150
|
- Use `openxiangda app snapshot <APP_XXX> --profile <name> --json` for diagnosis before changing an existing app.
|
|
@@ -181,7 +184,7 @@ Core CLI / state:
|
|
|
181
184
|
- `references/architecture-design.md` — OpenXiangda architecture design gate, anti-pattern rejection, question gates, final design template, and black-box demo scenario.
|
|
182
185
|
- `references/workspace-state.md` — `.openxiangda/state.json` shape and profile isolation rules.
|
|
183
186
|
- `references/connector-resources.md` — `src/resources` manifests, connector schema, App Function boundary, and platform runtime calls.
|
|
184
|
-
- `references/resource-manifest-cheatsheet.md` — 复制即用的 connector / data-view / notification / App Function / workflow / automation / JS_CODE / role / permission group / settings / menu manifest 骨架与运行时调用示例。在创建任何 `src/resources/` 资源前先看这份。
|
|
187
|
+
- `references/resource-manifest-cheatsheet.md` — 复制即用的 auth / connector / data-view / notification / App Function / workflow / automation / JS_CODE / role / permission group / settings / menu manifest 骨架与运行时调用示例。在创建任何 `src/resources/` 资源前先看这份。
|
|
185
188
|
- `references/data-views.md` — `src/resources/data-views` materialized/live data view resources, DSL, permissions, refresh, CLI commands, and runtime SDK usage.
|
|
186
189
|
- `references/notifications.md` — `src/resources/notifications`, notification templates/type bindings, and `sdk.notification` / `ctx.notification`.
|
|
187
190
|
- `references/best-practices.md` — initialized examples for modular pages, state lifecycles, role governance, permission isolation, high-performance queries, portal shells, workflow boundaries, and automation patterns.
|
|
@@ -24,6 +24,7 @@ This design flow is for applications built on OpenXiangda. The output is not a g
|
|
|
24
24
|
- status machines or workflows
|
|
25
25
|
- automations, JS_CODE V2 nodes, and App Functions
|
|
26
26
|
- notifications and connectors
|
|
27
|
+
- application login/auth methods, registration policy, identity matching, and provider boundaries
|
|
27
28
|
- publish and acceptance steps
|
|
28
29
|
|
|
29
30
|
The design is complete only when another agent can implement it without deciding major architecture tradeoffs.
|
|
@@ -72,6 +73,8 @@ Do not insult the person. Criticize the proposal and explain the technical conse
|
|
|
72
73
|
| Materialized view for hard realtime data | Users see stale data | `storageMode: "live"` only for bounded realtime joins, or direct source queries/App Function |
|
|
73
74
|
| Live view for unbounded heavy joins | Slow runtime queries | Materialized view with scheduled refresh and indexes |
|
|
74
75
|
| JS_CODE for reusable business service | Logic gets duplicated in graph nodes | App Function for shared backend logic; JS_CODE only for node-local trigger logic |
|
|
76
|
+
| Auth provider issues tokens or writes users directly | Breaks platform account, binding, permission, and audit boundaries | Provider returns identity assertion only; platform creates/binds/rejects and issues token |
|
|
77
|
+
| Phone-code login auto-registers by default | Allows uncontrolled account creation from weak identity proof | Default registration is reject; enable auto-create/whitelist only after explicit confirmation |
|
|
75
78
|
| Raw native form controls | Inconsistent with platform runtime and validation | OpenXiangda platform components first, Ant Design wrappers second |
|
|
76
79
|
| Multiple form permission groups without stable local codes | CLI state and platform `resourceCode` cannot reliably distinguish groups on the same form | Give every group a unique `code`, for example `ticket_reporter_view` and `ticket_repairer_view` |
|
|
77
80
|
| Assuming resource publish means workflow is active | Workflow resources may exist as drafts until explicitly published | Verify `workflow list` shows `isPublished: true`; run `workflow publish <workflowCode>` when needed |
|
|
@@ -153,6 +156,20 @@ Default: reusable calculations and validations become App Functions; one-off bac
|
|
|
153
156
|
- Are templates reusable resources?
|
|
154
157
|
- Are external credentials needed? If yes, use connectors/resources, never page source secrets.
|
|
155
158
|
|
|
159
|
+
### Login And Auth
|
|
160
|
+
|
|
161
|
+
Ask these before generating any login/auth design:
|
|
162
|
+
|
|
163
|
+
- Which methods are enabled: password, DingTalk in-app free login, CAS/SSO, phone code, guest?
|
|
164
|
+
- What is the phone registration policy: reject, auto-create, bind existing only, whitelist, or manual approval?
|
|
165
|
+
- What are the identity match keys and priorities: `phone`, `email`, `externalId`, `unionId`, `jobNumber`, `username`?
|
|
166
|
+
- Which App Function provider validates each external credential, and what identity assertion fields must it return?
|
|
167
|
+
- Which default app roles/page groups/data scopes should new users receive, if registration is enabled?
|
|
168
|
+
- What are the security parameters: code TTL, send frequency, failed attempts, IP/device limits, audit fields, and failure message disclosure?
|
|
169
|
+
- Is CAS/DingTalk configured by the platform tenant, or delegated to an app-level provider function?
|
|
170
|
+
|
|
171
|
+
Default: registration is rejected; provider functions return identity assertions only; platform auth service owns account creation, binding, permission assignment, cookies, and tokens.
|
|
172
|
+
|
|
156
173
|
### UI Design
|
|
157
174
|
|
|
158
175
|
Ask whether to use Product Design or another design skill for:
|
|
@@ -213,6 +230,14 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
213
230
|
- After publishing workflow resources, verify `workflow list` and record whether `isPublished` is true.
|
|
214
231
|
- In React SPA and classic workspaces, JS_CODE/App Function TypeScript lives under `src/js-code-nodes`, `src/automations`, or `src/functions`; build with `pnpm build-js-code`.
|
|
215
232
|
|
|
233
|
+
### Auth
|
|
234
|
+
|
|
235
|
+
- Auth resources use stable local codes under `src/resources/auth/`.
|
|
236
|
+
- Use `/view/:appType/login` or `LoginPage` from `openxiangda/runtime/react` for default React login.
|
|
237
|
+
- Use `createAuthClient({ appType, servicePrefix })` for custom React login pages.
|
|
238
|
+
- Phone-code providers are App Functions. They validate send/verify events and return identity assertions; they do not issue tokens or mutate platform users.
|
|
239
|
+
- New-user registration must be explicit, with default roles and identity conflict behavior documented.
|
|
240
|
+
|
|
216
241
|
## Final Document Template
|
|
217
242
|
|
|
218
243
|
```markdown
|
|
@@ -262,17 +287,26 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
262
287
|
- JS_CODE V2 nodes:
|
|
263
288
|
- App Functions:
|
|
264
289
|
|
|
265
|
-
## 8.
|
|
290
|
+
## 8. Login And Auth
|
|
291
|
+
|
|
292
|
+
- Enabled methods:
|
|
293
|
+
- Registration policy:
|
|
294
|
+
- Identity matching keys:
|
|
295
|
+
- Provider functions:
|
|
296
|
+
- Default roles/permissions:
|
|
297
|
+
- Security parameters:
|
|
298
|
+
|
|
299
|
+
## 9. Notifications And Connectors
|
|
266
300
|
|
|
267
301
|
| Scenario | Trigger | Channel | Recipients | Template/resource | Notes |
|
|
268
302
|
|---|---|---|---|---|---|
|
|
269
303
|
|
|
270
|
-
##
|
|
304
|
+
## 10. Development Task Table
|
|
271
305
|
|
|
272
306
|
| Phase | Task | Resources/files | Validation | Publish step |
|
|
273
307
|
|---|---|---|---|---|
|
|
274
308
|
|
|
275
|
-
##
|
|
309
|
+
## 11. Acceptance Criteria
|
|
276
310
|
|
|
277
311
|
- Functional checks:
|
|
278
312
|
- Permission checks:
|
|
@@ -281,7 +315,7 @@ For simple CRUD/admin lists, design can proceed with the appropriate OpenXiangda
|
|
|
281
315
|
- Notification/automation checks:
|
|
282
316
|
- Workflow checks: real approval flows show `isPublished: true`.
|
|
283
317
|
|
|
284
|
-
##
|
|
318
|
+
## 12. Open Questions And Confirmed Assumptions
|
|
285
319
|
|
|
286
320
|
- Confirmed:
|
|
287
321
|
- Still open:
|
|
@@ -12,6 +12,7 @@ Common folders:
|
|
|
12
12
|
- `automations`
|
|
13
13
|
- `data-views`
|
|
14
14
|
- `functions`
|
|
15
|
+
- `auth`
|
|
15
16
|
- `permissions/page-groups`
|
|
16
17
|
- `permissions/form-groups`
|
|
17
18
|
- `settings/forms`
|
|
@@ -33,6 +34,8 @@ Data view manifests live under `src/resources/data-views/` and define read-only
|
|
|
33
34
|
|
|
34
35
|
App Function manifests live under `src/resources/functions/`, with source in `src/functions/<functionCode>/index.ts`. Use them for reusable server-side logic that pages, automations, and workflows can share through `sdk.function.invoke` or `function_call` nodes. App Functions expose controlled runtime helpers such as `ctx.resources`, `ctx.form`, `ctx.dataView`, `ctx.connector`, `ctx.notification`, and `ctx.platform.api`; they do not expose raw SQL or Redis in the current MVP. Use JS_CODE V2 only for node-local workflow/automation scripts.
|
|
35
36
|
|
|
37
|
+
Auth manifests live under `src/resources/auth/`. Use them to enable app-level login methods and bind phone-code/CAS/custom providers to App Functions. Auth provider functions are called only by the platform auth flow. They validate external credentials and return identity assertions such as `phone`, `email`, `externalId`, or `unionId`; they must not issue tokens, set cookies, or mutate platform user/binding tables.
|
|
38
|
+
|
|
36
39
|
```json
|
|
37
40
|
{
|
|
38
41
|
"code": "crm",
|
|
@@ -14,6 +14,7 @@ Guidelines:
|
|
|
14
14
|
- Use `sdk.function.invoke(code, { input })` for reusable backend business logic declared under `src/resources/functions/` and `src/functions/`. Do not implement multi-form orchestration, connector fan-out, notification orchestration, or permission-sensitive backend rules directly in a page component.
|
|
15
15
|
- Use `sdk.connector.invoke`, `sdk.connector.call("connector.api")`, or `sdk.connector.download` for external services. The SDK calls the platform runtime connector endpoint; it must not call third-party domains directly.
|
|
16
16
|
- Use `sdk.notification.sendByType` and `batchSendByType` for reusable business messages. Custom notification types must be declared in `src/resources/notifications/` and published with `openxiangda resource publish`.
|
|
17
|
+
- Use `createAuthClient` from `openxiangda/runtime` or `LoginPage` / `useAuth` from `openxiangda/runtime/react` for application login pages. Auth provider App Functions return identity assertions only; platform auth owns create/bind/reject/token decisions.
|
|
17
18
|
- For the current user's department hierarchy, use `sdk.department.getCurrentUserParentDepartments()`; do not hardcode `GET /department/:id/parentDepartments` in page code.
|
|
18
19
|
- Use `sdk.auth.logoutAndRedirect({ loginUrl })` for user logout when the page should return after login. It calls the platform logout endpoint, appends the current page URL as `callback`, and redirects to the login URL. Use `sdk.auth.logout()` only when the page wants to handle redirect itself.
|
|
19
20
|
- Use `sdk.role.getMyRoles()`, `sdk.role.getCurrentRole()`, and `sdk.role.switchAppRole()` for current-user app role switching. Pass `roleId: ""` to switch back to all app roles.
|
|
@@ -102,6 +103,44 @@ const result = await sdk.function.invoke("reservation_reminder_summary", {
|
|
|
102
103
|
|
|
103
104
|
Use App Functions when the logic must run server-side and be reusable by pages, automations, or workflows. Current runtime invocation requires the caller to have app automation management permission; ordinary page users should normally consume function-backed data through a permission-aware page or a narrower backend resource.
|
|
104
105
|
|
|
106
|
+
Application auth:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import { LoginPage, useAuth, useLoginMethods } from "openxiangda/runtime/react";
|
|
110
|
+
|
|
111
|
+
export function DefaultLogin() {
|
|
112
|
+
return <LoginPage />;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function CustomPhoneLogin() {
|
|
116
|
+
const auth = useAuth();
|
|
117
|
+
const methods = useLoginMethods();
|
|
118
|
+
|
|
119
|
+
async function submit(phone: string, code: string, challengeId: string) {
|
|
120
|
+
await auth.phoneCodeLogin({ phone, code, challengeId });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Standalone client:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { createAuthClient } from "openxiangda/runtime";
|
|
131
|
+
|
|
132
|
+
const auth = createAuthClient({ appType, servicePrefix: "/service" });
|
|
133
|
+
const methods = await auth.getMethods();
|
|
134
|
+
const sent = await auth.sendPhoneCode({ phone, purpose: "login" });
|
|
135
|
+
const result = await auth.phoneCodeLogin({
|
|
136
|
+
phone,
|
|
137
|
+
code,
|
|
138
|
+
challengeId: sent.challengeId,
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
For phone-code auth, the App Function provider receives `event`, `appType`, `method`, `credential`, `requestId`, `request`, and `challenge`. It may return `{ ok, providerState }` for send and `{ ok, identity }` for verify. It must not return `token`, `accessToken`, `refreshToken`, cookies, or mutate platform account tables.
|
|
143
|
+
|
|
105
144
|
Logout and current-user role switching:
|
|
106
145
|
|
|
107
146
|
```ts
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- **平台 ID 由 CLI 解析**:`openxiangda resource publish --profile <name>` 把 code 解析成当前 profile 的真实 ID 并写回 `.openxiangda/state.json`。
|
|
10
10
|
- **永远不写密钥**:第三方 API key、token、secret、password、authorization、headers 等绝不出现在 `src/resources/`,平台管理员在后台配置。
|
|
11
11
|
- **多 profile 不共享 ID**:`dev` / `prod` 各自维护一份资源 ID 映射,复制 manifest 即可复用,不要复制平台 ID。
|
|
12
|
+
- **Auth provider 只返回身份声明**:登录 provider App Function 不能发 token、写 cookie、直接改用户表或绑定表;平台按策略创建/绑定/拒绝。
|
|
12
13
|
|
|
13
14
|
## 命令
|
|
14
15
|
|
|
@@ -20,6 +21,86 @@ openxiangda resource publish --profile <name> --prune # 删除 manifest 未声
|
|
|
20
21
|
openxiangda resource pull --profile <name> # 拉取平台资源回写到本地(用于初次同步)
|
|
21
22
|
```
|
|
22
23
|
|
|
24
|
+
## 0. Auth — `src/resources/auth/<code>.json`
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"code": "default",
|
|
29
|
+
"name": "Default App Login",
|
|
30
|
+
"status": "active",
|
|
31
|
+
"configJson": {
|
|
32
|
+
"methods": [
|
|
33
|
+
{ "type": "password", "enabled": true, "label": "账号密码" },
|
|
34
|
+
{ "type": "dingtalk", "enabled": true, "label": "钉钉免登" },
|
|
35
|
+
{ "type": "sso", "enabled": true, "label": "CAS", "protocol": "cas" },
|
|
36
|
+
{
|
|
37
|
+
"type": "phone_code",
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"label": "手机号验证码",
|
|
40
|
+
"ttlSeconds": 300,
|
|
41
|
+
"sendFrequencySeconds": 60,
|
|
42
|
+
"maxAttempts": 5,
|
|
43
|
+
"provider": { "functionCode": "auth_phone_code" }
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"registration": { "mode": "reject" },
|
|
47
|
+
"binding": { "mode": "auto" },
|
|
48
|
+
"matching": {
|
|
49
|
+
"keys": ["phone", "email", "externalId", "unionId", "jobNumber", "username"]
|
|
50
|
+
},
|
|
51
|
+
"defaultRoleCodes": []
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Provider function:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
export default async function authPhoneCodeProvider(ctx, input) {
|
|
60
|
+
if (input.event === "phone_code.send") {
|
|
61
|
+
return { ok: true, providerState: { nonce: "vendor-message-id" } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (input.event === "phone_code.verify") {
|
|
65
|
+
if (input.credential.code !== "123456") {
|
|
66
|
+
return { ok: false, message: "验证码错误" };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
identity: {
|
|
71
|
+
phone: input.credential.phone,
|
|
72
|
+
externalId: `phone:${input.credential.phone}`
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { ok: false, message: "不支持的认证事件" };
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
React 默认页:
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import { LoginPage } from "openxiangda/runtime/react";
|
|
85
|
+
|
|
86
|
+
export default function AppLogin() {
|
|
87
|
+
return <LoginPage />;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
自定义登录页:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { createAuthClient } from "openxiangda/runtime";
|
|
95
|
+
|
|
96
|
+
const auth = createAuthClient({ appType, servicePrefix: "/service" });
|
|
97
|
+
const methods = await auth.getMethods();
|
|
98
|
+
const sent = await auth.sendPhoneCode({ phone, purpose: "login" });
|
|
99
|
+
await auth.phoneCodeLogin({ phone, code, challengeId: sent.challengeId });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
设计前必须确认启用方式、注册策略、身份匹配键、provider 边界、默认权限、安全参数、第三方配置归属。
|
|
103
|
+
|
|
23
104
|
## 1. Connector — `src/resources/connectors/<code>.json`
|
|
24
105
|
|
|
25
106
|
```json
|