浏览代码

apps status and ExtendAttr

MaxKey 2 年之前
父节点
当前提交
2fddb2910c
共有 40 个文件被更改,包括 1450 次插入162 次删除
  1. 3 0
      maxkey-core/src/main/java/org/maxkey/entity/apps/Apps.java
  2. 1 0
      maxkey-core/src/main/java/org/maxkey/entity/apps/AppsOAuth20Details.java
  3. 1 1
      maxkey-core/src/main/java/org/maxkey/persistence/repository/LoginRepository.java
  4. 1 1
      maxkey-persistence/src/main/java/org/maxkey/persistence/mapper/AppsMapper.java
  5. 0 1
      maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsCasDetailsMapper.xml
  6. 0 1
      maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsFormBasedDetailsMapper.xml
  7. 0 1
      maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsJwtDetailsMapper.xml
  8. 22 16
      maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsMapper.xml
  9. 0 1
      maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsSaml20DetailsMapper.xml
  10. 0 1
      maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsTokenBasedDetailsMapper.xml
  11. 2 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/Apps.ts
  12. 2 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsCasDetails.ts
  13. 2 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsExtendApiDetails.ts
  14. 25 23
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsFormBasedDetails.ts
  15. 33 31
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsJwtDetails.ts
  16. 2 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsOauth20Details.ts
  17. 61 59
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsSamlDetails.ts
  18. 2 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsTokenBasedDetails.ts
  19. 27 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/ExtraAttr.ts
  20. 66 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-basic-details-editer/app-basic-details-editer.component.html
  21. 88 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-basic-details-editer/app-basic-details-editer.component.ts
  22. 66 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-cas-details-editer/app-cas-details-editer.component.html
  23. 87 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-cas-details-editer/app-cas-details-editer.component.ts
  24. 66 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-extend-api-details-editer/app-extend-api-details-editer.component.html
  25. 88 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-extend-api-details-editer/app-extend-api-details-editer.component.ts
  26. 66 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-form-based-details-editer/app-form-based-details-editer.component.html
  27. 88 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-form-based-details-editer/app-form-based-details-editer.component.ts
  28. 66 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-jwt-details-editer/app-jwt-details-editer.component.html
  29. 88 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-jwt-details-editer/app-jwt-details-editer.component.ts
  30. 66 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-oauth20-details-editer/app-oauth20-details-editer.component.html
  31. 88 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-oauth20-details-editer/app-oauth20-details-editer.component.ts
  32. 66 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-saml20-details-editer/app-saml20-details-editer.component.html
  33. 88 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-saml20-details-editer/app-saml20-details-editer.component.ts
  34. 66 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-token-based-details-editer/app-token-based-details-editer.component.html
  35. 88 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-token-based-details-editer/app-token-based-details-editer.component.ts
  36. 8 4
      maxkey-web-frontend/maxkey-web-mgt-app/src/app/service/apps.service.ts
  37. 8 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/assets/i18n/en-US.json
  38. 8 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/assets/i18n/zh-CN.json
  39. 8 0
      maxkey-web-frontend/maxkey-web-mgt-app/src/assets/i18n/zh-TW.json
  40. 3 22
      maxkey-webs/maxkey-web-mgt/src/main/java/org/maxkey/web/apps/contorller/ApplicationsController.java

+ 3 - 0
maxkey-core/src/main/java/org/maxkey/entity/apps/Apps.java

@@ -30,6 +30,8 @@ import org.apache.mybatis.jpa.persistence.JpaBaseEntity;
 import org.maxkey.constants.ConstsBoolean;
 import org.maxkey.crypto.Base64Utils;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
