/* * Copyright (c) 2025 SimcuTeam. All rights reserved. * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 * AuthSDK/SimApiAuthCenter:认证中心远程 SDK。 */ package simapi.authsdk import std.collection.* import stdx.net.tls.* import stdx.net.tls.common.* import soulsoft_net_http.{HttpClient, HttpRequestMessage, JsonContent} import soulsoft_net_http.{HttpMethod as NetHttpMethod} import simapi.communications.* import simapi.helpers.* /** * 认证中心远程 SDK(对齐 C# SimApiAuthCenter): * 群组 / Profile / 内部应用 / 系统登录 / 安全验证 等接口,走签名请求。 */ public class SimApiAuthCenter { private let _client: SimApiAuthClient public init(client: SimApiAuthClient) { this._client = client } public prop client: SimApiAuthClient { get() { _client } } // ===== 公共 ===== /** * 委托 AuthCenter 进行应用签名验证(对齐 C# VerifySign)。 */ public func verifySign(appId: String, timestamp: String, nonce: String, sign: String): Unit { let url = "${_client.server}/api/auth/sign/verify?appId=${appId}×tamp=${timestamp}&nonce=${nonce}&sign=${sign}" let http = HttpClient.create { builder => builder.noProxy() var tls = TlsClientConfig() tls.verifyMode = CertificateVerifyMode.TrustAll match (_client.server.indexOf("://")) { case Some(i) => let rest = _client.server[i + 3..] let slash = rest.indexOf("/") ?? rest.size let q = rest.indexOf("?") ?? rest.size let end = if (slash < q) { slash } else { q } let host = rest[0..end] if (!host.isEmpty()) { tls.serverName = Some(host) } case None => () } builder.tlsConfig(tls) } try { let request = HttpRequestMessage(NetHttpMethod.Post, url) request.content = JsonContent.create("{}") let response = http.send(request) try { response.ensureSuccessStatusCode() let resp = response.content.readFromJson() SimApiError.errorWhen(resp._code != 200, code: 400, message: "签名验证失败") } finally { response.close() } } finally { http.close() } } // ===== 群组相关 ===== /** * 根据 profileId 获取群组列表(对齐 C# GroupRelated)。 */ public func groupRelated(profileId: String): Array { _client.signQuery>("/api/auth/group/related", body: simpleBody("profileId", profileId)) } /** * 按关键字搜索群组,输入群组 ID 精准搜索(对齐 C# GroupSearch)。 */ public func groupSearch(keyword: String, skip!: Int64 = 0, take!: Int64 = 20): Array { var body = HashMap() body["keyword"] = keyword body["skip"] = skip body["take"] = take _client.signQuery>("/api/auth/group/search", body: SimApiJson.json(Some(body))) } /** * 使用组 ID 以及组内成员/管理员 profile 获取组的详细树结构(对齐 C# GroupDetail)。 */ public func groupDetail(groupId: String, profileId: String): GroupDetailTreeNode { var body = HashMap() body["profileId"] = profileId body["groupId"] = groupId _client.signQuery("/api/auth/group/detail", body: SimApiJson.json(Some(body))) } /** * 获取 profile 在本组的所有子组(对齐 C# GroupRelatedIndex)。 */ public func groupRelatedIndex(groupId: String, profileId: String): Array { var body = HashMap() body["groupId"] = groupId body["profileId"] = profileId _client.signQuery>("/api/auth/internal/group/related-group-ids", body: SimApiJson.json(Some(body))) } // ===== Profile 相关 ===== /** * 按关键字搜索用户 Profile(对齐 C# ProfileSearch)。 */ public func profileSearch(keyword: String, skip!: Int64 = 0, take!: Int64 = 20): Array { var body = HashMap() body["keyword"] = keyword body["skip"] = skip body["take"] = take _client.signQuery>("/api/auth/profile/search", body: SimApiJson.json(Some(body))) } /** * 通过 id 批量获取用户基本信息(对齐 C# ProfileList)。 */ public func profileList(ids: Array): Array { var body = HashMap() var arr = ArrayList() for (id in ids) { arr.add(id) } body["ids"] = arr.toArray() _client.signQuery>("/api/auth/profile/list", body: SimApiJson.json(Some(body))) } // ===== AuthGate 内部应用专用 ===== /** * 获取是否为 App 的拥有者(对齐 C# CheckIsAppOwner,字段为 PascalCase)。 */ public func checkIsAppOwner(profileId: String, applicationId: String): Bool { var body = HashMap() body["ProfileId"] = profileId body["AppId"] = applicationId _client.signQuery("/api/auth/internal/app/check-owner", body: SimApiJson.json(Some(body))) } /** * 根据用户 profileId 和提供的 appIds 获取应用列表(对齐 C# GetAppList,字段为 PascalCase)。 */ public func getAppList(profileId: String, appIds: Array): Array { var body = HashMap() body["ProfileId"] = profileId var arr = ArrayList() for (id in appIds) { arr.add(id) } body["AllowedAppIds"] = arr.toArray() _client.signQuery>("/api/auth/internal/app/related", body: SimApiJson.json(Some(body))) } // ===== 系统登录 ===== /** * 获取登录授权 CODE(对齐 C# GetLoginCode)。 * @param scene 场景标识。 * @param data 附加数据。 * @param backUrl 回调地址。 * @return GetCodeResponse(含 Code/Server/FullUrl)。 */ public func getLoginCode(scene!: ?String = None, data!: ?HashMap = None, backUrl!: ?String = None): GetCodeResponse { var body = HashMap() if (let Some(scene) <- scene) { body["scene"] = scene } if (let Some(data) <- data) { body["data"] = data } if (let Some(backUrl) <- backUrl) { body["backUrl"] = backUrl } let code = _client.signQuery("/api/auth/login/code", body: SimApiJson.json(Some(body))) let server = _client.server GetCodeResponse(code, server, "${server}/auth?code=${code}") } /** * 使用 code 获取登录信息(对齐 C# GetLoginInfo,场景不匹配抛 403003)。 */ public func getLoginInfo(code: String, scene!: ?String = None): LoginInfoResponse { var body = HashMap() body["code"] = code // 说明:C# 的 ErrorWhenNull(resp, 400232, "登录信息获取失败") 对应 signQuery 内部 _data.getOrThrow() 的 // None 分支;仓颉版 signQuery 返回非空 T(data 缺失即抛异常),故此处无需重复判空。 let resp = _client.signQuery("/api/auth/login/get", body: SimApiJson.json(Some(body))) SimApiError.errorWhen(resp._scene != scene, code: 403003, message: "登录场景不匹配") resp } // ===== 安全验证 ===== /** * 获取安全验证代码(对齐 C# GetConfirmCode)。 */ public func getConfirmCode(scene: String, userId: String, data!: ?HashMap = None, backUrl!: ?String = None): GetCodeResponse { var body = HashMap() body["scene"] = scene if (let Some(data) <- data) { body["data"] = data } if (let Some(backUrl) <- backUrl) { body["backUrl"] = backUrl } body["profileId"] = userId let code = _client.signQuery("/api/auth/confirm/code", body: SimApiJson.json(Some(body))) let server = _client.server GetCodeResponse(code, server, "${server}/confirm?code=${code}") } /** * 使用安全验证 code 获取验证结果(对齐 C# Confirm,身份/场景不匹配分别抛 403002/403003)。 */ public func confirm(code: String, scene: String, userId!: ?String = None): ConfirmResponse { var body = HashMap() body["code"] = code let resp = _client.signQuery("/api/auth/confirm/get", body: SimApiJson.json(Some(body))) SimApiError.errorWhen(userId != Some(resp._profileId), code: 403002, message: "安全确认身份不匹配") SimApiError.errorWhen(resp._scene != scene, code: 403003, message: "安全确认场景不匹配") resp } /// 简单单字段请求体:{"field":"value"} private static func simpleBody(field: String, value: String): String { "{\"${field}\":\"${SimApiJson.escapeJson(value)}\"}" } }