CreateIdentity.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. <template>
  2. <Navbar title="新建认证源" :is-go-back="true" />
  3. <a-card
  4. :bordered="false"
  5. style="margin-top: 20px; box-shadow: 0 2px 4px 0 rgba(54, 58, 80, 0.32)"
  6. >
  7. <a-steps class="steps" :current="current" type="navigation">
  8. <a-step v-for="item in steps" :key="item.title" :title="item.title" />
  9. </a-steps>
  10. <a-divider />
  11. <div class="steps-content">
  12. <div v-if="steps[current].contentTemplate === 'First'">
  13. <div class="box">
  14. <div style="display: flex">
  15. <label class="labelStyle">选择认证源 <span style="color: red">*</span></label>
  16. <ul class="selectUl">
  17. <li
  18. v-for="(item, index) in selectedItem"
  19. :key="index"
  20. :class="{ active: activeIndex === index }"
  21. @click="setActiveItem(index)"
  22. >
  23. <img class="picture" :src="item.imgSrc" />
  24. <div class="info">
  25. <h3 style="font-size: 14px; font-weight: 700">
  26. {{ item.title }}
  27. </h3>
  28. <p v-tooltip.bottom="item.tooltip" class="text">
  29. {{ item.tooltip }}
  30. </p>
  31. </div>
  32. </li>
  33. </ul>
  34. </div>
  35. </div>
  36. </div>
  37. <div v-else-if="steps[current].contentTemplate === 'Second'">
  38. <a-form
  39. :model="identityInfo"
  40. :label-col="labelCol"
  41. :wrapper-col="wrapperCol"
  42. autocomplete="off"
  43. >
  44. <a-form-item
  45. has-feedback
  46. label="认证源名称"
  47. name="name"
  48. :rules="[{ required: true, message: '请输入认证源名称!' }]"
  49. >
  50. <a-input
  51. v-model:value="identityInfo.name"
  52. placeholder="必填,请输入认证源名称"
  53. />
  54. </a-form-item>
  55. <a-form-item
  56. label="认证源LOGO"
  57. name="logo"
  58. :rules="[{ required: true, message: '请选择LOGO!' }]"
  59. >
  60. <div v-tooltip.top="'点击查看大图'">
  61. <a-image :width="48" :src="logoUrl" />
  62. </div>
  63. <a-upload
  64. v-model:file-list="identityInfo.logo"
  65. :max-count="1"
  66. :before-upload="beforeUpload"
  67. :show-upload-list="false"
  68. @change="logoFileChange"
  69. >
  70. <a style="margin-left: 6px">去上传</a>
  71. </a-upload>
  72. <span>
  73. <a style="margin-left: 6px" @click="deleteLogo">删除</a>
  74. </span>
  75. </a-form-item>
  76. <a-form-item
  77. label="认证源描述"
  78. name="description"
  79. style="margin-top: 6px"
  80. >
  81. <a-textarea
  82. v-model:value="identityInfo.description"
  83. :rows="4"
  84. placeholder="非必填,请输入认证源描述"
  85. />
  86. </a-form-item>
  87. </a-form>
  88. </div>
  89. <div v-else-if="steps[current].contentTemplate === 'Third'">
  90. <a-form
  91. name="basic"
  92. :label-col="labelCol"
  93. :wrapper-col="wrapperCol"
  94. :model="identitySetting"
  95. autocomplete="off"
  96. >
  97. <a-form-item
  98. has-feedback
  99. label="身份提供商 id"
  100. name="entityID"
  101. :rules="[{ required: true, message: '请输入身份提供商 id!' }]"
  102. >
  103. <a-input
  104. v-model:value="identitySetting.entityID"
  105. placeholder="必填,请输入身份提供商 id"
  106. />
  107. </a-form-item>
  108. <a-form-item
  109. has-feedback
  110. label="SSO 地址"
  111. name="ssoUrl"
  112. :rules="[{ required: true, message: '请输入 SSO 地址' }]"
  113. >
  114. <a-input
  115. v-model:value="identitySetting.ssoUrl"
  116. placeholder="必填,请输入 SSO 地址"
  117. />
  118. </a-form-item>
  119. <a-form-item
  120. has-feedback
  121. label="证书"
  122. name="certificate"
  123. :rules="[{ required: true, message: '请输入证书' }]"
  124. >
  125. <a-textarea
  126. v-model:value="identitySetting.certificate"
  127. :rows="4"
  128. placeholder="必填,请输入证书"
  129. @blur="certificateBlur"
  130. />
  131. </a-form-item>
  132. <a-form-item
  133. label="退出 URL"
  134. name="loginOutUrl"
  135. style="margin-top: 10px"
  136. >
  137. <a-input
  138. v-model:value="identitySetting.loginOutUrl"
  139. placeholder="非必填,请输入退出 URL"
  140. />
  141. </a-form-item>
  142. </a-form>
  143. </div>
  144. <div v-else>
  145. <a-form
  146. name="basic"
  147. :label-col="{ style: { width: '186px' } }"
  148. :wrapper-col="wrapperCol"
  149. :model="identitySetting"
  150. autocomplete="off"
  151. >
  152. <a-form-item label="uid(用户ID)" name="uid">
  153. <a-input
  154. v-model:value="identitySetting.uid"
  155. placeholder="选填,IDP 中用户ID"
  156. />
  157. </a-form-item>
  158. <a-form-item label="clientId(公司ID)" name="clientId">
  159. <a-input-number
  160. v-model:value="identitySetting.clientId"
  161. :controls="false"
  162. style="width: 100%"
  163. placeholder="选填,IDP 中公司ID"
  164. />
  165. </a-form-item>
  166. <a-form-item label="userName(用户姓名)" name="userName">
  167. <a-input
  168. v-model:value="identitySetting.userName"
  169. placeholder="选填,IDP 中用户姓名"
  170. />
  171. </a-form-item>
  172. <a-form-item label="userNo(员工编号)" name="userNo">
  173. <a-input
  174. v-model:value="identitySetting.userNo"
  175. placeholder="选填,IDP 中员工编号"
  176. />
  177. </a-form-item>
  178. <a-form-item label="nickName(员工昵称)" name="nickName">
  179. <a-input
  180. v-model:value="identitySetting.nickName"
  181. placeholder="选填,IDP 中员工昵称"
  182. />
  183. </a-form-item>
  184. <a-form-item label="email(员工邮箱)" name="email">
  185. <a-input
  186. v-model:value="identitySetting.email"
  187. placeholder="选填,IDP 中员工邮箱"
  188. />
  189. </a-form-item>
  190. <a-form-item label="phoneNumber(员工电话)" name="phoneNumber">
  191. <a-input
  192. v-model:value="identitySetting.phoneNumber"
  193. placeholder="选填,IDP 中员工电话"
  194. />
  195. </a-form-item>
  196. <a-form-item label="roleTemplateNo(角色模板)" name="roleTemplateNo">
  197. <a-input
  198. v-model:value="identitySetting.roleTemplateNo"
  199. placeholder="选填,如果有多个角色模板编号,使用逗号分隔"
  200. />
  201. </a-form-item>
  202. </a-form>
  203. <div class="proDog-setting">
  204. <h3>Prodog 配置</h3>
  205. <a-form
  206. name="basic"
  207. :label-col="{ style: { width: '186px' } }"
  208. :wrapper-col="wrapperCol"
  209. :rules="rules"
  210. :model="identitySetting"
  211. autocomplete="off"
  212. >
  213. <a-form-item has-feedback label="Prodog 实体ID" name="spEntityID">
  214. <a-input
  215. v-model:value="identitySetting.spEntityID"
  216. placeholder="必填, Prodog 实体 ID "
  217. />
  218. </a-form-item>
  219. <a-form-item
  220. has-feedback
  221. label="Prodog断言解析地址"
  222. name="spAssertionConsumeService"
  223. >
  224. <a-input
  225. v-model:value="identitySetting.spAssertionConsumeService"
  226. placeholder="必填,Prodog 断言解析地址"
  227. />
  228. </a-form-item>
  229. <a-form-item
  230. has-feedback
  231. label="Prodog断言解析成功跳转地址"
  232. name="spAssertionConsumeSuccessRedirectService"
  233. >
  234. <a-input
  235. v-model:value="
  236. identitySetting.spAssertionConsumeSuccessRedirectService
  237. "
  238. placeholder="必填,Prodog 断言解析成功跳转地址"
  239. />
  240. </a-form-item>
  241. </a-form>
  242. </div>
  243. </div>
  244. </div>
  245. <div class="steps-action">
  246. <a-button v-if="current > 0" @click="prev"> 上一步 </a-button>
  247. <a-button v-if="current === 0" disabled @click="prev"> 上一步 </a-button>
  248. <a-button
  249. v-if="current === 0"
  250. type="primary"
  251. style="margin-left: 8px"
  252. @click="next"
  253. >
  254. 下一步
  255. </a-button>
  256. <a-button
  257. v-if="current === 1"
  258. type="primary"
  259. style="margin-left: 8px"
  260. :disabled="
  261. !identityInfo.name ||
  262. logoUrl === '/static/assets/client-base-v4/image/logo.png'
  263. ? true
  264. : false
  265. "
  266. @click="next"
  267. >
  268. 下一步
  269. </a-button>
  270. <a-button
  271. v-if="current === 2"
  272. type="primary"
  273. style="margin-left: 8px"
  274. :disabled="
  275. !identitySetting.entityID ||
  276. !identitySetting.ssoUrl ||
  277. !identitySetting.certificate
  278. ? true
  279. : false
  280. "
  281. @click="next"
  282. >
  283. 下一步
  284. </a-button>
  285. <a-button
  286. v-if="current == steps.length - 1"
  287. type="primary"
  288. :disabled="
  289. !identitySetting.spEntityID || service || redirect ? true : false
  290. "
  291. style="margin-left: 8px"
  292. @click="createIdentity"
  293. >
  294. 完成
  295. </a-button>
  296. </div>
  297. </a-card>
  298. </template>
  299. <script setup>
  300. import Common from '../common/Common';
  301. import { message } from 'ant-design-vue';
  302. import { ref, reactive, onMounted } from 'vue';
  303. import { useRoute, useRouter } from 'vue-router';
  304. import { getImageSrc } from '../common/image-src';
  305. import {
  306. getBase64,
  307. imageToBase64,
  308. base64toFile,
  309. saveUpdateAuth,
  310. queryById,
  311. } from './configData.js';
  312. const route = useRoute();
  313. const router = useRouter();
  314. const current = ref(0); // 当前步骤
  315. const activeIndex = ref(0); // 所选认证源
  316. const logoUrl = ref('/static/assets/client-base-v4/image/logo.png'); // logo地址
  317. // 步骤一步骤二数据
  318. const identityInfo = reactive({
  319. id: '',
  320. name: '',
  321. logo: [],
  322. file: '',
  323. active: true,
  324. description: '',
  325. authType: 'OAUTH',
  326. });
  327. // 步骤三步骤四数据
  328. const identitySetting = ref({
  329. entityID: '',
  330. ssoUrl: '',
  331. certificate: '',
  332. loginOutUrl: '',
  333. uid: '',
  334. userName: '',
  335. userNo: '',
  336. nickName: '',
  337. email: '',
  338. phoneNumber: '',
  339. roleTemplateNo: '',
  340. spEntityID: 'com.leanwo.prodog.sp',
  341. spAssertionConsumeService: 'http://xxxx:xx/api/saml/sso/${id}',
  342. spAssertionConsumeSuccessRedirectService:
  343. 'http://xxxx:xx/index.html#/samlLogin',
  344. });
  345. const service = ref(false);
  346. const redirect = ref(false);
  347. const logoName = ref('');
  348. const logoClassName = ref('');
  349. // 设置form样式
  350. const labelCol = ref({
  351. style: {
  352. width: '120px',
  353. },
  354. });
  355. const wrapperCol = ref({
  356. span: 8,
  357. });
  358. // 验证断言解析地址结束字符是否正确
  359. let validateService = async (_rule, value) => {
  360. if (!value) {
  361. return Promise.reject('请输入 Prodog 断言解析地址');
  362. }
  363. if (!value.endsWith('/api/saml/sso/${id}')) {
  364. service.value = true;
  365. return Promise.reject('断言解析地址必须以/api/saml/sso/${ id }结束');
  366. } else {
  367. service.value = false;
  368. }
  369. };
  370. // 验证断言解析成功跳转地址结束字符是否正确
  371. let redirectService = async (_rule, value) => {
  372. if (!value) {
  373. return Promise.reject('请输入 Prodog 断言解析成功跳转地址');
  374. }
  375. if (!value.endsWith('index.html#/samlLogin')) {
  376. redirect.value = true;
  377. return Promise.reject(
  378. '断言解析成功跳转地址必须以index.html#/samlLogin结束',
  379. );
  380. } else {
  381. redirect.value = false;
  382. }
  383. };
  384. const rules = {
  385. spEntityID: [
  386. {
  387. required: true,
  388. message: '请输入 Prodog 实体 ID',
  389. },
  390. ],
  391. spAssertionConsumeService: [
  392. {
  393. required: true,
  394. validator: validateService,
  395. trigger: 'change',
  396. },
  397. ],
  398. spAssertionConsumeSuccessRedirectService: [
  399. {
  400. required: true,
  401. validator: redirectService,
  402. trigger: 'change',
  403. },
  404. ],
  405. };
  406. // 获取更新Id
  407. onMounted(() => {
  408. const { identityId } = route.query;
  409. if (identityId) {
  410. identityInfo.id = identityId;
  411. queryAuthById(identityId);
  412. }
  413. });
  414. // 根据id查询详情(更新时数据回显)
  415. const queryAuthById = id => {
  416. const params = new FormData();
  417. params.append('id', id);
  418. queryById(params).then(
  419. success => {
  420. if (success.errorCode === 0) {
  421. const {
  422. active,
  423. authType,
  424. attribute,
  425. className,
  426. description,
  427. logo,
  428. name,
  429. } = success.data;
  430. logoUrl.value = getImageSrc(className, logo);
  431. imgToBase64();
  432. logoName.value = logo;
  433. identityInfo.name = name;
  434. identityInfo.active = active;
  435. logoClassName.value = className;
  436. identityInfo.description = description;
  437. const datas = JSON.parse(attribute);
  438. identitySetting.value = datas;
  439. if (authType === 'OAuth2.0') {
  440. activeIndex.value = 0;
  441. identityInfo.authType = 'OAUTH';
  442. } else {
  443. activeIndex.value = 1;
  444. identityInfo.authType = 'SAML';
  445. }
  446. } else {
  447. message.error(success.errorMessage);
  448. }
  449. },
  450. error => {
  451. Common.processException(error);
  452. },
  453. );
  454. };
  455. // 图片转file
  456. const imgToBase64 = () => {
  457. var image = new Image();
  458. image.crossOrigin = '';
  459. image.src = logoUrl.value;
  460. image.onload = function () {
  461. let base64 = imageToBase64(image); //图片转base64
  462. identityInfo.file = base64toFile(base64, logoName.value); //base64转File
  463. };
  464. };
  465. // 新建或更新认证源
  466. const createIdentity = () => {
  467. const jsonStr = JSON.stringify(identitySetting.value);
  468. const formData = new FormData();
  469. formData.append('attribute', jsonStr);
  470. formData.append('id', identityInfo.id);
  471. formData.append('name', identityInfo.name);
  472. formData.append('logo', identityInfo.file);
  473. formData.append('active', identityInfo.active);
  474. formData.append('authType', identityInfo.authType);
  475. formData.append('description', identityInfo.description);
  476. saveUpdateAuth(formData).then(
  477. success => {
  478. if (success.errorCode === 0) {
  479. if (!identityInfo.id) {
  480. message.success('新建认证源成功!');
  481. } else {
  482. message.success('更新认证源成功!');
  483. }
  484. router.push('/desktop/identityManager');
  485. } else {
  486. message.error(success.errorMessage);
  487. }
  488. },
  489. error => {
  490. Common.processException(error);
  491. },
  492. );
  493. };
  494. // 获取logo文件
  495. const logoFileChange = async e => {
  496. identityInfo.file = e.file;
  497. logoUrl.value = await getBase64(e.fileList[0].originFileObj);
  498. };
  499. // 删除logo文件
  500. const deleteLogo = () => {
  501. identityInfo.file = '';
  502. identityInfo.logo.splice(0, 1);
  503. logoUrl.value = '/static/assets/client-base-v4/image/logo.png';
  504. message.warning('请上传logo!');
  505. };
  506. // 证书base64格式失去焦点后清除空格换行、回车
  507. const certificateBlur = e => {
  508. identitySetting.value.certificate = e.target.value.replace(/[\s\n\r]+/g, '');
  509. };
  510. // 禁用antd自动上传
  511. const beforeUpload = () => {
  512. return false;
  513. };
  514. // 步骤条配置
  515. const steps = ref([
  516. {
  517. title: '选择认证源协议类型',
  518. contentTemplate: 'First',
  519. },
  520. {
  521. title: '定义认证源名称及logo',
  522. contentTemplate: 'Second',
  523. },
  524. {
  525. title: '认证源基础配置',
  526. contentTemplate: 'Third',
  527. },
  528. {
  529. title: '账号关联',
  530. contentTemplate: 'Last',
  531. },
  532. ]);
  533. // 选择认证源数据
  534. const selectedItem = ref([
  535. {
  536. title: 'OAuth 2.0',
  537. imgSrc: '/static/assets/client-base-v4/image/oAuth.png',
  538. tooltip: 'OAuth 2.0是行业标准的授权协议。',
  539. },
  540. {
  541. title: 'SAML',
  542. imgSrc: '/static/assets/client-base-v4/image/saml.png',
  543. tooltip: 'SAML是安全断言标记语言,是一个基于XML的开源标准数据格式。',
  544. },
  545. ]);
  546. // 下一步
  547. const next = () => {
  548. current.value++;
  549. };
  550. // 上一步
  551. const prev = () => {
  552. current.value--;
  553. };
  554. // 设置当前点击的认证源为活动项
  555. const setActiveItem = index => {
  556. activeIndex.value = index;
  557. if (index === 0) {
  558. identityInfo.authType = 'OAUTH';
  559. } else {
  560. identityInfo.authType = 'SAML';
  561. }
  562. };
  563. </script>
  564. <style scoped>
  565. .steps {
  566. padding: 0;
  567. }
  568. .steps-content {
  569. margin-top: 16px;
  570. border-radius: 6px;
  571. min-height: 100px;
  572. }
  573. .steps-action {
  574. margin-top: 24px;
  575. }
  576. .labelStyle {
  577. color: rgba(0, 0, 0, 0.4);
  578. padding-bottom: 6px;
  579. padding-right: 20px;
  580. padding-top: 6px;
  581. vertical-align: baseline;
  582. width: 100px;
  583. }
  584. .selectUl {
  585. display: flex;
  586. flex-direction: row;
  587. flex-wrap: wrap;
  588. font-size: 12px;
  589. width: 100%;
  590. list-style: none;
  591. padding: 0;
  592. }
  593. .selectUl li {
  594. align-items: center;
  595. border: 1px solid #dcdcdc;
  596. box-sizing: border-box;
  597. cursor: pointer;
  598. display: flex;
  599. flex-direction: row;
  600. height: 80px;
  601. margin-bottom: 20px;
  602. margin-right: 16px;
  603. overflow: hidden;
  604. padding: 20px;
  605. width: 295px;
  606. }
  607. .selectUl li:hover {
  608. border-color: #006eff;
  609. }
  610. .selectUl li.active {
  611. border: 1px solid #006eff;
  612. }
  613. .box {
  614. display: table;
  615. font-size: 12px;
  616. line-height: 1.5;
  617. }
  618. .picture {
  619. height: 32px;
  620. margin-right: 16px;
  621. width: 32px;
  622. }
  623. .info {
  624. flex: 1;
  625. overflow: hidden;
  626. text-align: left;
  627. }
  628. .text {
  629. margin-top: 5px;
  630. display: inline-block;
  631. max-width: 100%;
  632. overflow: hidden;
  633. text-overflow: ellipsis;
  634. vertical-align: middle;
  635. white-space: nowrap;
  636. color: rgba(0, 0, 0, 0.4) !important;
  637. }
  638. .ant-card-body {
  639. padding: 20px !important;
  640. }
  641. :deep(.ant-form-item-control-input-content) {
  642. display: flex;
  643. align-items: center;
  644. }
  645. .ant-upload-select-picture-card i {
  646. font-size: 32px;
  647. color: #999;
  648. }
  649. .ant-upload-select-picture-card .ant-upload-text {
  650. margin-top: 8px;
  651. color: #666;
  652. }
  653. :deep(.ant-form-item-label > label) {
  654. font-size: 12px !important;
  655. color: rgba(0, 0, 0, 0.4);
  656. }
  657. .ant-form-item {
  658. margin-bottom: 4px;
  659. }
  660. .proDog-setting > h3 {
  661. font-size: 14px !important;
  662. font-weight: 700;
  663. }
  664. </style>