+
 @Entity
 @Table(name = "MXK_APPS")
 public class Apps extends JpaBaseEntity implements Serializable {
@@ -118,6 +120,7 @@ public class Apps extends JpaBaseEntity implements Serializable {
     /*
      * extendAttr
      */
+    @JsonFormat(shape = JsonFormat.Shape.STRING)
     private int isExtendAttr;
     private String extendAttr;
     

+ 1 - 0
maxkey-core/src/main/java/org/maxkey/entity/apps/AppsOAuth20Details.java

@@ -107,6 +107,7 @@ public class AppsOAuth20Details extends Apps {
         this.setAdapterId(application.getAdapterId());
         this.setAdapterName(application.getAdapterName());
         this.setFrequently(application.getFrequently());
+        this.setStatus(application.getStatus());
         
         this.clientSecret = baseClientDetails.getClientSecret();
         this.scope = StringUtils

+ 1 - 1
maxkey-core/src/main/java/org/maxkey/persistence/repository/LoginRepository.java

@@ -60,7 +60,7 @@ public class LoginRepository {
     
     private static final String DEFAULT_USERINFO_SELECT_STATEMENT_USERNAME_MOBILE_EMAIL = "select * from  mxk_userinfo where (username = ? or mobile = ? or email = ?) ";
     
-    private static final String DEFAULT_MYAPPS_SELECT_STATEMENT = "select distinct app.id,app.appname from mxk_apps app,mxk_role_permissions pm,mxk_roles r  where app.id=pm.appid and pm.roleid=r.id and r.id in(%s)";
+    private static final String DEFAULT_MYAPPS_SELECT_STATEMENT = "select distinct app.id,app.appname from mxk_apps app,mxk_role_permissions pm,mxk_roles r  where app.id=pm.appid and app.status =	1 and pm.roleid=r.id and r.id in(%s)";
     
     protected JdbcTemplate jdbcTemplate;
     

+ 1 - 1
maxkey-persistence/src/main/java/org/maxkey/persistence/mapper/AppsMapper.java

@@ -37,7 +37,7 @@ public  interface AppsMapper extends IJpaBaseMapper<Apps> {
 	
 	public int updateApp(Apps app);
 	
-	@Update("update mxk_apps set isextendattr=#{isExtendAttr},	extendattr=#{extendAttr} where id = #{id}")
+	@Update("update mxk_apps set extendattr=#{extendAttr} where id = #{id}")
 	public int updateExtendAttr(Apps app);  
 	
 

+ 0 - 1
maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsCasDetailsMapper.xml

@@ -8,7 +8,6 @@
     		mxk_apps_cas_details cd,
     		mxk_apps app
     	where 	app.instid = cd.instid
-    		and app.status	=	1
     		and cd.id	=	app.id
     		and (
     			app.id	=	#{value}

+ 0 - 1
maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsFormBasedDetailsMapper.xml

@@ -12,6 +12,5 @@
     		and app.id	=	#{value}
     		and fbd.id	=	#{value}
     		and fbd.id	=	app.id
-    		and status	=	1
     </select>
 </mapper>

+ 0 - 1
maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsJwtDetailsMapper.xml

@@ -11,7 +11,6 @@
     	where 	app.instid  =   jd.instid
     		and app.id	    =	#{value}
     		and jd.id	    =	#{value}
-    		and status        =   1
     		and jd.id	    =	app.id
     </select>
 	

+ 22 - 16
maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsMapper.xml

@@ -143,8 +143,9 @@
 			sharedusername		=	#{sharedUsername},
 			sharedpassword		=	#{sharedPassword},
 			systemuserattr		=	#{systemUserAttr},
-			<!--
+			
 			isextendattr		=	#{isExtendAttr},
+			<!--
 			extendattr			=	#{extendAttr},
 			-->
 			userpropertys		=	#{userPropertys},
@@ -184,21 +185,26 @@
             and p.instid = #{instId}
             and r.instid = #{instId}
             and app.visible != 0
-            and (
-            	 r.id='ROLE_ALL_USER'
-                 or r.id in(
-                      select 
-                        rm.roleid 
-                      from 
-                        mxk_role_member rm,mxk_userinfo u 
-                      where rm.memberid    =   u.id 
-                        <if test="userId != null and userId != ''">
-                            and u.id        =   #{userId}
-                        </if>
-                        <if test="username != null and username != ''">
-                            and  u.username =   #{username}
-                        </if>
-                    )
+            and r.id in(
+                    <!-- ROLE_ALL_USER -->
+                    select id as roleid 
+                    from mxk_roles 
+                    where rolecode = 'ROLE_ALL_USER'
+                    
+                    union 
+                    
+                    <!-- role member -->
+                    select 
+                      rm.roleid 
+                    from 
+                      mxk_role_member rm,mxk_userinfo u 
+                    where rm.memberid    =   u.id 
+                      <if test="userId != null and userId != ''">
+                          and u.id        =   #{userId}
+                      </if>
+                      <if test="username != null and username != ''">
+                          and  u.username =   #{username}
+                      </if>
             )
         <if test="appName != null and appName != ''">
             and appname    =   #{appName}

+ 0 - 1
maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsSaml20DetailsMapper.xml

@@ -28,7 +28,6 @@
     		and app.id		=	#{value}
     		and svd.id		=	#{value}
     		and svd.id		=	app.id
-    		and app.status	=	1
     </select>
     
 </mapper>

+ 0 - 1
maxkey-persistence/src/main/resources/org/maxkey/persistence/mapper/xml/mysql/AppsTokenBasedDetailsMapper.xml

@@ -12,7 +12,6 @@
     		and app.id	=	#{value}
     		and tbd.id	=	#{value}
     		and tbd.id	=	app.id
-    		and status	=	1
     </select>
 	
 </mapper>

+ 2 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/Apps.ts

@@ -64,6 +64,8 @@ export class Apps extends BaseEntity {
         Object.assign(this, data);
         if (this.status == 1) {
             this.switch_status = true;
+        } else {
+            this.switch_status = false;
         }
         this.isAdapter = `${data.isAdapter}`;
         this.isExtendAttr = `${data.isExtendAttr}`;

+ 2 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsCasDetails.ts

@@ -35,6 +35,8 @@ export class AppsCasDetails extends Apps {
         super.init(data);
         if (this.status == 1) {
             this.switch_status = true;
+        } else {
+            this.switch_status = false;
         }
     }
 

+ 2 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsExtendApiDetails.ts

@@ -28,6 +28,8 @@ export class AppsExtendApiDetails extends Apps {
         super.init(data);
         if (this.status == 1) {
             this.switch_status = true;
+        } else {
+            this.switch_status = false;
         }
     }
 

+ 25 - 23
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsFormBasedDetails.ts

@@ -19,32 +19,34 @@ import format from 'date-fns/format';
 import { Apps } from './Apps';
 
 export class AppsFormBasedDetails extends Apps {
-    redirectUri!: String;
-    usernameMapping!: String;
-    passwordMapping!: String;
-    passwordAlgorithm!: String;
-    authorizeView!: String;
+  redirectUri!: String;
+  usernameMapping!: String;
+  passwordMapping!: String;
+  passwordAlgorithm!: String;
+  authorizeView!: String;
 
-    constructor() {
-        super();
-        this.usernameMapping = 'username';
-        this.passwordMapping = 'password';
-        this.passwordAlgorithm = 'NONE';
-    }
+  constructor() {
+    super();
+    this.usernameMapping = 'username';
+    this.passwordMapping = 'password';
+    this.passwordAlgorithm = 'NONE';
+  }
 
-    override init(data: any): void {
-        Object.assign(this, data);
-        super.init(data);
-        if (this.status == 1) {
-            this.switch_status = true;
-        }
+  override init(data: any): void {
+    Object.assign(this, data);
+    super.init(data);
+    if (this.status == 1) {
+      this.switch_status = true;
+    } else {
+      this.switch_status = false;
     }
+  }
 
-    override trans(): void {
-        if (this.switch_status) {
-            this.status = 1;
-        } else {
-            this.status = 0;
-        }
+  override trans(): void {
+    if (this.switch_status) {
+      this.status = 1;
+    } else {
+      this.status = 0;
     }
+  }
 }

+ 33 - 31
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsJwtDetails.ts

@@ -19,41 +19,43 @@ import format from 'date-fns/format';
 import { Apps } from './Apps';
 
 export class AppsJwtDetails extends Apps {
-    subject!: String;
-    issuer!: String;
-    audience!: String;
+  subject!: String;
+  issuer!: String;
+  audience!: String;
 
-    redirectUri!: String;
-    tokenType!: String;
-    jwtName!: String;
-    algorithm!: String;
-    algorithmKey!: String;
-    encryptionMethod!: String;
-    signature!: String;
-    signatureKey!: String;
-    expires!: Number;
+  redirectUri!: String;
+  tokenType!: String;
+  jwtName!: String;
+  algorithm!: String;
+  algorithmKey!: String;
+  encryptionMethod!: String;
+  signature!: String;
+  signatureKey!: String;
+  expires!: Number;
 
-    constructor() {
-        super();
-        this.expires = 300;
-        this.jwtName = 'jwt';
-        this.subject = 'username';
-        this.tokenType = 'POST';
-    }
+  constructor() {
+    super();
+    this.expires = 300;
+    this.jwtName = 'jwt';
+    this.subject = 'username';
+    this.tokenType = 'POST';
+  }
 
-    override init(data: any): void {
-        Object.assign(this, data);
-        super.init(data);
-        if (this.status == 1) {
-            this.switch_status = true;
-        }
+  override init(data: any): void {
+    Object.assign(this, data);
+    super.init(data);
+    if (this.status == 1) {
+      this.switch_status = true;
+    } else {
+      this.switch_status = false;
     }
+  }
 
-    override trans(): void {
-        if (this.switch_status) {
-            this.status = 1;
-        } else {
-            this.status = 0;
-        }
+  override trans(): void {
+    if (this.switch_status) {
+      this.status = 1;
+    } else {
+      this.status = 0;
     }
+  }
 }

+ 2 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsOauth20Details.ts

@@ -81,6 +81,8 @@ export class AppsOauth20Details extends Apps {
         super.init(data);
         if (this.status == 1) {
             this.switch_status = true;
+        } else {
+            this.switch_status = false;
         }
         if (this.approvalPrompt == '') {
         }

+ 61 - 59
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsSamlDetails.ts

@@ -19,86 +19,88 @@ import format from 'date-fns/format';
 import { Apps } from './Apps';
 
 export class AppsSamlDetails extends Apps {
-    certIssuer!: String;
+  certIssuer!: String;
 
-    certSubject!: String;
+  certSubject!: String;
 
-    certExpiration!: String;
+  certExpiration!: String;
 
-    signature!: String;
+  signature!: String;
 
-    digestMethod!: String;
+  digestMethod!: String;
 
-    entityId!: String;
+  entityId!: String;
 
-    spAcsUrl!: String;
+  spAcsUrl!: String;
 
-    issuer!: String;
+  issuer!: String;
 
-    audience!: String;
+  audience!: String;
 
-    nameidFormat!: String;
+  nameidFormat!: String;
 
-    validityInterval!: String;
-    /**
-     * Redirect-Post Post-Post IdpInit-Post Redirect-PostSimpleSign
-     * Post-PostSimpleSign IdpInit-PostSimpleSign
-     */
+  validityInterval!: String;
+  /**
+   * Redirect-Post Post-Post IdpInit-Post Redirect-PostSimpleSign
+   * Post-PostSimpleSign IdpInit-PostSimpleSign
+   */
 
-    binding!: String;
+  binding!: String;
 
-    /**
-     * yes or no
-     */
+  /**
+   * yes or no
+   */
 
-    encrypted!: String;
-    /**
-     * metadata_file metadata_url or certificate
-     */
-    fileType!: String;
+  encrypted!: String;
+  /**
+   * metadata_file metadata_url or certificate
+   */
+  fileType!: String;
 
-    metaUrl!: String;
+  metaUrl!: String;
 
-    metaFileId!: String;
+  metaFileId!: String;
 
-    /**
-     * original , uppercase  or lowercase
-     */
+  /**
+   * original , uppercase  or lowercase
+   */
 
-    nameIdConvert!: String;
+  nameIdConvert!: String;
 
-    nameIdSuffix!: String;
+  nameIdSuffix!: String;
 
-    constructor() {
-        super();
-        this.fileType = 'certificate';
-        this.validityInterval = '300';
-        this.nameidFormat = 'persistent';
-        this.nameIdConvert = 'original';
-        this.signature = 'RSAwithSHA1';
-        this.digestMethod = 'SHA1';
-        this.encrypted = 'no';
-        this.binding = 'Redirect-Post';
-    }
+  constructor() {
+    super();
+    this.fileType = 'certificate';
+    this.validityInterval = '300';
+    this.nameidFormat = 'persistent';
+    this.nameIdConvert = 'original';
+    this.signature = 'RSAwithSHA1';
+    this.digestMethod = 'SHA1';
+    this.encrypted = 'no';
+    this.binding = 'Redirect-Post';
+  }
 
-    override init(data: any): void {
-        Object.assign(this, data);
-        super.init(data);
-        this.fileType = 'certificate';
-        this.metaUrl = '';
-        if (this.category == null || this.category == '') {
-            this.category = 'NONE';
-        }
-        if (this.status == 1) {
-            this.switch_status = true;
-        }
+  override init(data: any): void {
+    Object.assign(this, data);
+    super.init(data);
+    this.fileType = 'certificate';
+    this.metaUrl = '';
+    if (this.category == null || this.category == '') {
+      this.category = 'NONE';
+    }
+    if (this.status == 1) {
+      this.switch_status = true;
+    } else {
+      this.switch_status = false;
     }
+  }
 
-    override trans(): void {
-        if (this.switch_status) {
-            this.status = 1;
-        } else {
-            this.status = 0;
-        }
+  override trans(): void {
+    if (this.switch_status) {
+      this.status = 1;
+    } else {
+      this.status = 0;
     }
+  }
 }

+ 2 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/AppsTokenBasedDetails.ts

@@ -39,6 +39,8 @@ export class AppsTokenBasedDetails extends Apps {
         super.init(data);
         if (this.status == 1) {
             this.switch_status = true;
+        } else {
+            this.switch_status = false;
         }
     }
 

+ 27 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/entity/ExtraAttr.ts

@@ -0,0 +1,27 @@
+/*
+ * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import format from 'date-fns/format';
+
+import { BaseEntity } from './BaseEntity';
+
+export class ExtraAttr {
+    id!: string;
+    attr!: string;
+    type!: string;
+    value!: string;
+    constructor() { }
+}

+ 66 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-basic-details-editer/app-basic-details-editer.component.html

@@ -135,6 +135,30 @@
             </nz-form-control>
           </nz-form-item>
         </div>
+        <div nz-row>
+          <nz-form-item>
+            <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="status">{{ 'mxk.text.status' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid status!">
+              <nz-switch [(ngModel)]="form.model.switch_status" [ngModelOptions]="{ standalone: true }" name="status"
+                [nzCheckedChildren]="statuscheckedTemplate" [nzUnCheckedChildren]="statusunCheckedTemplate"></nz-switch>
+              <ng-template #statuscheckedTemplate><i nz-icon nzType="check"></i></ng-template>
+              <ng-template #statusunCheckedTemplate><i nz-icon nzType="close"></i></ng-template>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label [nzSm]="8" [nzXs]="24" nzFor="isExtendAttr">{{ 'mxk.apps.isExtendAttr' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="16" [nzMd]="16" [nzXs]="36" [nzXl]="48"
+              nzErrorTip="The input is not valid isExtendAttr!">
+              <nz-radio-group [(ngModel)]="form.model.isExtendAttr" [ngModelOptions]="{ standalone: true }"
+                nzButtonStyle="solid">
+                <label nz-radio-button nzValue="0">{{ 'mxk.text.no' | i18n }}</label>
+                <label nz-radio-button nzValue="1">{{ 'mxk.text.yes' | i18n }}</label>
+              </nz-radio-group>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
       </nz-tab>
       <nz-tab nzTitle="{{ 'mxk.apps.tab.extra' | i18n }}">
         <div nz-row>
@@ -245,6 +269,48 @@
           </nz-form-item>
         </div>
       </nz-tab>
+      <nz-tab nzTitle="{{ 'mxk.apps.tab.custom' | i18n }}" *ngIf="isEdit && form.model.isExtendAttr === '1'">
+        <button nz-button (click)="addExtraAttrRow($event)" nzType="primary">{{ 'mxk.text.add' | i18n }}</button>
+        <nz-table #editRowTable nzBordered [nzData]="extraAttrListOfData" nzTableLayout="fixed">
+          <thead>
+            <tr>
+              <th nzWidth="25%">{{ 'mxk.custom.extraAttr.attr' | i18n }}</th>
+              <th nzWidth="15%">{{ 'mxk.custom.extraAttr.type' | i18n }}</th>
+              <th nzWidth="40%">{{ 'mxk.custom.extraAttr.value' | i18n }}</th>
+              <th>{{ 'mxk.text.action' | i18n }}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr *ngFor="let data of editRowTable.data">
+              <ng-container *ngIf="!extraAttrEditCache[data.id].edit; else editTemplate">
+                <td>{{ data.attr }}</td>
+                <td>{{ data.type }}</td>
+                <td>{{ data.value }}</td>
+                <td>
+                  <button nz-button type="button" (click)="startExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.edit' | i18n
+                    }}</button>
+                  <button nz-button type="button" (click)="deleteExtraAttrRow(data.id)" nzDanger>{{ 'mxk.text.delete' |
+                    i18n }}</button>
+                </td>
+              </ng-container>
+              <ng-template #editTemplate>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.attr"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.type"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.value"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td>
+                  <button nz-button type="button" (click)="saveExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.submit' | i18n
+                    }}</button>
+                </td>
+              </ng-template>
+            </tr>
+          </tbody>
+        </nz-table>
+      </nz-tab>
     </nz-tabset>
   </form>
 </div>

+ 88 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-basic-details-editer/app-basic-details-editer.component.ts

@@ -24,6 +24,7 @@ import { NzModalRef, NzModalService } from 'ng-zorro-antd/modal';
 import { NzUploadFile, NzUploadChangeParam } from 'ng-zorro-antd/upload';
 
 import { Apps } from '../../../entity/Apps';
+import { ExtraAttr } from '../../../entity/ExtraAttr';
 import { AppsService } from '../../../service/apps.service';
 import { SelectAdaptersComponent } from '../../config/adapters/select-adapters/select-adapters.component';
 
@@ -70,6 +71,10 @@ export class AppBasicDetailsEditerComponent implements OnInit {
   previewImage: string | ArrayBuffer | undefined | null = '';
   previewVisible = false;
 
+  extraAttrIndex: number = 1;
+  extraAttrEditCache: { [key: string]: { edit: boolean; data: ExtraAttr } } = {};
+  extraAttrListOfData: ExtraAttr[] = [];
+
   constructor(
     private modal: NzModalRef,
     private appsService: AppsService,
@@ -94,6 +99,7 @@ export class AppBasicDetailsEditerComponent implements OnInit {
             url: this.previewImage
           }
         ];
+        this.initExtraAttr(res.data);
       });
     } else {
       this.appsService.init().subscribe(res => {
@@ -173,4 +179,86 @@ export class AppBasicDetailsEditerComponent implements OnInit {
       this.cdr.detectChanges();
     });
   }
+
+  initExtraAttr(extraData: any): void {
+    if (extraData.extendAttr != null && extraData.extendAttr != '') {
+      let extraAttrDataArray = JSON.parse(extraData.extendAttr);
+      console.log(extraAttrDataArray);
+      const data = [];
+      while (this.extraAttrIndex <= extraAttrDataArray.length) {
+        let extraAttrData = extraAttrDataArray[this.extraAttrIndex - 1];
+        data.push({
+          id: `${this.extraAttrIndex}`,
+          attr: extraAttrData.attr,
+          type: extraAttrData.type,
+          value: extraAttrData.value
+        });
+        this.extraAttrIndex++;
+      }
+      this.extraAttrListOfData = data;
+      this.updateExtraAttrEditCache();
+    }
+  }
+
+  addExtraAttrRow(e: MouseEvent): void {
+    e.preventDefault();
+    this.extraAttrListOfData = [
+      ...this.extraAttrListOfData,
+      {
+        id: `${this.extraAttrIndex}`,
+        attr: `Attr ${this.extraAttrIndex}`,
+        type: 'string',
+        value: `value ${this.extraAttrIndex}`
+      }
+    ];
+    this.updateExtraAttrEditCache();
+    this.startExtraAttrEdit(`${this.extraAttrIndex}`);
+    this.extraAttrIndex++;
+  }
+
+  deleteExtraAttrRow(id: string): void {
+    this.extraAttrListOfData = this.extraAttrListOfData.filter(d => d.id !== id);
+    this.submitExtraAttr();
+  }
+
+  startExtraAttrEdit(id: string): void {
+    this.extraAttrEditCache[id].edit = true;
+  }
+
+  cancelExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    console.log(index);
+    this.extraAttrEditCache[id] = {
+      data: { ...this.extraAttrListOfData[index] },
+      edit: false
+    };
+  }
+
+  saveExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    Object.assign(this.extraAttrListOfData[index], this.extraAttrEditCache[id].data);
+    this.extraAttrEditCache[id].edit = false;
+    this.submitExtraAttr();
+  }
+
+  submitExtraAttr() {
+    let extraAttrString = JSON.stringify(this.extraAttrListOfData);
+    this.appsService.updateExtendAttr({ id: this.form.model.id, extendAttr: extraAttrString }).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(this.i18n.fanyi('mxk.alert.update.success'));
+      } else {
+        this.msg.error(this.i18n.fanyi('mxk.alert.update.error'));
+      }
+      this.cdr.detectChanges();
+    });
+  }
+
+  updateExtraAttrEditCache(): void {
+    this.extraAttrListOfData.forEach(item => {
+      this.extraAttrEditCache[item.id] = {
+        edit: false,
+        data: { ...item }
+      };
+    });
+  }
 }

+ 66 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-cas-details-editer/app-cas-details-editer.component.html

@@ -135,6 +135,30 @@
             </nz-form-control>
           </nz-form-item>
         </div>
+        <div nz-row>
+          <nz-form-item>
+            <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="status">{{ 'mxk.text.status' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid status!">
+              <nz-switch [(ngModel)]="form.model.switch_status" [ngModelOptions]="{ standalone: true }" name="status"
+                [nzCheckedChildren]="statuscheckedTemplate" [nzUnCheckedChildren]="statusunCheckedTemplate"></nz-switch>
+              <ng-template #statuscheckedTemplate><i nz-icon nzType="check"></i></ng-template>
+              <ng-template #statusunCheckedTemplate><i nz-icon nzType="close"></i></ng-template>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label [nzSm]="8" [nzXs]="24" nzFor="isExtendAttr">{{ 'mxk.apps.isExtendAttr' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="16" [nzMd]="16" [nzXs]="36" [nzXl]="48"
+              nzErrorTip="The input is not valid isExtendAttr!">
+              <nz-radio-group [(ngModel)]="form.model.isExtendAttr" [ngModelOptions]="{ standalone: true }"
+                nzButtonStyle="solid">
+                <label nz-radio-button nzValue="0">{{ 'mxk.text.no' | i18n }}</label>
+                <label nz-radio-button nzValue="1">{{ 'mxk.text.yes' | i18n }}</label>
+              </nz-radio-group>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
       </nz-tab>
       <nz-tab nzTitle="{{ 'mxk.apps.cas.tab' | i18n }}">
         <div nz-row>
@@ -297,6 +321,48 @@
           </nz-form-item>
         </div>
       </nz-tab>
+      <nz-tab nzTitle="{{ 'mxk.apps.tab.custom' | i18n }}" *ngIf="isEdit && form.model.isExtendAttr === '1'">
+        <button nz-button (click)="addExtraAttrRow($event)" nzType="primary">{{ 'mxk.text.add' | i18n }}</button>
+        <nz-table #editRowTable nzBordered [nzData]="extraAttrListOfData" nzTableLayout="fixed">
+          <thead>
+            <tr>
+              <th nzWidth="25%">{{ 'mxk.custom.extraAttr.attr' | i18n }}</th>
+              <th nzWidth="15%">{{ 'mxk.custom.extraAttr.type' | i18n }}</th>
+              <th nzWidth="40%">{{ 'mxk.custom.extraAttr.value' | i18n }}</th>
+              <th>{{ 'mxk.text.action' | i18n }}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr *ngFor="let data of editRowTable.data">
+              <ng-container *ngIf="!extraAttrEditCache[data.id].edit; else editTemplate">
+                <td>{{ data.attr }}</td>
+                <td>{{ data.type }}</td>
+                <td>{{ data.value }}</td>
+                <td>
+                  <button nz-button type="button" (click)="startExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.edit' | i18n
+                    }}</button>
+                  <button nz-button type="button" (click)="deleteExtraAttrRow(data.id)" nzDanger>{{ 'mxk.text.delete' |
+                    i18n }}</button>
+                </td>
+              </ng-container>
+              <ng-template #editTemplate>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.attr"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.type"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.value"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td>
+                  <button nz-button type="button" (click)="saveExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.submit' | i18n
+                    }}</button>
+                </td>
+              </ng-template>
+            </tr>
+          </tbody>
+        </nz-table>
+      </nz-tab>
     </nz-tabset>
   </form>
 </div>

+ 87 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-cas-details-editer/app-cas-details-editer.component.ts

@@ -24,6 +24,7 @@ import { NzModalRef, NzModalService } from 'ng-zorro-antd/modal';
 import { NzUploadFile, NzUploadChangeParam } from 'ng-zorro-antd/upload';
 
 import { AppsCasDetails } from '../../../entity/AppsCasDetails';
+import { ExtraAttr } from '../../../entity/ExtraAttr';
 import { AppsCasDetailsService } from '../../../service/apps-cas-details.service';
 import { AppsService } from '../../../service/apps.service';
 import { SelectAdaptersComponent } from '../../config/adapters/select-adapters/select-adapters.component';
@@ -70,6 +71,9 @@ export class AppCasDetailsEditerComponent implements OnInit {
   previewImage: string | ArrayBuffer | undefined | null = '';
   previewVisible = false;
 
+  extraAttrIndex: number = 1;
+  extraAttrEditCache: { [key: string]: { edit: boolean; data: ExtraAttr } } = {};
+  extraAttrListOfData: ExtraAttr[] = [];
   constructor(
     private modal: NzModalRef,
     private modalService: NzModalService,
@@ -95,6 +99,7 @@ export class AppCasDetailsEditerComponent implements OnInit {
             url: this.previewImage
           }
         ];
+        this.initExtraAttr(res.data);
       });
     } else {
       this.appsCasDetailsService.init().subscribe(res => {
@@ -174,4 +179,86 @@ export class AppCasDetailsEditerComponent implements OnInit {
       this.cdr.detectChanges();
     });
   }
+
+  initExtraAttr(extraData: any): void {
+    if (extraData.extendAttr != null && extraData.extendAttr != '') {
+      let extraAttrDataArray = JSON.parse(extraData.extendAttr);
+      console.log(extraAttrDataArray);
+      const data = [];
+      while (this.extraAttrIndex <= extraAttrDataArray.length) {
+        let extraAttrData = extraAttrDataArray[this.extraAttrIndex - 1];
+        data.push({
+          id: `${this.extraAttrIndex}`,
+          attr: extraAttrData.attr,
+          type: extraAttrData.type,
+          value: extraAttrData.value
+        });
+        this.extraAttrIndex++;
+      }
+      this.extraAttrListOfData = data;
+      this.updateExtraAttrEditCache();
+    }
+  }
+
+  addExtraAttrRow(e: MouseEvent): void {
+    e.preventDefault();
+    this.extraAttrListOfData = [
+      ...this.extraAttrListOfData,
+      {
+        id: `${this.extraAttrIndex}`,
+        attr: `Attr ${this.extraAttrIndex}`,
+        type: 'string',
+        value: `value ${this.extraAttrIndex}`
+      }
+    ];
+    this.updateExtraAttrEditCache();
+    this.startExtraAttrEdit(`${this.extraAttrIndex}`);
+    this.extraAttrIndex++;
+  }
+
+  deleteExtraAttrRow(id: string): void {
+    this.extraAttrListOfData = this.extraAttrListOfData.filter(d => d.id !== id);
+    this.submitExtraAttr();
+  }
+
+  startExtraAttrEdit(id: string): void {
+    this.extraAttrEditCache[id].edit = true;
+  }
+
+  cancelExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    console.log(index);
+    this.extraAttrEditCache[id] = {
+      data: { ...this.extraAttrListOfData[index] },
+      edit: false
+    };
+  }
+
+  saveExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    Object.assign(this.extraAttrListOfData[index], this.extraAttrEditCache[id].data);
+    this.extraAttrEditCache[id].edit = false;
+    this.submitExtraAttr();
+  }
+
+  submitExtraAttr() {
+    let extraAttrString = JSON.stringify(this.extraAttrListOfData);
+    this.appsService.updateExtendAttr({ id: this.form.model.id, extendAttr: extraAttrString }).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(this.i18n.fanyi('mxk.alert.update.success'));
+      } else {
+        this.msg.error(this.i18n.fanyi('mxk.alert.update.error'));
+      }
+      this.cdr.detectChanges();
+    });
+  }
+
+  updateExtraAttrEditCache(): void {
+    this.extraAttrListOfData.forEach(item => {
+      this.extraAttrEditCache[item.id] = {
+        edit: false,
+        data: { ...item }
+      };
+    });
+  }
 }

+ 66 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-extend-api-details-editer/app-extend-api-details-editer.component.html

@@ -135,6 +135,30 @@
             </nz-form-control>
           </nz-form-item>
         </div>
+        <div nz-row>
+          <nz-form-item>
+            <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="status">{{ 'mxk.text.status' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid status!">
+              <nz-switch [(ngModel)]="form.model.switch_status" [ngModelOptions]="{ standalone: true }" name="status"
+                [nzCheckedChildren]="statuscheckedTemplate" [nzUnCheckedChildren]="statusunCheckedTemplate"></nz-switch>
+              <ng-template #statuscheckedTemplate><i nz-icon nzType="check"></i></ng-template>
+              <ng-template #statusunCheckedTemplate><i nz-icon nzType="close"></i></ng-template>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label [nzSm]="8" [nzXs]="24" nzFor="isExtendAttr">{{ 'mxk.apps.isExtendAttr' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="16" [nzMd]="16" [nzXs]="36" [nzXl]="48"
+              nzErrorTip="The input is not valid isExtendAttr!">
+              <nz-radio-group [(ngModel)]="form.model.isExtendAttr" [ngModelOptions]="{ standalone: true }"
+                nzButtonStyle="solid">
+                <label nz-radio-button nzValue="0">{{ 'mxk.text.no' | i18n }}</label>
+                <label nz-radio-button nzValue="1">{{ 'mxk.text.yes' | i18n }}</label>
+              </nz-radio-group>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
       </nz-tab>
       <nz-tab nzTitle="{{ 'mxk.apps.extendapi.tab' | i18n }}">
         <div nz-row>
@@ -321,6 +345,48 @@
           </nz-form-item>
         </div>
       </nz-tab>
+      <nz-tab nzTitle="{{ 'mxk.apps.tab.custom' | i18n }}" *ngIf="isEdit && form.model.isExtendAttr === '1'">
+        <button nz-button (click)="addExtraAttrRow($event)" nzType="primary">{{ 'mxk.text.add' | i18n }}</button>
+        <nz-table #editRowTable nzBordered [nzData]="extraAttrListOfData" nzTableLayout="fixed">
+          <thead>
+            <tr>
+              <th nzWidth="25%">{{ 'mxk.custom.extraAttr.attr' | i18n }}</th>
+              <th nzWidth="15%">{{ 'mxk.custom.extraAttr.type' | i18n }}</th>
+              <th nzWidth="40%">{{ 'mxk.custom.extraAttr.value' | i18n }}</th>
+              <th>{{ 'mxk.text.action' | i18n }}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr *ngFor="let data of editRowTable.data">
+              <ng-container *ngIf="!extraAttrEditCache[data.id].edit; else editTemplate">
+                <td>{{ data.attr }}</td>
+                <td>{{ data.type }}</td>
+                <td>{{ data.value }}</td>
+                <td>
+                  <button nz-button type="button" (click)="startExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.edit' | i18n
+                    }}</button>
+                  <button nz-button type="button" (click)="deleteExtraAttrRow(data.id)" nzDanger>{{ 'mxk.text.delete' |
+                    i18n }}</button>
+                </td>
+              </ng-container>
+              <ng-template #editTemplate>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.attr"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.type"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.value"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td>
+                  <button nz-button type="button" (click)="saveExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.submit' | i18n
+                    }}</button>
+                </td>
+              </ng-template>
+            </tr>
+          </tbody>
+        </nz-table>
+      </nz-tab>
     </nz-tabset>
   </form>
 </div>

+ 88 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-extend-api-details-editer/app-extend-api-details-editer.component.ts

@@ -24,6 +24,7 @@ import { NzModalRef, NzModalService } from 'ng-zorro-antd/modal';
 import { NzUploadFile, NzUploadChangeParam } from 'ng-zorro-antd/upload';
 
 import { Apps } from '../../../entity/Apps';
+import { ExtraAttr } from '../../../entity/ExtraAttr';
 import { AppsExtendApiDetailsService } from '../../../service/apps-extend-api-details.service';
 import { AppsService } from '../../../service/apps.service';
 import { SelectAdaptersComponent } from '../../config/adapters/select-adapters/select-adapters.component';
@@ -70,6 +71,10 @@ export class AppExtendApiDetailsEditerComponent implements OnInit {
   previewImage: string | ArrayBuffer | undefined | null = '';
   previewVisible = false;
 
+  extraAttrIndex: number = 1;
+  extraAttrEditCache: { [key: string]: { edit: boolean; data: ExtraAttr } } = {};
+  extraAttrListOfData: ExtraAttr[] = [];
+
   constructor(
     private modalRef: NzModalRef,
     private modalService: NzModalService,
@@ -86,6 +91,7 @@ export class AppExtendApiDetailsEditerComponent implements OnInit {
     if (this.isEdit) {
       this.appsExtendApiDetailsService.get(`${this.id}`).subscribe(res => {
         this.form.model.init(res.data);
+        this.initExtraAttr(res.data);
       });
     } else {
       this.appsExtendApiDetailsService.init().subscribe(res => {
@@ -173,4 +179,86 @@ export class AppExtendApiDetailsEditerComponent implements OnInit {
       this.cdr.detectChanges();
     });
   }
+
+  initExtraAttr(extraData: any): void {
+    if (extraData.extendAttr != null && extraData.extendAttr != '') {
+      let extraAttrDataArray = JSON.parse(extraData.extendAttr);
+      console.log(extraAttrDataArray);
+      const data = [];
+      while (this.extraAttrIndex <= extraAttrDataArray.length) {
+        let extraAttrData = extraAttrDataArray[this.extraAttrIndex - 1];
+        data.push({
+          id: `${this.extraAttrIndex}`,
+          attr: extraAttrData.attr,
+          type: extraAttrData.type,
+          value: extraAttrData.value
+        });
+        this.extraAttrIndex++;
+      }
+      this.extraAttrListOfData = data;
+      this.updateExtraAttrEditCache();
+    }
+  }
+
+  addExtraAttrRow(e: MouseEvent): void {
+    e.preventDefault();
+    this.extraAttrListOfData = [
+      ...this.extraAttrListOfData,
+      {
+        id: `${this.extraAttrIndex}`,
+        attr: `Attr ${this.extraAttrIndex}`,
+        type: 'string',
+        value: `value ${this.extraAttrIndex}`
+      }
+    ];
+    this.updateExtraAttrEditCache();
+    this.startExtraAttrEdit(`${this.extraAttrIndex}`);
+    this.extraAttrIndex++;
+  }
+
+  deleteExtraAttrRow(id: string): void {
+    this.extraAttrListOfData = this.extraAttrListOfData.filter(d => d.id !== id);
+    this.submitExtraAttr();
+  }
+
+  startExtraAttrEdit(id: string): void {
+    this.extraAttrEditCache[id].edit = true;
+  }
+
+  cancelExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    console.log(index);
+    this.extraAttrEditCache[id] = {
+      data: { ...this.extraAttrListOfData[index] },
+      edit: false
+    };
+  }
+
+  saveExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    Object.assign(this.extraAttrListOfData[index], this.extraAttrEditCache[id].data);
+    this.extraAttrEditCache[id].edit = false;
+    this.submitExtraAttr();
+  }
+
+  submitExtraAttr() {
+    let extraAttrString = JSON.stringify(this.extraAttrListOfData);
+    this.appsService.updateExtendAttr({ id: this.form.model.id, extendAttr: extraAttrString }).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(this.i18n.fanyi('mxk.alert.update.success'));
+      } else {
+        this.msg.error(this.i18n.fanyi('mxk.alert.update.error'));
+      }
+      this.cdr.detectChanges();
+    });
+  }
+
+  updateExtraAttrEditCache(): void {
+    this.extraAttrListOfData.forEach(item => {
+      this.extraAttrEditCache[item.id] = {
+        edit: false,
+        data: { ...item }
+      };
+    });
+  }
 }

+ 66 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-form-based-details-editer/app-form-based-details-editer.component.html

@@ -135,6 +135,30 @@
             </nz-form-control>
           </nz-form-item>
         </div>
+        <div nz-row>
+          <nz-form-item>
+            <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="status">{{ 'mxk.text.status' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid status!">
+              <nz-switch [(ngModel)]="form.model.switch_status" [ngModelOptions]="{ standalone: true }" name="status"
+                [nzCheckedChildren]="statuscheckedTemplate" [nzUnCheckedChildren]="statusunCheckedTemplate"></nz-switch>
+              <ng-template #statuscheckedTemplate><i nz-icon nzType="check"></i></ng-template>
+              <ng-template #statusunCheckedTemplate><i nz-icon nzType="close"></i></ng-template>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label [nzSm]="8" [nzXs]="24" nzFor="isExtendAttr">{{ 'mxk.apps.isExtendAttr' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="16" [nzMd]="16" [nzXs]="36" [nzXl]="48"
+              nzErrorTip="The input is not valid isExtendAttr!">
+              <nz-radio-group [(ngModel)]="form.model.isExtendAttr" [ngModelOptions]="{ standalone: true }"
+                nzButtonStyle="solid">
+                <label nz-radio-button nzValue="0">{{ 'mxk.text.no' | i18n }}</label>
+                <label nz-radio-button nzValue="1">{{ 'mxk.text.yes' | i18n }}</label>
+              </nz-radio-group>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
       </nz-tab>
       <nz-tab nzTitle="{{ 'mxk.apps.formbased.tab' | i18n }}">
         <div nz-row>
@@ -370,6 +394,48 @@
           </nz-form-item>
         </div>
       </nz-tab>
+      <nz-tab nzTitle="{{ 'mxk.apps.tab.custom' | i18n }}" *ngIf="isEdit && form.model.isExtendAttr === '1'">
+        <button nz-button (click)="addExtraAttrRow($event)" nzType="primary">{{ 'mxk.text.add' | i18n }}</button>
+        <nz-table #editRowTable nzBordered [nzData]="extraAttrListOfData" nzTableLayout="fixed">
+          <thead>
+            <tr>
+              <th nzWidth="25%">{{ 'mxk.custom.extraAttr.attr' | i18n }}</th>
+              <th nzWidth="15%">{{ 'mxk.custom.extraAttr.type' | i18n }}</th>
+              <th nzWidth="40%">{{ 'mxk.custom.extraAttr.value' | i18n }}</th>
+              <th>{{ 'mxk.text.action' | i18n }}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr *ngFor="let data of editRowTable.data">
+              <ng-container *ngIf="!extraAttrEditCache[data.id].edit; else editTemplate">
+                <td>{{ data.attr }}</td>
+                <td>{{ data.type }}</td>
+                <td>{{ data.value }}</td>
+                <td>
+                  <button nz-button type="button" (click)="startExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.edit' | i18n
+                    }}</button>
+                  <button nz-button type="button" (click)="deleteExtraAttrRow(data.id)" nzDanger>{{ 'mxk.text.delete' |
+                    i18n }}</button>
+                </td>
+              </ng-container>
+              <ng-template #editTemplate>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.attr"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.type"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.value"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td>
+                  <button nz-button type="button" (click)="saveExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.submit' | i18n
+                    }}</button>
+                </td>
+              </ng-template>
+            </tr>
+          </tbody>
+        </nz-table>
+      </nz-tab>
     </nz-tabset>
   </form>
 </div>

+ 88 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-form-based-details-editer/app-form-based-details-editer.component.ts

@@ -24,6 +24,7 @@ import { NzModalRef, NzModalService } from 'ng-zorro-antd/modal';
 import { NzUploadFile, NzUploadChangeParam } from 'ng-zorro-antd/upload';
 
 import { AppsFormBasedDetails } from '../../../entity/AppsFormBasedDetails';
+import { ExtraAttr } from '../../../entity/ExtraAttr';
 import { AppsFormBasedDetailsService } from '../../../service/apps-form-based-details.service';
 import { AppsService } from '../../../service/apps.service';
 import { SelectAdaptersComponent } from '../../config/adapters/select-adapters/select-adapters.component';
@@ -70,6 +71,10 @@ export class AppFormBasedDetailsEditerComponent implements OnInit {
   previewImage: string | ArrayBuffer | undefined | null = '';
   previewVisible = false;
 
+  extraAttrIndex: number = 1;
+  extraAttrEditCache: { [key: string]: { edit: boolean; data: ExtraAttr } } = {};
+  extraAttrListOfData: ExtraAttr[] = [];
+
   constructor(
     private modalRef: NzModalRef,
     private modalService: NzModalService,
@@ -95,6 +100,7 @@ export class AppFormBasedDetailsEditerComponent implements OnInit {
             url: this.previewImage
           }
         ];
+        this.initExtraAttr(res.data);
       });
     } else {
       this.appsFormBasedDetailsService.init().subscribe(res => {
@@ -173,4 +179,86 @@ export class AppFormBasedDetailsEditerComponent implements OnInit {
       this.cdr.detectChanges();
     });
   }
+
+  initExtraAttr(extraData: any): void {
+    if (extraData.extendAttr != null && extraData.extendAttr != '') {
+      let extraAttrDataArray = JSON.parse(extraData.extendAttr);
+      console.log(extraAttrDataArray);
+      const data = [];
+      while (this.extraAttrIndex <= extraAttrDataArray.length) {
+        let extraAttrData = extraAttrDataArray[this.extraAttrIndex - 1];
+        data.push({
+          id: `${this.extraAttrIndex}`,
+          attr: extraAttrData.attr,
+          type: extraAttrData.type,
+          value: extraAttrData.value
+        });
+        this.extraAttrIndex++;
+      }
+      this.extraAttrListOfData = data;
+      this.updateExtraAttrEditCache();
+    }
+  }
+
+  addExtraAttrRow(e: MouseEvent): void {
+    e.preventDefault();
+    this.extraAttrListOfData = [
+      ...this.extraAttrListOfData,
+      {
+        id: `${this.extraAttrIndex}`,
+        attr: `Attr ${this.extraAttrIndex}`,
+        type: 'string',
+        value: `value ${this.extraAttrIndex}`
+      }
+    ];
+    this.updateExtraAttrEditCache();
+    this.startExtraAttrEdit(`${this.extraAttrIndex}`);
+    this.extraAttrIndex++;
+  }
+
+  deleteExtraAttrRow(id: string): void {
+    this.extraAttrListOfData = this.extraAttrListOfData.filter(d => d.id !== id);
+    this.submitExtraAttr();
+  }
+
+  startExtraAttrEdit(id: string): void {
+    this.extraAttrEditCache[id].edit = true;
+  }
+
+  cancelExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    console.log(index);
+    this.extraAttrEditCache[id] = {
+      data: { ...this.extraAttrListOfData[index] },
+      edit: false
+    };
+  }
+
+  saveExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    Object.assign(this.extraAttrListOfData[index], this.extraAttrEditCache[id].data);
+    this.extraAttrEditCache[id].edit = false;
+    this.submitExtraAttr();
+  }
+
+  submitExtraAttr() {
+    let extraAttrString = JSON.stringify(this.extraAttrListOfData);
+    this.appsService.updateExtendAttr({ id: this.form.model.id, extendAttr: extraAttrString }).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(this.i18n.fanyi('mxk.alert.update.success'));
+      } else {
+        this.msg.error(this.i18n.fanyi('mxk.alert.update.error'));
+      }
+      this.cdr.detectChanges();
+    });
+  }
+
+  updateExtraAttrEditCache(): void {
+    this.extraAttrListOfData.forEach(item => {
+      this.extraAttrEditCache[item.id] = {
+        edit: false,
+        data: { ...item }
+      };
+    });
+  }
 }

+ 66 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-jwt-details-editer/app-jwt-details-editer.component.html

@@ -135,6 +135,30 @@
             </nz-form-control>
           </nz-form-item>
         </div>
+        <div nz-row>
+          <nz-form-item>
+            <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="status">{{ 'mxk.text.status' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid status!">
+              <nz-switch [(ngModel)]="form.model.switch_status" [ngModelOptions]="{ standalone: true }" name="status"
+                [nzCheckedChildren]="statuscheckedTemplate" [nzUnCheckedChildren]="statusunCheckedTemplate"></nz-switch>
+              <ng-template #statuscheckedTemplate><i nz-icon nzType="check"></i></ng-template>
+              <ng-template #statusunCheckedTemplate><i nz-icon nzType="close"></i></ng-template>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label [nzSm]="8" [nzXs]="24" nzFor="isExtendAttr">{{ 'mxk.apps.isExtendAttr' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="16" [nzMd]="16" [nzXs]="36" [nzXl]="48"
+              nzErrorTip="The input is not valid isExtendAttr!">
+              <nz-radio-group [(ngModel)]="form.model.isExtendAttr" [ngModelOptions]="{ standalone: true }"
+                nzButtonStyle="solid">
+                <label nz-radio-button nzValue="0">{{ 'mxk.text.no' | i18n }}</label>
+                <label nz-radio-button nzValue="1">{{ 'mxk.text.yes' | i18n }}</label>
+              </nz-radio-group>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
       </nz-tab>
       <nz-tab nzTitle="{{ 'mxk.apps.jwt.tab' | i18n }}">
         <div nz-row>
@@ -411,6 +435,48 @@
           </nz-form-item>
         </div>
       </nz-tab>
+      <nz-tab nzTitle="{{ 'mxk.apps.tab.custom' | i18n }}" *ngIf="isEdit && form.model.isExtendAttr === '1'">
+        <button nz-button (click)="addExtraAttrRow($event)" nzType="primary">{{ 'mxk.text.add' | i18n }}</button>
+        <nz-table #editRowTable nzBordered [nzData]="extraAttrListOfData" nzTableLayout="fixed">
+          <thead>
+            <tr>
+              <th nzWidth="25%">{{ 'mxk.custom.extraAttr.attr' | i18n }}</th>
+              <th nzWidth="15%">{{ 'mxk.custom.extraAttr.type' | i18n }}</th>
+              <th nzWidth="40%">{{ 'mxk.custom.extraAttr.value' | i18n }}</th>
+              <th>{{ 'mxk.text.action' | i18n }}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr *ngFor="let data of editRowTable.data">
+              <ng-container *ngIf="!extraAttrEditCache[data.id].edit; else editTemplate">
+                <td>{{ data.attr }}</td>
+                <td>{{ data.type }}</td>
+                <td>{{ data.value }}</td>
+                <td>
+                  <button nz-button type="button" (click)="startExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.edit' | i18n
+                    }}</button>
+                  <button nz-button type="button" (click)="deleteExtraAttrRow(data.id)" nzDanger>{{ 'mxk.text.delete' |
+                    i18n }}</button>
+                </td>
+              </ng-container>
+              <ng-template #editTemplate>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.attr"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.type"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.value"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td>
+                  <button nz-button type="button" (click)="saveExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.submit' | i18n
+                    }}</button>
+                </td>
+              </ng-template>
+            </tr>
+          </tbody>
+        </nz-table>
+      </nz-tab>
     </nz-tabset>
   </form>
 </div>

+ 88 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-jwt-details-editer/app-jwt-details-editer.component.ts

@@ -25,6 +25,7 @@ import { NzUploadFile, NzUploadChangeParam } from 'ng-zorro-antd/upload';
 import { AppsJwtDetails } from 'src/app/entity/AppsJwtDetails';
 
 import { Apps } from '../../../entity/Apps';
+import { ExtraAttr } from '../../../entity/ExtraAttr';
 import { AppsJwtDetailsService } from '../../../service/apps-jwt-details.service';
 import { AppsService } from '../../../service/apps.service';
 import { SelectAdaptersComponent } from '../../config/adapters/select-adapters/select-adapters.component';
@@ -71,6 +72,10 @@ export class AppJwtDetailsEditerComponent implements OnInit {
   previewImage: string | ArrayBuffer | undefined | null = '';
   previewVisible = false;
 
+  extraAttrIndex: number = 1;
+  extraAttrEditCache: { [key: string]: { edit: boolean; data: ExtraAttr } } = {};
+  extraAttrListOfData: ExtraAttr[] = [];
+
   constructor(
     private modalRef: NzModalRef,
     private appsJwtDetailsService: AppsJwtDetailsService,
@@ -96,6 +101,7 @@ export class AppJwtDetailsEditerComponent implements OnInit {
             url: this.previewImage
           }
         ];
+        this.initExtraAttr(res.data);
       });
     } else {
       this.appsJwtDetailsService.init().subscribe(res => {
@@ -183,4 +189,86 @@ export class AppJwtDetailsEditerComponent implements OnInit {
       this.cdr.detectChanges();
     });
   }
+
+  initExtraAttr(extraData: any): void {
+    if (extraData.extendAttr != null && extraData.extendAttr != '') {
+      let extraAttrDataArray = JSON.parse(extraData.extendAttr);
+      console.log(extraAttrDataArray);
+      const data = [];
+      while (this.extraAttrIndex <= extraAttrDataArray.length) {
+        let extraAttrData = extraAttrDataArray[this.extraAttrIndex - 1];
+        data.push({
+          id: `${this.extraAttrIndex}`,
+          attr: extraAttrData.attr,
+          type: extraAttrData.type,
+          value: extraAttrData.value
+        });
+        this.extraAttrIndex++;
+      }
+      this.extraAttrListOfData = data;
+      this.updateExtraAttrEditCache();
+    }
+  }
+
+  addExtraAttrRow(e: MouseEvent): void {
+    e.preventDefault();
+    this.extraAttrListOfData = [
+      ...this.extraAttrListOfData,
+      {
+        id: `${this.extraAttrIndex}`,
+        attr: `Attr ${this.extraAttrIndex}`,
+        type: 'string',
+        value: `value ${this.extraAttrIndex}`
+      }
+    ];
+    this.updateExtraAttrEditCache();
+    this.startExtraAttrEdit(`${this.extraAttrIndex}`);
+    this.extraAttrIndex++;
+  }
+
+  deleteExtraAttrRow(id: string): void {
+    this.extraAttrListOfData = this.extraAttrListOfData.filter(d => d.id !== id);
+    this.submitExtraAttr();
+  }
+
+  startExtraAttrEdit(id: string): void {
+    this.extraAttrEditCache[id].edit = true;
+  }
+
+  cancelExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    console.log(index);
+    this.extraAttrEditCache[id] = {
+      data: { ...this.extraAttrListOfData[index] },
+      edit: false
+    };
+  }
+
+  saveExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    Object.assign(this.extraAttrListOfData[index], this.extraAttrEditCache[id].data);
+    this.extraAttrEditCache[id].edit = false;
+    this.submitExtraAttr();
+  }
+
+  submitExtraAttr() {
+    let extraAttrString = JSON.stringify(this.extraAttrListOfData);
+    this.appsService.updateExtendAttr({ id: this.form.model.id, extendAttr: extraAttrString }).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(this.i18n.fanyi('mxk.alert.update.success'));
+      } else {
+        this.msg.error(this.i18n.fanyi('mxk.alert.update.error'));
+      }
+      this.cdr.detectChanges();
+    });
+  }
+
+  updateExtraAttrEditCache(): void {
+    this.extraAttrListOfData.forEach(item => {
+      this.extraAttrEditCache[item.id] = {
+        edit: false,
+        data: { ...item }
+      };
+    });
+  }
 }

+ 66 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-oauth20-details-editer/app-oauth20-details-editer.component.html

@@ -138,6 +138,30 @@
             </nz-form-control>
           </nz-form-item>
         </div>
+        <div nz-row>
+          <nz-form-item>
+            <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="status">{{ 'mxk.text.status' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid status!">
+              <nz-switch [(ngModel)]="form.model.switch_status" [ngModelOptions]="{ standalone: true }" name="status"
+                [nzCheckedChildren]="statuscheckedTemplate" [nzUnCheckedChildren]="statusunCheckedTemplate"></nz-switch>
+              <ng-template #statuscheckedTemplate><i nz-icon nzType="check"></i></ng-template>
+              <ng-template #statusunCheckedTemplate><i nz-icon nzType="close"></i></ng-template>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label [nzSm]="8" [nzXs]="24" nzFor="isExtendAttr">{{ 'mxk.apps.isExtendAttr' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="16" [nzMd]="16" [nzXs]="36" [nzXl]="48"
+              nzErrorTip="The input is not valid isExtendAttr!">
+              <nz-radio-group [(ngModel)]="form.model.isExtendAttr" [ngModelOptions]="{ standalone: true }"
+                nzButtonStyle="solid">
+                <label nz-radio-button nzValue="0">{{ 'mxk.text.no' | i18n }}</label>
+                <label nz-radio-button nzValue="1">{{ 'mxk.text.yes' | i18n }}</label>
+              </nz-radio-group>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
       </nz-tab>
       <nz-tab nzTitle="{{ 'mxk.apps.oauth.v2.0.tab' | i18n }}">
         <div nz-row>
@@ -490,6 +514,48 @@
           </nz-form-item>
         </div>
       </nz-tab>
+      <nz-tab nzTitle="{{ 'mxk.apps.tab.custom' | i18n }}" *ngIf="isEdit && form.model.isExtendAttr === '1'">
+        <button nz-button (click)="addExtraAttrRow($event)" nzType="primary">{{ 'mxk.text.add' | i18n }}</button>
+        <nz-table #editRowTable nzBordered [nzData]="extraAttrListOfData" nzTableLayout="fixed">
+          <thead>
+            <tr>
+              <th nzWidth="25%">{{ 'mxk.custom.extraAttr.attr' | i18n }}</th>
+              <th nzWidth="15%">{{ 'mxk.custom.extraAttr.type' | i18n }}</th>
+              <th nzWidth="40%">{{ 'mxk.custom.extraAttr.value' | i18n }}</th>
+              <th>{{ 'mxk.text.action' | i18n }}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr *ngFor="let data of editRowTable.data">
+              <ng-container *ngIf="!extraAttrEditCache[data.id].edit; else editTemplate">
+                <td>{{ data.attr }}</td>
+                <td>{{ data.type }}</td>
+                <td>{{ data.value }}</td>
+                <td>
+                  <button nz-button type="button" (click)="startExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.edit' | i18n
+                    }}</button>
+                  <button nz-button type="button" (click)="deleteExtraAttrRow(data.id)" nzDanger>{{ 'mxk.text.delete' |
+                    i18n }}</button>
+                </td>
+              </ng-container>
+              <ng-template #editTemplate>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.attr"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.type"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.value"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td>
+                  <button nz-button type="button" (click)="saveExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.submit' | i18n
+                    }}</button>
+                </td>
+              </ng-template>
+            </tr>
+          </tbody>
+        </nz-table>
+      </nz-tab>
     </nz-tabset>
   </form>
 </div>

+ 88 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-oauth20-details-editer/app-oauth20-details-editer.component.ts

@@ -24,6 +24,7 @@ import { NzModalRef, NzModalService } from 'ng-zorro-antd/modal';
 import { NzUploadFile, NzUploadChangeParam } from 'ng-zorro-antd/upload';
 
 import { AppsOauth20Details } from '../../../entity/AppsOauth20Details';
+import { ExtraAttr } from '../../../entity/ExtraAttr';
 import { AppsOauth20DetailsService } from '../../../service/apps-oauth20-details.service';
 import { AppsService } from '../../../service/apps.service';
 import { SelectAdaptersComponent } from '../../config/adapters/select-adapters/select-adapters.component';
@@ -70,6 +71,10 @@ export class AppOauth20DetailsEditerComponent implements OnInit {
   previewImage: string | ArrayBuffer | undefined | null = '';
   previewVisible = false;
 
+  extraAttrIndex: number = 1;
+  extraAttrEditCache: { [key: string]: { edit: boolean; data: ExtraAttr } } = {};
+  extraAttrListOfData: ExtraAttr[] = [];
+
   constructor(
     private modal: NzModalRef,
     private modalService: NzModalService,
@@ -96,6 +101,7 @@ export class AppOauth20DetailsEditerComponent implements OnInit {
             url: this.previewImage
           }
         ];
+        this.initExtraAttr(res.data);
       });
     } else {
       this.appsOauth20DetailsService.init().subscribe(res => {
@@ -193,4 +199,86 @@ export class AppOauth20DetailsEditerComponent implements OnInit {
       }
     );
   }
+
+  initExtraAttr(extraData: any): void {
+    if (extraData.extendAttr != null && extraData.extendAttr != '') {
+      let extraAttrDataArray = JSON.parse(extraData.extendAttr);
+      console.log(extraAttrDataArray);
+      const data = [];
+      while (this.extraAttrIndex <= extraAttrDataArray.length) {
+        let extraAttrData = extraAttrDataArray[this.extraAttrIndex - 1];
+        data.push({
+          id: `${this.extraAttrIndex}`,
+          attr: extraAttrData.attr,
+          type: extraAttrData.type,
+          value: extraAttrData.value
+        });
+        this.extraAttrIndex++;
+      }
+      this.extraAttrListOfData = data;
+      this.updateExtraAttrEditCache();
+    }
+  }
+
+  addExtraAttrRow(e: MouseEvent): void {
+    e.preventDefault();
+    this.extraAttrListOfData = [
+      ...this.extraAttrListOfData,
+      {
+        id: `${this.extraAttrIndex}`,
+        attr: `Attr ${this.extraAttrIndex}`,
+        type: 'string',
+        value: `value ${this.extraAttrIndex}`
+      }
+    ];
+    this.updateExtraAttrEditCache();
+    this.startExtraAttrEdit(`${this.extraAttrIndex}`);
+    this.extraAttrIndex++;
+  }
+
+  deleteExtraAttrRow(id: string): void {
+    this.extraAttrListOfData = this.extraAttrListOfData.filter(d => d.id !== id);
+    this.submitExtraAttr();
+  }
+
+  startExtraAttrEdit(id: string): void {
+    this.extraAttrEditCache[id].edit = true;
+  }
+
+  cancelExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    console.log(index);
+    this.extraAttrEditCache[id] = {
+      data: { ...this.extraAttrListOfData[index] },
+      edit: false
+    };
+  }
+
+  saveExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    Object.assign(this.extraAttrListOfData[index], this.extraAttrEditCache[id].data);
+    this.extraAttrEditCache[id].edit = false;
+    this.submitExtraAttr();
+  }
+
+  submitExtraAttr() {
+    let extraAttrString = JSON.stringify(this.extraAttrListOfData);
+    this.appsService.updateExtendAttr({ id: this.form.model.id, extendAttr: extraAttrString }).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(this.i18n.fanyi('mxk.alert.update.success'));
+      } else {
+        this.msg.error(this.i18n.fanyi('mxk.alert.update.error'));
+      }
+      this.cdr.detectChanges();
+    });
+  }
+
+  updateExtraAttrEditCache(): void {
+    this.extraAttrListOfData.forEach(item => {
+      this.extraAttrEditCache[item.id] = {
+        edit: false,
+        data: { ...item }
+      };
+    });
+  }
 }

+ 66 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-saml20-details-editer/app-saml20-details-editer.component.html

@@ -129,6 +129,30 @@
             </nz-form-control>
           </nz-form-item>
         </div>
+        <div nz-row>
+          <nz-form-item>
+            <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="status">{{ 'mxk.text.status' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid status!">
+              <nz-switch [(ngModel)]="form.model.switch_status" [ngModelOptions]="{ standalone: true }" name="status"
+                [nzCheckedChildren]="statuscheckedTemplate" [nzUnCheckedChildren]="statusunCheckedTemplate"></nz-switch>
+              <ng-template #statuscheckedTemplate><i nz-icon nzType="check"></i></ng-template>
+              <ng-template #statusunCheckedTemplate><i nz-icon nzType="close"></i></ng-template>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label [nzSm]="8" [nzXs]="24" nzFor="isExtendAttr">{{ 'mxk.apps.isExtendAttr' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="16" [nzMd]="16" [nzXs]="36" [nzXl]="48"
+              nzErrorTip="The input is not valid isExtendAttr!">
+              <nz-radio-group [(ngModel)]="form.model.isExtendAttr" [ngModelOptions]="{ standalone: true }"
+                nzButtonStyle="solid">
+                <label nz-radio-button nzValue="0">{{ 'mxk.text.no' | i18n }}</label>
+                <label nz-radio-button nzValue="1">{{ 'mxk.text.yes' | i18n }}</label>
+              </nz-radio-group>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
       </nz-tab>
       <nz-tab nzTitle="{{ 'mxk.apps.saml.tab' | i18n }}">
         <div nz-row>
@@ -473,6 +497,48 @@
           </nz-form-item>
         </div>
       </nz-tab>
+      <nz-tab nzTitle="{{ 'mxk.apps.tab.custom' | i18n }}" *ngIf="isEdit && form.model.isExtendAttr === '1'">
+        <button nz-button (click)="addExtraAttrRow($event)" nzType="primary">{{ 'mxk.text.add' | i18n }}</button>
+        <nz-table #editRowTable nzBordered [nzData]="extraAttrListOfData" nzTableLayout="fixed">
+          <thead>
+            <tr>
+              <th nzWidth="25%">{{ 'mxk.custom.extraAttr.attr' | i18n }}</th>
+              <th nzWidth="15%">{{ 'mxk.custom.extraAttr.type' | i18n }}</th>
+              <th nzWidth="40%">{{ 'mxk.custom.extraAttr.value' | i18n }}</th>
+              <th>{{ 'mxk.text.action' | i18n }}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr *ngFor="let data of editRowTable.data">
+              <ng-container *ngIf="!extraAttrEditCache[data.id].edit; else editTemplate">
+                <td>{{ data.attr }}</td>
+                <td>{{ data.type }}</td>
+                <td>{{ data.value }}</td>
+                <td>
+                  <button nz-button type="button" (click)="startExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.edit' | i18n
+                    }}</button>
+                  <button nz-button type="button" (click)="deleteExtraAttrRow(data.id)" nzDanger>{{ 'mxk.text.delete' |
+                    i18n }}</button>
+                </td>
+              </ng-container>
+              <ng-template #editTemplate>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.attr"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.type"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.value"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td>
+                  <button nz-button type="button" (click)="saveExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.submit' | i18n
+                    }}</button>
+                </td>
+              </ng-template>
+            </tr>
+          </tbody>
+        </nz-table>
+      </nz-tab>
     </nz-tabset>
   </form>
 </div>

+ 88 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-saml20-details-editer/app-saml20-details-editer.component.ts

@@ -24,6 +24,7 @@ import { NzModalRef, NzModalService } from 'ng-zorro-antd/modal';
 import { NzUploadFile, NzUploadChangeParam } from 'ng-zorro-antd/upload';
 
 import { AppsSamlDetails } from '../../../entity/AppsSamlDetails';
+import { ExtraAttr } from '../../../entity/ExtraAttr';
 import { AppsSamlDetailsService } from '../../../service/apps-saml-details.service';
 import { AppsService } from '../../../service/apps.service';
 import { SelectAdaptersComponent } from '../../config/adapters/select-adapters/select-adapters.component';
@@ -70,6 +71,10 @@ export class AppSaml20DetailsEditerComponent implements OnInit {
   previewImage: string | ArrayBuffer | undefined | null = '';
   previewVisible = false;
 
+  extraAttrIndex: number = 1;
+  extraAttrEditCache: { [key: string]: { edit: boolean; data: ExtraAttr } } = {};
+  extraAttrListOfData: ExtraAttr[] = [];
+
   constructor(
     private modal: NzModalRef,
     private modalService: NzModalService,
@@ -95,6 +100,7 @@ export class AppSaml20DetailsEditerComponent implements OnInit {
             url: this.previewImage
           }
         ];
+        this.initExtraAttr(res.data);
       });
     } else {
       this.appsSamlDetailsService.init().subscribe(res => {
@@ -183,4 +189,86 @@ export class AppSaml20DetailsEditerComponent implements OnInit {
       }
     );
   }
+
+  initExtraAttr(extraData: any): void {
+    if (extraData.extendAttr != null && extraData.extendAttr != '') {
+      let extraAttrDataArray = JSON.parse(extraData.extendAttr);
+      console.log(extraAttrDataArray);
+      const data = [];
+      while (this.extraAttrIndex <= extraAttrDataArray.length) {
+        let extraAttrData = extraAttrDataArray[this.extraAttrIndex - 1];
+        data.push({
+          id: `${this.extraAttrIndex}`,
+          attr: extraAttrData.attr,
+          type: extraAttrData.type,
+          value: extraAttrData.value
+        });
+        this.extraAttrIndex++;
+      }
+      this.extraAttrListOfData = data;
+      this.updateExtraAttrEditCache();
+    }
+  }
+
+  addExtraAttrRow(e: MouseEvent): void {
+    e.preventDefault();
+    this.extraAttrListOfData = [
+      ...this.extraAttrListOfData,
+      {
+        id: `${this.extraAttrIndex}`,
+        attr: `Attr ${this.extraAttrIndex}`,
+        type: 'string',
+        value: `value ${this.extraAttrIndex}`
+      }
+    ];
+    this.updateExtraAttrEditCache();
+    this.startExtraAttrEdit(`${this.extraAttrIndex}`);
+    this.extraAttrIndex++;
+  }
+
+  deleteExtraAttrRow(id: string): void {
+    this.extraAttrListOfData = this.extraAttrListOfData.filter(d => d.id !== id);
+    this.submitExtraAttr();
+  }
+
+  startExtraAttrEdit(id: string): void {
+    this.extraAttrEditCache[id].edit = true;
+  }
+
+  cancelExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    console.log(index);
+    this.extraAttrEditCache[id] = {
+      data: { ...this.extraAttrListOfData[index] },
+      edit: false
+    };
+  }
+
+  saveExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    Object.assign(this.extraAttrListOfData[index], this.extraAttrEditCache[id].data);
+    this.extraAttrEditCache[id].edit = false;
+    this.submitExtraAttr();
+  }
+
+  submitExtraAttr() {
+    let extraAttrString = JSON.stringify(this.extraAttrListOfData);
+    this.appsService.updateExtendAttr({ id: this.form.model.id, extendAttr: extraAttrString }).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(this.i18n.fanyi('mxk.alert.update.success'));
+      } else {
+        this.msg.error(this.i18n.fanyi('mxk.alert.update.error'));
+      }
+      this.cdr.detectChanges();
+    });
+  }
+
+  updateExtraAttrEditCache(): void {
+    this.extraAttrListOfData.forEach(item => {
+      this.extraAttrEditCache[item.id] = {
+        edit: false,
+        data: { ...item }
+      };
+    });
+  }
 }

+ 66 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-token-based-details-editer/app-token-based-details-editer.component.html

@@ -135,6 +135,30 @@
             </nz-form-control>
           </nz-form-item>
         </div>
+        <div nz-row>
+          <nz-form-item>
+            <nz-form-label [nzSm]="6" [nzXs]="24" nzRequired nzFor="status">{{ 'mxk.text.status' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="14" [nzXs]="24" nzErrorTip="The input is not valid status!">
+              <nz-switch [(ngModel)]="form.model.switch_status" [ngModelOptions]="{ standalone: true }" name="status"
+                [nzCheckedChildren]="statuscheckedTemplate" [nzUnCheckedChildren]="statusunCheckedTemplate"></nz-switch>
+              <ng-template #statuscheckedTemplate><i nz-icon nzType="check"></i></ng-template>
+              <ng-template #statusunCheckedTemplate><i nz-icon nzType="close"></i></ng-template>
+            </nz-form-control>
+          </nz-form-item>
+          <nz-form-item>
+            <nz-form-label [nzSm]="8" [nzXs]="24" nzFor="isExtendAttr">{{ 'mxk.apps.isExtendAttr' | i18n }}
+            </nz-form-label>
+            <nz-form-control [nzSm]="16" [nzMd]="16" [nzXs]="36" [nzXl]="48"
+              nzErrorTip="The input is not valid isExtendAttr!">
+              <nz-radio-group [(ngModel)]="form.model.isExtendAttr" [ngModelOptions]="{ standalone: true }"
+                nzButtonStyle="solid">
+                <label nz-radio-button nzValue="0">{{ 'mxk.text.no' | i18n }}</label>
+                <label nz-radio-button nzValue="1">{{ 'mxk.text.yes' | i18n }}</label>
+              </nz-radio-group>
+            </nz-form-control>
+          </nz-form-item>
+        </div>
       </nz-tab>
       <nz-tab nzTitle="{{ 'mxk.apps.tokenbased.tab' | i18n }}">
         <div nz-row>
@@ -338,6 +362,48 @@
           </nz-form-item>
         </div>
       </nz-tab>
+      <nz-tab nzTitle="{{ 'mxk.apps.tab.custom' | i18n }}" *ngIf="isEdit && form.model.isExtendAttr === '1'">
+        <button nz-button (click)="addExtraAttrRow($event)" nzType="primary">{{ 'mxk.text.add' | i18n }}</button>
+        <nz-table #editRowTable nzBordered [nzData]="extraAttrListOfData" nzTableLayout="fixed">
+          <thead>
+            <tr>
+              <th nzWidth="25%">{{ 'mxk.custom.extraAttr.attr' | i18n }}</th>
+              <th nzWidth="15%">{{ 'mxk.custom.extraAttr.type' | i18n }}</th>
+              <th nzWidth="40%">{{ 'mxk.custom.extraAttr.value' | i18n }}</th>
+              <th>{{ 'mxk.text.action' | i18n }}</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr *ngFor="let data of editRowTable.data">
+              <ng-container *ngIf="!extraAttrEditCache[data.id].edit; else editTemplate">
+                <td>{{ data.attr }}</td>
+                <td>{{ data.type }}</td>
+                <td>{{ data.value }}</td>
+                <td>
+                  <button nz-button type="button" (click)="startExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.edit' | i18n
+                    }}</button>
+                  <button nz-button type="button" (click)="deleteExtraAttrRow(data.id)" nzDanger>{{ 'mxk.text.delete' |
+                    i18n }}</button>
+                </td>
+              </ng-container>
+              <ng-template #editTemplate>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.attr"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.type"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td><input type="text" nz-input [(ngModel)]="extraAttrEditCache[data.id].data.value"
+                    [ngModelOptions]="{ standalone: true }" /></td>
+                <td>
+                  <button nz-button type="button" (click)="saveExtraAttrEdit(data.id)" style="float: left">{{
+                    'mxk.text.submit' | i18n
+                    }}</button>
+                </td>
+              </ng-template>
+            </tr>
+          </tbody>
+        </nz-table>
+      </nz-tab>
     </nz-tabset>
   </form>
 </div>

+ 88 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/app/routes/apps/app-token-based-details-editer/app-token-based-details-editer.component.ts

@@ -25,6 +25,7 @@ import { NzUploadFile, NzUploadChangeParam } from 'ng-zorro-antd/upload';
 
 import { Apps } from '../../../entity/Apps';
 import { AppsTokenBasedDetails } from '../../../entity/AppsTokenBasedDetails';
+import { ExtraAttr } from '../../../entity/ExtraAttr';
 import { AppsTokenBasedDetailsService } from '../../../service/apps-token-based-details.service';
 import { AppsService } from '../../../service/apps.service';
 import { SelectAdaptersComponent } from '../../config/adapters/select-adapters/select-adapters.component';
@@ -71,6 +72,10 @@ export class AppTokenBasedDetailsEditerComponent implements OnInit {
   previewImage: string | ArrayBuffer | undefined | null = '';
   previewVisible = false;
 
+  extraAttrIndex: number = 1;
+  extraAttrEditCache: { [key: string]: { edit: boolean; data: ExtraAttr } } = {};
+  extraAttrListOfData: ExtraAttr[] = [];
+
   constructor(
     private modalRef: NzModalRef,
     private modalService: NzModalService,
@@ -96,6 +101,7 @@ export class AppTokenBasedDetailsEditerComponent implements OnInit {
             url: this.previewImage
           }
         ];
+        this.initExtraAttr(res.data);
       });
     } else {
       this.appsTokenBasedDetailsService.init().subscribe(res => {
@@ -182,4 +188,86 @@ export class AppTokenBasedDetailsEditerComponent implements OnInit {
       this.cdr.detectChanges();
     });
   }
+
+  initExtraAttr(extraData: any): void {
+    if (extraData.extendAttr != null && extraData.extendAttr != '') {
+      let extraAttrDataArray = JSON.parse(extraData.extendAttr);
+      console.log(extraAttrDataArray);
+      const data = [];
+      while (this.extraAttrIndex <= extraAttrDataArray.length) {
+        let extraAttrData = extraAttrDataArray[this.extraAttrIndex - 1];
+        data.push({
+          id: `${this.extraAttrIndex}`,
+          attr: extraAttrData.attr,
+          type: extraAttrData.type,
+          value: extraAttrData.value
+        });
+        this.extraAttrIndex++;
+      }
+      this.extraAttrListOfData = data;
+      this.updateExtraAttrEditCache();
+    }
+  }
+
+  addExtraAttrRow(e: MouseEvent): void {
+    e.preventDefault();
+    this.extraAttrListOfData = [
+      ...this.extraAttrListOfData,
+      {
+        id: `${this.extraAttrIndex}`,
+        attr: `Attr ${this.extraAttrIndex}`,
+        type: 'string',
+        value: `value ${this.extraAttrIndex}`
+      }
+    ];
+    this.updateExtraAttrEditCache();
+    this.startExtraAttrEdit(`${this.extraAttrIndex}`);
+    this.extraAttrIndex++;
+  }
+
+  deleteExtraAttrRow(id: string): void {
+    this.extraAttrListOfData = this.extraAttrListOfData.filter(d => d.id !== id);
+    this.submitExtraAttr();
+  }
+
+  startExtraAttrEdit(id: string): void {
+    this.extraAttrEditCache[id].edit = true;
+  }
+
+  cancelExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    console.log(index);
+    this.extraAttrEditCache[id] = {
+      data: { ...this.extraAttrListOfData[index] },
+      edit: false
+    };
+  }
+
+  saveExtraAttrEdit(id: string): void {
+    const index = this.extraAttrListOfData.findIndex(item => item.id === id);
+    Object.assign(this.extraAttrListOfData[index], this.extraAttrEditCache[id].data);
+    this.extraAttrEditCache[id].edit = false;
+    this.submitExtraAttr();
+  }
+
+  submitExtraAttr() {
+    let extraAttrString = JSON.stringify(this.extraAttrListOfData);
+    this.appsService.updateExtendAttr({ id: this.form.model.id, extendAttr: extraAttrString }).subscribe(res => {
+      if (res.code == 0) {
+        this.msg.success(this.i18n.fanyi('mxk.alert.update.success'));
+      } else {
+        this.msg.error(this.i18n.fanyi('mxk.alert.update.error'));
+      }
+      this.cdr.detectChanges();
+    });
+  }
+
+  updateExtraAttrEditCache(): void {
+    this.extraAttrListOfData.forEach(item => {
+      this.extraAttrEditCache[item.id] = {
+        edit: false,
+        data: { ...item }
+      };
+    });
+  }
 }

+ 8 - 4
maxkey-web-frontend/maxkey-web-mgt-app/src/app/service/apps.service.ts

@@ -1,19 +1,18 @@
 /*
  * Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *     http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
- 
 
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
@@ -22,6 +21,7 @@ import { Observable } from 'rxjs';
 
 import { Apps } from '../entity/Apps';
 import { Message } from '../entity/Message';
+import { PageResults } from '../entity/PageResults';
 import { BaseService } from './base.service';
 
 @Injectable({
@@ -43,4 +43,8 @@ export class AppsService extends BaseService<Apps> {
   generateKeys(id: String, type: String): Observable<Message<Apps>> {
     return this.getByParams({}, `/apps/generate/secret/${type}?id=${id}`);
   }
+
+  updateExtendAttr(params: NzSafeAny): Observable<Message<PageResults>> {
+    return this.http.post<Message<PageResults>>(`${this.server.urls.base}/updateExtendAttr`, params);
+  }
 }

+ 8 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/assets/i18n/en-US.json

@@ -265,6 +265,7 @@
 		"apps": {
 			"tab.basic": "Basic",
 			"tab.extra": "Extra",
+			"tab.custom": "Custom",
 			"extendapi.tab": "API",
 			"resources":"Resources",
 			"id": "App Id",
@@ -453,6 +454,13 @@
 			"oauth.connect.userInfoResponse": "UserInfoResponse"
 
 		},
+		"custom":{
+			"extraAttr":{
+				"attr":"Attribute",
+				"type":"Type",
+				"value":"Value"
+			}
+		},
 		"roles": {
 			"name": "Role Name",
 			"type": {

+ 8 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/assets/i18n/zh-CN.json

@@ -266,6 +266,7 @@
 		"apps": {
 			"tab.basic": "基本信息",
 			"tab.extra": "扩展信息",
+			"tab.custom": "自定义属性",
 			"extendapi.tab": "API配置",
 			"resources":"资源",
 			"id": "应用编码",
@@ -451,6 +452,13 @@
 			"oauth.connect.issuer": "签发人(Issuer)",
 			"oauth.connect.userInfoResponse": "用户接口类型"
 		},
+		"custom":{
+			"extraAttr":{
+				"attr":"属性",
+				"type":"类型",
+				"value":"值"
+			}
+		},
 		"roles": {
 			"name": "角色名称",
 			"type": {

+ 8 - 0
maxkey-web-frontend/maxkey-web-mgt-app/src/assets/i18n/zh-TW.json

@@ -267,6 +267,7 @@
 		"apps": {
 			"tab.basic": "基本信息",
 			"tab.extra": "擴展信息",
+			"tab.custom": "自訂屬性",
 			"extendapi.tab": "API配置",
 			"resources":"資源",
 			"id": "應用編碼",
@@ -452,6 +453,13 @@
 			"oauth.connect.issuer": "簽發人(Issuer)",
 			"oauth.connect.userInfoResponse": "用戶接口類型"
 		},
+		"custom":{
+			"extraAttr":{
+				"attr":"屬性",
+				"type":"類型",
+				"value":"值"
+			}
+		},
 		"roles": {
 			"name": "角色名稱",
 			"type": {

+ 3 - 22
maxkey-webs/maxkey-web-mgt/src/main/java/org/maxkey/web/apps/contorller/ApplicationsController.java

@@ -22,8 +22,6 @@ import org.apache.mybatis.jpa.persistence.JpaPageResults;
 import org.maxkey.authn.annotation.CurrentUser;
 import org.maxkey.constants.ConstsProtocols;
 import org.maxkey.crypto.ReciprocalUtils;
-import org.maxkey.entity.ExtraAttr;
-import org.maxkey.entity.ExtraAttrs;
 import org.maxkey.entity.Message;
 import org.maxkey.entity.UserInfo;
 import org.maxkey.entity.apps.Apps;
@@ -134,28 +132,11 @@ public class ApplicationsController extends BaseAppContorller {
 		}
 	}
 	
-	
-	@RequestMapping(value = { "/forwardAppsExtendAttr/{id}" })
-	public ResponseEntity<?> forwardExtendAttr(@PathVariable("id") String id) {
-		Apps apps = appsService.get(id);
-		return new Message<Apps>(apps).buildResponse();
-	}
-	
 	@ResponseBody
 	@RequestMapping(value = { "/updateExtendAttr" })
-	public ResponseEntity<?> updateExtendAttr(@ModelAttribute("application") Apps application,@ModelAttribute("extraAttrs") ExtraAttr extraAttr) {
-		if(extraAttr.getAttr()!=null){
-			String []attributes=extraAttr.getAttr().split(",");
-			String []attributeType=extraAttr.getType().split(",");
-			String []attributeValue=extraAttr.getValue().split(",");
-			ExtraAttrs extraAttrs=new ExtraAttrs();
-			for(int i=0;i<attributes.length;i++){
-				extraAttrs.put(attributes[i],attributeType[i], attributeValue[i]);
-			}
-			application.setExtendAttr(extraAttrs.toJsonString());
-		}
-		
-		if (appsService.updateExtendAttr(application)) {
+	public ResponseEntity<?> updateExtendAttr(@RequestBody Apps app) {
+		_logger.debug("-updateExtendAttr  id : {} , ExtendAttr : {}" , app.getId(),app.getExtendAttr());
+		if (appsService.updateExtendAttr(app)) {
 			return new Message<Apps>(Message.SUCCESS).buildResponse();
 		} else {
 			return new Message<Apps>(Message.FAIL).buildResponse();