Repository: bestaone/HiAuth
Branch: master
Commit: dba8e2850dee
Files: 2040
Total size: 6.2 MB
Directory structure:
gitextract_2zakr5hl/
├── .github/
│ └── workflows/
│ └── deploy.yml
├── .gitignore
├── LICENSE
├── README.md
├── cicd/
│ ├── Dockerfile
│ ├── Jenkinsfile
│ ├── hiauth.properties
│ └── nginx.conf
├── docs/
│ ├── .postcssrc.json
│ ├── .vitepress/
│ │ ├── config/
│ │ │ ├── en.ts
│ │ │ ├── index.ts
│ │ │ ├── shared.ts
│ │ │ └── zh.ts
│ │ └── theme/
│ │ ├── index.ts
│ │ └── styles.css
│ ├── en/
│ │ ├── guide/
│ │ │ ├── about-topic.md
│ │ │ ├── backend.md
│ │ │ ├── docker.md
│ │ │ ├── frontend.md
│ │ │ ├── hiauth-client.md
│ │ │ ├── issue.md
│ │ │ ├── k8s.md
│ │ │ ├── quick-start.md
│ │ │ ├── saas.md
│ │ │ ├── sourcecode.md
│ │ │ ├── test.md
│ │ │ └── what-is-hiauth.md
│ │ └── index.md
│ ├── lunaria.config.json
│ ├── package.json
│ ├── public/
│ │ ├── hiauth-logo-large.psd
│ │ └── pure.html
│ └── zh/
│ ├── guide/
│ │ ├── about-topic.md
│ │ ├── backend.md
│ │ ├── docker.md
│ │ ├── frontend.md
│ │ ├── hiauth-client.md
│ │ ├── issue.md
│ │ ├── k8s.md
│ │ ├── quick-start.md
│ │ ├── saas.md
│ │ ├── sourcecode.md
│ │ ├── test.md
│ │ └── what-is-hiauth.md
│ └── index.md
├── example/
│ ├── demo/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── demo/
│ │ │ ├── DemoApplication.java
│ │ │ └── IndexController.java
│ │ └── resources/
│ │ └── application.yml
│ ├── hiauth-client/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── hiauthclient/
│ │ │ ├── HiauthClientStarter.java
│ │ │ ├── config/
│ │ │ │ └── WebMvcConfig.java
│ │ │ └── controller/
│ │ │ ├── ApiController.java
│ │ │ └── IndexController.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── logback.xml
│ │ ├── static/
│ │ │ └── css/
│ │ │ └── index.css
│ │ └── templates/
│ │ ├── demo.html
│ │ ├── index.html
│ │ └── profile.html
│ ├── hiauth-client-exp/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── client/
│ │ │ ├── ClientStarter.java
│ │ │ ├── config/
│ │ │ │ ├── BeanConfig.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── WebMvcConfig.java
│ │ │ └── controller/
│ │ │ └── ClientController.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── logback.xml
│ │ └── templates/
│ │ └── index.html
│ ├── hiauth-server-exp/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── server/
│ │ │ ├── ServerStarter.java
│ │ │ ├── config/
│ │ │ │ ├── AuthServerConfig.java
│ │ │ │ ├── BeanConfig.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── WebMvcConfig.java
│ │ │ ├── controller/
│ │ │ │ ├── IndexController.java
│ │ │ │ └── LoginController.java
│ │ │ ├── federation/
│ │ │ │ ├── FederatedIdentityAuthenticationSuccessHandler.java
│ │ │ │ └── FederatedIdentityIdTokenCustomizer.java
│ │ │ ├── mapper/
│ │ │ │ └── SimpleJdbcRegisteredClientRepository.java
│ │ │ └── utils/
│ │ │ ├── jose/
│ │ │ │ ├── Jwks.java
│ │ │ │ └── KeyGeneratorUtils.java
│ │ │ └── package-info.java
│ │ └── resources/
│ │ ├── application-hiauth.yml
│ │ ├── application-redis.yml
│ │ ├── application.yml
│ │ ├── logback.xml
│ │ └── templates/
│ │ └── index.html
│ ├── himall/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── himall/
│ │ │ ├── HiMallStarter.java
│ │ │ ├── config/
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ └── WebMvcConfig.java
│ │ │ └── controller/
│ │ │ ├── AuthController.java
│ │ │ └── IndexController.java
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── logback.xml
│ │ ├── static/
│ │ │ └── css/
│ │ │ └── index.css
│ │ └── templates/
│ │ ├── demo.html
│ │ ├── index.html
│ │ └── profile.html
│ ├── resource/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── resource/
│ │ │ ├── ResourceStarter.java
│ │ │ ├── config/
│ │ │ │ ├── ResourceServerConfig.java
│ │ │ │ └── auth/
│ │ │ │ ├── SimpleAccessDeniedHandler.java
│ │ │ │ └── SimpleAuthenticationEntryPoint.java
│ │ │ ├── controller/
│ │ │ │ ├── IndexController.java
│ │ │ │ ├── ProfileController.java
│ │ │ │ ├── UnpapiController.java
│ │ │ │ └── UserController.java
│ │ │ └── utils/
│ │ │ └── ResponseTools.java
│ │ └── resources/
│ │ ├── application.yml
│ │ └── logback.xml
│ ├── spring-cloud/
│ │ ├── README.md
│ │ ├── gateway/
│ │ │ ├── pom.xml
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── java/
│ │ │ │ └── cn/
│ │ │ │ └── hiauth/
│ │ │ │ └── gateway/
│ │ │ │ ├── ApiController.java
│ │ │ │ ├── GatewayStarter.java
│ │ │ │ ├── IndexController.java
│ │ │ │ └── SecurityConfig.java
│ │ │ └── resources/
│ │ │ ├── application.yml
│ │ │ └── logback.xml
│ │ ├── ordersvc/
│ │ │ ├── pom.xml
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── java/
│ │ │ │ └── cn/
│ │ │ │ └── hiauth/
│ │ │ │ └── gateway/
│ │ │ │ ├── IndexController.java
│ │ │ │ └── OrderStarter.java
│ │ │ └── resources/
│ │ │ ├── application.yml
│ │ │ └── logback.xml
│ │ └── pom.xml
│ ├── spring-cloud-with-hiauth-client/
│ │ ├── README.md
│ │ ├── gateway1/
│ │ │ ├── pom.xml
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── java/
│ │ │ │ └── cn/
│ │ │ │ └── hiauth/
│ │ │ │ └── gateway/
│ │ │ │ ├── ApiController.java
│ │ │ │ ├── GatewayStarter.java
│ │ │ │ └── IndexController.java
│ │ │ └── resources/
│ │ │ ├── application.yml
│ │ │ └── logback.xml
│ │ ├── ordersvc1/
│ │ │ ├── pom.xml
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── java/
│ │ │ │ └── cn/
│ │ │ │ └── hiauth/
│ │ │ │ └── gateway/
│ │ │ │ ├── IndexController.java
│ │ │ │ └── OrderStarter.java
│ │ │ └── resources/
│ │ │ ├── application.yml
│ │ │ └── logback.xml
│ │ └── pom.xml
│ └── wechat-login/
│ ├── pom.xml
│ └── src/
│ └── main/
│ ├── java/
│ │ └── cn/
│ │ └── hiauth/
│ │ └── wechatlogin/
│ │ ├── WechatLoginStarter.java
│ │ ├── config/
│ │ │ ├── SecurityConfig.java
│ │ │ ├── WebMvcConfig.java
│ │ │ └── web/
│ │ │ ├── auth/
│ │ │ │ └── package-info.java
│ │ │ └── security/
│ │ │ ├── phone/
│ │ │ │ ├── SmsCodeAuthenticationFilter.java
│ │ │ │ ├── SmsCodeAuthenticationProvider.java
│ │ │ │ └── SmsCodeAuthenticationToken.java
│ │ │ └── wechat/
│ │ │ ├── QrCodeAuthenticationFilter.java
│ │ │ ├── QrCodeAuthenticationProvider.java
│ │ │ └── QrCodeAuthenticationToken.java
│ │ ├── controller/
│ │ │ ├── AuthController.java
│ │ │ └── IndexController.java
│ │ ├── entity/
│ │ │ └── CustomUserDetails.java
│ │ └── service/
│ │ └── CustomUserDetailsService.java
│ └── resources/
│ ├── application.yml
│ ├── logback.xml
│ └── templates/
│ ├── home.html
│ ├── index.html
│ └── login.html
├── hiauth-client-starter/
│ ├── hiauth-client-commons/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ └── java/
│ │ └── cn/
│ │ └── hiauth/
│ │ └── client/
│ │ ├── Authentication.java
│ │ ├── Client.java
│ │ ├── Constant.java
│ │ ├── HiAuthToken.java
│ │ ├── JwtUtils.java
│ │ ├── SecurityCorp.java
│ │ ├── SecurityService.java
│ │ ├── SecurityUser.java
│ │ ├── SessionContext.java
│ │ ├── SessionContextHolder.java
│ │ ├── TokenVo.java
│ │ └── UserinfoVo.java
│ ├── hiauth-client-resource-spring-boot-starter/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── client/
│ │ │ └── resource/
│ │ │ ├── HiAuthClientResourceAutoConfig.java
│ │ │ └── HiAuthClientResourceProperties.java
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring-configuration-metadata.json
│ │ └── spring.factories
│ ├── hiauth-client-session-spring-boot-starter/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── client/
│ │ │ └── session/
│ │ │ ├── AuthFilter.java
│ │ │ ├── HiAuthClientSessionAutoConfig.java
│ │ │ ├── HiAuthClientSessionCacheConfig.java
│ │ │ ├── HiAuthClientSessionController.java
│ │ │ ├── HiAuthClientSessionProperties.java
│ │ │ └── HiAuthClientSessionRunner.java
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring-configuration-metadata.json
│ │ └── spring.factories
│ ├── hiauth-client-spring-boot-starter/
│ │ ├── docs/
│ │ │ └── apisvc-oms.yml
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── client/
│ │ │ ├── AuthFilter.java
│ │ │ ├── HiAuthCacheConfig.java
│ │ │ ├── HiAuthClientAutoConfig.java
│ │ │ ├── HiAuthClientController.java
│ │ │ ├── HiAuthClientProperties.java
│ │ │ ├── HiAuthClientProviderProperties.java
│ │ │ ├── HiAuthClientRegistrationProperties.java
│ │ │ ├── HiAuthClientRunner.java
│ │ │ └── api/
│ │ │ ├── TokenVo.java
│ │ │ └── UserPwdUpdateDto.java
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring-configuration-metadata.json
│ │ └── spring.factories
│ ├── hiauth-client-spring-cloud-gateway-starter/
│ │ ├── pom.xml
│ │ └── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── client/
│ │ │ └── gateway/
│ │ │ ├── AuthGatewayFilterFactory.java
│ │ │ ├── HiAuthClientGatewayAutoConfig.java
│ │ │ ├── HiAuthClientGatewayController.java
│ │ │ ├── HiAuthClientGatewayProperties.java
│ │ │ ├── HiAuthClientGatewayRunner.java
│ │ │ └── UserPwdUpdateDto.java
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring-configuration-metadata.json
│ │ └── spring.factories
│ └── pom.xml
├── hiauth-front/
│ ├── .browserslistrc
│ ├── .changeset/
│ │ ├── README.md
│ │ └── config.json
│ ├── .commitlintrc.js
│ ├── .dockerignore
│ ├── .editorconfig
│ ├── .gitattributes
│ ├── .gitconfig
│ ├── .gitignore
│ ├── .gitpod.yml
│ ├── .node-version
│ ├── .npmrc
│ ├── .prettierignore
│ ├── .prettierrc.mjs
│ ├── .stylelintignore
│ ├── Dockerfile
│ ├── LICENSE
│ ├── README.ja-JP.md
│ ├── README.md
│ ├── README.zh-CN.md
│ ├── apps/
│ │ ├── backend-mock/
│ │ │ ├── README.md
│ │ │ ├── api/
│ │ │ │ ├── auth/
│ │ │ │ │ ├── codes.ts
│ │ │ │ │ ├── login.post.ts
│ │ │ │ │ ├── logout.post.ts
│ │ │ │ │ └── refresh.post.ts
│ │ │ │ ├── demo/
│ │ │ │ │ └── bigint.ts
│ │ │ │ ├── menu/
│ │ │ │ │ └── all.ts
│ │ │ │ ├── status.ts
│ │ │ │ ├── system/
│ │ │ │ │ ├── dept/
│ │ │ │ │ │ ├── .post.ts
│ │ │ │ │ │ ├── [id].delete.ts
│ │ │ │ │ │ ├── [id].put.ts
│ │ │ │ │ │ └── list.ts
│ │ │ │ │ ├── menu/
│ │ │ │ │ │ ├── list.ts
│ │ │ │ │ │ ├── name-exists.ts
│ │ │ │ │ │ └── path-exists.ts
│ │ │ │ │ └── role/
│ │ │ │ │ └── list.ts
│ │ │ │ ├── table/
│ │ │ │ │ └── list.ts
│ │ │ │ ├── test.get.ts
│ │ │ │ ├── test.post.ts
│ │ │ │ ├── upload.ts
│ │ │ │ └── user/
│ │ │ │ └── info.ts
│ │ │ ├── error.ts
│ │ │ ├── middleware/
│ │ │ │ └── 1.api.ts
│ │ │ ├── nitro.config.ts
│ │ │ ├── package.json
│ │ │ ├── routes/
│ │ │ │ └── [...].ts
│ │ │ ├── tsconfig.build.json
│ │ │ ├── tsconfig.json
│ │ │ └── utils/
│ │ │ ├── cookie-utils.ts
│ │ │ ├── jwt-utils.ts
│ │ │ ├── mock-data.ts
│ │ │ └── response.ts
│ │ ├── web-antd/
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── postcss.config.mjs
│ │ │ ├── src/
│ │ │ │ ├── adapter/
│ │ │ │ │ ├── component/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── form.ts
│ │ │ │ │ └── vxe-table.ts
│ │ │ │ ├── api/
│ │ │ │ │ ├── core/
│ │ │ │ │ │ ├── auth.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── menu.ts
│ │ │ │ │ │ └── user.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── request.ts
│ │ │ │ ├── app.vue
│ │ │ │ ├── bootstrap.ts
│ │ │ │ ├── layouts/
│ │ │ │ │ ├── auth.vue
│ │ │ │ │ ├── basic.vue
│ │ │ │ │ └── index.ts
│ │ │ │ ├── locales/
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── langs/
│ │ │ │ │ ├── en-US/
│ │ │ │ │ │ ├── demos.json
│ │ │ │ │ │ └── page.json
│ │ │ │ │ └── zh-CN/
│ │ │ │ │ ├── demos.json
│ │ │ │ │ └── page.json
│ │ │ │ ├── main.ts
│ │ │ │ ├── preferences.ts
│ │ │ │ ├── router/
│ │ │ │ │ ├── access.ts
│ │ │ │ │ ├── guard.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── routes/
│ │ │ │ │ ├── core.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── modules/
│ │ │ │ │ ├── dashboard.ts
│ │ │ │ │ ├── demos.ts
│ │ │ │ │ └── vben.ts
│ │ │ │ ├── store/
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── views/
│ │ │ │ ├── _core/
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── about/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── authentication/
│ │ │ │ │ │ ├── code-login.vue
│ │ │ │ │ │ ├── forget-password.vue
│ │ │ │ │ │ ├── login.vue
│ │ │ │ │ │ ├── qrcode-login.vue
│ │ │ │ │ │ └── register.vue
│ │ │ │ │ └── fallback/
│ │ │ │ │ ├── coming-soon.vue
│ │ │ │ │ ├── forbidden.vue
│ │ │ │ │ ├── internal-error.vue
│ │ │ │ │ ├── not-found.vue
│ │ │ │ │ └── offline.vue
│ │ │ │ ├── dashboard/
│ │ │ │ │ ├── analytics/
│ │ │ │ │ │ ├── analytics-trends.vue
│ │ │ │ │ │ ├── analytics-visits-data.vue
│ │ │ │ │ │ ├── analytics-visits-sales.vue
│ │ │ │ │ │ ├── analytics-visits-source.vue
│ │ │ │ │ │ ├── analytics-visits.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── workspace/
│ │ │ │ │ └── index.vue
│ │ │ │ └── demos/
│ │ │ │ └── antd/
│ │ │ │ └── index.vue
│ │ │ ├── tailwind.config.mjs
│ │ │ ├── tsconfig.json
│ │ │ ├── tsconfig.node.json
│ │ │ └── vite.config.mts
│ │ ├── web-auth/
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── postcss.config.mjs
│ │ │ ├── src/
│ │ │ │ ├── adapter/
│ │ │ │ │ ├── component/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── form.ts
│ │ │ │ │ └── vxe-table.ts
│ │ │ │ ├── api/
│ │ │ │ │ ├── core/
│ │ │ │ │ │ ├── app.ts
│ │ │ │ │ │ ├── appClient.ts
│ │ │ │ │ │ ├── appResource.ts
│ │ │ │ │ │ ├── auth.ts
│ │ │ │ │ │ ├── common.ts
│ │ │ │ │ │ ├── corp.ts
│ │ │ │ │ │ ├── dep.ts
│ │ │ │ │ │ ├── dict.ts
│ │ │ │ │ │ ├── emp.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── menu.ts
│ │ │ │ │ │ ├── role.ts
│ │ │ │ │ │ └── user.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── request.ts
│ │ │ │ ├── app.vue
│ │ │ │ ├── bootstrap.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── context.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── layouts/
│ │ │ │ │ ├── auth.vue
│ │ │ │ │ ├── basic.vue
│ │ │ │ │ ├── change-corp.vue
│ │ │ │ │ ├── go-admin-space-button.vue
│ │ │ │ │ └── index.ts
│ │ │ │ ├── locales/
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── langs/
│ │ │ │ │ ├── en-US/
│ │ │ │ │ │ ├── demos.json
│ │ │ │ │ │ └── page.json
│ │ │ │ │ └── zh-CN/
│ │ │ │ │ ├── demos.json
│ │ │ │ │ └── page.json
│ │ │ │ ├── main.ts
│ │ │ │ ├── preferences.ts
│ │ │ │ ├── router/
│ │ │ │ │ ├── access.ts
│ │ │ │ │ ├── guard.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── routes/
│ │ │ │ │ ├── core.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── modules/
│ │ │ │ │ ├── adminSpace.ts
│ │ │ │ │ ├── common.ts
│ │ │ │ │ ├── corpSpace.ts
│ │ │ │ │ └── dashboard.ts
│ │ │ │ ├── store/
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── content.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── utils/
│ │ │ │ │ └── rsa.ts
│ │ │ │ └── views/
│ │ │ │ ├── _core/
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── about/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── authentication/
│ │ │ │ │ │ ├── code-login.vue
│ │ │ │ │ │ ├── forget-password.vue
│ │ │ │ │ │ ├── login.vue
│ │ │ │ │ │ ├── qrcode-login.vue
│ │ │ │ │ │ └── register.vue
│ │ │ │ │ └── fallback/
│ │ │ │ │ ├── coming-soon.vue
│ │ │ │ │ ├── forbidden.vue
│ │ │ │ │ ├── internal-error.vue
│ │ │ │ │ ├── not-found.vue
│ │ │ │ │ └── offline.vue
│ │ │ │ ├── adminSpace/
│ │ │ │ │ ├── corpMgr/
│ │ │ │ │ │ ├── corp-drawer.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── userMgr/
│ │ │ │ │ ├── index.vue
│ │ │ │ │ └── user-drawer.vue
│ │ │ │ ├── common/
│ │ │ │ │ ├── appMgr/
│ │ │ │ │ │ ├── app-drawer.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── appResourceMgr/
│ │ │ │ │ ├── app-resource-drawer.vue
│ │ │ │ │ └── index.vue
│ │ │ │ ├── corpSpace/
│ │ │ │ │ ├── appClientMgr/
│ │ │ │ │ │ ├── app-client-add-modal.vue
│ │ │ │ │ │ ├── app-client-drawer.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── depMgr/
│ │ │ │ │ │ ├── dep-drawer.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── empMgr/
│ │ │ │ │ │ ├── emp-drawer.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── roleMgr/
│ │ │ │ │ │ ├── auth-modal.vue
│ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ └── role-drawer.vue
│ │ │ │ │ └── sysMgr/
│ │ │ │ │ └── dictMgr/
│ │ │ │ │ ├── dict-drawer.vue
│ │ │ │ │ └── index.vue
│ │ │ │ ├── dashboard/
│ │ │ │ │ ├── analytics/
│ │ │ │ │ │ ├── analytics-trends.vue
│ │ │ │ │ │ ├── analytics-visits-data.vue
│ │ │ │ │ │ ├── analytics-visits-sales.vue
│ │ │ │ │ │ ├── analytics-visits-source.vue
│ │ │ │ │ │ ├── analytics-visits.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── workspace/
│ │ │ │ │ └── index.vue
│ │ │ │ └── demos/
│ │ │ │ └── antd/
│ │ │ │ └── index.vue
│ │ │ ├── tailwind.config.mjs
│ │ │ ├── tsconfig.json
│ │ │ ├── tsconfig.node.json
│ │ │ └── vite.config.mts
│ │ ├── web-ele/
│ │ │ ├── index.html
│ │ │ ├── package.json
│ │ │ ├── postcss.config.mjs
│ │ │ ├── src/
│ │ │ │ ├── adapter/
│ │ │ │ │ ├── component/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── form.ts
│ │ │ │ │ └── vxe-table.ts
│ │ │ │ ├── api/
│ │ │ │ │ ├── core/
│ │ │ │ │ │ ├── auth.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── menu.ts
│ │ │ │ │ │ └── user.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── request.ts
│ │ │ │ ├── app.vue
│ │ │ │ ├── bootstrap.ts
│ │ │ │ ├── layouts/
│ │ │ │ │ ├── auth.vue
│ │ │ │ │ ├── basic.vue
│ │ │ │ │ └── index.ts
│ │ │ │ ├── locales/
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── langs/
│ │ │ │ │ ├── en-US/
│ │ │ │ │ │ ├── demos.json
│ │ │ │ │ │ └── page.json
│ │ │ │ │ └── zh-CN/
│ │ │ │ │ ├── demos.json
│ │ │ │ │ └── page.json
│ │ │ │ ├── main.ts
│ │ │ │ ├── preferences.ts
│ │ │ │ ├── router/
│ │ │ │ │ ├── access.ts
│ │ │ │ │ ├── guard.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── routes/
│ │ │ │ │ ├── core.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── modules/
│ │ │ │ │ ├── dashboard.ts
│ │ │ │ │ ├── demos.ts
│ │ │ │ │ └── vben.ts
│ │ │ │ ├── store/
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── views/
│ │ │ │ ├── _core/
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── about/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── authentication/
│ │ │ │ │ │ ├── code-login.vue
│ │ │ │ │ │ ├── forget-password.vue
│ │ │ │ │ │ ├── login.vue
│ │ │ │ │ │ ├── qrcode-login.vue
│ │ │ │ │ │ └── register.vue
│ │ │ │ │ └── fallback/
│ │ │ │ │ ├── coming-soon.vue
│ │ │ │ │ ├── forbidden.vue
│ │ │ │ │ ├── internal-error.vue
│ │ │ │ │ ├── not-found.vue
│ │ │ │ │ └── offline.vue
│ │ │ │ ├── dashboard/
│ │ │ │ │ ├── analytics/
│ │ │ │ │ │ ├── analytics-trends.vue
│ │ │ │ │ │ ├── analytics-visits-data.vue
│ │ │ │ │ │ ├── analytics-visits-sales.vue
│ │ │ │ │ │ ├── analytics-visits-source.vue
│ │ │ │ │ │ ├── analytics-visits.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── workspace/
│ │ │ │ │ └── index.vue
│ │ │ │ └── demos/
│ │ │ │ ├── element/
│ │ │ │ │ └── index.vue
│ │ │ │ └── form/
│ │ │ │ └── basic.vue
│ │ │ ├── tailwind.config.mjs
│ │ │ ├── tsconfig.json
│ │ │ ├── tsconfig.node.json
│ │ │ └── vite.config.mts
│ │ └── web-naive/
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── src/
│ │ │ ├── adapter/
│ │ │ │ ├── component/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── form.ts
│ │ │ │ ├── naive.ts
│ │ │ │ └── vxe-table.ts
│ │ │ ├── api/
│ │ │ │ ├── core/
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── menu.ts
│ │ │ │ │ └── user.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── request.ts
│ │ │ ├── app.vue
│ │ │ ├── bootstrap.ts
│ │ │ ├── layouts/
│ │ │ │ ├── auth.vue
│ │ │ │ ├── basic.vue
│ │ │ │ └── index.ts
│ │ │ ├── locales/
│ │ │ │ ├── README.md
│ │ │ │ ├── index.ts
│ │ │ │ └── langs/
│ │ │ │ ├── en-US/
│ │ │ │ │ ├── demos.json
│ │ │ │ │ └── page.json
│ │ │ │ └── zh-CN/
│ │ │ │ ├── demos.json
│ │ │ │ └── page.json
│ │ │ ├── main.ts
│ │ │ ├── preferences.ts
│ │ │ ├── router/
│ │ │ │ ├── access.ts
│ │ │ │ ├── guard.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── routes/
│ │ │ │ ├── core.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── modules/
│ │ │ │ ├── dashboard.ts
│ │ │ │ ├── demos.ts
│ │ │ │ └── vben.ts
│ │ │ ├── store/
│ │ │ │ ├── auth.ts
│ │ │ │ └── index.ts
│ │ │ └── views/
│ │ │ ├── _core/
│ │ │ │ ├── README.md
│ │ │ │ ├── about/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── authentication/
│ │ │ │ │ ├── code-login.vue
│ │ │ │ │ ├── forget-password.vue
│ │ │ │ │ ├── login.vue
│ │ │ │ │ ├── qrcode-login.vue
│ │ │ │ │ └── register.vue
│ │ │ │ └── fallback/
│ │ │ │ ├── coming-soon.vue
│ │ │ │ ├── forbidden.vue
│ │ │ │ ├── internal-error.vue
│ │ │ │ ├── not-found.vue
│ │ │ │ └── offline.vue
│ │ │ ├── dashboard/
│ │ │ │ ├── analytics/
│ │ │ │ │ ├── analytics-trends.vue
│ │ │ │ │ ├── analytics-visits-data.vue
│ │ │ │ │ ├── analytics-visits-sales.vue
│ │ │ │ │ ├── analytics-visits-source.vue
│ │ │ │ │ ├── analytics-visits.vue
│ │ │ │ │ └── index.vue
│ │ │ │ └── workspace/
│ │ │ │ └── index.vue
│ │ │ └── demos/
│ │ │ ├── form/
│ │ │ │ ├── basic.vue
│ │ │ │ └── modal.vue
│ │ │ ├── naive/
│ │ │ │ └── index.vue
│ │ │ └── table/
│ │ │ └── index.vue
│ │ ├── tailwind.config.mjs
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.mts
│ ├── changlist.txt
│ ├── cspell.json
│ ├── deploy.yaml
│ ├── docs/
│ │ ├── .vitepress/
│ │ │ ├── components/
│ │ │ │ ├── demo-preview.vue
│ │ │ │ ├── index.ts
│ │ │ │ └── preview-group.vue
│ │ │ ├── config/
│ │ │ │ ├── en.mts
│ │ │ │ ├── index.mts
│ │ │ │ ├── plugins/
│ │ │ │ │ └── demo-preview.ts
│ │ │ │ ├── shared.mts
│ │ │ │ └── zh.mts
│ │ │ └── theme/
│ │ │ ├── components/
│ │ │ │ ├── site-layout.vue
│ │ │ │ └── vben-contributors.vue
│ │ │ ├── index.ts
│ │ │ ├── plugins/
│ │ │ │ └── hm.ts
│ │ │ └── styles/
│ │ │ ├── base.css
│ │ │ ├── index.ts
│ │ │ └── variables.css
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── _env/
│ │ │ │ ├── adapter/
│ │ │ │ │ ├── component.ts
│ │ │ │ │ ├── form.ts
│ │ │ │ │ └── vxe-table.ts
│ │ │ │ └── node/
│ │ │ │ └── adapter/
│ │ │ │ ├── form.ts
│ │ │ │ └── vxe-table.ts
│ │ │ ├── commercial/
│ │ │ │ ├── community.md
│ │ │ │ ├── customized.md
│ │ │ │ └── technical-support.md
│ │ │ ├── components/
│ │ │ │ ├── common-ui/
│ │ │ │ │ ├── vben-alert.md
│ │ │ │ │ ├── vben-api-component.md
│ │ │ │ │ ├── vben-count-to-animator.md
│ │ │ │ │ ├── vben-drawer.md
│ │ │ │ │ ├── vben-ellipsis-text.md
│ │ │ │ │ ├── vben-form.md
│ │ │ │ │ ├── vben-modal.md
│ │ │ │ │ └── vben-vxe-table.md
│ │ │ │ ├── introduction.md
│ │ │ │ └── layout-ui/
│ │ │ │ └── page.md
│ │ │ ├── demos/
│ │ │ │ ├── vben-alert/
│ │ │ │ │ ├── alert/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── confirm/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── prompt/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── vben-api-component/
│ │ │ │ │ └── cascader/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── vben-count-to-animator/
│ │ │ │ │ ├── basic/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── custom/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── vben-drawer/
│ │ │ │ │ ├── auto-height/
│ │ │ │ │ │ ├── drawer.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── basic/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── dynamic/
│ │ │ │ │ │ ├── drawer.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── extra/
│ │ │ │ │ │ ├── drawer.vue
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── shared-data/
│ │ │ │ │ ├── drawer.vue
│ │ │ │ │ └── index.vue
│ │ │ │ ├── vben-ellipsis-text/
│ │ │ │ │ ├── auto-display/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── expand/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── line/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── tooltip/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── vben-form/
│ │ │ │ │ ├── api/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── basic/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── custom/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── dynamic/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── query/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── rules/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── vben-modal/
│ │ │ │ │ ├── animation-type/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── auto-height/
│ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ └── modal.vue
│ │ │ │ │ ├── basic/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── draggable/
│ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ └── modal.vue
│ │ │ │ │ ├── dynamic/
│ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ └── modal.vue
│ │ │ │ │ ├── extra/
│ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ └── modal.vue
│ │ │ │ │ └── shared-data/
│ │ │ │ │ ├── index.vue
│ │ │ │ │ └── modal.vue
│ │ │ │ └── vben-vxe-table/
│ │ │ │ ├── basic/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── custom-cell/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── edit-cell/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── edit-row/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── fixed/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── form/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── mock-api.ts
│ │ │ │ ├── remote/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── table-data.ts
│ │ │ │ ├── tree/
│ │ │ │ │ └── index.vue
│ │ │ │ └── virtual/
│ │ │ │ └── index.vue
│ │ │ ├── en/
│ │ │ │ ├── guide/
│ │ │ │ │ ├── essentials/
│ │ │ │ │ │ ├── build.md
│ │ │ │ │ │ ├── concept.md
│ │ │ │ │ │ ├── development.md
│ │ │ │ │ │ ├── external-module.md
│ │ │ │ │ │ ├── icons.md
│ │ │ │ │ │ ├── route.md
│ │ │ │ │ │ ├── server.md
│ │ │ │ │ │ ├── settings.md
│ │ │ │ │ │ └── styles.md
│ │ │ │ │ ├── in-depth/
│ │ │ │ │ │ ├── access.md
│ │ │ │ │ │ ├── check-updates.md
│ │ │ │ │ │ ├── features.md
│ │ │ │ │ │ ├── layout.md
│ │ │ │ │ │ ├── loading.md
│ │ │ │ │ │ ├── locale.md
│ │ │ │ │ │ ├── login.md
│ │ │ │ │ │ ├── theme.md
│ │ │ │ │ │ └── ui-framework.md
│ │ │ │ │ ├── introduction/
│ │ │ │ │ │ ├── changelog.md
│ │ │ │ │ │ ├── quick-start.md
│ │ │ │ │ │ ├── roadmap.md
│ │ │ │ │ │ ├── thin.md
│ │ │ │ │ │ ├── vben.md
│ │ │ │ │ │ └── why.md
│ │ │ │ │ ├── other/
│ │ │ │ │ │ ├── faq.md
│ │ │ │ │ │ ├── project-update.md
│ │ │ │ │ │ └── remove-code.md
│ │ │ │ │ └── project/
│ │ │ │ │ ├── changeset.md
│ │ │ │ │ ├── cli.md
│ │ │ │ │ ├── dir.md
│ │ │ │ │ ├── standard.md
│ │ │ │ │ ├── tailwindcss.md
│ │ │ │ │ ├── test.md
│ │ │ │ │ └── vite.md
│ │ │ │ └── index.md
│ │ │ ├── friend-links/
│ │ │ │ └── index.md
│ │ │ ├── guide/
│ │ │ │ ├── essentials/
│ │ │ │ │ ├── build.md
│ │ │ │ │ ├── concept.md
│ │ │ │ │ ├── development.md
│ │ │ │ │ ├── external-module.md
│ │ │ │ │ ├── icons.md
│ │ │ │ │ ├── route.md
│ │ │ │ │ ├── server.md
│ │ │ │ │ ├── settings.md
│ │ │ │ │ └── styles.md
│ │ │ │ ├── in-depth/
│ │ │ │ │ ├── access.md
│ │ │ │ │ ├── check-updates.md
│ │ │ │ │ ├── features.md
│ │ │ │ │ ├── layout.md
│ │ │ │ │ ├── loading.md
│ │ │ │ │ ├── locale.md
│ │ │ │ │ ├── login.md
│ │ │ │ │ ├── theme.md
│ │ │ │ │ └── ui-framework.md
│ │ │ │ ├── introduction/
│ │ │ │ │ ├── changelog.md
│ │ │ │ │ ├── quick-start.md
│ │ │ │ │ ├── roadmap.md
│ │ │ │ │ ├── thin.md
│ │ │ │ │ ├── vben.md
│ │ │ │ │ └── why.md
│ │ │ │ ├── other/
│ │ │ │ │ ├── faq.md
│ │ │ │ │ ├── project-update.md
│ │ │ │ │ └── remove-code.md
│ │ │ │ └── project/
│ │ │ │ ├── changeset.md
│ │ │ │ ├── cli.md
│ │ │ │ ├── dir.md
│ │ │ │ ├── standard.md
│ │ │ │ ├── tailwindcss.md
│ │ │ │ ├── test.md
│ │ │ │ └── vite.md
│ │ │ ├── index.md
│ │ │ └── sponsor/
│ │ │ └── personal.md
│ │ ├── tailwind.config.mjs
│ │ └── tsconfig.json
│ ├── eslint.config.mjs
│ ├── internal/
│ │ ├── lint-configs/
│ │ │ ├── commitlint-config/
│ │ │ │ ├── index.mjs
│ │ │ │ └── package.json
│ │ │ ├── eslint-config/
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── configs/
│ │ │ │ │ │ ├── command.ts
│ │ │ │ │ │ ├── comments.ts
│ │ │ │ │ │ ├── disableds.ts
│ │ │ │ │ │ ├── ignores.ts
│ │ │ │ │ │ ├── import.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── javascript.ts
│ │ │ │ │ │ ├── jsdoc.ts
│ │ │ │ │ │ ├── jsonc.ts
│ │ │ │ │ │ ├── node.ts
│ │ │ │ │ │ ├── perfectionist.ts
│ │ │ │ │ │ ├── prettier.ts
│ │ │ │ │ │ ├── regexp.ts
│ │ │ │ │ │ ├── test.ts
│ │ │ │ │ │ ├── turbo.ts
│ │ │ │ │ │ ├── typescript.ts
│ │ │ │ │ │ ├── unicorn.ts
│ │ │ │ │ │ └── vue.ts
│ │ │ │ │ ├── custom-config.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── util.ts
│ │ │ │ └── tsconfig.json
│ │ │ ├── prettier-config/
│ │ │ │ ├── index.mjs
│ │ │ │ └── package.json
│ │ │ └── stylelint-config/
│ │ │ ├── index.mjs
│ │ │ └── package.json
│ │ ├── node-utils/
│ │ │ ├── build.config.ts
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── __tests__/
│ │ │ │ │ ├── hash.test.ts
│ │ │ │ │ └── path.test.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── date.ts
│ │ │ │ ├── fs.ts
│ │ │ │ ├── git.ts
│ │ │ │ ├── hash.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── monorepo.ts
│ │ │ │ ├── path.ts
│ │ │ │ ├── prettier.ts
│ │ │ │ └── spinner.ts
│ │ │ └── tsconfig.json
│ │ ├── tailwind-config/
│ │ │ ├── build.config.ts
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ ├── module.d.ts
│ │ │ │ ├── plugins/
│ │ │ │ │ └── entry.ts
│ │ │ │ └── postcss.config.ts
│ │ │ └── tsconfig.json
│ │ ├── tsconfig/
│ │ │ ├── base.json
│ │ │ ├── library.json
│ │ │ ├── node.json
│ │ │ ├── package.json
│ │ │ ├── web-app.json
│ │ │ └── web.json
│ │ └── vite-config/
│ │ ├── build.config.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── config/
│ │ │ │ ├── application.ts
│ │ │ │ ├── common.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── library.ts
│ │ │ ├── index.ts
│ │ │ ├── options.ts
│ │ │ ├── plugins/
│ │ │ │ ├── archiver.ts
│ │ │ │ ├── extra-app-config.ts
│ │ │ │ ├── importmap.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── inject-app-loading/
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── default-loading-antd.html
│ │ │ │ │ ├── default-loading.html
│ │ │ │ │ └── index.ts
│ │ │ │ ├── inject-metadata.ts
│ │ │ │ ├── license.ts
│ │ │ │ ├── nitro-mock.ts
│ │ │ │ ├── print.ts
│ │ │ │ └── vxe-table.ts
│ │ │ ├── typing.ts
│ │ │ └── utils/
│ │ │ └── env.ts
│ │ └── tsconfig.json
│ ├── package.json
│ ├── packages/
│ │ ├── @core/
│ │ │ ├── README.md
│ │ │ ├── base/
│ │ │ │ ├── README.md
│ │ │ │ ├── design/
│ │ │ │ │ ├── package.json
│ │ │ │ │ ├── src/
│ │ │ │ │ │ ├── css/
│ │ │ │ │ │ │ ├── global.css
│ │ │ │ │ │ │ ├── nprogress.css
│ │ │ │ │ │ │ ├── transition.css
│ │ │ │ │ │ │ └── ui.css
│ │ │ │ │ │ ├── design-tokens/
│ │ │ │ │ │ │ ├── dark.css
│ │ │ │ │ │ │ ├── default.css
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── scss-bem/
│ │ │ │ │ │ ├── bem.scss
│ │ │ │ │ │ └── constants.scss
│ │ │ │ │ ├── tsconfig.json
│ │ │ │ │ └── vite.config.mts
│ │ │ │ ├── icons/
│ │ │ │ │ ├── build.config.ts
│ │ │ │ │ ├── package.json
│ │ │ │ │ ├── src/
│ │ │ │ │ │ ├── create-icon.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── lucide.ts
│ │ │ │ │ └── tsconfig.json
│ │ │ │ ├── shared/
│ │ │ │ │ ├── build.config.ts
│ │ │ │ │ ├── package.json
│ │ │ │ │ ├── src/
│ │ │ │ │ │ ├── cache/
│ │ │ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ │ │ └── storage-manager.test.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── storage-manager.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── color/
│ │ │ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ │ │ └── convert.test.ts
│ │ │ │ │ │ │ ├── color.ts
│ │ │ │ │ │ │ ├── convert.ts
│ │ │ │ │ │ │ ├── generator.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── constants/
│ │ │ │ │ │ │ ├── globals.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── vben.ts
│ │ │ │ │ │ ├── global-state.ts
│ │ │ │ │ │ ├── store.ts
│ │ │ │ │ │ └── utils/
│ │ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ │ ├── diff.test.ts
│ │ │ │ │ │ │ ├── dom.test.ts
│ │ │ │ │ │ │ ├── inference.test.ts
│ │ │ │ │ │ │ ├── letter.test.ts
│ │ │ │ │ │ │ ├── resources.test.ts
│ │ │ │ │ │ │ ├── state-handler.test.ts
│ │ │ │ │ │ │ ├── tree.test.ts
│ │ │ │ │ │ │ ├── unique.test.ts
│ │ │ │ │ │ │ ├── update-css-variables.test.ts
│ │ │ │ │ │ │ ├── util.test.ts
│ │ │ │ │ │ │ └── window.test.ts
│ │ │ │ │ │ ├── cn.ts
│ │ │ │ │ │ ├── date.ts
│ │ │ │ │ │ ├── diff.ts
│ │ │ │ │ │ ├── dom.ts
│ │ │ │ │ │ ├── download.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── inference.ts
│ │ │ │ │ │ ├── letter.ts
│ │ │ │ │ │ ├── merge.ts
│ │ │ │ │ │ ├── nprogress.ts
│ │ │ │ │ │ ├── resources.ts
│ │ │ │ │ │ ├── state-handler.ts
│ │ │ │ │ │ ├── to.ts
│ │ │ │ │ │ ├── tree.ts
│ │ │ │ │ │ ├── unique.ts
│ │ │ │ │ │ ├── update-css-variables.ts
│ │ │ │ │ │ ├── util.ts
│ │ │ │ │ │ └── window.ts
│ │ │ │ │ └── tsconfig.json
│ │ │ │ └── typings/
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── app.d.ts
│ │ │ │ │ ├── basic.d.ts
│ │ │ │ │ ├── helper.d.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── menu-record.ts
│ │ │ │ │ ├── tabs.ts
│ │ │ │ │ └── vue-router.d.ts
│ │ │ │ ├── tsconfig.json
│ │ │ │ └── vue-router.d.ts
│ │ │ ├── composables/
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ └── use-sortable.test.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── use-is-mobile.ts
│ │ │ │ │ ├── use-layout-style.ts
│ │ │ │ │ ├── use-namespace.ts
│ │ │ │ │ ├── use-priority-value.ts
│ │ │ │ │ ├── use-scroll-lock.ts
│ │ │ │ │ ├── use-simple-locale/
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── messages.ts
│ │ │ │ │ └── use-sortable.ts
│ │ │ │ └── tsconfig.json
│ │ │ ├── preferences/
│ │ │ │ ├── __tests__/
│ │ │ │ │ ├── __snapshots__/
│ │ │ │ │ │ └── config.test.ts.snap
│ │ │ │ │ ├── config.test.ts
│ │ │ │ │ └── preferences.test.ts
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── constants.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── preferences.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── update-css-variables.ts
│ │ │ │ │ └── use-preferences.ts
│ │ │ │ └── tsconfig.json
│ │ │ └── ui-kit/
│ │ │ ├── README.md
│ │ │ ├── form-ui/
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── form-api.test.ts
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── postcss.config.mjs
│ │ │ │ ├── src/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ └── form-actions.vue
│ │ │ │ │ ├── config.ts
│ │ │ │ │ ├── form-api.ts
│ │ │ │ │ ├── form-render/
│ │ │ │ │ │ ├── context.ts
│ │ │ │ │ │ ├── dependencies.ts
│ │ │ │ │ │ ├── expandable.ts
│ │ │ │ │ │ ├── form-field.vue
│ │ │ │ │ │ ├── form-label.vue
│ │ │ │ │ │ ├── form.vue
│ │ │ │ │ │ ├── helper.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── use-form-context.ts
│ │ │ │ │ ├── use-vben-form.ts
│ │ │ │ │ ├── vben-form.vue
│ │ │ │ │ └── vben-use-form.vue
│ │ │ │ ├── tailwind.config.mjs
│ │ │ │ └── tsconfig.json
│ │ │ ├── layout-ui/
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── postcss.config.mjs
│ │ │ │ ├── src/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── layout-content.vue
│ │ │ │ │ │ ├── layout-footer.vue
│ │ │ │ │ │ ├── layout-header.vue
│ │ │ │ │ │ ├── layout-sidebar.vue
│ │ │ │ │ │ ├── layout-tabbar.vue
│ │ │ │ │ │ └── widgets/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── sidebar-collapse-button.vue
│ │ │ │ │ │ └── sidebar-fixed-button.vue
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ └── use-layout.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── vben-layout.ts
│ │ │ │ │ └── vben-layout.vue
│ │ │ │ ├── tailwind.config.mjs
│ │ │ │ └── tsconfig.json
│ │ │ ├── menu-ui/
│ │ │ │ ├── README.md
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── postcss.config.mjs
│ │ │ │ ├── src/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── collapse-transition.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── menu-badge-dot.vue
│ │ │ │ │ │ ├── menu-badge.vue
│ │ │ │ │ │ ├── menu-item.vue
│ │ │ │ │ │ ├── menu.vue
│ │ │ │ │ │ ├── normal-menu/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── normal-menu.ts
│ │ │ │ │ │ │ └── normal-menu.vue
│ │ │ │ │ │ ├── sub-menu-content.vue
│ │ │ │ │ │ └── sub-menu.vue
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── use-menu-context.ts
│ │ │ │ │ │ ├── use-menu-scroll.ts
│ │ │ │ │ │ └── use-menu.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── menu.vue
│ │ │ │ │ ├── sub-menu.vue
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── utils/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── tailwind.config.mjs
│ │ │ │ └── tsconfig.json
│ │ │ ├── popup-ui/
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── package.json
│ │ │ │ ├── postcss.config.mjs
│ │ │ │ ├── src/
│ │ │ │ │ ├── alert/
│ │ │ │ │ │ ├── AlertBuilder.ts
│ │ │ │ │ │ ├── alert.ts
│ │ │ │ │ │ ├── alert.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── drawer/
│ │ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ │ └── drawer-api.test.ts
│ │ │ │ │ │ ├── drawer-api.ts
│ │ │ │ │ │ ├── drawer.ts
│ │ │ │ │ │ ├── drawer.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── use-drawer.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── modal/
│ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ └── modal-api.test.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── modal-api.ts
│ │ │ │ │ ├── modal.ts
│ │ │ │ │ ├── modal.vue
│ │ │ │ │ ├── use-modal-draggable.ts
│ │ │ │ │ └── use-modal.ts
│ │ │ │ ├── tailwind.config.mjs
│ │ │ │ └── tsconfig.json
│ │ │ ├── shadcn-ui/
│ │ │ │ ├── build.config.ts
│ │ │ │ ├── components.json
│ │ │ │ ├── package.json
│ │ │ │ ├── postcss.config.mjs
│ │ │ │ ├── src/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── avatar/
│ │ │ │ │ │ │ ├── avatar.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── back-top/
│ │ │ │ │ │ │ ├── back-top.vue
│ │ │ │ │ │ │ ├── backtop.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── use-backtop.ts
│ │ │ │ │ │ ├── breadcrumb/
│ │ │ │ │ │ │ ├── breadcrumb-background.vue
│ │ │ │ │ │ │ ├── breadcrumb-view.vue
│ │ │ │ │ │ │ ├── breadcrumb.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── button/
│ │ │ │ │ │ │ ├── button-group.vue
│ │ │ │ │ │ │ ├── button.ts
│ │ │ │ │ │ │ ├── button.vue
│ │ │ │ │ │ │ ├── check-button-group.vue
│ │ │ │ │ │ │ ├── icon-button.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── checkbox/
│ │ │ │ │ │ │ ├── checkbox.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── context-menu/
│ │ │ │ │ │ │ ├── context-menu.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── interface.ts
│ │ │ │ │ │ ├── count-to-animator/
│ │ │ │ │ │ │ ├── count-to-animator.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── dropdown-menu/
│ │ │ │ │ │ │ ├── dropdown-menu.vue
│ │ │ │ │ │ │ ├── dropdown-radio-menu.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── interface.ts
│ │ │ │ │ │ ├── expandable-arrow/
│ │ │ │ │ │ │ ├── expandable-arrow.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── full-screen/
│ │ │ │ │ │ │ ├── full-screen.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── hover-card/
│ │ │ │ │ │ │ ├── hover-card.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── icon/
│ │ │ │ │ │ │ ├── icon.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── input-password/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── input-password.vue
│ │ │ │ │ │ │ └── password-strength.vue
│ │ │ │ │ │ ├── logo/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── logo.vue
│ │ │ │ │ │ ├── pin-input/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── input.vue
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── popover/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── popover.vue
│ │ │ │ │ │ ├── render-content/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── render-content.vue
│ │ │ │ │ │ ├── scrollbar/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── scrollbar.vue
│ │ │ │ │ │ ├── segmented/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── segmented.vue
│ │ │ │ │ │ │ ├── tabs-indicator.vue
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── select/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── select.vue
│ │ │ │ │ │ ├── spine-text/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── spine-text.vue
│ │ │ │ │ │ ├── spinner/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── loading.vue
│ │ │ │ │ │ │ └── spinner.vue
│ │ │ │ │ │ └── tooltip/
│ │ │ │ │ │ ├── help-tooltip.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── tooltip.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── ui/
│ │ │ │ │ ├── accordion/
│ │ │ │ │ │ ├── Accordion.vue
│ │ │ │ │ │ ├── AccordionContent.vue
│ │ │ │ │ │ ├── AccordionItem.vue
│ │ │ │ │ │ ├── AccordionTrigger.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── alert-dialog/
│ │ │ │ │ │ ├── AlertDialog.vue
│ │ │ │ │ │ ├── AlertDialogAction.vue
│ │ │ │ │ │ ├── AlertDialogCancel.vue
│ │ │ │ │ │ ├── AlertDialogContent.vue
│ │ │ │ │ │ ├── AlertDialogDescription.vue
│ │ │ │ │ │ ├── AlertDialogOverlay.vue
│ │ │ │ │ │ ├── AlertDialogTitle.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── avatar/
│ │ │ │ │ │ ├── Avatar.vue
│ │ │ │ │ │ ├── AvatarFallback.vue
│ │ │ │ │ │ ├── AvatarImage.vue
│ │ │ │ │ │ ├── avatar.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── badge/
│ │ │ │ │ │ ├── Badge.vue
│ │ │ │ │ │ ├── badge.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── breadcrumb/
│ │ │ │ │ │ ├── Breadcrumb.vue
│ │ │ │ │ │ ├── BreadcrumbEllipsis.vue
│ │ │ │ │ │ ├── BreadcrumbItem.vue
│ │ │ │ │ │ ├── BreadcrumbLink.vue
│ │ │ │ │ │ ├── BreadcrumbList.vue
│ │ │ │ │ │ ├── BreadcrumbPage.vue
│ │ │ │ │ │ ├── BreadcrumbSeparator.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── button/
│ │ │ │ │ │ ├── Button.vue
│ │ │ │ │ │ ├── button.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── card/
│ │ │ │ │ │ ├── Card.vue
│ │ │ │ │ │ ├── CardContent.vue
│ │ │ │ │ │ ├── CardDescription.vue
│ │ │ │ │ │ ├── CardFooter.vue
│ │ │ │ │ │ ├── CardHeader.vue
│ │ │ │ │ │ ├── CardTitle.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── checkbox/
│ │ │ │ │ │ ├── Checkbox.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── context-menu/
│ │ │ │ │ │ ├── ContextMenu.vue
│ │ │ │ │ │ ├── ContextMenuCheckboxItem.vue
│ │ │ │ │ │ ├── ContextMenuContent.vue
│ │ │ │ │ │ ├── ContextMenuGroup.vue
│ │ │ │ │ │ ├── ContextMenuItem.vue
│ │ │ │ │ │ ├── ContextMenuLabel.vue
│ │ │ │ │ │ ├── ContextMenuPortal.vue
│ │ │ │ │ │ ├── ContextMenuRadioGroup.vue
│ │ │ │ │ │ ├── ContextMenuRadioItem.vue
│ │ │ │ │ │ ├── ContextMenuSeparator.vue
│ │ │ │ │ │ ├── ContextMenuShortcut.vue
│ │ │ │ │ │ ├── ContextMenuSub.vue
│ │ │ │ │ │ ├── ContextMenuSubContent.vue
│ │ │ │ │ │ ├── ContextMenuSubTrigger.vue
│ │ │ │ │ │ ├── ContextMenuTrigger.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── dialog/
│ │ │ │ │ │ ├── Dialog.vue
│ │ │ │ │ │ ├── DialogClose.vue
│ │ │ │ │ │ ├── DialogContent.vue
│ │ │ │ │ │ ├── DialogDescription.vue
│ │ │ │ │ │ ├── DialogFooter.vue
│ │ │ │ │ │ ├── DialogHeader.vue
│ │ │ │ │ │ ├── DialogOverlay.vue
│ │ │ │ │ │ ├── DialogScrollContent.vue
│ │ │ │ │ │ ├── DialogTitle.vue
│ │ │ │ │ │ ├── DialogTrigger.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── dropdown-menu/
│ │ │ │ │ │ ├── DropdownMenu.vue
│ │ │ │ │ │ ├── DropdownMenuCheckboxItem.vue
│ │ │ │ │ │ ├── DropdownMenuContent.vue
│ │ │ │ │ │ ├── DropdownMenuGroup.vue
│ │ │ │ │ │ ├── DropdownMenuItem.vue
│ │ │ │ │ │ ├── DropdownMenuLabel.vue
│ │ │ │ │ │ ├── DropdownMenuRadioGroup.vue
│ │ │ │ │ │ ├── DropdownMenuRadioItem.vue
│ │ │ │ │ │ ├── DropdownMenuSeparator.vue
│ │ │ │ │ │ ├── DropdownMenuShortcut.vue
│ │ │ │ │ │ ├── DropdownMenuSub.vue
│ │ │ │ │ │ ├── DropdownMenuSubContent.vue
│ │ │ │ │ │ ├── DropdownMenuSubTrigger.vue
│ │ │ │ │ │ ├── DropdownMenuTrigger.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── form/
│ │ │ │ │ │ ├── FormControl.vue
│ │ │ │ │ │ ├── FormDescription.vue
│ │ │ │ │ │ ├── FormItem.vue
│ │ │ │ │ │ ├── FormLabel.vue
│ │ │ │ │ │ ├── FormMessage.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── injectionKeys.ts
│ │ │ │ │ │ └── useFormField.ts
│ │ │ │ │ ├── hover-card/
│ │ │ │ │ │ ├── HoverCard.vue
│ │ │ │ │ │ ├── HoverCardContent.vue
│ │ │ │ │ │ ├── HoverCardTrigger.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── input/
│ │ │ │ │ │ ├── Input.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── label/
│ │ │ │ │ │ ├── Label.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── number-field/
│ │ │ │ │ │ ├── NumberField.vue
│ │ │ │ │ │ ├── NumberFieldContent.vue
│ │ │ │ │ │ ├── NumberFieldDecrement.vue
│ │ │ │ │ │ ├── NumberFieldIncrement.vue
│ │ │ │ │ │ ├── NumberFieldInput.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── pagination/
│ │ │ │ │ │ ├── PaginationEllipsis.vue
│ │ │ │ │ │ ├── PaginationFirst.vue
│ │ │ │ │ │ ├── PaginationLast.vue
│ │ │ │ │ │ ├── PaginationNext.vue
│ │ │ │ │ │ ├── PaginationPrev.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── pin-input/
│ │ │ │ │ │ ├── PinInput.vue
│ │ │ │ │ │ ├── PinInputGroup.vue
│ │ │ │ │ │ ├── PinInputInput.vue
│ │ │ │ │ │ ├── PinInputSeparator.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── popover/
│ │ │ │ │ │ ├── Popover.vue
│ │ │ │ │ │ ├── PopoverContent.vue
│ │ │ │ │ │ ├── PopoverTrigger.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── radio-group/
│ │ │ │ │ │ ├── RadioGroup.vue
│ │ │ │ │ │ ├── RadioGroupItem.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── resizable/
│ │ │ │ │ │ ├── ResizableHandle.vue
│ │ │ │ │ │ ├── ResizablePanelGroup.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── scroll-area/
│ │ │ │ │ │ ├── ScrollArea.vue
│ │ │ │ │ │ ├── ScrollBar.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── select/
│ │ │ │ │ │ ├── Select.vue
│ │ │ │ │ │ ├── SelectContent.vue
│ │ │ │ │ │ ├── SelectGroup.vue
│ │ │ │ │ │ ├── SelectItem.vue
│ │ │ │ │ │ ├── SelectItemText.vue
│ │ │ │ │ │ ├── SelectLabel.vue
│ │ │ │ │ │ ├── SelectScrollDownButton.vue
│ │ │ │ │ │ ├── SelectScrollUpButton.vue
│ │ │ │ │ │ ├── SelectSeparator.vue
│ │ │ │ │ │ ├── SelectTrigger.vue
│ │ │ │ │ │ ├── SelectValue.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── separator/
│ │ │ │ │ │ ├── Separator.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── sheet/
│ │ │ │ │ │ ├── Sheet.vue
│ │ │ │ │ │ ├── SheetClose.vue
│ │ │ │ │ │ ├── SheetContent.vue
│ │ │ │ │ │ ├── SheetDescription.vue
│ │ │ │ │ │ ├── SheetFooter.vue
│ │ │ │ │ │ ├── SheetHeader.vue
│ │ │ │ │ │ ├── SheetOverlay.vue
│ │ │ │ │ │ ├── SheetTitle.vue
│ │ │ │ │ │ ├── SheetTrigger.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── sheet.ts
│ │ │ │ │ ├── switch/
│ │ │ │ │ │ ├── Switch.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── tabs/
│ │ │ │ │ │ ├── Tabs.vue
│ │ │ │ │ │ ├── TabsContent.vue
│ │ │ │ │ │ ├── TabsList.vue
│ │ │ │ │ │ ├── TabsTrigger.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── textarea/
│ │ │ │ │ │ ├── Textarea.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── toggle/
│ │ │ │ │ │ ├── Toggle.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── toggle.ts
│ │ │ │ │ ├── toggle-group/
│ │ │ │ │ │ ├── ToggleGroup.vue
│ │ │ │ │ │ ├── ToggleGroupItem.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── tooltip/
│ │ │ │ │ │ ├── Tooltip.vue
│ │ │ │ │ │ ├── TooltipContent.vue
│ │ │ │ │ │ ├── TooltipProvider.vue
│ │ │ │ │ │ ├── TooltipTrigger.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── tree/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── tree.vue
│ │ │ │ │ └── types.ts
│ │ │ │ ├── tailwind.config.mjs
│ │ │ │ └── tsconfig.json
│ │ │ └── tabs-ui/
│ │ │ ├── build.config.ts
│ │ │ ├── package.json
│ │ │ ├── postcss.config.mjs
│ │ │ ├── src/
│ │ │ │ ├── components/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── tabs/
│ │ │ │ │ │ └── tabs.vue
│ │ │ │ │ ├── tabs-chrome/
│ │ │ │ │ │ └── tabs.vue
│ │ │ │ │ └── widgets/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── tool-more.vue
│ │ │ │ │ └── tool-screen.vue
│ │ │ │ ├── index.ts
│ │ │ │ ├── tabs-view.vue
│ │ │ │ ├── types.ts
│ │ │ │ ├── use-tabs-drag.ts
│ │ │ │ └── use-tabs-view-scroll.ts
│ │ │ ├── tailwind.config.mjs
│ │ │ └── tsconfig.json
│ │ ├── constants/
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── core.ts
│ │ │ │ └── index.ts
│ │ │ └── tsconfig.json
│ │ ├── effects/
│ │ │ ├── README.md
│ │ │ ├── access/
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── access-control.vue
│ │ │ │ │ ├── accessible.ts
│ │ │ │ │ ├── directive.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── use-access.ts
│ │ │ │ └── tsconfig.json
│ │ │ ├── common-ui/
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── api-component/
│ │ │ │ │ │ │ ├── api-component.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── captcha/
│ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ └── useCaptchaPoints.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── point-selection-captcha/
│ │ │ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ │ │ └── point-selection-captcha-card.vue
│ │ │ │ │ │ │ ├── slider-captcha/
│ │ │ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ │ │ ├── slider-captcha-action.vue
│ │ │ │ │ │ │ │ ├── slider-captcha-bar.vue
│ │ │ │ │ │ │ │ └── slider-captcha-content.vue
│ │ │ │ │ │ │ ├── slider-rotate-captcha/
│ │ │ │ │ │ │ │ └── index.vue
│ │ │ │ │ │ │ ├── slider-translate-captcha/
│ │ │ │ │ │ │ │ └── index.vue
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── col-page/
│ │ │ │ │ │ │ ├── col-page.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── count-to/
│ │ │ │ │ │ │ ├── count-to.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── ellipsis-text/
│ │ │ │ │ │ │ ├── ellipsis-text.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── icon-picker/
│ │ │ │ │ │ │ ├── icon-picker.vue
│ │ │ │ │ │ │ ├── icons.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── json-viewer/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ │ ├── style.scss
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── loading/
│ │ │ │ │ │ │ ├── directive.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── loading.vue
│ │ │ │ │ │ │ └── spinner.vue
│ │ │ │ │ │ ├── page/
│ │ │ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ │ │ └── page.test.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── page.vue
│ │ │ │ │ │ │ └── types.ts
│ │ │ │ │ │ ├── resize/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── resize.vue
│ │ │ │ │ │ ├── tippy/
│ │ │ │ │ │ │ ├── directive.ts
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ └── tree/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── tree.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── ui/
│ │ │ │ │ ├── about/
│ │ │ │ │ │ ├── about.ts
│ │ │ │ │ │ ├── about.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── authentication/
│ │ │ │ │ │ ├── auth-title.vue
│ │ │ │ │ │ ├── code-login.vue
│ │ │ │ │ │ ├── dingding-login.vue
│ │ │ │ │ │ ├── forget-password.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── login-expired-modal.vue
│ │ │ │ │ │ ├── login.vue
│ │ │ │ │ │ ├── qrcode-login.vue
│ │ │ │ │ │ ├── register.vue
│ │ │ │ │ │ ├── third-party-login.vue
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── dashboard/
│ │ │ │ │ │ ├── analysis/
│ │ │ │ │ │ │ ├── analysis-chart-card.vue
│ │ │ │ │ │ │ ├── analysis-charts-tabs.vue
│ │ │ │ │ │ │ ├── analysis-overview.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── typing.ts
│ │ │ │ │ │ └── workbench/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── workbench-header.vue
│ │ │ │ │ │ ├── workbench-project.vue
│ │ │ │ │ │ ├── workbench-quick-nav.vue
│ │ │ │ │ │ ├── workbench-todo.vue
│ │ │ │ │ │ └── workbench-trends.vue
│ │ │ │ │ ├── fallback/
│ │ │ │ │ │ ├── fallback.ts
│ │ │ │ │ │ ├── fallback.vue
│ │ │ │ │ │ ├── icons/
│ │ │ │ │ │ │ ├── icon-403.vue
│ │ │ │ │ │ │ ├── icon-404.vue
│ │ │ │ │ │ │ ├── icon-500.vue
│ │ │ │ │ │ │ ├── icon-coming-soon.vue
│ │ │ │ │ │ │ └── icon-offline.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── tsconfig.json
│ │ │ ├── hooks/
│ │ │ │ ├── README.md
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── use-app-config.ts
│ │ │ │ │ ├── use-content-maximize.ts
│ │ │ │ │ ├── use-design-tokens.ts
│ │ │ │ │ ├── use-hover-toggle.ts
│ │ │ │ │ ├── use-pagination.ts
│ │ │ │ │ ├── use-refresh.ts
│ │ │ │ │ ├── use-tabs.ts
│ │ │ │ │ └── use-watermark.ts
│ │ │ │ └── tsconfig.json
│ │ │ ├── layouts/
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── authentication/
│ │ │ │ │ │ ├── authentication.vue
│ │ │ │ │ │ ├── form.vue
│ │ │ │ │ │ ├── icons/
│ │ │ │ │ │ │ └── slogan.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── toolbar.vue
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── basic/
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── content/
│ │ │ │ │ │ │ ├── content-spinner.vue
│ │ │ │ │ │ │ ├── content.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── use-content-spinner.ts
│ │ │ │ │ │ ├── copyright/
│ │ │ │ │ │ │ ├── copyright.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── footer/
│ │ │ │ │ │ │ ├── footer.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── header/
│ │ │ │ │ │ │ ├── header.vue
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── layout.vue
│ │ │ │ │ │ ├── menu/
│ │ │ │ │ │ │ ├── extra-menu.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── menu.vue
│ │ │ │ │ │ │ ├── mixed-menu.vue
│ │ │ │ │ │ │ ├── use-extra-menu.ts
│ │ │ │ │ │ │ ├── use-mixed-menu.ts
│ │ │ │ │ │ │ └── use-navigation.ts
│ │ │ │ │ │ └── tabbar/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── tabbar.vue
│ │ │ │ │ │ └── use-tabbar.ts
│ │ │ │ │ ├── iframe/
│ │ │ │ │ │ ├── iframe-router-view.vue
│ │ │ │ │ │ ├── iframe-view.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── widgets/
│ │ │ │ │ ├── breadcrumb.vue
│ │ │ │ │ ├── check-updates/
│ │ │ │ │ │ ├── check-updates.vue
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── color-toggle.vue
│ │ │ │ │ ├── global-search/
│ │ │ │ │ │ ├── global-search.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── search-panel.vue
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── language-toggle.vue
│ │ │ │ │ ├── layout-toggle.vue
│ │ │ │ │ ├── lock-screen/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── lock-screen-modal.vue
│ │ │ │ │ │ └── lock-screen.vue
│ │ │ │ │ ├── notification/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── notification.vue
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ ├── preferences/
│ │ │ │ │ │ ├── blocks/
│ │ │ │ │ │ │ ├── block.vue
│ │ │ │ │ │ │ ├── checkbox-item.vue
│ │ │ │ │ │ │ ├── general/
│ │ │ │ │ │ │ │ ├── animation.vue
│ │ │ │ │ │ │ │ └── general.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── input-item.vue
│ │ │ │ │ │ │ ├── layout/
│ │ │ │ │ │ │ │ ├── breadcrumb.vue
│ │ │ │ │ │ │ │ ├── content.vue
│ │ │ │ │ │ │ │ ├── copyright.vue
│ │ │ │ │ │ │ │ ├── footer.vue
│ │ │ │ │ │ │ │ ├── header.vue
│ │ │ │ │ │ │ │ ├── layout.vue
│ │ │ │ │ │ │ │ ├── navigation.vue
│ │ │ │ │ │ │ │ ├── sidebar.vue
│ │ │ │ │ │ │ │ ├── tabbar.vue
│ │ │ │ │ │ │ │ └── widget.vue
│ │ │ │ │ │ │ ├── number-field-item.vue
│ │ │ │ │ │ │ ├── select-item.vue
│ │ │ │ │ │ │ ├── shortcut-keys/
│ │ │ │ │ │ │ │ └── global.vue
│ │ │ │ │ │ │ ├── switch-item.vue
│ │ │ │ │ │ │ ├── theme/
│ │ │ │ │ │ │ │ ├── builtin.vue
│ │ │ │ │ │ │ │ ├── color-mode.vue
│ │ │ │ │ │ │ │ ├── radius.vue
│ │ │ │ │ │ │ │ └── theme.vue
│ │ │ │ │ │ │ └── toggle-item.vue
│ │ │ │ │ │ ├── icons/
│ │ │ │ │ │ │ ├── content-compact.vue
│ │ │ │ │ │ │ ├── full-content.vue
│ │ │ │ │ │ │ ├── header-mixed-nav.vue
│ │ │ │ │ │ │ ├── header-nav.vue
│ │ │ │ │ │ │ ├── header-sidebar-nav.vue
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── mixed-nav.vue
│ │ │ │ │ │ │ ├── setting.vue
│ │ │ │ │ │ │ ├── sidebar-mixed-nav.vue
│ │ │ │ │ │ │ └── sidebar-nav.vue
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── preferences-button.vue
│ │ │ │ │ │ ├── preferences-drawer.vue
│ │ │ │ │ │ ├── preferences.vue
│ │ │ │ │ │ └── use-open-preferences.ts
│ │ │ │ │ ├── theme-toggle/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── theme-button.vue
│ │ │ │ │ │ └── theme-toggle.vue
│ │ │ │ │ └── user-dropdown/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── user-dropdown.vue
│ │ │ │ └── tsconfig.json
│ │ │ ├── plugins/
│ │ │ │ ├── README.md
│ │ │ │ ├── package.json
│ │ │ │ ├── src/
│ │ │ │ │ ├── echarts/
│ │ │ │ │ │ ├── echarts-ui.vue
│ │ │ │ │ │ ├── echarts.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── use-echarts.ts
│ │ │ │ │ ├── motion/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── types.ts
│ │ │ │ │ └── vxe-table/
│ │ │ │ │ ├── api.ts
│ │ │ │ │ ├── extends.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── init.ts
│ │ │ │ │ ├── style.css
│ │ │ │ │ ├── types.ts
│ │ │ │ │ ├── use-vxe-grid.ts
│ │ │ │ │ └── use-vxe-grid.vue
│ │ │ │ └── tsconfig.json
│ │ │ └── request/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ └── request-client/
│ │ │ │ ├── index.ts
│ │ │ │ ├── modules/
│ │ │ │ │ ├── downloader.test.ts
│ │ │ │ │ ├── downloader.ts
│ │ │ │ │ ├── interceptor.ts
│ │ │ │ │ ├── sse.test.ts
│ │ │ │ │ ├── sse.ts
│ │ │ │ │ ├── uploader.test.ts
│ │ │ │ │ └── uploader.ts
│ │ │ │ ├── preset-interceptors.ts
│ │ │ │ ├── request-client.test.ts
│ │ │ │ ├── request-client.ts
│ │ │ │ └── types.ts
│ │ │ └── tsconfig.json
│ │ ├── icons/
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── iconify/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── icons/
│ │ │ │ │ └── empty-icon.vue
│ │ │ │ ├── index.ts
│ │ │ │ └── svg/
│ │ │ │ ├── index.ts
│ │ │ │ └── load.ts
│ │ │ └── tsconfig.json
│ │ ├── locales/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── i18n.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── langs/
│ │ │ │ │ ├── en-US/
│ │ │ │ │ │ ├── authentication.json
│ │ │ │ │ │ ├── common.json
│ │ │ │ │ │ ├── preferences.json
│ │ │ │ │ │ └── ui.json
│ │ │ │ │ └── zh-CN/
│ │ │ │ │ ├── authentication.json
│ │ │ │ │ ├── common.json
│ │ │ │ │ ├── preferences.json
│ │ │ │ │ └── ui.json
│ │ │ │ └── typing.ts
│ │ │ └── tsconfig.json
│ │ ├── preferences/
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ └── index.ts
│ │ │ └── tsconfig.json
│ │ ├── stores/
│ │ │ ├── package.json
│ │ │ ├── shim-pinia.d.ts
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ ├── modules/
│ │ │ │ │ ├── access.test.ts
│ │ │ │ │ ├── access.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── tabbar.test.ts
│ │ │ │ │ ├── tabbar.ts
│ │ │ │ │ ├── user.test.ts
│ │ │ │ │ └── user.ts
│ │ │ │ └── setup.ts
│ │ │ └── tsconfig.json
│ │ ├── styles/
│ │ │ ├── README.md
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── antd/
│ │ │ │ │ └── index.css
│ │ │ │ ├── ele/
│ │ │ │ │ └── index.css
│ │ │ │ ├── global/
│ │ │ │ │ └── index.scss
│ │ │ │ ├── index.ts
│ │ │ │ └── naive/
│ │ │ │ └── index.css
│ │ │ └── tsconfig.json
│ │ ├── types/
│ │ │ ├── README.md
│ │ │ ├── global.d.ts
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ └── user.ts
│ │ │ └── tsconfig.json
│ │ └── utils/
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── helpers/
│ │ │ │ ├── __tests__/
│ │ │ │ │ ├── find-menu-by-path.test.ts
│ │ │ │ │ ├── generate-menus.test.ts
│ │ │ │ │ ├── generate-routes-frontend.test.ts
│ │ │ │ │ └── merge-route-modules.test.ts
│ │ │ │ ├── find-menu-by-path.ts
│ │ │ │ ├── generate-menus.ts
│ │ │ │ ├── generate-routes-backend.ts
│ │ │ │ ├── generate-routes-frontend.ts
│ │ │ │ ├── get-popup-container.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── merge-route-modules.ts
│ │ │ │ ├── reset-routes.ts
│ │ │ │ └── unmount-global-loading.ts
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── playground/
│ │ ├── __tests__/
│ │ │ └── e2e/
│ │ │ ├── auth-login.spec.ts
│ │ │ └── common/
│ │ │ └── auth.ts
│ │ ├── index.html
│ │ ├── package.json
│ │ ├── playwright.config.ts
│ │ ├── postcss.config.mjs
│ │ ├── src/
│ │ │ ├── adapter/
│ │ │ │ ├── component/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── form.ts
│ │ │ │ └── vxe-table.ts
│ │ │ ├── api/
│ │ │ │ ├── core/
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── menu.ts
│ │ │ │ │ └── user.ts
│ │ │ │ ├── examples/
│ │ │ │ │ ├── download.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── json-bigint.ts
│ │ │ │ │ ├── params.ts
│ │ │ │ │ ├── status.ts
│ │ │ │ │ ├── table.ts
│ │ │ │ │ └── upload.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── request.ts
│ │ │ │ └── system/
│ │ │ │ ├── dept.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── menu.ts
│ │ │ │ └── role.ts
│ │ │ ├── app.vue
│ │ │ ├── bootstrap.ts
│ │ │ ├── layouts/
│ │ │ │ ├── auth.vue
│ │ │ │ ├── basic.vue
│ │ │ │ └── index.ts
│ │ │ ├── locales/
│ │ │ │ ├── README.md
│ │ │ │ ├── index.ts
│ │ │ │ └── langs/
│ │ │ │ ├── en-US/
│ │ │ │ │ ├── demos.json
│ │ │ │ │ ├── examples.json
│ │ │ │ │ ├── page.json
│ │ │ │ │ └── system.json
│ │ │ │ └── zh-CN/
│ │ │ │ ├── demos.json
│ │ │ │ ├── examples.json
│ │ │ │ ├── page.json
│ │ │ │ └── system.json
│ │ │ ├── main.ts
│ │ │ ├── preferences.ts
│ │ │ ├── router/
│ │ │ │ ├── access.ts
│ │ │ │ ├── guard.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── routes/
│ │ │ │ ├── core.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── modules/
│ │ │ │ ├── dashboard.ts
│ │ │ │ ├── demos.ts
│ │ │ │ ├── examples.ts
│ │ │ │ ├── system.ts
│ │ │ │ └── vben.ts
│ │ │ ├── store/
│ │ │ │ ├── auth.ts
│ │ │ │ └── index.ts
│ │ │ └── views/
│ │ │ ├── _core/
│ │ │ │ ├── README.md
│ │ │ │ ├── about/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── authentication/
│ │ │ │ │ ├── code-login.vue
│ │ │ │ │ ├── forget-password.vue
│ │ │ │ │ ├── login.vue
│ │ │ │ │ ├── qrcode-login.vue
│ │ │ │ │ └── register.vue
│ │ │ │ └── fallback/
│ │ │ │ ├── coming-soon.vue
│ │ │ │ ├── forbidden.vue
│ │ │ │ ├── internal-error.vue
│ │ │ │ ├── not-found.vue
│ │ │ │ └── offline.vue
│ │ │ ├── dashboard/
│ │ │ │ ├── analytics/
│ │ │ │ │ ├── analytics-trends.vue
│ │ │ │ │ ├── analytics-visits-data.vue
│ │ │ │ │ ├── analytics-visits-sales.vue
│ │ │ │ │ ├── analytics-visits-source.vue
│ │ │ │ │ ├── analytics-visits.vue
│ │ │ │ │ └── index.vue
│ │ │ │ └── workspace/
│ │ │ │ └── index.vue
│ │ │ ├── demos/
│ │ │ │ ├── access/
│ │ │ │ │ ├── admin-visible.vue
│ │ │ │ │ ├── button-control.vue
│ │ │ │ │ ├── index.vue
│ │ │ │ │ ├── menu-visible-403.vue
│ │ │ │ │ ├── super-visible.vue
│ │ │ │ │ └── user-visible.vue
│ │ │ │ ├── active-icon/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── badge/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── breadcrumb/
│ │ │ │ │ ├── lateral-detail.vue
│ │ │ │ │ ├── lateral.vue
│ │ │ │ │ └── level-detail.vue
│ │ │ │ ├── features/
│ │ │ │ │ ├── clipboard/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── file-download/
│ │ │ │ │ │ ├── base64.ts
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── full-screen/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── hide-menu-children/
│ │ │ │ │ │ ├── children.vue
│ │ │ │ │ │ └── parent.vue
│ │ │ │ │ ├── icons/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── json-bigint/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── login-expired/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── menu-query/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── new-window/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── request-params-serializer/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ ├── tabs/
│ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ └── tab-detail.vue
│ │ │ │ │ ├── vue-query/
│ │ │ │ │ │ ├── concurrency-caching.vue
│ │ │ │ │ │ ├── index.vue
│ │ │ │ │ │ ├── infinite-queries.vue
│ │ │ │ │ │ ├── paginated-queries.vue
│ │ │ │ │ │ ├── query-retries.vue
│ │ │ │ │ │ └── typing.ts
│ │ │ │ │ └── watermark/
│ │ │ │ │ └── index.vue
│ │ │ │ └── nested/
│ │ │ │ ├── menu-1.vue
│ │ │ │ ├── menu-2-1.vue
│ │ │ │ ├── menu-3-1.vue
│ │ │ │ └── menu-3-2-1.vue
│ │ │ ├── examples/
│ │ │ │ ├── button-group/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── captcha/
│ │ │ │ │ ├── point-selection-captcha.vue
│ │ │ │ │ ├── slider-captcha.vue
│ │ │ │ │ ├── slider-rotate-captcha.vue
│ │ │ │ │ └── slider-translate-captcha.vue
│ │ │ │ ├── count-to/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── doc-button.vue
│ │ │ │ ├── drawer/
│ │ │ │ │ ├── auto-height-demo.vue
│ │ │ │ │ ├── base-demo.vue
│ │ │ │ │ ├── dynamic-demo.vue
│ │ │ │ │ ├── form-drawer-demo.vue
│ │ │ │ │ ├── in-content-demo.vue
│ │ │ │ │ ├── index.vue
│ │ │ │ │ └── shared-data-demo.vue
│ │ │ │ ├── ellipsis/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── form/
│ │ │ │ │ ├── api.vue
│ │ │ │ │ ├── basic.vue
│ │ │ │ │ ├── custom-layout.vue
│ │ │ │ │ ├── custom.vue
│ │ │ │ │ ├── dynamic.vue
│ │ │ │ │ ├── merge.vue
│ │ │ │ │ ├── modules/
│ │ │ │ │ │ └── two-fields.vue
│ │ │ │ │ ├── query.vue
│ │ │ │ │ ├── rules.vue
│ │ │ │ │ └── scroll-to-error-test.vue
│ │ │ │ ├── json-viewer/
│ │ │ │ │ ├── data.ts
│ │ │ │ │ └── index.vue
│ │ │ │ ├── layout/
│ │ │ │ │ └── col-page.vue
│ │ │ │ ├── loading/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── modal/
│ │ │ │ │ ├── auto-height-demo.vue
│ │ │ │ │ ├── base-demo.vue
│ │ │ │ │ ├── blur-demo.vue
│ │ │ │ │ ├── drag-demo.vue
│ │ │ │ │ ├── dynamic-demo.vue
│ │ │ │ │ ├── form-modal-demo.vue
│ │ │ │ │ ├── in-content-demo.vue
│ │ │ │ │ ├── index.vue
│ │ │ │ │ ├── nested-demo.vue
│ │ │ │ │ └── shared-data-demo.vue
│ │ │ │ ├── motion/
│ │ │ │ │ └── index.vue
│ │ │ │ ├── resize/
│ │ │ │ │ └── basic.vue
│ │ │ │ ├── tippy/
│ │ │ │ │ └── index.vue
│ │ │ │ └── vxe-table/
│ │ │ │ ├── basic.vue
│ │ │ │ ├── custom-cell.vue
│ │ │ │ ├── edit-cell.vue
│ │ │ │ ├── edit-row.vue
│ │ │ │ ├── fixed.vue
│ │ │ │ ├── form.vue
│ │ │ │ ├── remote.vue
│ │ │ │ ├── table-data.ts
│ │ │ │ ├── tree.vue
│ │ │ │ └── virtual.vue
│ │ │ └── system/
│ │ │ ├── dept/
│ │ │ │ ├── data.ts
│ │ │ │ ├── list.vue
│ │ │ │ └── modules/
│ │ │ │ └── form.vue
│ │ │ ├── menu/
│ │ │ │ ├── data.ts
│ │ │ │ ├── list.vue
│ │ │ │ └── modules/
│ │ │ │ └── form.vue
│ │ │ └── role/
│ │ │ ├── data.ts
│ │ │ ├── list.vue
│ │ │ └── modules/
│ │ │ └── form.vue
│ │ ├── tailwind.config.mjs
│ │ ├── tsconfig.json
│ │ ├── tsconfig.node.json
│ │ └── vite.config.mts
│ ├── pnpm-workspace.yaml
│ ├── scripts/
│ │ ├── clean.mjs
│ │ ├── deploy/
│ │ │ ├── Dockerfile
│ │ │ ├── build-local-docker-image.sh
│ │ │ └── nginx.conf
│ │ ├── turbo-run/
│ │ │ ├── README.md
│ │ │ ├── bin/
│ │ │ │ └── turbo-run.mjs
│ │ │ ├── build.config.ts
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── index.ts
│ │ │ │ └── run.ts
│ │ │ └── tsconfig.json
│ │ └── vsh/
│ │ ├── README.md
│ │ ├── bin/
│ │ │ └── vsh.mjs
│ │ ├── build.config.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── check-circular/
│ │ │ │ └── index.ts
│ │ │ ├── check-dep/
│ │ │ │ └── index.ts
│ │ │ ├── code-workspace/
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ ├── lint/
│ │ │ │ └── index.ts
│ │ │ └── publint/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ ├── stylelint.config.mjs
│ ├── tea.yaml
│ ├── turbo.json
│ ├── vben-admin.code-workspace
│ ├── vitest.config.ts
│ └── vitest.workspace.ts
├── hiauth-server/
│ ├── pom.xml
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── cn/
│ │ │ └── hiauth/
│ │ │ └── server/
│ │ │ ├── ServerStarter.java
│ │ │ ├── api/
│ │ │ │ ├── dto/
│ │ │ │ │ ├── KeywordPageUserDto.java
│ │ │ │ │ ├── PageDepDto.java
│ │ │ │ │ ├── PageEmpDto.java
│ │ │ │ │ ├── PageRoleDto.java
│ │ │ │ │ ├── RegisterDto.java
│ │ │ │ │ ├── app/
│ │ │ │ │ │ ├── AppCreateDto.java
│ │ │ │ │ │ ├── AppLimitDto.java
│ │ │ │ │ │ ├── AppPageDto.java
│ │ │ │ │ │ └── AppUpdateDto.java
│ │ │ │ │ ├── appClient/
│ │ │ │ │ │ ├── AppClientCreateDto.java
│ │ │ │ │ │ ├── AppClientDeleteDto.java
│ │ │ │ │ │ ├── AppClientLimitDto.java
│ │ │ │ │ │ ├── AppClientPageDto.java
│ │ │ │ │ │ └── AppClientUpdateDto.java
│ │ │ │ │ ├── appResource/
│ │ │ │ │ │ ├── AppResourceCreateDto.java
│ │ │ │ │ │ ├── AppResourcePageDto.java
│ │ │ │ │ │ ├── AppResourceUpdateDto.java
│ │ │ │ │ │ └── FindAppResourceIdsByRoleAndAppDto.java
│ │ │ │ │ ├── corp/
│ │ │ │ │ │ ├── CorpCreateDto.java
│ │ │ │ │ │ ├── CorpPageDto.java
│ │ │ │ │ │ └── CorpUpdateDto.java
│ │ │ │ │ ├── dep/
│ │ │ │ │ │ ├── DepCreateDto.java
│ │ │ │ │ │ ├── DepLimitDto.java
│ │ │ │ │ │ ├── DepPageDto.java
│ │ │ │ │ │ └── DepUpdateDto.java
│ │ │ │ │ ├── dict/
│ │ │ │ │ │ ├── DictCreateDto.java
│ │ │ │ │ │ ├── DictLimitDto.java
│ │ │ │ │ │ ├── DictPageDto.java
│ │ │ │ │ │ └── DictUpdateDto.java
│ │ │ │ │ ├── emp/
│ │ │ │ │ │ ├── EmpCreateDto.java
│ │ │ │ │ │ ├── EmpPageDto.java
│ │ │ │ │ │ └── EmpUpdateDto.java
│ │ │ │ │ ├── login/
│ │ │ │ │ │ ├── CaptchaVerifyDto.java
│ │ │ │ │ │ ├── SmsCodeDto.java
│ │ │ │ │ │ └── SmsCodeLoginDto.java
│ │ │ │ │ ├── role/
│ │ │ │ │ │ ├── RoleAuthDto.java
│ │ │ │ │ │ ├── RoleCreateDto.java
│ │ │ │ │ │ ├── RoleLimitDto.java
│ │ │ │ │ │ ├── RolePageDto.java
│ │ │ │ │ │ └── RoleUpdateDto.java
│ │ │ │ │ └── user/
│ │ │ │ │ ├── UserCreateDto.java
│ │ │ │ │ ├── UserLimitDto.java
│ │ │ │ │ ├── UserPageDto.java
│ │ │ │ │ ├── UserPwdUpdateDto.java
│ │ │ │ │ └── UserUpdateDto.java
│ │ │ │ └── vo/
│ │ │ │ ├── CommonTreeNodeVo.java
│ │ │ │ ├── CorpAppVo.java
│ │ │ │ ├── CorpResourceTreeNodeVo.java
│ │ │ │ ├── CorpVo.java
│ │ │ │ ├── CurrentLoginUserVo.java
│ │ │ │ ├── EmpVo.java
│ │ │ │ ├── IndexCorpAppVo.java
│ │ │ │ ├── SysMenuVo.java
│ │ │ │ └── UserVo.java
│ │ │ ├── config/
│ │ │ │ ├── AuthServerConfig.java
│ │ │ │ ├── BeanConfig.java
│ │ │ │ ├── DocConfig.java
│ │ │ │ ├── SecurityConfig.java
│ │ │ │ ├── WebMvcConfig.java
│ │ │ │ ├── props/
│ │ │ │ │ ├── AppProperties.java
│ │ │ │ │ └── WechatProperties.java
│ │ │ │ ├── rest/
│ │ │ │ │ ├── ApiExceptionAdvice.java
│ │ │ │ │ ├── ResourceAccessDeniedHandler.java
│ │ │ │ │ ├── ResourceApi.java
│ │ │ │ │ ├── ResourceAuthenticationEntryPoint.java
│ │ │ │ │ └── security/
│ │ │ │ │ ├── ApiFilter.java
│ │ │ │ │ ├── MySecurityUser.java
│ │ │ │ │ └── ReadonlyFilter.java
│ │ │ │ └── web/
│ │ │ │ ├── auth/
│ │ │ │ │ ├── AuthFailureHandler.java
│ │ │ │ │ ├── AuthGrantedAuthority.java
│ │ │ │ │ ├── AuthGrantedAuthorityDeserializer.java
│ │ │ │ │ ├── AuthGrantedAuthorityMixin.java
│ │ │ │ │ ├── AuthUser.java
│ │ │ │ │ ├── AuthUserDeserializer.java
│ │ │ │ │ ├── AuthUserMixin.java
│ │ │ │ │ ├── CustomAuthUserAttrs.java
│ │ │ │ │ ├── CustomAuthorizationResponseHandler.java
│ │ │ │ │ ├── CustomJdbcRegisteredClientRepository.java
│ │ │ │ │ ├── CustomOidcUserInfoMapper.java
│ │ │ │ │ └── FederatedIdentityIdTokenCustomizer.java
│ │ │ │ └── security/
│ │ │ │ ├── CaptchaFilter.java
│ │ │ │ ├── CustomAuthenticationFailureHandler.java
│ │ │ │ ├── CustomAuthenticationSuccessHandler.java
│ │ │ │ ├── CustomLoginUrlAuthenticationEntryPoint.java
│ │ │ │ ├── MultiAppHttpSessionRequestCache.java
│ │ │ │ ├── MultiAuthUserService.java
│ │ │ │ ├── MultiAuthenticationProvider.java
│ │ │ │ ├── account/
│ │ │ │ │ ├── AccountAuthenticationFilter.java
│ │ │ │ │ ├── AccountAuthenticationProvider.java
│ │ │ │ │ └── AccountAuthenticationToken.java
│ │ │ │ ├── phone/
│ │ │ │ │ ├── SmsCodeAuthenticationFilter.java
│ │ │ │ │ ├── SmsCodeAuthenticationProvider.java
│ │ │ │ │ └── SmsCodeAuthenticationToken.java
│ │ │ │ └── wechat/
│ │ │ │ ├── QrCodeAuthenticationFilter.java
│ │ │ │ ├── QrCodeAuthenticationProvider.java
│ │ │ │ └── QrCodeAuthenticationToken.java
│ │ │ ├── controller/
│ │ │ │ ├── AuthConsentController.java
│ │ │ │ ├── IndexController.java
│ │ │ │ ├── LoginController.java
│ │ │ │ ├── OauthController.java
│ │ │ │ ├── TestController.java
│ │ │ │ ├── UnpController.java
│ │ │ │ ├── adminspace/
│ │ │ │ │ ├── CorpMgrController.java
│ │ │ │ │ └── UserMgrController.java
│ │ │ │ ├── common/
│ │ │ │ │ ├── AppMgrController.java
│ │ │ │ │ ├── AppResourceMgrController.java
│ │ │ │ │ └── CommonController.java
│ │ │ │ └── corpspace/
│ │ │ │ ├── AppClientMgrController.java
│ │ │ │ ├── DepMgrController.java
│ │ │ │ ├── DictMgrController.java
│ │ │ │ ├── EmpMgrController.java
│ │ │ │ └── RoleMgrController.java
│ │ │ ├── entity/
│ │ │ │ ├── App.java
│ │ │ │ ├── AppResource.java
│ │ │ │ ├── AuthorizationConsent.java
│ │ │ │ ├── Corp.java
│ │ │ │ ├── CorpApp.java
│ │ │ │ ├── CorpAppInfo.java
│ │ │ │ ├── Department.java
│ │ │ │ ├── Dict.java
│ │ │ │ ├── Employee.java
│ │ │ │ ├── File.java
│ │ │ │ ├── Oauth2Authorization.java
│ │ │ │ ├── Oauth2AuthorizationConsent.java
│ │ │ │ ├── Oauth2RegisteredClient.java
│ │ │ │ ├── Role.java
│ │ │ │ ├── RoleAppResource.java
│ │ │ │ ├── SysLog.java
│ │ │ │ └── User.java
│ │ │ ├── mapper/
│ │ │ │ ├── AppMapper.java
│ │ │ │ ├── AppResourceMapper.java
│ │ │ │ ├── CorpAppMapper.java
│ │ │ │ ├── CorpMapper.java
│ │ │ │ ├── DepartmentMapper.java
│ │ │ │ ├── DictMapper.java
│ │ │ │ ├── EmployeeMapper.java
│ │ │ │ ├── FileMapper.java
│ │ │ │ ├── Oauth2AuthorizationConsentMapper.java
│ │ │ │ ├── Oauth2AuthorizationMapper.java
│ │ │ │ ├── Oauth2RegisteredClientMapper.java
│ │ │ │ ├── RoleAppResourceMapper.java
│ │ │ │ ├── RoleMapper.java
│ │ │ │ ├── SysLogMapper.java
│ │ │ │ └── UserMapper.java
│ │ │ ├── service/
│ │ │ │ ├── AppResourceService.java
│ │ │ │ ├── AppService.java
│ │ │ │ ├── CorpAppService.java
│ │ │ │ ├── CorpService.java
│ │ │ │ ├── DepartmentService.java
│ │ │ │ ├── DictService.java
│ │ │ │ ├── EmployeeService.java
│ │ │ │ ├── FileService.java
│ │ │ │ ├── Oauth2AuthorizationConsentService.java
│ │ │ │ ├── Oauth2AuthorizationService.java
│ │ │ │ ├── Oauth2RegisteredClientService.java
│ │ │ │ ├── RoleService.java
│ │ │ │ ├── SimpleSecurityService.java
│ │ │ │ ├── SysLogService.java
│ │ │ │ ├── UserService.java
│ │ │ │ └── impl/
│ │ │ │ ├── AppResourceServiceImpl.java
│ │ │ │ ├── AppServiceImpl.java
│ │ │ │ ├── CorpAppServiceImpl.java
│ │ │ │ ├── CorpServiceImpl.java
│ │ │ │ ├── DepartmentServiceImpl.java
│ │ │ │ ├── DictServiceImpl.java
│ │ │ │ ├── EmployeeServiceImpl.java
│ │ │ │ ├── FileServiceImpl.java
│ │ │ │ ├── Oauth2AuthorizationConsentServiceImpl.java
│ │ │ │ ├── Oauth2AuthorizationServiceImpl.java
│ │ │ │ ├── Oauth2RegisteredClientServiceImpl.java
│ │ │ │ ├── RoleServiceImpl.java
│ │ │ │ ├── SysLogServiceImpl.java
│ │ │ │ └── UserServiceImpl.java
│ │ │ └── utils/
│ │ │ ├── AliyunSmsUtils.java
│ │ │ ├── AppResourceUtils.java
│ │ │ ├── Constant.java
│ │ │ ├── DateTimeUtils.java
│ │ │ ├── DepartmentUtils.java
│ │ │ ├── Oauth2RegisteredClientUtils.java
│ │ │ ├── RsaUtils.java
│ │ │ ├── SmsUtils.java
│ │ │ └── jose/
│ │ │ ├── Jwks.java
│ │ │ └── KeyGeneratorUtils.java
│ │ └── resources/
│ │ ├── application-common.yml
│ │ ├── application-doc.yml
│ │ ├── application-hiauth.yml
│ │ ├── application-mybatis.yml
│ │ ├── application-redis.yml
│ │ ├── application.yml
│ │ ├── logback.xml
│ │ ├── mapper/
│ │ │ ├── AppMapper.xml
│ │ │ ├── AppResourceMapper.xml
│ │ │ ├── CorpAppMapper.xml
│ │ │ ├── CorpMapper.xml
│ │ │ ├── DepartmentMapper.xml
│ │ │ ├── DictMapper.xml
│ │ │ ├── EmployeeMapper.xml
│ │ │ ├── FileMapper.xml
│ │ │ ├── Oauth2AuthorizationConsentMapper.xml
│ │ │ ├── Oauth2AuthorizationMapper.xml
│ │ │ ├── Oauth2RegisteredClientMapper.xml
│ │ │ ├── RoleMapper.xml
│ │ │ ├── SysLogMapper.xml
│ │ │ └── UserMapper.xml
│ │ ├── static/
│ │ │ ├── bootstrap-5.3.0/
│ │ │ │ ├── css/
│ │ │ │ │ ├── bootstrap.min.css
│ │ │ │ │ ├── bootstrap.min.css.map
│ │ │ │ │ ├── bootstrap.rtl.min.css
│ │ │ │ │ └── bootstrap.rtl.min.css.map
│ │ │ │ └── js/
│ │ │ │ ├── bootstrap.bundle.min.js
│ │ │ │ └── bootstrap.bundle.min.js.map
│ │ │ ├── css/
│ │ │ │ ├── all.min.css
│ │ │ │ ├── common.css
│ │ │ │ ├── fontawesome.min.css
│ │ │ │ ├── index.css
│ │ │ │ ├── jquery.treetable.css
│ │ │ │ ├── jquery.treetable.theme.default.css
│ │ │ │ ├── login.css
│ │ │ │ ├── login1.css
│ │ │ │ ├── login2.css
│ │ │ │ ├── login3.css
│ │ │ │ ├── navbar.css
│ │ │ │ └── user_me.css
│ │ │ ├── fontawesome-5.15.4/
│ │ │ │ └── css/
│ │ │ │ ├── all.css
│ │ │ │ ├── all.min.css
│ │ │ │ ├── brands.css
│ │ │ │ ├── brands.min.css
│ │ │ │ ├── fontawesome.css
│ │ │ │ ├── fontawesome.min.css
│ │ │ │ ├── regular.css
│ │ │ │ ├── regular.min.css
│ │ │ │ ├── solid.css
│ │ │ │ ├── solid.min.css
│ │ │ │ ├── svg-with-js.css
│ │ │ │ ├── svg-with-js.min.css
│ │ │ │ ├── v4-shims.css
│ │ │ │ └── v4-shims.min.css
│ │ │ ├── img/
│ │ │ │ └── logo.psd
│ │ │ └── js/
│ │ │ ├── depTree.js
│ │ │ ├── jquery.min.js
│ │ │ ├── jquery.treetable.js
│ │ │ ├── jquery.validate.min.js
│ │ │ ├── login.js
│ │ │ ├── particle.js
│ │ │ ├── sliderCaptcha.js
│ │ │ └── wxLogin.js
│ │ └── templates/
│ │ ├── common/
│ │ │ └── include.html
│ │ ├── consent.html
│ │ ├── consent_bak.html
│ │ ├── error/
│ │ │ ├── 401.html
│ │ │ ├── 403.html
│ │ │ ├── 404.html
│ │ │ ├── 500.html
│ │ │ └── unconsent.html
│ │ ├── index.html
│ │ ├── login.html
│ │ ├── login1.html
│ │ ├── login2.html
│ │ ├── login3.html
│ │ ├── oauth/
│ │ │ ├── oauth_approval.html
│ │ │ └── oauth_error.html
│ │ ├── profile.html
│ │ ├── setting.html
│ │ └── user/
│ │ ├── detail.html
│ │ ├── list.html
│ │ ├── me.html
│ │ └── user.html
│ └── test/
│ └── java/
│ └── cn/
│ └── hiauth/
│ └── server/
│ ├── AuthServerTests.java
│ ├── CustomJdbcRegisteredClientRepositoryTests.java
│ ├── DefaultAuthorizationServerApplicationTests.java
│ ├── DefaultAuthorizationServerConsentTests.java
│ └── service/
│ ├── AppResourceServiceTests.java
│ ├── AppServiceTests.java
│ ├── CorpServiceTests.java
│ ├── DepartmentServiceTests.java
│ ├── DictServiceTests.java
│ ├── EmployeeServiceTests.java
│ ├── FileServiceTests.java
│ ├── Oauth2AuthorizationConsentServiceTests.java
│ ├── Oauth2AuthorizationServiceTests.java
│ ├── Oauth2RegisteredClientServiceTests.java
│ ├── RoleServiceTests.java
│ ├── SysLogServiceTests.java
│ └── UserServiceTests.java
├── other/
│ └── hiauth.sql
└── pom.xml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/deploy.yml
================================================
name: Deploy Docs Pages
on:
push:
#分支名字
branches: [master]
# 设置tokenn访问权限
permissions:
contents: read
pages: write
id-token: write
# 只允许同时进行一次部署,跳过正在运行和最新队列之间的运行队列
# 但是,不要取消正在进行的运行,因为我们希望允许这些生产部署完成
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 如果未启用 lastUpdated,则不需要
- name: Setup pnpm
uses: pnpm/action-setup@v2 # 安装pnpm并添加到环境变量
with:
version: 10.10.0 # 指定 pnpm 版本
- name: Setup Pages
uses: actions/configure-pages@v4 # 在工作流程自动配置GithubPages
- name: Install dependencies
run: |
cd docs
pnpm install # 安装依赖
- name: Build with VitePress
run: |
cd docs
pnpm run build # 启动项目
touch .vitepress/dist/.nojekyll # 通知githubpages不要使用Jekyll处理这个站点
- name: Upload artifact
uses: actions/upload-pages-artifact@v3 # 上传构建产物
with:
path: docs/.vitepress/dist # 指定上传的路径
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }} # 从后续的输出中获取部署后的页面URL
needs: build # 在build后面完成
runs-on: ubuntu-latest # 运行在最新版本的ubuntu系统上
name: Deploy
steps:
- name: Deploy to GitHub Pages
id: deployment # 指定id
uses: actions/deploy-pages@v4 # 将之前的构建产物部署到github pages中
================================================
FILE: .gitignore
================================================
HELP.md
target/
target/*
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**
*.pdf
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.ipr
*.iws
*.iml
**/.idea
**/*.iml
rebel.xml
### IntelliJ IDEA Local ###
target
*.log
*.log.*
logs
.DS_Store
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
### VS Code ###
.vscode/
/logs/
**/node_modules
**/.umi
temp
### java ###
*.class
/docs/.vitepress/dist
/docs/.vitepress/cache
/docs/package-lock.json
/docs/pnpm-lock.yaml
lefthook.yml
================================================
FILE: LICENSE
================================================
MIT License (MIT)
Copyright © 2025 HiAuth
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the “Software”), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
Hi Auth
HiAuth是一个开源的基于OAuth2.0协议的认证、授权系统,除了标准的OAuth2.0授权流程功能外,还提供了应用管理、用户管理、权限管理等相关功能。
[](https://github.com/bestaone/HiAuth/stargazers)
[](https://github.com/bestaone/HiAuth/network/members)
[](https://github.com/bestaone/HiAuth)
[](https://github.com/bestaone/HiAuth/blob/master/LICENSE)
[](https://github.com/bestaone)
## 介绍
除了认证相关功能外,还提供了`/example/demo`、`/example/himall`项目,供用户参考如何集成。
- 参考`demo`实例,你可以几分钟之内快速验证如何集成HiAuth;
- 参考`himall`实例,你可以快速的启动一个带页面的实例;
### LIVE
- HiAuth Docs:http://docs.hiauth.cn
- HiAuth Admin:http://auth.hiauth.cn/admin
- HiAuth 授权页:http://auth.hiauth.cn
### 目录结构
```
├─cicd 持续集成
├─docs 开发文档
├─example 样例
│ ├─demo 基础样例
│ ├─hiauth-client 使用hiauth-client-spring-boot-starter集成hiauth的样例
│ ├─hiauth-client-exp hiauth-client的简易版,用于做实验
│ ├─hiauth-server-exp hiauth-server的简易版,用于做实验
│ ├─himall 带有页面的样例
│ ├─resource 资源服务样例
│ ├─spring-cloud spring-cloud微服务集成样例,原生集成
│ ├─spring-cloud-with-hiauth-client spring-cloud微服务集成样例,使用starter集成
│ ├─wechat-login 微信登录样例
├─hiauth-client-starter hiauth-client SDK
│ ├─hiauth-client-commons 基础包
│ ├─hiauth-client-spring-boot-starter 适用于SpringBoot直接集成
│ ├─hiauth-client-session-spring-boot-starter SpringCloud架构中,业务服务中的session管理SDK
│ ├─hiauth-client-spring-cloud-gateway-starter SpringCloudGateway中集成认证授权
├─hiauth-front 管理端前端项目
├─hiauth-server HiAuth服务端
├─other 其他内容,数据库脚本等
```
## 效果图
- 认证中心登录页
- 管理后台登录页
- 超级管理员-用户管理页
- 企业管理员-部门列表页
- 企业管理员-员工列表页
**如果你觉得此项目对你有帮助,请给我点个star,谢谢!**
## 快速尝试
### 环境要求
- Git
- JDK17+
- Maven 3.8+
### 下载源码
```sh
$ git clone https://github.com/bestaone/HiAuth.git
```
### 构建、启动
```sh
# 启动himall实例
$ cd HiAuth/example/himall
$ mvn clean install
$ mvn spring-boot:run
```
### 验证
- 访问HiMall:http://127.0.0.1:9000 点击`Login`按钮,登录账号:`corpadmin/123456`
> 注意:`127.0.0.1`不能使用`localhost`代替,因为数据库中配置了回调地址为`http://127.0.0.1:9000`。
## 认证模式
**authorization_code模式:**
- 访问授权端点获取`授权码`: http://auth.hiauth.cn/oauth2/authorize?response_type=code&client_id=himall&scope=openid%20profile&redirect_uri=http://127.0.0.1:9000/login/oauth2/code/hiauth-code
- 用户登录并授权后,重定向到`redirect_uri`并附带`授权码`,如下(注意:浏览器开发模式下,网络控制台中,url的参数code值):
```shell
http://127.0.0.1:9000/login/oauth2/code/hiauth-code?code=R4vhO65LvdsNqQ9A3KHwjb...
```
- 使用`授权码`换取访问`令牌`
```shell
# 最后的YourCode替换为上面步骤获取的授权码
$ curl --location --request POST 'http://auth.hiauth.cn/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic aGltYWxsOnNlY3JldA==' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'redirect_uri=http://127.0.0.1:9000/login/oauth2/code/hiauth-code' \
--data-urlencode 'code=YourCode'
# 或者
$ curl --location --request POST 'http://auth.hiauth.cn/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8' \
--header 'Authorization: Basic aGltYWxsOnNlY3JldA==' \
--data 'grant_type=authorization_code&redirect_uri=http://127.0.0.1:9000/login/oauth2/code/hiauth-code&client_id=himall&client_secret=secret&code=YourCode'
```
> 上述“Authorization: Basic aGltYWxsOnNlY3JldA==”中的值“aGltYWxsOnNlY3JldA==”,
> 计算方式为:Base64.encode(client_id:client_secret),
> 例如:client_id=himall,client_secret=secret时,base64解码为:Base64.encode("himall:secret")
返回结果:
```json
{
"access_token": "eyJraWQiOiJkZTYxMjVmNi0wYTQ5LTQwMGYtYWMzMC02M2U2Zm",
"refresh_token": "8WS6liiSW0gmUy8yudFAPIHGor3Hf6yBtaBTUNjj3-q9y4JXRlBZ",
"scope": "openid profile",
"token_type": "Bearer",
"expires_in": 35999
}
```
**client_credentials模式:**
```shell
$ curl --location --request POST 'http://auth.hiauth.cn/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=himall' \
--data-urlencode 'client_secret=secret' \
--data-urlencode 'scope=profile'
# 或者
$ curl --location --request POST 'http://auth.hiauth.cn/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'grant_type=client_credentials&client_id=himall&client_secret=secret&scope=profile'
```
返回结果:
```json
{
"access_token": "eyJraWQiOiJkZTYxMjVmNi0wYTQ5LTQwMGYtYWMzMC02M2U2Zm",
"scope": "profile user",
"token_type": "Bearer",
"expires_in": 35999
}
```
**用户信息获取:**
```shell
# 将accessToken替换为上面步骤获取的访问令牌
$ curl --location --request POST 'http://auth.hiauth.cn/userinfo' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer accessToken'
```
> 注意:只在code码模式`grant_type=authorization_code`下生效。
返回结果:
```shell
{
"sub": "corpadmin",
"empId": 1,
"avatarUrl": "/unpapi/image/2c924149ddfe4bd181959ee9bede10c0.jpeg",
"appId": 91,
"name": "企业管理员",
"phoneNum": "13400000001",
"userId": 11,
"authorities": [],
"cid": 1,
"username": "corpadmin"
}
```
**scop权限:**
- 在授权请求中包含所需scope
- 获取的访问令牌将包含授予的scope
- 资源服务器验证请求的scope是否匹配
```java
@PreAuthorize("hasAuthority('SCOPE_profile')")
@GetMapping("/protected")
public String protectedResource() {
return "Accessed protected resource";
}
```
### 其他
- 获取认证服务器配置信息:http://auth.hiauth.cn/.well-known/openid-configuration
### 更多集成方式
- 云端SaaS版集成,[参考文档](http://docs.hiauth.cn/guide/saas);
- 本地Docker版集成,[参考文档](http://docs.hiauth.cn/guide/docker);
- 源码编译安装集成,[参考文档](http://docs.hiauth.cn/guide/sourcecode);
## 社区与作者
>如果群二维码失效了,请先添加我的微信,然我我拉你入群。
## 授权协议
本项目执行 [MIT](https://github.com/bestaone/HiAuth/blob/master/LICENSE) 协议
================================================
FILE: cicd/Dockerfile
================================================
FROM ubuntu:jdk21-ng-24
# 设置语言
ENV LANG en_US.UTF-8
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
# 设置地理位置
ENV TZ=Asia/Shanghai
# 设置时区
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 安装工具
RUN apt-get update && apt-get install -y curl && apt-get clean
# 挂在目录
VOLUME /data
# 安装jar包
ADD ./hiauth-server/target/hiauth-server.jar /hiauth/app.jar
COPY ./cicd/hiauth.properties /hiauth/conf/hiauth.properties
# 安装前端
ADD ./hiauth-front/apps/web-auth/dist.zip dist.zip
RUN mkdir -p /html && chmod a+rwx -R /html
RUN unzip dist.zip -d /html/admin
# 安装文档
# COPY ./docs/.vitepress/dist /html/docs
# 配置nginx
COPY ./cicd/nginx.conf /etc/nginx/nginx.conf
# 配置启动脚本
RUN echo "#!/bin/bash" > /hiauth/run.sh
RUN echo "java -jar /hiauth/app.jar & nginx -g 'daemon off;'" >> /hiauth/run.sh
# 设置权限
RUN chmod +x /hiauth/run.sh
# 暴露端口
EXPOSE 80 8080
# 设置容器启动时执行的命令
ENTRYPOINT ["/hiauth/run.sh"]
================================================
FILE: cicd/Jenkinsfile
================================================
pipeline {
agent any
// 设置环境变量
environment {
BUILD_VERSION = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
CONFIG_FILE = '/var/jenkins_home/hiauth/conf/hiauth.properties'
}
// 设置参数, 设置后,构建时会提示填写
// parameters {
// string(name: 'DEPLOY_ENV', defaultValue: 'dev', description: 'Environment to deploy to')
// }
stages {
stage('Check Env') {
steps {
sh 'echo ${BUILD_VERSION}'
sh 'docker -v'
sh 'java -version'
sh 'mvn -v'
sh 'node -v'
sh 'pnpm -v'
}
}
stage('Install') {
steps {
sh "mvn clean install -Dmaven.test.skip=false"
sh "cd hiauth-front && pnpm install --no-frozen-lockfile && pnpm build:auth"
// sh "cd docs && pnpm install && pnpm run build"
sh 'docker stop hiauth || true'
sh 'docker rm -f hiauth || true'
sh "docker rmi -f bestaone/hiauth:3.0.0 || true"
sh "docker build -f ./cicd/Dockerfile -t bestaone/hiauth:3.0.0 ."
sh """
docker run -d \
--restart=always \
-p 9080:80 \
-v /opt/install/hiauth/conf:/hiauth/conf \
-v /opt/install/hiauth/logs:/hiauth/logs \
--name hiauth bestaone/hiauth:3.0.0
"""
}
}
}
}
================================================
FILE: cicd/hiauth.properties
================================================
# login page title, default:
loginPage.title=\u7edf\u4e00\u8ba4\u8bc1\u4e2d\u5fc3
# login page static file name
loginPage.path=login
# login page default username
loginPage.username=corpadmin
# login page default password
loginPage.password=123456
# login page username placeholder
loginPage.usernamePlaceholder=\u4f01\u4e1a\u7ba1\u7406\u5458\uff1acorpadmin\u3001\u7cfb\u7edf\u7ba1\u7406\u5458\uff1aadmin
# login page password placeholder
loginPage.passwordPlaceholder=123456
# login types
loginPage.loginTypes=phone,account,wechat
# aliyun sms
aliyun.sms.accessKeyId=abcdefghi
aliyun.sms.accessKeySecret=jklmnopqrstuvwxyz
aliyun.sms.sign=HiAuth
aliyun.sms.smsTemplateCode=SMS_00000001
aliyun.sms.superSmsCode=888888
# supported wechat qrcode login, config in wx open platform
wechat.open.appid=wx9b04c3f125a24962
wechat.open.appSecret=abcdefghijklmnopqrstuvwxyz
wechat.open.redirectUri=http://127.0.0.1:8080/wechat/doLogin
wechat.open.style=black
wechat.open.href=
# only supported postgresql
datasource.type=com.alibaba.druid.pool.DruidDataSource
datasource.driverClassName=org.postgresql.Driver
datasource.url=jdbc:postgresql://127.0.0.1:5432/hiauth?stringtype=unspecified
datasource.username=test
datasource.password=123456
# redis
redis.host=127.0.0.1
redis.port=6379
redis.database=0
redis.username=
redis.password=
================================================
FILE: cicd/nginx.conf
================================================
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen 80;
# hiauth授权服务
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://127.0.0.1:8080/;
}
# hiauth管理后台静态页面
location /admin {
root /html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# hiauth管理后台接口服务
location /gateway/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://127.0.0.1:8080/;
rewrite "^/gateway/(.*)$" /$1 break;
}
# 文档
# location /docs {
# root /html;
# index index.html index.htm;
# try_files $uri $uri/ /index.html;
# }
}
}
================================================
FILE: docs/.postcssrc.json
================================================
{
"plugins": {
"postcss-rtlcss": {
"ltrPrefix": ":where([dir=\"ltr\"])",
"rtlPrefix": ":where([dir=\"rtl\"])"
}
}
}
================================================
FILE: docs/.vitepress/config/en.ts
================================================
import { createRequire } from 'module'
import { defineConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('../../package.json')
export const en = defineConfig({
lang: 'en-US',
description: 'Vite & Vue powered static site generator.',
themeConfig: {
nav: nav(),
sidebar: {
'/en/guide/': { base: '/en/guide/', items: sidebarGuide() },
'/en/other/': { base: '/en/other/', items: sidebarOther() }
},
editLink: {
pattern: 'https://github.com/bestaone/HiAuth/edit/main/docs/:path',
text: 'Edit this page on GitHub'
},
footer: {
message: 'Released under the MIT License.',
copyright: `Copyright © 2024-${new Date().getFullYear()} Earven`
}
}
})
function nav(): DefaultTheme.NavItem[] {
return [
{
text: 'Guide',
link: '/en/guide/what-is-hiauth',
activeMatch: '/en/guide/'
},
{
text: 'Other',
link: '/en/other/support',
activeMatch: '/en/other/'
},
{
text: pkg.version,
items: [
{
text: 'Changelog',
link: 'https://github.com/bestaone/HiAuth/blob/main/CHANGELOG.md'
},
{
text: 'Contributing',
link: 'https://github.com/bestaone/HiAuth/blob/main/.github/contributing.md'
}
]
}
]
}
function sidebarGuide(): DefaultTheme.SidebarItem[] {
return [
{
text: 'Introduction',
collapsed: false,
items: [
{ text: 'What is HiAuth', link: 'what-is-hiauth' },
{ text: 'Quick Start', link: 'quick-start' }
]
},
{
text: 'Deployment & Integration',
collapsed: false,
items: [
{ text: 'SaaS Edition', link: 'saas' },
{ text: 'Docker Edition', link: 'docker' },
{ text: 'Source Code Edition', link: 'sourcecode' },
{ text: 'hiauth-client Integration', link: 'hiauth-client' },
{ text: 'Deploying on K8S', link: 'k8s' },
{ text: 'Integration Testing', link: 'test' }
]
},
{
text: 'Secondary Development',
collapsed: false,
items: [
{ text: 'Project Structure', link: 'project' },
{ text: 'Front-end Development', link: 'frontend' },
{ text: 'Backend Development', link: 'backend' }
]
},
{
text: 'Other',
collapsed: false,
items: [
{ text: 'About Topic', link: 'about-topic' },
{ text: 'Issue', link: 'issue' }
]
}
]
}
function sidebarOther(): DefaultTheme.SidebarItem[] {
return [
{
text: 'other',
items: [
{ text: 'support', link: 'support' },
{ text: 'community', link: 'community' }
]
}
]
}
================================================
FILE: docs/.vitepress/config/index.ts
================================================
import { defineConfig } from 'vitepress'
import { shared } from './shared'
import { en } from './en'
import { zh } from './zh'
export default defineConfig({
...shared,
locales: {
root: { label: '简体中文', ...zh },
en: { label: 'English', ...en }
}
})
================================================
FILE: docs/.vitepress/config/shared.ts
================================================
import {defineConfig} from 'vitepress'
import {groupIconMdPlugin, groupIconVitePlugin, localIconLoader} from 'vitepress-plugin-group-icons'
import {search as zhSearch} from './zh'
export const shared = defineConfig({
title: 'HiAuth',
base: "/",
rewrites: {
'zh/:rest*': ':rest*'
},
lastUpdated: true,
cleanUrls: true,
metaChunk: true,
markdown: {
math: true,
codeTransformers: [
// We use `[!!code` in demo to prevent transformation, here we revert it back.
{
postprocess(code) {
return code.replace(/\[\!\!code/g, '[!code')
}
}
],
config(md) {
// TODO: remove when https://github.com/vuejs/vitepress/issues/4431 is fixed
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
const { localeIndex = 'root' } = env
const codeCopyButtonTitle = (() => {
switch (localeIndex) {
case 'zh':
return '复制代码'
case 'en':
return 'Copy code'
default:
return 'Copy code'
}
})()
return fence(tokens, idx, options, env, self).replace(
' ',
` `
)
}
md.use(groupIconMdPlugin)
}
},
sitemap: {
hostname: 'http://docs.hiauth.cn',
transformItems(items) {
return items.filter((item) => !item.url.includes('migration'))
}
},
/* prettier-ignore */
head: [
['link', { rel: 'icon', type: 'image/svg+xml', href: '/hiauth-logo-mini.svg' }],
['link', { rel: 'icon', type: 'image/png', href: '/hiauth-logo-mini.png' }],
['meta', { name: 'theme-color', content: '#5f67ee' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:locale', content: 'zh' }],
['meta', { property: 'og:title', content: 'HiAuth | 基于OAuth2.0协议的认证授权服务' }],
['meta', { property: 'og:site_name', content: 'VitePress' }],
['meta', { property: 'og:image', content: 'https://vitepress.dev/vitepress-og.jpg' }],
['meta', { property: 'og:url', content: 'http://docs.hiauth.cn' }],
['script', { src: 'https://cdn.usefathom.com/script.js', 'data-site': 'AZBRSFGG', 'data-spa': 'auto', defer: '' }]
],
themeConfig: {
logo: { src: '/hiauth-logo-mini.svg', width: 24, height: 24 },
socialLinks: [
{ icon: 'github', link: 'https://github.com/bestaone/HiAuth' }
],
search: {
provider: 'algolia',
options: {
appId: '8J64VVRP8K',
apiKey: '52f578a92b88ad6abde815aae2b0ad7c',
indexName: 'vitepress',
locales: {
...zhSearch
}
}
},
carbonAds: { code: 'CEBDT27Y', placement: 'vuejsorg' }
},
vite: {
plugins: [
groupIconVitePlugin({
customIcon: {
vitepress: localIconLoader(
import.meta.url,
'../../public/hiauth-logo-mini.svg'
),
firebase: 'logos:firebase'
}
})
]
}
})
================================================
FILE: docs/.vitepress/config/zh.ts
================================================
import { createRequire } from 'module'
import { defineConfig, type DefaultTheme } from 'vitepress'
const require = createRequire(import.meta.url)
const pkg = require('../../package.json')
export const zh = defineConfig({
lang: 'zh-Hans',
description: '基于OAuth2.0协议的认证授权服务',
themeConfig: {
nav: nav(),
sidebar: {
'/guide/': { base: '/guide/', items: sidebarGuide() },
'/other/': { base: '/other/', items: sidebarOther() }
},
editLink: {
pattern: 'https://github.com/bestaone/HiAuth/edit/main/docs/:path',
text: '在 GitHub 上编辑此页面'
},
footer: {
message: '基于 MIT 许可发布',
copyright: `版权所有 © 2024-${new Date().getFullYear()} 张国圣`
},
docFooter: {
prev: '上一页',
next: '下一页'
},
outline: {
label: '页面导航'
},
lastUpdated: {
text: '最后更新于',
formatOptions: {
dateStyle: 'short',
timeStyle: 'medium'
}
},
langMenuLabel: '多语言',
returnToTopLabel: '回到顶部',
sidebarMenuLabel: '菜单',
darkModeSwitchLabel: '主题',
lightModeSwitchTitle: '切换到浅色模式',
darkModeSwitchTitle: '切换到深色模式',
skipToContentLabel: '跳转到内容'
}
})
function nav(): DefaultTheme.NavItem[] {
return [
{
text: '文档',
link: '/guide/what-is-hiauth',
activeMatch: '/guide/'
},
{
text: '其他',
link: '/other/support',
activeMatch: '/other/'
},
{
text: pkg.version,
items: [
{
text: '更新日志',
link: 'https://github.com/bestaone/HiAuth/blob/main/CHANGELOG.md'
},
{
text: '参与贡献',
link: 'https://github.com/bestaone/HiAuth/blob/main/.github/contributing.md'
}
]
}
]
}
function sidebarGuide(): DefaultTheme.SidebarItem[] {
return [
{
text: '简介',
collapsed: false,
items: [
{ text: '什么是HiAuth', link: 'what-is-hiauth' },
{ text: '快速体验', link: 'quick-start' }
]
},
{
text: '部署&集成',
collapsed: false,
items: [
{ text: 'SaaS版', link: 'saas' },
{ text: 'Docker版', link: 'docker' },
{ text: '源码版', link: 'sourcecode' },
{ text: 'hiauth-client集成', link: 'hiauth-client' },
{ text: 'K8S上部署', link: 'k8s' },
{ text: '集成测试', link: 'test' }
]
},
{
text: '二次开发',
collapsed: false,
items: [
{ text: '项目结构', link: 'project' },
{ text: '前端开发', link: 'frontend' },
{ text: '后端开发', link: 'backend' }
]
},
{
text: '其他',
collapsed: false,
items: [
{ text: '相关资料', link: 'about-topic' },
{ text: '问题', link: 'issue' }
]
}
]
}
function sidebarOther(): DefaultTheme.SidebarItem[] {
return [
{
text: '其他',
items: [
{ text: '技术支持', link: 'support' },
{ text: '社区', link: 'community' }
]
}
]
}
export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = {
zh: {
placeholder: '搜索文档',
translations: {
button: {
buttonText: '搜索文档',
buttonAriaLabel: '搜索文档'
},
modal: {
searchBox: {
resetButtonTitle: '清除查询条件',
resetButtonAriaLabel: '清除查询条件',
cancelButtonText: '取消',
cancelButtonAriaLabel: '取消'
},
startScreen: {
recentSearchesTitle: '搜索历史',
noRecentSearchesText: '没有搜索历史',
saveRecentSearchButtonTitle: '保存至搜索历史',
removeRecentSearchButtonTitle: '从搜索历史中移除',
favoriteSearchesTitle: '收藏',
removeFavoriteSearchButtonTitle: '从收藏中移除'
},
errorScreen: {
titleText: '无法获取结果',
helpText: '你可能需要检查你的网络连接'
},
footer: {
selectText: '选择',
navigateText: '切换',
closeText: '关闭',
searchByText: '搜索提供者'
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
suggestedQueryText: '你可以尝试查询',
reportMissingResultsText: '你认为该查询应该有结果?',
reportMissingResultsLinkText: '点击反馈'
}
}
}
}
}
================================================
FILE: docs/.vitepress/theme/index.ts
================================================
import Theme from 'vitepress/theme'
import 'virtual:group-icons.css'
import './styles.css'
export default Theme
================================================
FILE: docs/.vitepress/theme/styles.css
================================================
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap');
:root:where(:lang(fa)) {
--vp-font-family-base:
'Vazirmatn', 'Inter', ui-sans-serif, system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#bd34fe 30%,
#41d1ff
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#bd34fe 50%,
#47caff 50%
);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
/* used in reference/default-theme-search */
img[src='/search.png'] {
width: 100%;
aspect-ratio: 1 / 1;
}
/* 屏蔽更新时间 */
.last-updated {
display: none !important;
}
================================================
FILE: docs/en/guide/about-topic.md
================================================
edit...
================================================
FILE: docs/en/guide/backend.md
================================================
edit...
================================================
FILE: docs/en/guide/docker.md
================================================
# Docker Edition {#docker}
The integration process for the Docker edition is similar to the SaaS edition, except that before integration, we need to deploy a Docker version of HiAuth. The local installation and deployment of the Docker edition rely on `PostgreSQL 16+` and `Redis`. If you need to run the HiAuth demo, you will also need a `JDK 17` and `Maven 3.8+` environment.
## Installation and Integration Steps:
- Check the server environment
- Initialize the HiAuth database by executing SQL scripts
- Configure the HiAuth startup configuration file
- Download the image and start the service
- Verify using the HiAuth source code demo
### Check the Server Environment {#check-env}
```shell
# Check the git version
$ git --version
git version 1.8.3.1
# Check the Docker version
$ docker -v
Docker version 26.1.4, build 5650f9b
# Check the JDK version
$ java -version
openjdk version "17.0.5" 2022-10-18
# Check the Maven version
$ mvn -v
Apache Maven 3.8.6 (36645f6c9b5079805ea5009217e36f2cffd34256)
```
### Initialize the HiAuth Database by Executing SQL Scripts {#init-db}
- Install `PostgreSQL 16+`;
- Create a database named `hiauth`;
- Execute the initialization script `HiAuth/other/hiauth.sql`;
### Configure the HiAuth Startup Configuration File {#config-file}
- Create the configuration file `/opt/install/hiauth/conf/hiauth.properties` and configure the correct parameters;
```properties [hiauth.properties]
# login page title, default:
loginPage.title=Welcome Login
# login page static file name
loginPage.path=login
# login page default username
loginPage.username=
# login page default password
loginPage.password=
# login page username placeholder
loginPage.usernamePlaceholder=
# login page password placeholder
loginPage.passwordPlaceholder=
# login types
loginPage.loginTypes=phone,account,wechat
# aliyun sms
aliyun.sms.accessKeyId=abcdefghi
aliyun.sms.accessKeySecret=abcdefghi
aliyun.sms.sign=HiAuth
aliyun.sms.smsTemplateCode=SMS_00000001
aliyun.sms.superSmsCode=888888
# supported wechat qrcode login, config in wx open platform
wechat.open.appid=wx123456789
wechat.open.appSecret=abcdefghijklmnopqrstuvwxyz
wechat.open.redirectUri=http://127.0.0.1:8080/wechat/doLogin
wechat.open.style=black
wechat.open.href=
# only supported postgresql
datasource.type=com.alibaba.druid.pool.DruidDataSource
datasource.driverClassName=org.postgresql.Driver
datasource.url=jdbc:postgresql://db_host:5432/hiauth?stringtype=unspecified
datasource.username=test
datasource.password=123456
# redis
redis.host=redis_host
redis.port=6379
redis.database=0
redis.username=test
redis.password=123456
```
> Configuration reference: [hiauth.properties](https://github.com/bestaone/HiAuth/blob/master/other/hiauth.properties)
### Download the Image and Start the Service {#download-image}
```shell
# You need to be able to access the Docker Hub central repository, which may require a proxy.
$ docker run -d \
--restart=always \
-p 9080:80 -p 8080:8080 \
-v /opt/install/hiauth/conf:/hiauth/conf \
-v /opt/install/hiauth/logs:/hiauth/logs \
--name hiauth bestaone/hiauth:3.0.0
# If you cannot access the Docker Hub central repository, you can download the image from the Alibaba Cloud repository.
$ docker run -d \
--restart=always \
-p 9080:80 -p 8080:8080 \
-v /opt/install/hiauth/conf:/hiauth/conf \
-v /opt/install/hiauth/logs:/hiauth/logs \
--name hiauth registry.cn-zhangjiakou.aliyuncs.com/bestaone/hiauth:3.0.0
# Check the images
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
bestaone/hiauth 3.0.0 c5e4140bd5aa 3 hours ago 810MB
registry-vpc.cn-zhangjiakou.aliyuncs.com/bestaone/hiauth 3.0.0 c5e4140bd5aa 3 hours ago 810MB
# Check the services
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3ea0fdb8a165 bestaone/hiauth:3.0.0 "/hiauth/run.sh" 3 hours ago Up 3 hours 8080/tcp, 0.0.0.0:9080->80/tcp, :::9080->80/tcp hiauth
# Check the logs
$ docker logs 3ea0fdb8a165
...
INFO 7 [main] org.springframework.boot.web.embedded.tomcat.TomcatWebServer Tomcat started on port 8080 (http) with context path '/'
INFO 7 [main] cn.hiauth.server.ServerStarter Started ServerStarter in 10.094 seconds (process running for 11.107)
...
# Access the service
$ curl http://127.0.0.1:9080
{ "code": 50000, "message": "Invalid or expired token" }
```
- The authorization address for the Docker edition of HiAuth is: http://127.0.0.1:9080
- The management address for the Docker edition of HiAuth is: http://127.0.0.1:9080/admin
### Verify Using the HiAuth Source Code Demo {#hiauth-himall}
- Download the source code
```shell
$ git clone https://github.com/bestaone/HiAuth.git
```
- Modify the configuration `HiAuth/example/himall/src/main/resources/application.yml`
```yaml
...
spring.security.oauth2.client:
provider:
hiauth-server:
# Change the value of issuer-uri from http://auth.hiauth.cn to http://127.0.0.1:9080
issuer-uri: http://auth.hiauth.cn
...
```
- Compile and run
```shell
$ cd HiAuth/example/himall
$ mvn clean install
$ mvn spring-boot:run
```
### Verification
- Open a browser and visit: http://127.0.0.1:9000
- Click the `Login` button, and you will be redirected to the unified authentication system. Enter the account: `corpadmin`, password: `123456`
- After successful login, you will see the home page and the logged-in user information!
## Video Tutorial
================================================
FILE: docs/en/guide/frontend.md
================================================
edit...
================================================
FILE: docs/en/guide/hiauth-client.md
================================================
edit...
================================================
FILE: docs/en/guide/issue.md
================================================
edit...
================================================
FILE: docs/en/guide/k8s.md
================================================
edit...
================================================
FILE: docs/en/guide/quick-start.md
================================================
# Quick Start {#quick-start}
## Admin Console {#admin}
Experience it on the [Admin End](http://auth.hiauth.cn/admin).
- **System Administrator Account**: `admin\123456`. This account can view all configurations. By switching tenants, you can view the configurations of the current tenant. To prevent unauthorized changes, this account has read-only access.
- **Corporate Administrator Account**: `corpadmin\123456`. This account can view all configurations within the tenant. To prevent unauthorized changes, this account has read-only access.
## Unified Authentication System {#auth}
Experience the [Authentication End](http://auth.hiauth.cn) for application login authorization. After integrating with third-party applications, you will be redirected to this page for authentication.
## Documentation {#docs}
Get help from the [Documentation](http://hiauth.cn).
================================================
FILE: docs/en/guide/saas.md
================================================
# SaaS Edition {#saas}
To integrate the SaaS edition, users need to sign up for an account on HiAuth, add applications, and perform relevant settings. Then, they can use the obtained `client-id` and `client-secret` for integration. For a quick validation, we will use the demo account provided in the system for integration. After completing the validation, users can replace it with their own account.
- The HiAuth authorization address is: http://auth.hiauth.cn
- The HiAuth management address is: http://auth.hiauth.cn/admin
## Example of Integrating with a SpringBoot Project
- Create a SpringBoot project
- Add dependencies
- Add configurations
- Add a home page
Integration with the SaaS edition can be completed with just these simple steps.
## Running the HiAuth Source Code Demo {#hiauth-demo}
### Environment Requirements
- Git
- JDK 17+
- Maven 3.8+
### Running Script
```shell
# Download HiAuth source code
$ git clone https://github.com/bestaone/HiAuth.git
# Enter the demo directory
$ cd HiAuth/example/himall
# Compile and install
$ mvn clean install
# Run
$ mvn spring-boot:run
```
### Verification
- Open a browser and visit: http://127.0.0.1:9000
- Click the `Login` button, and you will be redirected to the unified authentication system. Enter the account: `corpadmin`, password: `123456`
- After successful login, you will see the home page and the logged-in user information!
## Step-by-Step Integration {#hand-by-hand}
### Environment Requirements
- JDK 17+
- Maven 3.8+
### Create an Empty SpringBoot Project
Use [Spring Initializr](https://start.spring.io/) to create an empty SpringBoot project. The `pom.xml` file is as follows:
```xml [pom.xml]
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
demo
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
17
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
```
### Add Dependencies
```xml [pom.xml]
org.springframework.boot
spring-boot-starter-oauth2-client
```
### Add Configurations
```yml [application.yml]
server.port: 9000
spring.security.oauth2.client:
provider:
# Authentication server information
hiauth-server:
# If you have deployed HiAuth privately, replace this address with the private deployment authentication server address
issuer-uri: http://auth.hiauth.cn
authorizationUri: http://auth.hiauth.cn/oauth2/authorize
# Token acquisition address
tokenUri: http://auth.hiauth.cn/oauth2/token
userInfoUri: http://auth.hiauth.cn/userinfo
jwkSetUri: http://auth.hiauth.cn/oauth2/jwks
#userNameAttribute: name
registration:
hiauth-code:
# Authentication provider, indicating which authentication server to use for authentication, associated with the above hiauth-server
provider: hiauth-server
# Client name
client-name: himall
# Client ID, obtained from the authentication platform
client-id: himall
# Client secret
client-secret: secret
# Client authentication method client_secret_basic\client_secret_post
client-authentication-method: client_secret_basic
# Use authorization code mode to obtain token
authorization-grant-type: authorization_code
# Callback address after authentication, this address needs to be registered in the oauth2_registered_client table,
# otherwise the callback will be rejected
redirect-uri: http://127.0.0.1:9000/login/oauth2/code/hiauth-code
scope:
- profile
- openid
```
> Note: After adding the `application.yml` file, delete the `application.properties` file.
### Add a Controller
```java [IndexController.java]
package cn.hiauth.demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@RestController
public class IndexController {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${spring.security.oauth2.client.provider.hiauth-server.userInfoUri}")
private String userInfoUri;
@GetMapping("/")
public Map, ?> index(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
// After authentication, the accessToken will be obtained
String accessToken = oAuth2AuthorizedClient.getAccessToken().getTokenValue();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// Set the request header and include the accessToken in it
headers.add("Authorization", "Bearer " + accessToken);
MultiValueMap map = new LinkedMultiValueMap<>();
HttpEntity> entity = new HttpEntity<>(map, headers);
// Request the authentication server to obtain user information
return restTemplate.postForObject(this.userInfoUri, entity, Map.class);
}
}
```
### Verification
- Start the project and visit: http://127.0.0.1:9000. The system will detect that you are not logged in and redirect you to the unified authentication system;
- Enter the account: `corpadmin`, password: `123456`. After successful login, you will be redirected back to the application;
- In this demo, the home page directly outputs the logged-in user information in `json` format;
```json
{
"sub": "corpadmin",
"empId": 1,
"avatarUrl": "/unpapi/image/2c924149ddfe4bd181959ee9bede10c0.jpeg",
"appId": 91,
"name": "Corporate Administrator",
"phoneNum": "13400000001",
"userId": 11,
"authorities": [],
"cid": 1,
"username": "corpadmin"
}
```
## Video Tutorial
================================================
FILE: docs/en/guide/sourcecode.md
================================================
# Source Code Edition {#sourcecode}
The integration process for the source code edition is similar to the Docker edition, except that the Docker version of HiAuth is replaced with a source code compilation and startup.
## Environment Requirements {#env-demand}
- Git
- JDK 17+
- Maven 3.8+
- Node.js v20.15+
- pnpm 10.10+
- PostgreSQL 16+
- Redis
## Installation and Integration Steps
- Check the server environment
- Download the source code
- Initialize the HiAuth database by executing SQL scripts
- Configure the HiAuth startup configuration file
- Compile the source code and start the service
- Verify using the HiAuth source code demo
### Check the Server Environment {#check-env}
```shell
# Check the git version
$ git --version
git version 1.8.3.1
# Check the JDK version
$ java -version
openjdk version "17.0.5" 2022-10-18
# Check the Maven version
$ mvn -v
Apache Maven 3.8.6 (36645f6c9b5079805ea5009217e36f2cffd34256)
# Check the Node.js version
$ node -v
v20.15.0
# Check the pnpm version
$ pnpm -v
9.14.2
```
### Download the Source Code {#download-source}
```shell
$ git clone https://github.com/bestaone/HiAuth.git
```
### Initialize the HiAuth Database by Executing SQL Scripts {#init-db}
- Install `PostgreSQL 16+`;
- Create a database named `hiauth`;
- Execute the initialization script `HiAuth/other/hiauth.sql`;
### Configure the HiAuth Startup Configuration File {#config-file}
- Modify the configuration file `HiAuth/cicd/hiauth.properties` to your own settings;
```properties [hiauth.properties]
# Only PostgreSQL is supported
datasource.type=com.alibaba.druid.pool.DruidDataSource
datasource.driverClassName=org.postgresql.Driver
datasource.url=jdbc:postgresql://db_host:5432/hiauth
datasource.username=test
datasource.password=123456
redis.host=redis_host
redis.port=6379
redis.database=0
redis.username=test
redis.password=123456
```
- Apply the configuration file by modifying `HiAuth/hiauth-server/src/main/resources/application.yml`;
```yaml
...
# Replace `/hiauth/conf/hiauth.properties` with the file path you configured above
spring.config.import: ${CONFIG_FILE:optional:/hiauth/conf/hiauth.properties}
...
```
### Compile the Source Code and Start the Service {#compile-start}
```shell
# Compile and start the backend service
$ cd HiAuth/hiauth-server
$ mvn clean install
$ mvn spring-boot:run
# Compile and start the frontend service
$ cd HiAuth/hiauth-front
$ pnpm install
$ pnpm dev:auth
# Access the service
$ curl http://127.0.0.1:8080
{ "code": 50000, "message": "Invalid or expired token" }
```
- Check the backend service: http://127.0.0.1:8080
- Check the frontend service: http://127.0.0.1:5666/admin (The port may change, please check the console)
### Verify Using the HiAuth Source Code Demo {#hiauth-himall}
- Modify the configuration `HiAuth/example/himall/src/main/resources/application.yml`
```yaml
...
spring.security.oauth2.client:
provider:
hiauth-server:
# Change the value of issuer-uri from http://auth.hiauth.cn to http://127.0.0.1:8080
issuer-uri: http://auth.hiauth.cn
...
```
- Compile and run
```shell
$ cd HiAuth/example/himall
$ mvn clean install
$ mvn spring-boot:run
```
### Verification
- Open a browser and visit: http://127.0.0.1:9000
- Click the `Login` button, and you will be redirected to the unified authentication system. Enter the account: `corpadmin`, password: `123456`
- After successful login, you will see the home page and the logged-in user information!
## Video Tutorial
================================================
FILE: docs/en/guide/test.md
================================================
edit...
================================================
FILE: docs/en/guide/what-is-hiauth.md
================================================
# What is HiAuth? {#what-is-hiauth}
HiAuth is an authentication and authorization system based on the OAuth2.0 protocol. By integrating the HiAuth system, you can quickly enable authentication and authorization functions for your application. HiAuth supports SaaS mode, Docker private deployment mode, and source code compilation installation mode. Moreover, it is completely free and open-source.
Want to give it a try? Jump to [Quick Start](./quick-start).
## Functions {#function}
### **1. Authentication**
Based on the `OAuth2.0` authorization protocol, it allows third-party applications to access user resources with user authorization, without sharing user credentials.
- **Core Process**:
- **Authorization Code Mode**: Suitable for web applications, obtaining an access token through an authorization code exchange (most secure).
- **Implicit Mode**: Suitable for Single Page Applications (SPAs), directly returning an access token (no refresh token).
- **Password Mode**: Users directly provide their account and password to the client (for use only in trusted scenarios).
- **Client Credentials Mode**: Applications obtain tokens directly with their own identity (for service-to-service communication).
- **Token Management**:
- Access Token is used for resource access, with a relatively short validity period.
- Refresh Token is used to obtain a new access token and must be securely stored.
- **Security**:
- Enforces HTTPS transmission to prevent token leakage.
- Token Binding to prevent man-in-the-middle attacks.
### **2. Access Authorization**
Access authorization (`Authorization`) determines whether users/applications can perform specific operations or access resources. It is based on `RBAC` (Role-Based Access Control), assigning permissions through roles (e.g., administrator, regular user).
### **3. Application Management**
Manages the integration and lifecycle of third-party or internal applications.
- **Core Functions**:
- **Application Registration**: Assigns unique `ClientID`/`ClientSecret`, defining permission scopes.
- **Key Rotation**: Regularly updates `ClientSecret` to prevent leakage risks.
- **Permission Control**: Limits the API or data scope that applications can access.
- **Security Auditing**:
- Logs application behavior (e.g., API call frequency).
- Reviews application permission requests to ensure the principle of least privilege.
### **4. Organization Management**
Manages the hierarchical structure and member relationships of enterprises or teams.
- **Functions**:
- **Multi-level Architecture**: Supports nested structures such as departments, teams, and project groups.
- **Multi-tenancy Support**: Isolates data and permissions of different organizations (e.g., in SaaS scenarios).
- **Administrator Assignment**: Sets organization administrators to manage members and permissions.
### **5. Permission Management**
Defines and manages the operations that users or roles can perform.
- **Core Mechanism**:
- **Permission Granularity**: Supports control at the API level, functional level, or data field level.
- **Permission Groups**: Packages permissions into templates (e.g., "Finance Approval Group").
- **Permission Inheritance and Overrides**: Sub-roles inherit permissions from parent roles, with the ability to make local adjustments.
- **Audit and Compliance**:
- Permission change logs: Records who modified permissions and when.
- Regularly reviews permission assignments to avoid redundancy or excessive authorization.
### **6. User Management**
Manages user identities, authentication, and lifecycle.
- **Core Functions**:
- **Identity Storage**: Supports local databases.
- **Lifecycle Management**: User registration, disabling, deletion, and profile updates.
- **Authentication Enhancement**: Multi-Factor Authentication (MFA), Single Sign-On (SSO).
## Video Tutorial
================================================
FILE: docs/en/index.md
================================================
---
layout: home
title: HiAuth
titleTemplate: Authentication and Authorization Service Based on OAuth2.0 Protocol
hero:
name: HiAuth
text: Authentication and Authorization Service Based on OAuth2.0 Protocol
tagline: With just a few simple steps, you can integrate into your application
actions:
- theme: brand
text: Quick Start ->
link: /zh/guide/what-is-vitepress
- theme: alt
text: Online Preview
link: http://auth.hiauth.cn
- theme: alt
text: GitHub
link: https://github.com/bestaone/HiAuth
image:
src: /hiauth-large.png
alt: HiAuth
features:
- icon: 📝
title: Login authentication
details: Single sign on, multi login, single end login, mutually exclusive login, no login Multiple login strategies can be configured.
- icon: 🔐
title: Rights Management
details: Permission authentication, role management, permission management, menu management, session management, interface authentication Multiple flexible authentication schemes.
- icon: 🌐
title: OAuth2.0 protocol
details: Authentication service based on OAuth2.0 protocol, supporting four authorization modes for easy integration with different development language environments.
- icon: 🚀
title: Out of box
details: Provide SAAS version, one click deployment Docker version, and source code local deployment version, which can be developed based on source code for secondary development.
---
================================================
FILE: docs/lunaria.config.json
================================================
{
"$schema": "./node_modules/@lunariajs/core/config.schema.json",
"repository": {
"name": "bestaone/HiAuth",
"rootDir": "docs"
},
"files": [
{
"location": ".vitepress/config/{en,zh,pt,ru,es,ko,fa}.ts",
"pattern": ".vitepress/config/@lang.ts",
"type": "universal"
},
{
"location": "**/*.md",
"pattern": "@lang/@path",
"type": "universal"
}
],
"defaultLocale": {
"label": "简体中文",
"lang": "zh"
},
"locales": [
{
"label": "English",
"lang": "en"
}
],
"outDir": ".vitepress/dist/_translations",
"ignoreKeywords": ["lunaria-ignore"]
}
================================================
FILE: docs/package.json
================================================
{
"name": "docs",
"version": "3.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vitepress dev",
"build": "vitepress build",
"preview": "vitepress preview",
"lunaria:build": "lunaria build",
"lunaria:open": "open-cli .vitepress/dist/_translations/index.html"
},
"devDependencies": {
"@lunariajs/core": "^0.1.1",
"markdown-it-mathjax3": "^4.3.2",
"open-cli": "^8.0.0",
"postcss-rtlcss": "^5.6.0",
"vitepress": "2.0.0-alpha.3",
"vitepress-plugin-group-icons": "^1.3.6"
}
}
================================================
FILE: docs/public/pure.html
================================================
Plain HTML page | VitePress
Not part of the main VitePress docs site
This page is plain HTML in the public directory.
================================================
FILE: docs/zh/guide/about-topic.md
================================================
编辑中...
================================================
FILE: docs/zh/guide/backend.md
================================================
编辑中...
================================================
FILE: docs/zh/guide/docker.md
================================================
# Docker版 {#docker}
Docker版的集成过程和SaaS版类似,只不过在集成前,我们需要部署一个Docker版本的HiAuth。Docker版的本地安装部署,依赖`PostgreSQL16+`、`Redis`,如果需要运行HiAuth自带的Demo的话,还需要 `JDK17` 和 `Maven 3.8+`环境。
## 安装及集成步骤:
- 检查服务器环境
- 初始化HiAuth数据库,执行SQL脚本
- 配置HiAuth的启动配置文件
- 下载镜像、启动服务
- 使用HiAuth源码Demo验证
### 检查服务器环境 {#check-env}
```shell
# 检查git版本
$ git --version
git version 1.8.3.1
# 检查daocker版本
$ docker -v
Docker version 26.1.4, build 5650f9b
# 检查JDK版本
$ java -version
openjdk version "17.0.5" 2022-10-18
# 检查maven版本
$ mvn -v
Apache Maven 3.8.6 (36645f6c9b5079805ea5009217e36f2cffd34256)
```
### 初始化HiAuth数据库,执行SQL脚本 {#init-db}
- 安装`PostgreSQL16+`;
- 创建数据库`hiauth`;
- 执行初始化脚本 `HiAuth/other/hiauth.sql` ;
### 配置HiAuth的启动配置文件 {#config-file}
- 创建配置文件 `/opt/install/hiauth/conf/hiauth.properties`,并配置正确的参数;
```properties [hiauth.properties]
# login page title, default:
loginPage.title=Welcome Login
# login page static file name
loginPage.path=login
# login page default username
loginPage.username=
# login page default password
loginPage.password=
# login page username placeholder
loginPage.usernamePlaceholder=
# login page password placeholder
loginPage.passwordPlaceholder=
# login types
loginPage.loginTypes=phone,account,wechat
# aliyun sms
aliyun.sms.accessKeyId=abcdefghi
aliyun.sms.accessKeySecret=abcdefghi
aliyun.sms.sign=HiAuth
aliyun.sms.smsTemplateCode=SMS_00000001
aliyun.sms.superSmsCode=888888
# supported wechat qrcode login, config in wx open platform
wechat.open.appid=wx123456789
wechat.open.appSecret=abcdefghijklmnopqrstuvwxyz
wechat.open.redirectUri=http://127.0.0.1:8080/wechat/doLogin
wechat.open.style=black
wechat.open.href=
# only supported postgresql
datasource.type=com.alibaba.druid.pool.DruidDataSource
datasource.driverClassName=org.postgresql.Driver
datasource.url=jdbc:postgresql://db_host:5432/hiauth?stringtype=unspecified
datasource.username=test
datasource.password=123456
# redis
redis.host=redis_host
redis.port=6379
redis.database=0
redis.username=test
redis.password=123456
```
> 配置参考:[hiauth.properties](https://github.com/bestaone/HiAuth/blob/master/other/hiauth.properties)
### 下载镜像、启动服务 {#download-image}
```shell
# 需要能够访问dockerhub中央仓库,可能需要梯子
$ docker run -d \
--restart=always \
-p 9080:80 -p 8080:8080 \
-v /opt/install/hiauth/conf:/hiauth/conf \
-v /opt/install/hiauth/logs:/hiauth/logs \
--name hiauth bestaone/hiauth:3.0.0
# 如果无法访问dockerhub中央仓库,可以从阿里云仓库下载镜像
$ docker run -d \
--restart=always \
-p 9080:80 -p 8080:8080 \
-v /opt/install/hiauth/conf:/hiauth/conf \
-v /opt/install/hiauth/logs:/hiauth/logs \
--name hiauth registry.cn-zhangjiakou.aliyuncs.com/bestaone/hiauth:3.0.0
# 查看镜像
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
bestaone/hiauth 3.0.0 c5e4140bd5aa 3 hours ago 810MB
registry-vpc.cn-zhangjiakou.aliyuncs.com/bestaone/hiauth 3.0.0 c5e4140bd5aa 3 hours ago 810MB
# 查看服务
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3ea0fdb8a165 bestaone/hiauth:3.0.0 "/hiauth/run.sh" 3 hours ago Up 3 hours 8080/tcp, 0.0.0.0:9080->80/tcp, :::9080->80/tcp hiauth
# 查看日志
$ docker logs 3ea0fdb8a165
...
INFO 7 [main] org.springframework.boot.web.embedded.tomcat.TomcatWebServer Tomcat started on port 8080 (http) with context path '/'
INFO 7 [main] cn.hiauth.server.ServerStarter Started ServerStarter in 10.094 seconds (process running for 11.107)
...
# 访问服务
$ curl http://127.0.0.1:9080
{ "code": 50000, "message": "令牌无效或已过期" }
```
- Docker 版 HiAuth 授权地址为:http://127.0.0.1:9080
- Docker 版 HiAuth 管理地址为:http://127.0.0.1:9080/admin
### 使用HiAuth源码Demo验证 {#hiauth-himall}
- 下载源码
```shell
$ git clone https://github.com/bestaone/HiAuth.git
```
- 修改配置`HiAuth/example/himall/src/main/resources/application.yml`
```yaml
...
spring.security.oauth2.client:
provider:
hiauth-server:
# 将 issuer-uri 的值从 http://auth.hiauth.cn 改为 http://127.0.0.1:9080
issuer-uri: http://auth.hiauth.cn
...
```
- 编译运行
```shell
$ cd HiAuth/example/himall
$ mvn clean install
$ mvn spring-boot:run
```
### 验证
- 浏览器访问: http://127.0.0.1:9000
- 点击`Login`按钮,会被重定向到统一认证系统,输入账号:`corpadmin`,密码:`123456`
- 登录成功后,会看到首页及登录用户信息!
## 视频教程
================================================
FILE: docs/zh/guide/frontend.md
================================================
编辑中...
================================================
FILE: docs/zh/guide/hiauth-client.md
================================================
编辑中...
================================================
FILE: docs/zh/guide/issue.md
================================================
编辑中...
================================================
FILE: docs/zh/guide/k8s.md
================================================
编辑中...
================================================
FILE: docs/zh/guide/quick-start.md
================================================
# 快速体验 {#quikc-start}
## 管理后台 {#admin}
在 [Admin端](http://auth.hiauth.cn/admin) 上体验。
- 系统管理员账号:admin\123456,可以看到所有的配置。通过切换租户,可以查看到当前租户的配置。为了防止乱改数据,此账号只有只读权限。
- 企业管理员账号:corpadmin\123456,可以看到租户内所有的配置。为了防止乱改数据,此账号只有只读权限。
## 统一认证系统 {#auth}
体验 [认证端](http://auth.hiauth.cn) 授权应用登录。三方应用集成后,会被重定向到此页面进行登录认证。
## 文档 {#docs}
在 [文档](http://hiauth.cn) 中获取帮助。
================================================
FILE: docs/zh/guide/saas.md
================================================
# SaaS版 {#saas}
SaaS版的集成,需要用户在HiAuth上开通账号、添加应,并进行相关的设置后,使用你获取的 `client-id` 和 `client-secret` 进行集成。这里为了快速验证,我们使用系统中提供的demo账号来进行集成。等完成验证后,用户可以替换成自己的账号即可。
- HiAuth 授权地址为:http://auth.hiauth.cn
- HiAuth 管理地址为:http://auth.hiauth.cn/admin
## 以SpringBoot项目为例,我们的集成需要以下几步:
- 创建一个SpringBoot项目
- 添加依赖
- 添加配置
- 添加一个首页
只需以上简单几步就可以完成集成SaaS版。
## 运行HiAuth源码自带demo {#hiauth-demo}
### 环境要求
- Git
- JDK 17+
- Maven 3.8+
### 运行脚本
```shell
# 下载HiAuth源码
$ git clone https://github.com/bestaone/HiAuth.git
# 进入demo
$ cd HiAuth/example/himall
# 编译和安装
$ mvn clean install
# 运行
$ mvn spring-boot:run
```
### 验证
- 浏览器访问: http://127.0.0.1:9000
- 点击`Login`按钮,会被重定向到统一认证系统,输入账号:`corpadmin`,密码:`123456`
- 登录成功后,会看到首页及登录用户信息!
## 手把手集成 {#hand-by-hand}
### 环境要求
- JDK 17+
- Maven 3.8+
### 创建一个空的SpringBoot项目
使用 [Spring Initializr](https://start.spring.io/) 创建一个空的SpringBoot项目。pom.xml 文件如下:
```xml [pom.xml]
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
demo
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
17
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
```
### 添加依赖
```xml [pom.xml]
org.springframework.boot
spring-boot-starter-oauth2-client
```
### 添加配置
```yml [application.yml]
server.port: 9000
spring.security.oauth2.client:
provider:
#认证服务器信息
hiauth-server:
# 如果你私有化部署了 HiAuth服务,请将此地址替换为私有部署的认证服务器地址
issuer-uri: http://auth.hiauth.cn
authorizationUri: http://auth.hiauth.cn/oauth2/authorize
#令牌获取地址
tokenUri: http://auth.hiauth.cn/oauth2/token
userInfoUri: http://auth.hiauth.cn/userinfo
jwkSetUri: http://auth.hiauth.cn/oauth2/jwks
#userNameAttribute: name
registration:
hiauth-code:
#认证提供者,标识由哪个认证服务器进行认证,和上面的hiauth-server进行关联
provider: hiauth-server
#客户端名称
client-name: himall
#客户端id,从认证平台申请的客户端id
client-id: himall
#客户端秘钥
client-secret: secret
#客户端认证方式 client_secret_basic\client_secret_post
client-authentication-method: client_secret_basic
#使用授权码模式获取令牌(token)
authorization-grant-type: authorization_code
# 认证完成后回调的地址,需要在数据库表oauth2_registered_client中登记这个地址,
# 否则会拒绝回调
redirect-uri: http://127.0.0.1:9000/login/oauth2/code/hiauth-code
scope:
- profile
- openid
```
> 注意:添加完成 `application.yml` 文件后,将 `application.properties` 文件删除掉
### 添加一个Controller
```java [IndexController.java]
package cn.hiauth.demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@RestController
public class IndexController {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${spring.security.oauth2.client.provider.hiauth-server.userInfoUri}")
private String userInfoUri;
@GetMapping("/")
public Map, ?> index(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
// 认证完后会获取到 accessToken
String accessToken = oAuth2AuthorizedClient.getAccessToken().getTokenValue();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 设置请求头,将 accessToken 放入请求头中
headers.add("Authorization", "Bearer " + accessToken);
MultiValueMap map = new LinkedMultiValueMap<>();
HttpEntity> entity = new HttpEntity<>(map, headers);
// 请求认证服务器获取用户信息
return restTemplate.postForObject(this.userInfoUri, entity, Map.class);
}
}
```
### 验证
- 启动项目,访问: http://127.0.0.1:9000,系统检测到未登录,会被重定向到统一认证系统;
- 输入账号:`corpadmin`,密码:`123456`,登录成功后,会跳转回应用;
- 这个demo中。首页直接输出了登录用户信息,以`json`格式;
```json
{
"sub": "corpadmin",
"empId": 1,
"avatarUrl": "/unpapi/image/2c924149ddfe4bd181959ee9bede10c0.jpeg",
"appId": 91,
"name": "企业管理员",
"phoneNum": "13400000001",
"userId": 11,
"authorities": [],
"cid": 1,
"username": "corpadmin"
}
```
## 视频教程
================================================
FILE: docs/zh/guide/sourcecode.md
================================================
# 源码版 {#sourcecode}
源码版的集成过程和Docker版类似,只不过将Docker版本的HiAuth改为源码编译启动。
## 环境要求 {#env-demand}
- Git
- JDK 17+
- Maven 3.8+
- Nodejs v20.15+
- pnpm 10.10+
- PostgreSQL16+
- Redis
## 安装及集成步骤
- 检查服务器环境
- 下载源码
- 初始化HiAuth数据库,执行SQL脚本
- 配置HiAuth的启动配置文件
- 编译源码、启动服务
- 使用HiAuth源码Demo验证
### 检查服务器环境 {#check-env}
```shell
# 检查git版本
$ git --version
git version 1.8.3.1
# 检查JDK版本
$ java -version
openjdk version "17.0.5" 2022-10-18
# 检查maven版本
$ mvn -v
Apache Maven 3.8.6 (36645f6c9b5079805ea5009217e36f2cffd34256)
# 检查node版本
$ node -v
v20.15.0
# 检查pnpm版本
$ pnpm -v
9.14.2
```
### 下载源码 {#download-source}
```shell
$ git clone https://github.com/bestaone/HiAuth.git
```
### 初始化HiAuth数据库,执行SQL脚本 {#init-db}
- 安装`PostgreSQL16+`;
- 创建数据库`hiauth`;
- 执行初始化脚本 `HiAuth/other/hiauth.sql` ;
### 配置HiAuth的启动配置文件 {#config-file}
- 修改配置文件 `HiAuth/cicd/hiauth.properties`,改成你自己的配置;
```properties [hiauth.properties]
# only supported postgresql
datasource.type=com.alibaba.druid.pool.DruidDataSource
datasource.driverClassName=org.postgresql.Driver
datasource.url=jdbc:postgresql://db_host:5432/hiauth
datasource.username=test
datasource.password=123456
redis.host=redis_host
redis.port=6379
redis.database=0
redis.username=test
redis.password=123456
```
- 应用配置文件,修改`HiAuth/hiauth-server/src/main/resources/application.yml`;
```yaml
...
# 将 /hiauth/conf/hiauth.properties 替换为你上面配置的文件路径
spring.config.import: ${CONFIG_FILE:optional:/hiauth/conf/hiauth.properties}
...
```
### 编译源码、启动服务 {#compile-start}
```shell
# 编译、启动服务端
$ cd HiAuth/hiauth-server
$ mvn clean install
$ mvn spring-boot:run
# 编译、启动前端
$ cd HiAuth/hiauth-front
$ pnpm install
$ pnpm dev:auth
# 访问服务
$ curl http://127.0.0.1:8080
{ "code": 50000, "message": "令牌无效或已过期" }
```
- 检查后端服务:http://127.0.0.1:8080
- 检查前端服务:http://127.0.0.1:5666/admin (端口可能会变化,请自行查看控制台)
### 使用HiAuth源码Demo验证 {#hiauth-himall}
- 修改配置`HiAuth/example/himall/src/main/resources/application.yml`
```yaml
...
spring.security.oauth2.client:
provider:
hiauth-server:
# 将 issuer-uri 的值从 http://auth.hiauth.cn 改为 http://127.0.0.1:8080
issuer-uri: http://auth.hiauth.cn
...
```
- 编译运行
```shell
$ cd HiAuth/example/himall
$ mvn clean install
$ mvn spring-boot:run
```
### 验证
- 浏览器访问: http://127.0.0.1:9000
- 点击`Login`按钮,会被重定向到统一认证系统,输入账号:`corpadmin`,密码:`123456`
- 登录成功后,会看到首页及登录用户信息!
## 视频教程
================================================
FILE: docs/zh/guide/test.md
================================================
编辑中...
================================================
FILE: docs/zh/guide/what-is-hiauth.md
================================================
# HiAuth是什么? {#what-is-hiauth}
HiAuth是一个基于OAuth2.0协议的认证授权系统,通过集成HiAuth系统可以快速的开启应用的认证和授权功能。HiAuth支持SaaS模式、Docker私有化部署模式、源码编译安装模式。而且完全免费,完全开源。
想尝试一下?跳到 [快速开始](./quick-start)。
## 功能 {#function}
### **1. 登录认证**
基于`OAuth2.0`授权协议,允许第三方应用在用户授权下访问其资源,而无需共享用户凭证。
- **核心流程**:
- **授权码模式**:适用于Web应用,通过授权码交换访问令牌(最安全)。
- **隐式模式**:适用于单页应用(SPA),直接返回访问令牌(无刷新令牌)。
- **密码模式**:用户直接提供账号密码给客户端(仅信任场景使用)。
- **客户端凭证模式**:应用直接以自身身份获取令牌(用于服务间通信)。
- **令牌管理**:
- 访问令牌(Access Token)用于资源访问,有效期较短。
- 刷新令牌(Refresh Token)用于获取新访问令牌,需安全存储。
- **安全性**:
- 强制HTTPS传输,防止令牌泄露。
- 令牌绑定(Token Binding)防止中间人攻击。
### **2. 访问授权**
访问授权(`Authorization`)决定用户/应用能否执行特定操作或访问资源。基于`RBAC`(基于角色的访问控制),通过角色分配权限(如管理员、普通用户)。
### **3. 应用管理**
管理第三方或内部应用的接入与生命周期。
- **核心功能**:
- **应用注册**:分配唯一`ClientID`/`ClientSecret`,定义权限范围`Scopes`。
- **密钥轮换**:定期更新`ClientSecret`,防止泄露风险。
- **权限控制**:限制应用可访问的API或数据范围。
- **安全审计**:
- 记录应用行为日志(如API调用频率)。
- 审核应用权限申请,确保最小权限原则。
### **4. 组织管理**
管理企业或团队的层级结构与成员关系。
- **功能**:
- **多层级架构**:支持部门、团队、项目组等嵌套结构。
- **多租户支持**:隔离不同组织的数据与权限(如SaaS场景)。
- **管理员分配**:设置组织管理员,管理成员与权限。
### **5. 权限管理**
定义和管理用户或角色可执行的操作。
- **核心机制**:
- **权限粒度**:支持API级、功能级或数据字段级控制。
- **权限组**:将权限打包为模板(如“财务审批组”)。
- **权限继承与覆盖**:子角色继承父角色权限,可局部调整。
- **审计与合规**:
- 权限变更日志:记录谁在何时修改了权限。
- 定期审查权限分配,避免冗余或过度授权。
### **6. 用户管理**
管理用户身份、认证与生命周期。
- **核心功能**:
- **身份存储**:支持本地数据库。
- **生命周期管理**:用户注册、禁用、删除及资料更新。
- **认证增强**:多因素认证(MFA)、单点登录(SSO)。
## 视频教程
================================================
FILE: docs/zh/index.md
================================================
---
layout: home
title: HiAuth
titleTemplate: 基于OAuth2.0协议的认证授权服务
hero:
name: HiAuth
text: 基于OAuth2.0协议的认证授权服务
tagline: 只需简单几步,即可将认证授权功能集成到你的应用中
actions:
- theme: brand
text: 快速开始 ->
link: /guide/what-is-hiauth
- theme: alt
text: 在线预览
link: http://auth.hiauth.cn/admin
- theme: alt
text: GitHub
link: https://github.com/bestaone/HiAuth
image:
src: /hiauth-large.png
alt: HiAuth
features:
- icon: 📝
title: 登录认证
details: 单点登录、多端登录、单端登录、互斥登录、免登录... 多种登录策略可配置。
- icon: 🔐
title: 权限管理
details: 权限认证、角色管理、权限管理、菜单管理、会话管理、接口鉴权... 多种方案灵活鉴权。
- icon: 🌐
title: OAuth2.0协议
details: 基于OAuth2.0协议的认证服务,支持四种授权模式,方便与不同开发语言环境对接。
- icon: 🚀
title: 开箱即用
details: 提供SaaS版、一键部署Docker版、源码本地部署版,可以基于源码二次开发,免费、开源。
---
================================================
FILE: example/demo/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
demo
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
17
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-oauth2-client
org.springframework.boot
spring-boot-maven-plugin
================================================
FILE: example/demo/src/main/java/cn/hiauth/demo/DemoApplication.java
================================================
package cn.hiauth.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
================================================
FILE: example/demo/src/main/java/cn/hiauth/demo/IndexController.java
================================================
package cn.hiauth.demo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@RestController
public class IndexController {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${spring.security.oauth2.client.provider.hiauth-server.userInfoUri}")
private String userInfoUri;
@GetMapping("/")
public Map, ?> index(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient, @AuthenticationPrincipal OidcUser oidcUser) {
// 认证完后会获取到 accessToken
String accessToken = oAuth2AuthorizedClient.getAccessToken().getTokenValue();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 设置请求头,将 accessToken 放入请求头中
headers.add("Authorization", "Bearer " + accessToken);
MultiValueMap map = new LinkedMultiValueMap<>();
HttpEntity> entity = new HttpEntity<>(map, headers);
// 请求认证服务器获取用户信息
return restTemplate.postForObject(this.userInfoUri, entity, Map.class);
}
}
================================================
FILE: example/demo/src/main/resources/application.yml
================================================
server.port: 9000
logging.level:
root: DEBUG
spring.security.oauth2.client:
provider:
#认证服务器信息
hiauth-server:
# 如果你私有化部署了 HiAuth服务,请将此地址替换为私有部署的认证服务器地址
issuer-uri: http://auth.hiauth.cn
authorizationUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/authorize
tokenUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/token
userInfoUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/userinfo
jwkSetUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/jwks
#userNameAttribute: name
registration:
hiauth-code:
#认证提供者,标识由哪个认证服务器进行认证,和上面的hiauth-server进行关联
provider: hiauth-server
client-name: himall
client-id: himall
client-secret: secret
#客户端认证方式 client_secret_basic\client_secret_post
client-authentication-method: client_secret_basic
#使用授权码模式获取令牌(token)
authorization-grant-type: authorization_code
# 认证完成后回调的地址,需要在数据库表oauth2_registered_client中登记这个地址,
# 否则会拒绝回调
redirect-uri: http://127.0.0.1:9000/login/oauth2/code/hiauth-code
scope: openid,profile
================================================
FILE: example/hiauth-client/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
hiauth-client
1.0.0-SNAPSHOT
hiauth-client
集成HiAuth认证系统
17
org.springframework.boot
spring-boot-devtools
true
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-thymeleaf
org.projectlombok
lombok
org.webjars
webjars-locator-core
org.webjars
bootstrap
3.3.7
org.webjars
jquery
3.6.3
org.springframework.boot
spring-boot-starter-data-redis
cn.hiauth
hiauth-client-spring-boot-starter
1.0.9
hiauth-client
src/main/resources
true
org.apache.maven.plugins
maven-resources-plugin
UTF-8
org.springframework.boot
spring-boot-maven-plugin
package
repackage
org.apache.maven.plugins
maven-compiler-plugin
17
17
UTF-8
================================================
FILE: example/hiauth-client/src/main/java/cn/hiauth/hiauthclient/HiauthClientStarter.java
================================================
package cn.hiauth.hiauthclient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackages = {"cn.hiauth.client", "cn.hiauth.hiauthclient"})
@SpringBootApplication
public class HiauthClientStarter {
public static void main(String[] args) {
SpringApplication.run(HiauthClientStarter.class, args);
}
}
================================================
FILE: example/hiauth-client/src/main/java/cn/hiauth/hiauthclient/config/WebMvcConfig.java
================================================
package cn.hiauth.hiauthclient.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.concurrent.TimeUnit;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**", "/webjars/**")
.addResourceLocations("classpath:/static/", "classpath:/META-INF/resources/webjars/")
.setCacheControl(CacheControl.maxAge(0, TimeUnit.HOURS).cachePrivate());
}
}
================================================
FILE: example/hiauth-client/src/main/java/cn/hiauth/hiauthclient/controller/ApiController.java
================================================
package cn.hiauth.hiauthclient.controller;
import cn.hiauth.client.Authentication;
import cn.hiauth.client.SessionContextHolder;
import cn.webestar.scms.commons.R;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@Validated
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/getAuth")
public R getAuth(HttpServletRequest request) {
Authentication auth = SessionContextHolder.getAuthentication();
return R.success(auth);
}
}
================================================
FILE: example/hiauth-client/src/main/java/cn/hiauth/hiauthclient/controller/IndexController.java
================================================
package cn.hiauth.hiauthclient.controller;
import cn.hiauth.client.Authentication;
import cn.hiauth.client.Constant;
import cn.hiauth.client.SessionContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import java.time.LocalDateTime;
@Slf4j
@Controller
public class IndexController {
@GetMapping({"/", "/index"})
public String index(HttpServletRequest request, HttpServletResponse response, Model model) {
// 这个为了简单就直接判断accessToken了,实际情况应该判断 SessionContextHolder.getContext().getAuth()
String accessToken = request.getParameter(Constant.PARAMETER_TOKEN_KEY);
if (StringUtils.hasText(accessToken)) {
request.getSession().setAttribute("isAuth", true);
model.addAttribute(Constant.PARAMETER_TOKEN_KEY, accessToken);
} else {
request.getSession().setAttribute("isAuth", false);
}
return "index";
}
@GetMapping("/api/profile")
public String profile(HttpServletRequest request, Model model) {
Authentication auth = SessionContextHolder.getContext().getAuth();
model.addAttribute(Constant.PARAMETER_TOKEN_KEY, request.getParameter(Constant.PARAMETER_TOKEN_KEY));
model.addAttribute("name", auth.getName());
model.addAttribute("username", auth.getUsername());
model.addAttribute("tel", auth.getPhoneNum());
model.addAttribute("lastLoginTime", LocalDateTime.now());
request.getSession().setAttribute("isAuth", true);
return "profile";
}
}
================================================
FILE: example/hiauth-client/src/main/resources/application.yml
================================================
server.port: 9000
logging.level:
root: INFO
cn.hiauth: DEBUG
spring.security.oauth2.client:
provider:
#认证服务器信息
hiauth-server:
# 如果你私有化部署了 HiAuth服务,请将此地址替换为私有部署的认证服务器地址
issuer-uri: http://auth.hiauth.cn
authorizationUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/authorize
tokenUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/token
userInfoUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/userinfo
jwkSetUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/jwks
#userNameAttribute: name
registration:
hiauth-code:
#认证提供者,标识由哪个认证服务器进行认证,和上面的hiauth-server进行关联
provider: hiauth-server
client-name: himall
client-id: himall
client-secret: secret
#客户端认证方式 client_secret_basic\client_secret_post
client-authentication-method: client_secret_basic
#使用授权码模式获取令牌(token)
authorization-grant-type: authorization_code
# 认证完成后回调的地址,需要在数据库表oauth2_registered_client中登记这个地址,否则会拒绝回调
redirect-uri: http://127.0.0.1:9000/oauth2/token/redirect
scope: openid,profile
app.security.enable: true
hiauth.client:
cachePrefix: hiauthclient
checkPermission: false
authSuccessRedirectUri: http://127.0.0.1:9000
authUris:
- /api/**
ignoreUris:
- /unpapi/**
spring.data.redis:
host: 192.168.3.143
port: 26379
database: 0
username:
password: Vking1357!
timeout: 10000
connect-timeout: 10000
================================================
FILE: example/hiauth-client/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/hiauth-client/src/main/resources/static/css/index.css
================================================
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
padding-right: 15px;
padding-left: 15px;
}
/* Custom page header */
.header {
padding-bottom: 20px;
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777;
border-top: 1px solid #e5e5e5;
}
/* Customize container */
@media (min-width: 768px) {
.container {
max-width: 730px;
}
}
.container-narrow > hr {
margin: 30px 0;
}
/* Main marketing message and sign up button */
.jumbotron {
text-align: center;
border-bottom: 1px solid #e5e5e5;
}
.jumbotron .btn {
padding: 14px 24px;
font-size: 21px;
}
/* Supporting marketing content */
.marketing {
margin: 40px 0;
}
.marketing p + h4 {
margin-top: 28px;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
/* Remove the padding we set earlier */
.header,
.marketing,
.footer {
padding-right: 0;
padding-left: 0;
}
/* Space out the masthead */
.header {
margin-bottom: 30px;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}
================================================
FILE: example/hiauth-client/src/main/resources/templates/demo.html
================================================
Spring Security OAuth 2.0 Sample
×
Authorize the client using grant_type :
================================================
FILE: example/hiauth-client/src/main/resources/templates/index.html
================================================
HiMall
HiMall Example
This example is a quick exercise to illustrate how Integrate HiAuth by Oauth2, an integrated instance of microservices is also included here, so you can find some best practices.
View HiMall docs
================================================
FILE: example/hiauth-client/src/main/resources/templates/profile.html
================================================
HiMall
================================================
FILE: example/hiauth-client-exp/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
hiauth-client-exp
3.0.0-SNAPSHOT
hiauth-server
Demo project for Spring Boot
17
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-oauth2-client
hiauth-client-exp
org.apache.maven.plugins
maven-compiler-plugin
org.graalvm.buildtools
native-maven-plugin
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
================================================
FILE: example/hiauth-client-exp/src/main/java/cn/hiauth/client/ClientStarter.java
================================================
package cn.hiauth.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ClientStarter {
public static void main(String[] args) {
SpringApplication.run(ClientStarter.class, args);
}
}
================================================
FILE: example/hiauth-client-exp/src/main/java/cn/hiauth/client/config/BeanConfig.java
================================================
package cn.hiauth.client.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfig {
}
================================================
FILE: example/hiauth-client-exp/src/main/java/cn/hiauth/client/config/SecurityConfig.java
================================================
package cn.hiauth.client.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
}
================================================
FILE: example/hiauth-client-exp/src/main/java/cn/hiauth/client/config/WebMvcConfig.java
================================================
package cn.hiauth.client.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.concurrent.TimeUnit;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${app.cacheControl.maxAge:0}")
private Integer maxAge;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**", "/webjars/**")
.addResourceLocations("classpath:/static/", "classpath:/META-INF/resources/webjars/")
.setCacheControl(CacheControl.maxAge(maxAge, TimeUnit.HOURS).cachePrivate());
}
}
================================================
FILE: example/hiauth-client-exp/src/main/java/cn/hiauth/client/controller/ClientController.java
================================================
package cn.hiauth.client.controller;
import org.springframework.stereotype.Controller;
@Controller
public class ClientController {
}
================================================
FILE: example/hiauth-client-exp/src/main/resources/application.yml
================================================
server.port: 9000
spring.security.oauth2.client:
provider:
hiauth-code:
issuer-uri: http://localhost:8080
authorizationUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/authorize
tokenUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/token
userInfoUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/userinfo
jwkSetUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/jwks
user-name-attribute: sub
registration:
hiauth-code:
client-name: HiAuth Authorization Server
client-id: himall
client-secret: secret
authorization-grant-type: authorization_code
redirect-uri: http://127.0.0.1:9000/login/oauth2/code/hiauth-code
scope: openid,profile
================================================
FILE: example/hiauth-client-exp/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/hiauth-client-exp/src/main/resources/templates/index.html
================================================
OAuth2 Client Demo
index
================================================
FILE: example/hiauth-server-exp/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
hiauth-server-exp
3.0.0-SNAPSHOT
hiauth-server
Demo project for Spring Boot
17
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-oauth2-authorization-server
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-jdbc
cn.hutool
hutool-all
5.8.38
org.projectlombok
lombok
true
cn.webestar.scms
commons
1.2.0
org.postgresql
postgresql
runtime
com.alibaba
druid-spring-boot-starter
1.2.24
org.springframework.security
spring-security-oauth2-client
hiauth-server
org.apache.maven.plugins
maven-compiler-plugin
org.graalvm.buildtools
native-maven-plugin
org.springframework.boot
spring-boot-maven-plugin
org.projectlombok
lombok
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/ServerStarter.java
================================================
package cn.hiauth.server;
import cn.webestar.scms.commons.Constant;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackages = {"cn.hiauth.server", Constant.SCMS_BASIC_PKG})
@SpringBootApplication
public class ServerStarter {
public static void main(String[] args) {
SpringApplication.run(ServerStarter.class, args);
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/config/AuthServerConfig.java
================================================
package cn.hiauth.server.config;
import cn.hiauth.server.federation.FederatedIdentityIdTokenCustomizer;
import cn.hiauth.server.mapper.SimpleJdbcRegisteredClientRepository;
import cn.hiauth.server.utils.jose.Jwks;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
@Configuration(proxyBeanMethods = false)
public class AuthServerConfig {
/**
* Spring Authorization Server 相关配置
* 主要配置OAuth 2.1和OpenID Connect 1.0
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage("/oauth2/consent"))
.oidc(Customizer.withDefaults())
)
.authorizeHttpRequests((authorize) ->
authorize.anyRequest().authenticated()
)
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
/**
* 客户端信息
* 对应表:oauth2_registered_client
*/
@Bean
public SimpleJdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
return new SimpleJdbcRegisteredClientRepository(jdbcTemplate);
}
@Bean
public JdbcOAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
@Bean
public JdbcOAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
@Bean
public OAuth2TokenCustomizer idTokenCustomizer() {
return new FederatedIdentityIdTokenCustomizer();
}
@Bean
public JWKSource jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/config/BeanConfig.java
================================================
package cn.hiauth.server.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class BeanConfig {
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/config/SecurityConfig.java
================================================
package cn.hiauth.server.config;
import cn.hiauth.server.federation.FederatedIdentityAuthenticationSuccessHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.session.HttpSessionEventPublisher;
@EnableWebSecurity
@Configuration(proxyBeanMethods = true)
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("123456")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
private AuthenticationSuccessHandler authenticationSuccessHandler() {
return new FederatedIdentityAuthenticationSuccessHandler();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/config/WebMvcConfig.java
================================================
package cn.hiauth.server.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.concurrent.TimeUnit;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${app.cacheControl.maxAge:0}")
private Integer maxAge;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**", "/webjars/**")
.addResourceLocations("classpath:/static/", "classpath:/META-INF/resources/webjars/")
.setCacheControl(CacheControl.maxAge(maxAge, TimeUnit.HOURS).cachePrivate());
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/controller/IndexController.java
================================================
package cn.hiauth.server.controller;
import org.springframework.stereotype.Controller;
@Controller
public class IndexController {
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/controller/LoginController.java
================================================
package cn.hiauth.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
@Slf4j
@Controller
public class LoginController {
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/federation/FederatedIdentityAuthenticationSuccessHandler.java
================================================
/*
* Copyright 2020-2025 the original author or authors.
*
* 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
*
* https://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.
*/
package cn.hiauth.server.federation;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import java.io.IOException;
import java.util.function.Consumer;
/**
* An {@link AuthenticationSuccessHandler} for capturing the {@link OidcUser} or
* {@link OAuth2User} for Federated Account Linking or JIT Account Provisioning.
*
* @author Steve Riesenberg
* @since 1.1
*/
public final class FederatedIdentityAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler();
private Consumer oauth2UserHandler = (user) -> {
};
private Consumer oidcUserHandler = (user) -> this.oauth2UserHandler.accept(user);
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication instanceof OAuth2AuthenticationToken) {
if (authentication.getPrincipal() instanceof OidcUser oidcUser) {
this.oidcUserHandler.accept(oidcUser);
} else if (authentication.getPrincipal() instanceof OAuth2User oauth2User) {
this.oauth2UserHandler.accept(oauth2User);
}
}
this.delegate.onAuthenticationSuccess(request, response, authentication);
}
public void setOAuth2UserHandler(Consumer oauth2UserHandler) {
this.oauth2UserHandler = oauth2UserHandler;
}
public void setOidcUserHandler(Consumer oidcUserHandler) {
this.oidcUserHandler = oidcUserHandler;
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/federation/FederatedIdentityIdTokenCustomizer.java
================================================
/*
* Copyright 2020-2025 the original author or authors.
*
* 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
*
* https://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.
*/
package cn.hiauth.server.federation;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import java.util.*;
/**
* An {@link OAuth2TokenCustomizer} to map claims from a federated identity to
* the {@code id_token} produced by this authorization server.
*
* @author Steve Riesenberg
* @since 1.1
*/
public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCustomizer {
private static final Set ID_TOKEN_CLAIMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
IdTokenClaimNames.ISS,
IdTokenClaimNames.SUB,
IdTokenClaimNames.AUD,
IdTokenClaimNames.EXP,
IdTokenClaimNames.IAT,
IdTokenClaimNames.AUTH_TIME,
IdTokenClaimNames.NONCE,
IdTokenClaimNames.ACR,
IdTokenClaimNames.AMR,
IdTokenClaimNames.AZP,
IdTokenClaimNames.AT_HASH,
IdTokenClaimNames.C_HASH
)));
@Override
public void customize(JwtEncodingContext context) {
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
Map thirdPartyClaims = extractClaims(context.getPrincipal());
context.getClaims().claims(existingClaims -> {
// Remove conflicting claims set by this authorization server
existingClaims.keySet().forEach(thirdPartyClaims::remove);
// Remove standard id_token claims that could cause problems with clients
ID_TOKEN_CLAIMS.forEach(thirdPartyClaims::remove);
// Add all other claims directly to id_token
existingClaims.putAll(thirdPartyClaims);
});
}
}
private Map extractClaims(Authentication principal) {
Map claims;
if (principal.getPrincipal() instanceof OidcUser oidcUser) {
OidcIdToken idToken = oidcUser.getIdToken();
claims = idToken.getClaims();
} else if (principal.getPrincipal() instanceof OAuth2User oauth2User) {
claims = oauth2User.getAttributes();
} else {
claims = Collections.emptyMap();
}
return new HashMap<>(claims);
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/mapper/SimpleJdbcRegisteredClientRepository.java
================================================
package cn.hiauth.server.mapper;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import java.util.List;
import java.util.Set;
public class SimpleJdbcRegisteredClientRepository extends JdbcRegisteredClientRepository {
private static final String TABLE_NAME = "oauth2_registered_client";
private static final String COLUMN_NAMES = "id, "
+ "client_id, "
+ "client_id_issued_at, "
+ "client_secret, "
+ "client_secret_expires_at, "
+ "client_name, "
+ "client_authentication_methods, "
+ "authorization_grant_types, "
+ "redirect_uris, "
+ "post_logout_redirect_uris, "
+ "scopes, "
+ "client_settings,"
+ "token_settings";
private static final String LOAD_REGISTERED_CLIENT_SQL = "SELECT " + COLUMN_NAMES + " FROM " + TABLE_NAME + " WHERE ";
public SimpleJdbcRegisteredClientRepository(JdbcOperations jdbcOperations) {
super(jdbcOperations);
}
public List findByClientIds(Set clientIds) {
MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("clientIds", clientIds);
StringBuilder sb = new StringBuilder();
sb.append(LOAD_REGISTERED_CLIENT_SQL).append("client_id IN (");
clientIds.forEach(i -> sb.append("'").append(i).append("',"));
sb.deleteCharAt(sb.length() - 1);
sb.append(")");
List result = this.getJdbcOperations().query(sb.toString(), this.getRegisteredClientRowMapper());
return !result.isEmpty() ? result : null;
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/utils/jose/Jwks.java
================================================
/*
* Copyright 2020-2023 the original author or authors.
*
* 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
*
* https://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.
*/
package cn.hiauth.server.utils.jose;
import com.nimbusds.jose.jwk.Curve;
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.OctetSequenceKey;
import com.nimbusds.jose.jwk.RSAKey;
import javax.crypto.SecretKey;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
/**
* @author Joe Grandja
* @since 1.1
*/
public final class Jwks {
private Jwks() {
}
public static RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// @formatter:off
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}
public static ECKey generateEc() {
KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
// @formatter:off
return new ECKey.Builder(curve, publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}
public static OctetSequenceKey generateSecret() {
SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
// @formatter:off
return new OctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/utils/jose/KeyGeneratorUtils.java
================================================
/*
* Copyright 2020-2023 the original author or authors.
*
* 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
*
* https://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.
*/
package cn.hiauth.server.utils.jose;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.EllipticCurve;
/**
* @author Joe Grandja
* @since 1.1
*/
final class KeyGeneratorUtils {
private KeyGeneratorUtils() {
}
static SecretKey generateSecretKey() {
SecretKey hmacKey;
try {
hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return hmacKey;
}
static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
static KeyPair generateEcKey() {
EllipticCurve ellipticCurve = new EllipticCurve(
new ECFieldFp(
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
ECPoint ecPoint = new ECPoint(
new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
ECParameterSpec ecParameterSpec = new ECParameterSpec(
ellipticCurve,
ecPoint,
new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
1);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(ecParameterSpec);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
================================================
FILE: example/hiauth-server-exp/src/main/java/cn/hiauth/server/utils/package-info.java
================================================
package cn.hiauth.server.utils;
================================================
FILE: example/hiauth-server-exp/src/main/resources/application-hiauth.yml
================================================
server.port: 8080
logging.level:
root: INFO
cn.hiauth: DEBUG
com.alibaba.nacos: INFO
org.springframework.data.redis: INFO
io.lettuce.core: INFO
spring.datasource:
type: ${datasource.type}
driver-class-name: ${datasource.driverClassName}
url: ${datasource.url}
username: ${datasource.username}
password: ${datasource.password}
spring.session.timeout: 600m
spring.session.redis:
# 支持发布session事件,默认值不支持发布session事件
repository-type: indexed
namespace: "hiauth:session"
# 每次保存或更新 session 时立即将数据同步到 Redis
flush-mode: on_save
# 每次请求结束时都保存 session
save-mode: always
# 关闭Spring Session的自动配置检查,否则在使用aliyun redis(Tair)时会报错
# 报错内容为:NOPERM this user has no permissions to run the 'config|get' command
configure-action: none
================================================
FILE: example/hiauth-server-exp/src/main/resources/application-redis.yml
================================================
# 单节点写法
spring.data.redis:
host: ${redis.host}
port: ${redis.port}
database: ${redis.database}
username: ${redis.username}
password: ${redis.password}
timeout: 10000
connect-timeout: 10000
================================================
FILE: example/hiauth-server-exp/src/main/resources/application.yml
================================================
app.version: '@project.version@'
app.build.timestamp: '@app.build.timestamp@'
spring.application.name: hiauth-server
# 加载模块化配置
spring.profiles.active: hiauth,redis
# 加载外部配置文件,覆盖内置配置文件
spring.config.import: ${CONFIG_FILE:optional:/hiauth/conf/hiauth.properties}
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: org.postgresql.Driver
url: jdbc:mysql://${DB_HOST:127.0.0.1}:3306/hiauth
username: ${DB_USER:test}
password: ${DB_pwd:123456}
redis:
host: ${REDIS_HOST:127.0.0.1}
username: ${REDIS_USERNAME:}
password: ${REDIS_PWD:}
port: 6379
database: 0
================================================
FILE: example/hiauth-server-exp/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/hiauth-server-exp/src/main/resources/templates/index.html
================================================
index
退出
================================================
FILE: example/himall/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
himall
1.0.0-SNAPSHOT
himall
集成HiAuth认证系统
17
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-thymeleaf
org.projectlombok
lombok
org.webjars
webjars-locator-core
org.webjars
bootstrap
3.3.7
org.webjars
jquery
3.6.3
org.springframework.boot
spring-boot-starter-oauth2-client
himall
src/main/resources
true
org.apache.maven.plugins
maven-resources-plugin
UTF-8
org.springframework.boot
spring-boot-maven-plugin
package
repackage
org.apache.maven.plugins
maven-compiler-plugin
17
17
UTF-8
================================================
FILE: example/himall/src/main/java/cn/hiauth/himall/HiMallStarter.java
================================================
package cn.hiauth.himall;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HiMallStarter {
public static void main(String[] args) {
SpringApplication.run(HiMallStarter.class, args);
}
}
================================================
FILE: example/himall/src/main/java/cn/hiauth/himall/config/SecurityConfig.java
================================================
package cn.hiauth.himall.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.client.RestTemplate;
import static org.springframework.security.config.Customizer.withDefaults;
@EnableWebSecurity
@Configuration(proxyBeanMethods = true)
public class SecurityConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/static/**", "/webjars/**");
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/", "/index").permitAll().anyRequest().authenticated()
)
.oauth2Login(oauth2Login -> oauth2Login.failureUrl("/login?error"))
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
)
.oauth2Client(withDefaults());
return http.build();
}
}
================================================
FILE: example/himall/src/main/java/cn/hiauth/himall/config/WebMvcConfig.java
================================================
package cn.hiauth.himall.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.concurrent.TimeUnit;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**", "/webjars/**")
.addResourceLocations("classpath:/static/", "classpath:/META-INF/resources/webjars/")
.setCacheControl(CacheControl.maxAge(0, TimeUnit.HOURS).cachePrivate());
}
}
================================================
FILE: example/himall/src/main/java/cn/hiauth/himall/controller/AuthController.java
================================================
package cn.hiauth.himall.controller;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
@ResponseBody
@GetMapping("/api/client")
public OAuth2AuthorizedClient client(HttpServletRequest request, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) {
// 通过OAuth2AuthorizedClient对象获取到客户端和令牌相关的信息,然后直接返回给前端页面
request.getSession().setAttribute("isAuth", true);
return oAuth2AuthorizedClient;
}
}
================================================
FILE: example/himall/src/main/java/cn/hiauth/himall/controller/IndexController.java
================================================
package cn.hiauth.himall.controller;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.util.Map;
@Slf4j
@Controller
public class IndexController {
@Autowired
private RestTemplate restTemplate;
@Value("${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}")
private String issuerUri;
@Value("${spring.security.oauth2.client.provider.hiauth-server.userInfoUri}")
private String userInfoUri;
@GetMapping({"/", "/index"})
public String index(HttpServletRequest request, HttpServletResponse response) {
return "index";
}
@GetMapping("/user/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
SecurityContextHolder.clearContext();
Cookie cookie = new Cookie("JSESSIONID", null);
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
return "redirect:" + issuerUri + "/unpapi/logoutWithRedirect?redirect_uri=http://127.0.0.1:9000";
}
@GetMapping("/profile")
public String profile(HttpServletRequest request, Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient, @AuthenticationPrincipal OidcUser oidcUser) {
String accessToken = oAuth2AuthorizedClient.getAccessToken().getTokenValue();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Authorization", "Bearer " + accessToken);
MultiValueMap map = new LinkedMultiValueMap<>();
HttpEntity> entity = new HttpEntity<>(map, headers);
Map, ?> res = restTemplate.postForObject(this.userInfoUri, entity, Map.class);
log.info("res:{}", res);
model.addAttribute("name", res.get("name"));
model.addAttribute("username", res.get("username"));
model.addAttribute("tel", res.get("phoneNum"));
model.addAttribute("lastLoginTime", LocalDateTime.now());
request.getSession().setAttribute("isAuth", true);
return "profile";
}
}
================================================
FILE: example/himall/src/main/resources/application.yml
================================================
server.port: 9000
logging.level:
root: DEBUG
cn.hiauth: DEBUG
spring.security.oauth2.client:
provider:
#认证服务器信息
hiauth-server:
# 如果你私有化部署了 HiAuth服务,请将此地址替换为私有部署的认证服务器地址
#issuer-uri: http://localhost:8080
issuer-uri: http://auth.hiauth.cn
authorizationUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/authorize
#令牌获取地址
tokenUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/token
userInfoUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/userinfo
jwkSetUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/jwks
#userNameAttribute: name
registration:
hiauth-code:
#认证提供者,标识由哪个认证服务器进行认证,和上面的hiauth-server进行关联
provider: hiauth-server
client-name: himall
client-id: himall
client-secret: secret
#客户端认证方式 client_secret_basic\client_secret_post
client-authentication-method: client_secret_basic
#使用授权码模式获取令牌(token)
authorization-grant-type: authorization_code
# 认证完成后回调的地址,需要在数据库表oauth2_registered_client中登记这个地址,否则会拒绝回调
redirect-uri: http://127.0.0.1:9000/login/oauth2/code/hiauth-code
scope: openid,profile
================================================
FILE: example/himall/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/himall/src/main/resources/static/css/index.css
================================================
/* Space out content a bit */
body {
padding-top: 20px;
padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
padding-right: 15px;
padding-left: 15px;
}
/* Custom page header */
.header {
padding-bottom: 20px;
border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
margin-top: 0;
margin-bottom: 0;
line-height: 40px;
}
/* Custom page footer */
.footer {
padding-top: 19px;
color: #777;
border-top: 1px solid #e5e5e5;
}
/* Customize container */
@media (min-width: 768px) {
.container {
max-width: 730px;
}
}
.container-narrow > hr {
margin: 30px 0;
}
/* Main marketing message and sign up button */
.jumbotron {
text-align: center;
border-bottom: 1px solid #e5e5e5;
}
.jumbotron .btn {
padding: 14px 24px;
font-size: 21px;
}
/* Supporting marketing content */
.marketing {
margin: 40px 0;
}
.marketing p + h4 {
margin-top: 28px;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
/* Remove the padding we set earlier */
.header,
.marketing,
.footer {
padding-right: 0;
padding-left: 0;
}
/* Space out the masthead */
.header {
margin-bottom: 30px;
}
/* Remove the bottom border on the jumbotron for visual effect */
.jumbotron {
border-bottom: 0;
}
}
================================================
FILE: example/himall/src/main/resources/templates/demo.html
================================================
Spring Security OAuth 2.0 Sample
×
Authorize the client using grant_type :
================================================
FILE: example/himall/src/main/resources/templates/index.html
================================================
HiMall
HiMall Example
This example is a quick exercise to illustrate how Integrate HiAuth by Oauth2, an integrated instance of microservices is also included here, so you can find some best practices.
View HiMall docs
================================================
FILE: example/himall/src/main/resources/templates/profile.html
================================================
HiMall
================================================
FILE: example/resource/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
resource
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
17
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-oauth2-resource-server
cn.webestar.scms
commons
1.2.0
org.springframework.boot
spring-boot-maven-plugin
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/ResourceStarter.java
================================================
package cn.hiauth.resource;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ResourceStarter {
public static void main(String[] args) {
SpringApplication.run(ResourceStarter.class, args);
}
}
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/config/ResourceServerConfig.java
================================================
package cn.hiauth.resource.config;
import cn.hiauth.resource.config.auth.SimpleAccessDeniedHandler;
import cn.hiauth.resource.config.auth.SimpleAuthenticationEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true)
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/unpapi/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
.jwt(Customizer.withDefaults())
.authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
.accessDeniedHandler(new SimpleAccessDeniedHandler())
);
return http.build();
}
}
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/config/auth/SimpleAccessDeniedHandler.java
================================================
package cn.hiauth.resource.config.auth;
import cn.hiauth.resource.utils.ResponseTools;
import cn.webestar.scms.commons.R;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.server.resource.authentication.AbstractOAuth2TokenAuthenticationToken;
import org.springframework.security.web.access.AccessDeniedHandler;
import java.io.IOException;
public class SimpleAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {
if (request.getUserPrincipal() instanceof AbstractOAuth2TokenAuthenticationToken) {
ResponseTools.write(response, R.fail(10403, "没有权限访问"));
} else {
ResponseTools.write(response, R.fail(10403, exception.getMessage()));
}
}
}
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/config/auth/SimpleAuthenticationEntryPoint.java
================================================
package cn.hiauth.resource.config.auth;
import cn.hiauth.resource.utils.ResponseTools;
import cn.webestar.scms.commons.R;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import java.io.IOException;
public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof InsufficientAuthenticationException) {
String accept = request.getHeader("accept");
if (accept.contains(MediaType.TEXT_HTML_VALUE)) {
//如果是html请求类型,则返回登录页
LoginUrlAuthenticationEntryPoint loginUrlAuthenticationEntryPoint = new LoginUrlAuthenticationEntryPoint("/login");
loginUrlAuthenticationEntryPoint.commence(request, response, exception);
} else {
//如果是api请求类型,则返回json
ResponseTools.write(response, R.fail(10401, "缺少访问令牌"));
}
} else if (exception instanceof InvalidBearerTokenException) {
ResponseTools.write(response, R.fail(10401, "令牌无效或过期"));
} else {
ResponseTools.write(response, R.fail(10401, exception.getMessage()));
}
}
}
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/controller/IndexController.java
================================================
package cn.hiauth.resource.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/controller/ProfileController.java
================================================
package cn.hiauth.resource.controller;
import cn.webestar.scms.commons.R;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@PreAuthorize("hasAuthority('SCOPE_profile')")
@RestController
@RequestMapping("/api/profile")
public class ProfileController {
@GetMapping("/info")
public R info() {
return R.success("profile:superman");
}
}
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/controller/UnpapiController.java
================================================
package cn.hiauth.resource.controller;
import cn.webestar.scms.commons.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/unpapi")
public class UnpapiController {
@GetMapping("/metadata")
public R> metadata() {
Map data = new HashMap<>(5);
data.put("name", "hiauth-resource");
data.put("description", "hiauth-resource");
data.put("version", "1.0.0");
return R.success(data);
}
}
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/controller/UserController.java
================================================
package cn.hiauth.resource.controller;
import cn.webestar.scms.commons.R;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@PreAuthorize("hasAuthority('SCOPE_user')")
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/info")
public R info() {
return R.success("user:zhangsan");
}
}
================================================
FILE: example/resource/src/main/java/cn/hiauth/resource/utils/ResponseTools.java
================================================
package cn.hiauth.resource.utils;
import cn.webestar.scms.commons.R;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ResponseTools {
private static final ObjectMapper om = new ObjectMapper();
static {
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
public static void write(HttpServletResponse response, R> r) throws IOException {
// 设置响应头
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
// 序列化并写入响应
try {
String jsonResponse = om.writeValueAsString(r);
response.getWriter().write(jsonResponse);
response.getWriter().flush();
} catch (IOException e) {
// 如果JSON序列化失败,使用简单文本响应
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("错误处理失败: " + e.getMessage());
}
}
}
================================================
FILE: example/resource/src/main/resources/application.yml
================================================
server.port: 9002
logging.level:
root: DEBUG
spring.jackson:
default-property-inclusion: NON_NULL
serialization:
indent-output: true
spring.security.oauth2.resourceServer:
jwt:
issuerUri: http://auth.hiauth.cn
jwkSetUri: ${spring.security.oauth2.resourceServer.jwt.issuerUri}/oauth2/jwks
================================================
FILE: example/resource/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/spring-cloud/README.md
================================================
- 访问:http://auth.hiauth.cn,如果是登录状态,先退出登录
- 然后访问:http://127.0.0.1:9000/ordersvc
================================================
FILE: example/spring-cloud/gateway/pom.xml
================================================
4.0.0
cn.hiauth
gateway
0.0.1-SNAPSHOT
gateway
Demo project for Spring Boot
17
org.springframework.boot
spring-boot-dependencies
3.3.10
pom
import
org.springframework.cloud
spring-cloud-dependencies
2023.0.6
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2023.0.3.3
pom
import
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-webflux
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-loadbalancer
org.springframework.boot
spring-boot-starter-oauth2-client
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-maven-plugin
================================================
FILE: example/spring-cloud/gateway/src/main/java/cn/hiauth/gateway/ApiController.java
================================================
package cn.hiauth.gateway;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/common")
public class ApiController {
@GetMapping("/userinfo")
public OAuth2User userinfo(OAuth2AuthenticationToken authentication) {
return authentication.getPrincipal();
}
}
================================================
FILE: example/spring-cloud/gateway/src/main/java/cn/hiauth/gateway/GatewayStarter.java
================================================
package cn.hiauth.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayStarter {
public static void main(String[] args) {
SpringApplication.run(GatewayStarter.class, args);
}
}
================================================
FILE: example/spring-cloud/gateway/src/main/java/cn/hiauth/gateway/IndexController.java
================================================
package cn.hiauth.gateway;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/")
public String index() {
return "gateway";
}
@GetMapping("/home")
public String home() {
return "home";
}
}
================================================
FILE: example/spring-cloud/gateway/src/main/java/cn/hiauth/gateway/SecurityConfig.java
================================================
package cn.hiauth.gateway;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.authentication.logout.LogoutWebFilter;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.security.web.server.authentication.logout.WebSessionServerLogoutHandler;
import reactor.core.publisher.Mono;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/login", "logout", "/oauth2/**").permitAll()
.anyExchange().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/home"))
)
.logout(logout -> logout
.logoutUrl("/logout") // 退出登录端点
.logoutHandler(new WebSessionServerLogoutHandler()) // 清除会话
.logoutSuccessHandler(logoutSuccessHandler()) // 退出成功处理
)
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.build();
}
@Bean
public ServerLogoutSuccessHandler logoutSuccessHandler() {
return new ServerLogoutSuccessHandler() {
@Override
public Mono onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
// 重定向到登录页面或首页
return Mono.fromRunnable(() -> {
exchange.getExchange().getResponse().setStatusCode(org.springframework.http.HttpStatus.FOUND);
exchange.getExchange().getResponse().getHeaders().setLocation(java.net.URI.create("/login?logout"));
});
}
};
}
/**
* 可选:添加自定义的LogoutWebFilter
*/
@Bean
public LogoutWebFilter logoutWebFilter() {
return new LogoutWebFilter();
}
}
================================================
FILE: example/spring-cloud/gateway/src/main/resources/application.yml
================================================
server.port: 9000
spring.application.name: gateway
logging.level.root: INFO
# Nacos注册中心配置
spring.cloud.nacos.discovery.server-addr: 192.168.1.250:8848
# 网关路由配置
spring.cloud.gateway.routes:
- id: ordersvc
uri: lb://ordersvc/
predicates:
- Path=/ordersvc/**
filters:
- StripPrefix=1
- TokenRelay=
# oauth2客户端配置
spring.security.oauth2.client:
provider:
#认证服务器信息
hiauth-server:
# 如果你私有化部署了 HiAuth服务,请将此地址替换为私有部署的认证服务器地址
issuer-uri: http://auth.hiauth.cn
authorizationUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/authorize
#令牌获取地址
tokenUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/token
userInfoUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/userinfo
jwkSetUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/jwks
#userNameAttribute: name
registration:
hiauth-code:
#认证提供者,标识由哪个认证服务器进行认证,和上面的hiauth-server进行关联
provider: hiauth-server
client-name: himall
client-id: himall
client-secret: secret
#客户端认证方式 client_secret_basic\client_secret_post
client-authentication-method: client_secret_basic
#使用授权码模式获取令牌(token)
authorization-grant-type: authorization_code
# 认证完成后回调的地址,需要在数据库表oauth2_registered_client中登记这个地址,否则会拒绝回调
redirect-uri: http://127.0.0.1:9000/login/oauth2/code/hiauth-code
#redirect-uri: http://127.0.0.1:9000/home
scope: openid,profile
================================================
FILE: example/spring-cloud/gateway/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/spring-cloud/ordersvc/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.3.10
cn.hiauth
ordersvc
0.0.1-SNAPSHOT
ordersvc
Demo project for Spring Boot
17
org.springframework.cloud
spring-cloud-dependencies
2023.0.6
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2023.0.3.3
pom
import
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.boot
spring-boot-maven-plugin
================================================
FILE: example/spring-cloud/ordersvc/src/main/java/cn/hiauth/gateway/IndexController.java
================================================
package cn.hiauth.gateway;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/")
public String index(@RequestHeader("Authorization") String authHeader) {
return "ordersvc" + authHeader;
}
}
================================================
FILE: example/spring-cloud/ordersvc/src/main/java/cn/hiauth/gateway/OrderStarter.java
================================================
package cn.hiauth.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class OrderStarter {
public static void main(String[] args) {
SpringApplication.run(OrderStarter.class, args);
}
}
================================================
FILE: example/spring-cloud/ordersvc/src/main/resources/application.yml
================================================
server.port: 9001
spring.application.name: ordersvc
logging.level.root: INFO
# Nacos注册中心配置
spring.cloud.nacos.discovery.server-addr: 192.168.1.250:8848
================================================
FILE: example/spring-cloud/ordersvc/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/spring-cloud/pom.xml
================================================
4.0.0
cn.hiauth
spring-cloud
0.0.1-SNAPSHOT
pom
gateway
ordersvc
================================================
FILE: example/spring-cloud-with-hiauth-client/README.md
================================================
- 清空登录状态:访问 http://auth.hiauth.cn,如果是登录状态,先退出登录;
- 登录授权:访问 http://127.0.0.1:9000/unpapi/himall/oauth2/login
- 访问接口:登录成功后,浏览器会自动跳转到配置文件中指定的地址,并在参数中携带accessToken;
- 退出:访问 http://127.0.0.1:9000/unpapi/himall/oauth2/logout
================================================
FILE: example/spring-cloud-with-hiauth-client/gateway1/pom.xml
================================================
4.0.0
cn.hiauth
gateway1
0.0.1-SNAPSHOT
gateway
Demo project for Spring Boot
17
org.springframework.boot
spring-boot-dependencies
3.3.10
pom
import
org.springframework.cloud
spring-cloud-dependencies
2023.0.6
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2023.0.3.3
pom
import
org.springframework.cloud
spring-cloud-starter-bootstrap
org.springframework.boot
spring-boot-starter-webflux
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-loadbalancer
org.springframework.boot
spring-boot-devtools
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-actuator
io.netty
netty-resolver-dns-native-macos
4.1.112.Final
runtime
org.springframework.boot
spring-boot-starter-data-redis
cn.hiauth
hiauth-client-spring-cloud-gateway-starter
1.0.8
org.springframework.boot
spring-boot-maven-plugin
================================================
FILE: example/spring-cloud-with-hiauth-client/gateway1/src/main/java/cn/hiauth/gateway/ApiController.java
================================================
package cn.hiauth.gateway;
import cn.hiauth.client.Authentication;
import cn.hiauth.client.SessionContextHolder;
import cn.hiauth.client.UserinfoVo;
import cn.webestar.scms.commons.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/common")
public class ApiController {
@ResponseBody
@GetMapping(value = "/api/userinfo")
public R userinfo() {
Authentication auth = SessionContextHolder.getContext().getAuth();
return R.success(UserinfoVo.toVo(auth));
}
}
================================================
FILE: example/spring-cloud-with-hiauth-client/gateway1/src/main/java/cn/hiauth/gateway/GatewayStarter.java
================================================
package cn.hiauth.gateway;
import cn.hiauth.client.Constant;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan(basePackages = {Constant.HIAUTH_BASIC_PKG, "cn.hiauth.gateway"})
public class GatewayStarter {
public static void main(String[] args) {
SpringApplication.run(GatewayStarter.class, args);
}
}
================================================
FILE: example/spring-cloud-with-hiauth-client/gateway1/src/main/java/cn/hiauth/gateway/IndexController.java
================================================
package cn.hiauth.gateway;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/")
public String index() {
return "gateway";
}
@GetMapping("/home")
public String home() {
return "home";
}
}
================================================
FILE: example/spring-cloud-with-hiauth-client/gateway1/src/main/resources/application.yml
================================================
server.port: 9000
spring.application.name: gateway
logging.level.root: INFO
# 网关启动方式
spring.main.web-application-type: reactive
# Nacos注册中心配置
spring.cloud.nacos.discovery.server-addr: 192.168.1.250:8848
# 网关路由配置
spring.cloud.gateway.routes:
- id: ordersvc
uri: lb://ordersvc/
predicates:
- Path=/ordersvc/**
filters:
- StripPrefix=1
# HiAuth客户端配置
hiauth.client.gateway:
issuerUri: http://auth.hiauth.cn
authorizationUri: ${hiauth.client.gateway.issuerUri}/oauth2/authorize
tokenUri: ${hiauth.client.gateway.issuerUri}/oauth2/token
userInfoUri: ${hiauth.client.gateway.issuerUri}/userinfo
clients:
himall:
clientId: himall
clientSecret: secret
scope: profile,openid
redirectUri: http://127.0.0.1:9000/unpapi/himall/oauth2/token/redirect
# 登录成功后会将accessToken通过这个地址返回给前端
authSuccessRedirectUri: http://127.0.0.1:9000/ordersvc/api/info
cachePrefix: himall
cacheExpire: 2592000
checkPermission: false
# Redis配置
spring.data.redis:
host: 192.168.3.143
port: 26379
database: 5
username:
password: Vking1357!
timeout: 10000
connect-timeout: 10000
================================================
FILE: example/spring-cloud-with-hiauth-client/gateway1/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/spring-cloud-with-hiauth-client/ordersvc1/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.3.10
cn.hiauth
ordersvc1
0.0.1-SNAPSHOT
ordersvc
Demo project for Spring Boot
17
org.springframework.cloud
spring-cloud-dependencies
2023.0.6
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2023.0.3.3
pom
import
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
cn.hiauth
hiauth-client-session-spring-boot-starter
1.0.0
org.springframework.boot
spring-boot-maven-plugin
================================================
FILE: example/spring-cloud-with-hiauth-client/ordersvc1/src/main/java/cn/hiauth/gateway/IndexController.java
================================================
package cn.hiauth.gateway;
import cn.hiauth.client.Authentication;
import cn.hiauth.client.SessionContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
@GetMapping("/api/info")
public Authentication index(@RequestHeader(value = "Authorization", required = false) String authHeader) {
Authentication auth = SessionContextHolder.getContext().getAuth();
return auth;
}
}
================================================
FILE: example/spring-cloud-with-hiauth-client/ordersvc1/src/main/java/cn/hiauth/gateway/OrderStarter.java
================================================
package cn.hiauth.gateway;
import cn.hiauth.client.Constant;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
@EnableDiscoveryClient
@SpringBootApplication
@ComponentScan(basePackages = {Constant.HIAUTH_BASIC_PKG, "cn.hiauth.gateway"})
public class OrderStarter {
public static void main(String[] args) {
SpringApplication.run(OrderStarter.class, args);
}
}
================================================
FILE: example/spring-cloud-with-hiauth-client/ordersvc1/src/main/resources/application.yml
================================================
server.port: 9001
spring.application.name: ordersvc
logging.level.root: INFO
# Nacos注册中心配置
spring.cloud.nacos.discovery.server-addr: 192.168.1.250:8848
# HiAuth Client 配置
app.security.enable: true
hiauth.client.cachePrefix: himall
# Redis配置
spring.data.redis:
host: 192.168.3.143
port: 26379
database: 5
username:
password: Vking1357!
timeout: 10000
connect-timeout: 10000
================================================
FILE: example/spring-cloud-with-hiauth-client/ordersvc1/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/spring-cloud-with-hiauth-client/pom.xml
================================================
4.0.0
cn.hiauth
spring-cloud-with-hiauth-client
0.0.1-SNAPSHOT
pom
gateway1
ordersvc1
================================================
FILE: example/wechat-login/pom.xml
================================================
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.5
cn.hiauth
wechat-login
1.0.0-SNAPSHOT
wechat-login
wechat-login
17
org.springframework.boot
spring-boot-devtools
runtime
true
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-thymeleaf
org.projectlombok
lombok
wechat-login
src/main/resources
true
org.apache.maven.plugins
maven-resources-plugin
UTF-8
org.springframework.boot
spring-boot-maven-plugin
package
repackage
org.apache.maven.plugins
maven-compiler-plugin
17
17
UTF-8
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/WechatLoginStarter.java
================================================
package cn.hiauth.wechatlogin;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WechatLoginStarter {
public static void main(String[] args) {
SpringApplication.run(WechatLoginStarter.class, args);
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/SecurityConfig.java
================================================
package cn.hiauth.wechatlogin.config;
import cn.hiauth.wechatlogin.config.web.security.phone.SmsCodeAuthenticationFilter;
import cn.hiauth.wechatlogin.config.web.security.phone.SmsCodeAuthenticationProvider;
import cn.hiauth.wechatlogin.config.web.security.wechat.QrCodeAuthenticationFilter;
import cn.hiauth.wechatlogin.config.web.security.wechat.QrCodeAuthenticationProvider;
import cn.hiauth.wechatlogin.service.CustomUserDetailsService;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.web.client.RestTemplate;
@EnableWebSecurity
@Configuration(proxyBeanMethods = true)
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService(passwordEncoder());
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/static/**", "/webjars/**");
}
/**
* 定义securityContextRepository,加入两种securityContextRepository
*/
@Bean
public SecurityContextRepository securityContextRepository() {
HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
return new DelegatingSecurityContextRepository(httpSecurityRepository, new RequestAttributeSecurityContextRepository());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
// 用户名密码登录配置
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/account/doLogin")
.defaultSuccessUrl("/index", true)
.permitAll()
)
// 添加手机验证码认证过滤器
.addFilterBefore(smsCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(qrCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 设置全局authenticationManager
.authenticationManager(authenticationManager())
// 设置全局securityContextRepository
.securityContext(c -> c.securityContextRepository(securityContextRepository()));
// .csrf(AbstractHttpConfigurer::disable)
return http.build();
}
@Bean
public SmsCodeAuthenticationFilter smsCodeAuthenticationFilter() {
// 手机验证码登录过滤器
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(authenticationManager());
smsCodeAuthenticationFilter.setSecurityContextRepository(securityContextRepository());
return smsCodeAuthenticationFilter;
}
@Bean
public QrCodeAuthenticationFilter qrCodeAuthenticationFilter() {
QrCodeAuthenticationFilter qrCodeAuthenticationFilter = new QrCodeAuthenticationFilter();
qrCodeAuthenticationFilter.setAuthenticationManager(authenticationManager());
qrCodeAuthenticationFilter.setSecurityContextRepository(securityContextRepository());
return qrCodeAuthenticationFilter;
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(customUserDetailsService());
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(customUserDetailsService());
QrCodeAuthenticationProvider qrCodeAuthenticationProvider = new QrCodeAuthenticationProvider(customUserDetailsService());
return new ProviderManager(daoAuthenticationProvider, smsCodeAuthenticationProvider, qrCodeAuthenticationProvider);
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/WebMvcConfig.java
================================================
package cn.hiauth.wechatlogin.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.concurrent.TimeUnit;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**", "/webjars/**")
.addResourceLocations("classpath:/static/", "classpath:/META-INF/resources/webjars/")
.setCacheControl(CacheControl.maxAge(0, TimeUnit.HOURS).cachePrivate());
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/web/auth/package-info.java
================================================
package cn.hiauth.wechatlogin.config.web.auth;
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/web/security/phone/SmsCodeAuthenticationFilter.java
================================================
package cn.hiauth.wechatlogin.config.web.security.phone;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/phone/doLogin", HttpMethod.POST.name()));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String method = request.getMethod();
if (!HttpMethod.POST.name().equalsIgnoreCase(method)) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String phone = request.getParameter("phone");
String code = request.getParameter("code");
SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(phone, code);
return this.getAuthenticationManager().authenticate(authenticationToken);
}
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/web/security/phone/SmsCodeAuthenticationProvider.java
================================================
package cn.hiauth.wechatlogin.config.web.security.phone;
import cn.hiauth.wechatlogin.service.CustomUserDetailsService;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService userDetailsService;
public SmsCodeAuthenticationProvider(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
String mobile = (String) authenticationToken.getPrincipal();
String code = (String) authenticationToken.getCredentials();
// 这里应该添加验证码校验逻辑,验证手机号和验证码是否匹配
// 示例中简化处理,实际应用中应该调用短信验证服务验证code是否正确
UserDetails userDetails = userDetailsService.loadUserByMobile(mobile);
if (userDetails == null) {
throw new BadCredentialsException("手机号未注册");
}
// 验证通过后,返回认证成功的令牌
SmsCodeAuthenticationToken token = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
token.setDetails(authenticationToken.getDetails());
return token;
}
@Override
public boolean supports(Class> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/web/security/phone/SmsCodeAuthenticationToken.java
================================================
package cn.hiauth.wechatlogin.config.web.security.phone;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 1L;
private final Object principal;
private String code;
public SmsCodeAuthenticationToken(String mobile, String code) {
super(null);
this.principal = mobile;
this.code = code;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return code;
}
@Override
public Object getPrincipal() {
return principal;
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/web/security/wechat/QrCodeAuthenticationFilter.java
================================================
package cn.hiauth.wechatlogin.config.web.security.wechat;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
public class QrCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public QrCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/wechat/qrcode/doLogin"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String code = request.getParameter("code");
QrCodeAuthenticationToken authenticationToken = new QrCodeAuthenticationToken(code);
return this.getAuthenticationManager().authenticate(authenticationToken);
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/web/security/wechat/QrCodeAuthenticationProvider.java
================================================
package cn.hiauth.wechatlogin.config.web.security.wechat;
import cn.hiauth.wechatlogin.service.CustomUserDetailsService;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
public class QrCodeAuthenticationProvider implements AuthenticationProvider {
private final CustomUserDetailsService userDetailsService;
public QrCodeAuthenticationProvider(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
QrCodeAuthenticationToken authenticationToken = (QrCodeAuthenticationToken) authentication;
String code = (String) authenticationToken.getPrincipal();
// 这里应该添加验证码校验逻辑,验证手机号和验证码是否匹配
// 示例中简化处理,实际应用中应该调用短信验证服务验证code是否正确
UserDetails userDetails = userDetailsService.loadUserWeChatCode(code);
if (userDetails == null) {
throw new BadCredentialsException("手机号未注册");
}
// 验证通过后,返回认证成功的令牌
QrCodeAuthenticationToken token = new QrCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
token.setDetails(authenticationToken.getDetails());
return token;
}
@Override
public boolean supports(Class> authentication) {
return QrCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/config/web/security/wechat/QrCodeAuthenticationToken.java
================================================
package cn.hiauth.wechatlogin.config.web.security.wechat;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class QrCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 1L;
private final Object principal;
private String code;
public QrCodeAuthenticationToken(String code) {
super(null);
this.principal = code;
this.code = code;
setAuthenticated(false);
}
public QrCodeAuthenticationToken(Object principal, Collection extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return code;
}
@Override
public Object getPrincipal() {
return principal;
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/controller/AuthController.java
================================================
package cn.hiauth.wechatlogin.controller;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/controller/IndexController.java
================================================
package cn.hiauth.wechatlogin.controller;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Slf4j
@Controller
public class IndexController {
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping({"/", "/index"})
public String index(HttpServletRequest request, HttpServletResponse response, Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
model.addAttribute("token", auth);
return "index";
}
@GetMapping({"/home"})
public String home() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return "home";
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/entity/CustomUserDetails.java
================================================
package cn.hiauth.wechatlogin.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class CustomUserDetails implements UserDetails {
private final String username;
private final String password;
private final boolean enabled;
private final Collection extends GrantedAuthority> authorities;
public CustomUserDetails(String username, String password, boolean enabled,
Collection extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.authorities = authorities;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
================================================
FILE: example/wechat-login/src/main/java/cn/hiauth/wechatlogin/service/CustomUserDetailsService.java
================================================
package cn.hiauth.wechatlogin.service;
import cn.hiauth.wechatlogin.entity.CustomUserDetails;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Collections;
public class CustomUserDetailsService implements UserDetailsService {
private PasswordEncoder passwordEncoder;
public CustomUserDetailsService(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("admin".equals(username)) {
return new CustomUserDetails(
"admin",
passwordEncoder.encode("123456"),
true,
Collections.singletonList(() -> "ROLE_ADMIN")
);
} else if ("user".equals(username)) {
return new CustomUserDetails(
"user",
passwordEncoder.encode("123456"),
true,
Collections.singletonList(() -> "ROLE_USER")
);
}
throw new UsernameNotFoundException("User not found: " + username);
}
public UserDetails loadUserByMobile(String mobile) throws UsernameNotFoundException {
if ("1388888888".equals(mobile)) {
return new CustomUserDetails(
"admin",
passwordEncoder.encode("123456"),
true,
Collections.singletonList(() -> "ROLE_ADMIN")
);
}
throw new UsernameNotFoundException("User not found: " + mobile);
}
public UserDetails loadUserWeChatCode(String code) {
return new CustomUserDetails(
"admin",
passwordEncoder.encode("123456"),
true,
Collections.singletonList(() -> "ROLE_ADMIN")
);
}
}
================================================
FILE: example/wechat-login/src/main/resources/application.yml
================================================
server.port: 9000
logging.level:
root: INFO
cn.hiauth: DEBUG
================================================
FILE: example/wechat-login/src/main/resources/logback.xml
================================================
false
${LOG_PATTERN_COLORED}
UTF-8
${LOG_PATTERN}
UTF-8
${LOG_FILE}
${LOG_FILE}.%i.zip
1
10
100MB
================================================
FILE: example/wechat-login/src/main/resources/templates/home.html
================================================
HiMall
HOME
================================================
FILE: example/wechat-login/src/main/resources/templates/index.html
================================================
HiMall
INDEX
================================================
FILE: example/wechat-login/src/main/resources/templates/login.html
================================================
Custom Login Page
Account Login
Phone Login
WeChat Login POST
WeChat Login GET
================================================
FILE: hiauth-client-starter/hiauth-client-commons/pom.xml
================================================
4.0.0
cn.hiauth
hiauth-client-commons
1.0.0
jar
hiauth-client-commons
hiauth-client-commons
https://github.com/bestaone/hiauth
17
17
UTF-8
org.springframework.boot
spring-boot-dependencies
3.4.5
pom
import
org.projectlombok
lombok
1.18.30
compile
cn.hutool
hutool-all
5.8.38
compile
org.springframework.boot
spring-boot-starter-data-redis
compile
org.apache.maven.plugins
maven-compiler-plugin
3.13.0
17
17
UTF-8
org.apache.maven.plugins
maven-source-plugin
3.3.1
attach-sources
package
jar
org.apache.maven.plugins
maven-javadoc-plugin
3.3.0
17
UTF-8
-Xdoclint:none
attach-javadocs
jar
org.apache.maven.plugins
maven-gpg-plugin
3.2.0
sign-artifacts
verify
sign
org.sonatype.central
central-publishing-maven-plugin
0.4.0
true
ossrh
true
${project.groupId}:${project.artifactId}:${project.version}
https://gitee.com/bestaone/scms
scm:git:https://github.com/bestaone/scms.git
scm:git:https://github.com/bestaone/hiauth.git
zgs
bestaone@163.com
https://github.com/bestaone/hiauth
+8
The Apache Software License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/Authentication.java
================================================
package cn.hiauth.client;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class Authentication {
private Long appId;
private Long cid;
private Long userId;
private Long empId;
private String name;
private String username;
private String phoneNum;
private String avatarUrl;
private List> authorities;
private SecurityUser principal;
private Boolean isCorpAdmin;
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/Client.java
================================================
package cn.hiauth.client;
import lombok.Data;
@Data
public class Client {
private String clientId;
private String clientSecret;
private String[] scope;
private String redirectUri;
/**
* TODO 登录成功后会将accessToken通过这个地址返回给前端
* 这里需优化,accessToken不能直接给到前端,存在安全隐患
*/
private String authSuccessRedirectUri;
private Boolean checkPermission;
private String cachePrefix;
private Integer cacheExpire = 60 * 60 * 5;
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/Constant.java
================================================
package cn.hiauth.client;
public class Constant {
public final static String RESULT_JSON = "{ \"code\": %d, \"message\": \"%s\" }";
public final static String HIAUTH_BASIC_PKG = "cn.hiauth.client";
public final static String PARAMETER_TOKEN_KEY = "accessToken";
public final static String TOKEN_HEADER = "Authorization";
public final static String TOKEN_PREFIX = "Bearer";
public final static String IGNORE_METHOD = "OPTIONS";
/**
* token 有效期
*/
// public final static Integer ACCESS_TOKEN_EXPIRE = 60 * 60 * 5;
//
// /**
// * refresh_token 有效期
// */
// public final static Integer REFRESH_TOKEN_EXPIRE = 60 * 60 * 24 * 3;
/**
* token key 的key格式
*/
public final static String ACCESS_TOKEN_CACHE_KEY = "%s:security:accessToken:%s:%s";
/**
* refresh_token 的key格式
*/
public final static String REFRESH_TOKEN_CACHE_KEY = "%s:security:refreshToken:%s:%s";
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/HiAuthToken.java
================================================
package cn.hiauth.client;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class HiAuthToken {
private String accessToken;
private String refreshToken;
private String scope;
private LocalDateTime expire;
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/JwtUtils.java
================================================
package cn.hiauth.client;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
public class JwtUtils {
public static final String SUB_KEY = "sub";
/**
* 10小时
*/
// public static final Integer EXPIRE = 60 * 60 * 10;
private static final String KEY = "jwtsecret";
// public static String generateToken(String sub) {
// return generateToken(sub, EXPIRE);
// }
public static String generateToken(String sub, Integer expire) {
Map payload = new HashMap<>(2);
payload.put(SUB_KEY, sub);
//过期时间
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireTime = now.plusSeconds(expire);
payload.put(JWTPayload.EXPIRES_AT, expireTime);
//签发时间
payload.put(JWTPayload.ISSUED_AT, now);
//生效时间
payload.put(JWTPayload.NOT_BEFORE, now);
return generateToken(payload);
}
private static String generateToken(Map payload) {
return JWTUtil.createToken(payload, KEY.getBytes());
}
public static JWT parseToken(String token) {
return JWT.of(token);
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/SecurityCorp.java
================================================
package cn.hiauth.client;
import lombok.Data;
@Data
public class SecurityCorp {
private Long id;
private String name;
public SecurityCorp(Long id, String name) {
this.id = id;
this.name = name;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/SecurityService.java
================================================
package cn.hiauth.client;
import java.util.List;
/**
* @author zgs
*/
public interface SecurityService {
SecurityUser loadSecurityUser(Authentication auth);
/**
* 根据用户ID查询所属租户列表,按照最近登录时间排序
*/
List loadUserCorps(Long userId);
/**
* 切换租户
*/
Boolean switchCorp(Long id);
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/SecurityUser.java
================================================
package cn.hiauth.client;
import lombok.Data;
@Data
public class SecurityUser {
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/SessionContext.java
================================================
package cn.hiauth.client;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* @author zgs
*/
@Data
public class SessionContext implements Serializable {
private String clientName;
private String cachePrefix;
private Integer cacheExpire;
private String accessToken;
private String refreshToken;
private LocalDateTime expire;
private Authentication auth;
private HiAuthToken token;
private Map extend = new HashMap<>();
public SessionContext(String clientName, String cachePrefix, Integer cacheExpire) {
this.clientName = clientName;
this.cachePrefix = cachePrefix;
this.cacheExpire = cacheExpire;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/SessionContextHolder.java
================================================
package cn.hiauth.client;
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.RedisTemplate;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author zgs
*/
public class SessionContextHolder {
private static final InheritableThreadLocal contexts = new InheritableThreadLocal<>();
private static RedisTemplate redisTemplate;
public static void setRedisTemplate(RedisTemplate redisTemplate) {
SessionContextHolder.redisTemplate = redisTemplate;
}
public static SessionContext getContext() {
return contexts.get();
}
public static void setContext(SessionContext context) {
contexts.set(context);
}
public static Authentication getAuthentication() {
SessionContext context = getContext();
return context == null ? null : context.getAuth();
}
public static SecurityUser getPrincipal() {
Authentication auth = getAuthentication();
return auth == null ? null : auth.getPrincipal();
}
public static void refresh() {
SessionContext context = contexts.get();
String tokenKey = String.format(Constant.ACCESS_TOKEN_CACHE_KEY, context.getCachePrefix(), context.getAuth().getUserId(), context.getAccessToken());
String refreshTokenKey = String.format(Constant.REFRESH_TOKEN_CACHE_KEY, context.getCachePrefix(), context.getAuth().getUserId(), context.getRefreshToken());
String json = JSONUtil.toJsonStr(context);
redisTemplate.opsForValue().set(tokenKey, json, context.getCacheExpire(), TimeUnit.SECONDS);
redisTemplate.expire(refreshTokenKey, context.getCacheExpire() * 2, TimeUnit.SECONDS);
}
public static SessionContext auth(String clientName, String cachePrefix, Integer cacheExpire, Authentication authentication) {
SessionContext context = new SessionContext(clientName, cachePrefix, cacheExpire);
context.setAuth(authentication);
return auth(context);
}
public static SessionContext auth(SessionContext context) {
return auth(context, context.getCachePrefix(), context.getCacheExpire());
}
public static SessionContext auth(SessionContext context, String cachePrefix, Integer expire) {
String userId = context.getAuth().getUserId().toString();
String accessToken = JwtUtils.generateToken(userId, expire);
String refreshToken = UUID.randomUUID().toString().replace("-", "");
context.setAccessToken(accessToken);
context.setRefreshToken(refreshToken);
context.setExpire(LocalDateTime.now().plusMinutes(expire));
SessionContextHolder.setContext(context);
String json = JSONUtil.toJsonStr(context);
redisTemplate.opsForValue().set(String.format(Constant.ACCESS_TOKEN_CACHE_KEY, cachePrefix, userId, accessToken), json, expire, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(String.format(Constant.REFRESH_TOKEN_CACHE_KEY, cachePrefix, userId, refreshToken), accessToken, expire * 2, TimeUnit.SECONDS);
return context;
}
public static void logout() {
SessionContext context = SessionContextHolder.getContext();
String username = context.getAuth().getUsername();
String accessToken = context.getAccessToken();
String refreshToken = context.getRefreshToken();
if (redisTemplate != null) {
redisTemplate.expire(String.format(Constant.ACCESS_TOKEN_CACHE_KEY, context.getCachePrefix(), username, accessToken), 0, TimeUnit.SECONDS);
redisTemplate.expire(String.format(Constant.REFRESH_TOKEN_CACHE_KEY, context.getCachePrefix(), username, refreshToken), 0, TimeUnit.SECONDS);
}
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/TokenVo.java
================================================
package cn.hiauth.client;
public class TokenVo {
private String accessToken;
private Integer expireIn;
private String refreshToken;
private String scope;
public TokenVo() {
}
public TokenVo(String accessToken, String refreshToken, Integer expireIn, String scope) {
this.accessToken = accessToken;
this.expireIn = expireIn;
this.refreshToken = refreshToken;
this.scope = scope;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public Integer getExpireIn() {
return expireIn;
}
public void setExpireIn(Integer expireIn) {
this.expireIn = expireIn;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-commons/src/main/java/cn/hiauth/client/UserinfoVo.java
================================================
package cn.hiauth.client;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class UserinfoVo {
private Long cid;
private Long appId;
private Long userId;
private Long empId;
private String name;
private String username;
private String phoneNum;
private String avatarUrl;
private List authorities;
private Boolean isCorpAdmin;
public static UserinfoVo toVo(Authentication auth) {
UserinfoVo vo = new UserinfoVo();
vo.setCid(auth.getCid());
vo.setAppId(auth.getAppId());
vo.setUserId(auth.getUserId());
vo.setEmpId(auth.getEmpId());
vo.setName(auth.getName());
vo.setUsername(auth.getUsername());
vo.setPhoneNum(auth.getPhoneNum());
vo.setAvatarUrl(auth.getAvatarUrl());
if (auth.getAuthorities() != null && !auth.getAuthorities().isEmpty()) {
vo.setAuthorities(new ArrayList<>());
auth.getAuthorities().forEach(item -> vo.getAuthorities().add(item.get("code")));
}
vo.setIsCorpAdmin(auth.getIsCorpAdmin());
return vo;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-resource-spring-boot-starter/pom.xml
================================================
4.0.0
cn.hiauth
hiauth-client-resource-spring-boot-starter
1.0.0
jar
hiauth-client-resource-spring-boot-starter
hiauth-client-resource-spring-boot-starter
https://github.com/bestaone/hiauth
17
17
UTF-8
org.springframework.boot
spring-boot-dependencies
3.4.5
pom
import
org.springframework.boot
spring-boot-configuration-processor
compile
true
org.springframework.boot
spring-boot-autoconfigure
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.projectlombok
lombok
cn.hutool
hutool-all
5.8.38
cn.webestar.scms
commons
1.2.0
cn.hiauth
hiauth-client-commons
1.0.0
org.apache.maven.plugins
maven-compiler-plugin
3.13.0
17
17
UTF-8
org.apache.maven.plugins
maven-source-plugin
3.3.1
attach-sources
package
jar
org.apache.maven.plugins
maven-javadoc-plugin
3.3.0
17
UTF-8
-Xdoclint:none
attach-javadocs
jar
org.apache.maven.plugins
maven-gpg-plugin
3.2.0
sign-artifacts
verify
sign
org.sonatype.central
central-publishing-maven-plugin
0.4.0
true
ossrh
true
${project.groupId}:${project.artifactId}:${project.version}
https://gitee.com/bestaone/scms
scm:git:https://github.com/bestaone/scms.git
scm:git:https://github.com/bestaone/hiauth.git
zgs
bestaone@163.com
https://github.com/bestaone/hiauth
+8
The Apache Software License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
================================================
FILE: hiauth-client-starter/hiauth-client-resource-spring-boot-starter/src/main/java/cn/hiauth/client/resource/HiAuthClientResourceAutoConfig.java
================================================
package cn.hiauth.client.resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author zgs
*/
@Slf4j
@Configuration
@EnableConfigurationProperties({HiAuthClientResourceProperties.class})
public class HiAuthClientResourceAutoConfig {
}
================================================
FILE: hiauth-client-starter/hiauth-client-resource-spring-boot-starter/src/main/java/cn/hiauth/client/resource/HiAuthClientResourceProperties.java
================================================
package cn.hiauth.client.resource;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
@Data
@ConfigurationProperties("hiauth.client.resource")
public class HiAuthClientResourceProperties implements Serializable {
}
================================================
FILE: hiauth-client-starter/hiauth-client-resource-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json
================================================
{
"properties": {
"hiauth.client.resource.url": {
"description": "授权服务端地址",
"type": "string"
}
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-resource-spring-boot-starter/src/main/resources/META-INF/spring.factories
================================================
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.hiauth.client.resource.HiAuthClientResourceAutoConfig
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/pom.xml
================================================
4.0.0
cn.hiauth
hiauth-client-session-spring-boot-starter
1.0.0
jar
hiauth-client-session-spring-boot-starter
hiauth-client-session-spring-boot-starter
https://github.com/bestaone/hiauth
17
17
UTF-8
org.springframework.boot
spring-boot-dependencies
3.4.5
pom
import
org.springframework.boot
spring-boot-configuration-processor
compile
true
org.springframework.boot
spring-boot-autoconfigure
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.projectlombok
lombok
cn.hutool
hutool-all
5.8.38
cn.webestar.scms
commons
1.2.0
cn.hiauth
hiauth-client-commons
1.0.0
org.apache.maven.plugins
maven-compiler-plugin
3.13.0
17
17
UTF-8
org.apache.maven.plugins
maven-source-plugin
3.3.1
attach-sources
package
jar
org.apache.maven.plugins
maven-javadoc-plugin
3.3.0
17
UTF-8
-Xdoclint:none
attach-javadocs
jar
org.apache.maven.plugins
maven-gpg-plugin
3.2.0
sign-artifacts
verify
sign
org.sonatype.central
central-publishing-maven-plugin
0.4.0
true
ossrh
true
${project.groupId}:${project.artifactId}:${project.version}
https://gitee.com/bestaone/scms
scm:git:https://github.com/bestaone/scms.git
scm:git:https://github.com/bestaone/hiauth.git
zgs
bestaone@163.com
https://github.com/bestaone/hiauth
+8
The Apache Software License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/src/main/java/cn/hiauth/client/session/AuthFilter.java
================================================
package cn.hiauth.client.session;
import cn.hiauth.client.Constant;
import cn.hiauth.client.JwtUtils;
import cn.hiauth.client.SessionContext;
import cn.hiauth.client.SessionContextHolder;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWT;
import cn.webestar.scms.commons.Assert;
import cn.webestar.scms.commons.CommonException;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
/**
* @author zgs
*/
@Slf4j
public class AuthFilter implements Filter {
private final AntPathMatcher matcher = new AntPathMatcher();
private final RedisTemplate redisTemplate;
private final HiAuthClientSessionProperties properties;
public AuthFilter(HiAuthClientSessionProperties properties, RedisTemplate redisTemplate) {
this.properties = properties;
this.redisTemplate = redisTemplate;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
try {
doIt(request, response, chain);
} catch (Exception e) {
printError(request, response, e);
}
}
private void doIt(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception {
if (Constant.IGNORE_METHOD.equalsIgnoreCase(request.getMethod())) {
chain.doFilter(request, response);
} else if (matcherUrl(request.getRequestURI())) {
SessionContext context = getSessionContext(request);
Assert.notNull(context, 10401, "request fail");
SessionContextHolder.setContext(context);
chain.doFilter(request, response);
} else {
chain.doFilter(request, response);
}
}
private SessionContext getSessionContext(HttpServletRequest request) throws Exception {
String accessToken = null;
String authHeader = request.getHeader(Constant.TOKEN_HEADER);
if (StringUtils.hasText(authHeader)) {
authHeader = URLDecoder.decode(authHeader, StandardCharsets.UTF_8);
Assert.isTrue(authHeader.startsWith(Constant.TOKEN_PREFIX), 10401, "miss bearer");
accessToken = authHeader.substring(Constant.TOKEN_PREFIX.length()).trim();
}
if (!StringUtils.hasText(accessToken)) {
accessToken = request.getParameter(Constant.PARAMETER_TOKEN_KEY);
}
Assert.notNull(accessToken, 10401, "miss token");
JWT jwt = JwtUtils.parseToken(accessToken);
Assert.notNull(jwt, 10401, "invalid token");
String username = (String) jwt.getPayload(JwtUtils.SUB_KEY);
Assert.notNull(username, 10401, "invalid token");
String accessTokenKey = String.format(Constant.ACCESS_TOKEN_CACHE_KEY, properties.getCachePrefix(), username, accessToken);
String json = redisTemplate.opsForValue().get(accessTokenKey);
Assert.notNull(json, 10401, "invalid token");
SessionContext context = JSONUtil.toBean(json, SessionContext.class);
Assert.notNull(context, 10401, "invalid token");
return context;
}
public boolean matcherUrl(String uri) {
for (String authUrl : properties.getAuthUris()) {
if (matcher.match(authUrl, uri)) {
return true;
}
}
return false;
}
public boolean ignoreUrl(String uri) {
for (String ignoreUrl : properties.getIgnoreUris()) {
if (matcher.match(ignoreUrl, uri)) {
return true;
}
}
return false;
}
private void printError(HttpServletRequest request, HttpServletResponse response, Exception e) {
log.error(e.getMessage(), e);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
Integer code = 50000;
String msg;
if (e instanceof CommonException ce) {
code = ce.getCode();
msg = e.getMessage();
} else {
msg = "系统异常";
}
PrintWriter out = null;
try {
out = response.getWriter();
out.write(String.format(Constant.RESULT_JSON, code, msg));
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (out != null) {
out.flush();
out.close();
}
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/src/main/java/cn/hiauth/client/session/HiAuthClientSessionAutoConfig.java
================================================
package cn.hiauth.client.session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
/**
* @author zgs
*/
@Slf4j
@Configuration
@EnableConfigurationProperties({HiAuthClientSessionProperties.class})
public class HiAuthClientSessionAutoConfig {
@Autowired
private HiAuthClientSessionProperties hiAuthClientSessionProperties;
@Autowired
private RedisTemplate redisTemplate;
@Bean
@ConditionalOnMissingBean
@ConditionalOnExpression("${app.security.enable:false}")
public FilterRegistrationBean authFilterRegister() {
FilterRegistrationBean registration = new FilterRegistrationBean<>();
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
registration.setFilter(new AuthFilter(hiAuthClientSessionProperties, redisTemplate));
registration.setName("authFilter");
registration.addUrlPatterns("/*");
log.info("Register AuthFilter,url=/*");
return registration;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/src/main/java/cn/hiauth/client/session/HiAuthClientSessionCacheConfig.java
================================================
package cn.hiauth.client.session;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Slf4j
@Configuration
public class HiAuthClientSessionCacheConfig {
@Bean
@ConditionalOnMissingBean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
log.info("[cache-spring-boot-starter]:Init RedisTemplate");
ObjectMapper om = new ObjectMapper();
// 支持 LocalDate、LocalDateTime的序列号
om.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(om, Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/src/main/java/cn/hiauth/client/session/HiAuthClientSessionController.java
================================================
package cn.hiauth.client.session;
import cn.hiauth.client.*;
import cn.webestar.scms.commons.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@Controller
@RequestMapping("/")
public class HiAuthClientSessionController {
@Autowired(required = false)
private SecurityService securityService;
@ResponseBody
@GetMapping(value = "/api/common/user/info")
public R userInfo() {
Authentication auth = SessionContextHolder.getContext().getAuth();
return R.success(UserinfoVo.toVo(auth));
}
@ResponseBody
@PostMapping(value = "/api/common/myCorps")
public R> myCorps() {
Authentication auth = SessionContextHolder.getContext().getAuth();
List corps = securityService.loadUserCorps(auth.getUserId());
return R.success(corps);
}
@ResponseBody
@PostMapping(value = "/api/common/switchCorp")
public R switchCorp(@RequestParam("id") Long id) {
return R.success(securityService.switchCorp(id));
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/src/main/java/cn/hiauth/client/session/HiAuthClientSessionProperties.java
================================================
package cn.hiauth.client.session;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
import java.util.Set;
@Data
@ConfigurationProperties("hiauth.client")
public class HiAuthClientSessionProperties implements Serializable {
/**
* 缓存前缀
*/
private String cachePrefix = "hiauth";
/**
* 无需登录也无需鉴权的接口(暂时未启用)
*/
private Set ignoreUris = Set.of("/unpapi/**");
/**
* 需要登录,并且需要拥有权限,才可访问的接口
*/
private Set authUris = Set.of("/api/**");
}
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/src/main/java/cn/hiauth/client/session/HiAuthClientSessionRunner.java
================================================
package cn.hiauth.client.session;
import cn.hiauth.client.SessionContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* @author zgs
*/
@Slf4j
@Component
public class HiAuthClientSessionRunner implements ApplicationRunner {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void run(ApplicationArguments args) {
SessionContextHolder.setRedisTemplate(redisTemplate);
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json
================================================
{
"properties": {
"app.security.enable": {
"description": "开启安全拦截。默认值:false",
"type": "boolean"
},
"hiauth.client.cachePrefix": {
"description": "缓存前缀。默认值:hiauth",
"type": "string"
},
"hiauth.client.authUris": {
"description": "需要登录,并且需要拥有权限,才可访问的接口。默认值:/api/**",
"type": "string[]"
},
"hiauth.client.ignoreUris": {
"description": "无需登录也无需鉴权的接口(暂时未启用)。默认值:/unpapi/**",
"type": "string[]"
}
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-session-spring-boot-starter/src/main/resources/META-INF/spring.factories
================================================
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.hiauth.client.session.HiAuthClientSessionAutoConfig
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/docs/apisvc-oms.yml
================================================
server.port: 9003
logging.level:
root: INFO
com.vking: DEBUG
app.cache.prefix: uavs:oms
app.security.enable: true
spring.security.oauth2.client:
provider:
hiauth-server:
issuer-uri: ${AUTH_URI:http://auth-dev.vking.fun:31000}
authorizationUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/authorize
tokenUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/token
userInfoUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/userinfo
jwkSetUri: ${spring.security.oauth2.client.provider.hiauth-server.issuer-uri}/oauth2/jwks
# userNameAttribute: name
registration:
hiauth-code:
# 认证提供者,标识由哪个认证服务器进行认证,和上面的auth-server进行关联
provider: hiauth-server
client-name: uavs-oms
client-id: uavs-oms
client-secret: 123456
# 客户端认证方式 client_secret_basic\client_secret_post
client-authentication-method: client_secret_basic
authorization-grant-type: authorization_code
# 回调地址,接收认证服务器回传code的接口地址,之前我们是使用http://www.baidu.com代替
# 注意:和认证服务器配置的回调地址要一致 '{baseUrl}/{action}/oauth2/code/{registrationId}'
redirect-uri: ${REDIRECT_URI:http://oms-dev.mayizhifei.com:30281/gateway/apisvc-oms/oauth2/token/redirect}
scope: profile,openid
hiauth.client:
cachePrefix: ${app.cache.prefix}
checkPermission: false
authSuccessRedirectUri: ${AUTH_SUCCESS_REDIRECT_URI:http://oms-dev.mayizhifei.com:30281}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/pom.xml
================================================
4.0.0
cn.hiauth
hiauth-client-spring-boot-starter
1.0.9
jar
hiauth-client-spring-boot-starter
hiauth-client-spring-boot-starter
https://github.com/bestaone/hiauth
17
17
UTF-8
org.springframework.boot
spring-boot-dependencies
3.4.5
pom
import
org.springframework.boot
spring-boot-configuration-processor
compile
true
org.springframework.boot
spring-boot-autoconfigure
org.springframework.boot
spring-boot-starter-web
compile
org.springframework.boot
spring-boot-starter-data-redis
org.projectlombok
lombok
cn.hutool
hutool-all
5.8.38
cn.webestar.scms
commons
1.2.0
cn.hiauth
hiauth-client-commons
1.0.0
org.apache.maven.plugins
maven-compiler-plugin
3.13.0
17
17
UTF-8
org.apache.maven.plugins
maven-source-plugin
3.3.1
attach-sources
package
jar
org.apache.maven.plugins
maven-javadoc-plugin
3.3.0
17
UTF-8
-Xdoclint:none
attach-javadocs
jar
org.apache.maven.plugins
maven-gpg-plugin
3.2.0
sign-artifacts
verify
sign
org.sonatype.central
central-publishing-maven-plugin
0.4.0
true
ossrh
true
${project.groupId}:${project.artifactId}:${project.version}
https://gitee.com/bestaone/scms
scm:git:https://github.com/bestaone/scms.git
scm:git:https://github.com/bestaone/hiauth.git
zgs
bestaone@163.com
https://github.com/bestaone/hiauth
+8
The Apache Software License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/AuthFilter.java
================================================
package cn.hiauth.client;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWT;
import cn.webestar.scms.commons.Assert;
import cn.webestar.scms.commons.CommonException;
import cn.webestar.scms.commons.SysCode;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
/**
* @author zgs
*/
@Slf4j
public class AuthFilter implements Filter {
private final static String ERROR_RESULT = "{ \"code\": %d, \"message\": \"%s\" }";
private final AntPathMatcher matcher = new AntPathMatcher();
private final RedisTemplate redisTemplate;
private final HiAuthClientProperties properties;
public AuthFilter(HiAuthClientProperties properties, RedisTemplate redisTemplate) {
this.properties = properties;
this.redisTemplate = redisTemplate;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
try {
doIt(request, response, chain);
} catch (Exception e) {
printError(request, response, e);
}
}
private void doIt(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception {
// String cid = request.getHeader("_CID_");
// if(StringUtils.hasText(cid)){
// RequestContext context = new RequestContext();
// context.setCid(Long.parseLong(cid));
// ApiRequestContextHolder.setContext(context);
// } else {
// ApiRequestContextHolder.setContext(null);
// }
if (Constant.IGNORE_METHOD.equalsIgnoreCase(request.getMethod())) {
chain.doFilter(request, response);
} else if (matcherUrl(request.getRequestURI())) {
SessionContext context = getSessionContext(request);
Assert.notNull(context, 10401, "request fail");
SessionContextHolder.setContext(context);
checkPermissions(request, context.getAuth().getAuthorities());
chain.doFilter(request, response);
} else {
chain.doFilter(request, response);
}
}
private void checkPermissions(HttpServletRequest request, List> authorities) {
//是否开启了权限检查
if (!properties.isCheckPermission()) {
return;
}
String uri = request.getRequestURI();
//检查是否为无需权限的api
for (String api : properties.getIgnorePermissionUris()) {
if (StringUtils.hasText(api) && StringUtils.hasText(uri) && matcher.match(api, uri)) {
return;
}
}
//检查是否分配了权限
for (Map o : authorities) {
String api = o.get("api");
if (StringUtils.hasText(api) && StringUtils.hasText(uri) && matcher.match(api, uri)) {
return;
}
}
throw new CommonException(SysCode.FORBIDDEN.getCode(), "没有权限");
}
private SessionContext getSessionContext(HttpServletRequest request) throws Exception {
String accessToken = null;
String authHeader = request.getHeader(Constant.TOKEN_HEADER);
if (StringUtils.hasText(authHeader)) {
authHeader = URLDecoder.decode(authHeader, StandardCharsets.UTF_8);
Assert.isTrue(authHeader.startsWith(Constant.TOKEN_PREFIX), 10401, "miss bearer");
accessToken = authHeader.substring(Constant.TOKEN_PREFIX.length()).trim();
}
if (!StringUtils.hasText(accessToken)) {
accessToken = request.getParameter(Constant.PARAMETER_TOKEN_KEY);
}
Assert.notNull(accessToken, 10401, "miss token");
JWT jwt = JwtUtils.parseToken(accessToken);
Assert.notNull(jwt, 10401, "invalid token");
String username = (String) jwt.getPayload(JwtUtils.SUB_KEY);
Assert.notNull(username, 10401, "invalid token");
String accessTokenKey = String.format(Constant.ACCESS_TOKEN_CACHE_KEY, properties.getCachePrefix(), username, accessToken);
String json = redisTemplate.opsForValue().get(accessTokenKey);
Assert.notNull(json, 10401, "invalid token");
SessionContext context = JSONUtil.toBean(json, SessionContext.class);
Assert.notNull(context, 10401, "invalid token");
return context;
}
public boolean matcherUrl(String uri) {
for (String authUrl : properties.getAuthUris()) {
if (matcher.match(authUrl, uri)) {
return true;
}
}
return false;
}
public boolean ignoreUrl(String uri) {
for (String ignoreUrl : properties.getIgnoreUris()) {
if (matcher.match(ignoreUrl, uri)) {
return true;
}
}
return false;
}
private void printError(HttpServletRequest request, HttpServletResponse response, Exception e) {
log.error(e.getMessage(), e);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
Integer code = 50000;
String msg;
if (e instanceof CommonException ce) {
code = ce.getCode();
msg = e.getMessage();
} else {
msg = "系统异常";
}
PrintWriter out = null;
try {
out = response.getWriter();
out.write(String.format(ERROR_RESULT, code, msg));
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (out != null) {
out.flush();
out.close();
}
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/HiAuthCacheConfig.java
================================================
package cn.hiauth.client;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Slf4j
@Configuration
public class HiAuthCacheConfig {
@Bean
@ConditionalOnMissingBean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
log.info("[cache-spring-boot-starter]:Init RedisTemplate");
ObjectMapper om = new ObjectMapper();
// 支持 LocalDate、LocalDateTime的序列号
om.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(om, Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/HiAuthClientAutoConfig.java
================================================
package cn.hiauth.client;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.client.RestTemplate;
/**
* @author zgs
*/
@Slf4j
@Configuration
@EnableConfigurationProperties({HiAuthClientProviderProperties.class, HiAuthClientRegistrationProperties.class, HiAuthClientProperties.class})
public class HiAuthClientAutoConfig {
@Autowired
private HiAuthClientProperties authClientProperties;
@Autowired
private RedisTemplate redisTemplate;
@Bean
@ConditionalOnMissingBean(RestTemplate.class)
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnExpression("${app.security.enable:false}")
public FilterRegistrationBean authFilterRegister() {
FilterRegistrationBean registration = new FilterRegistrationBean<>();
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
registration.setFilter(new AuthFilter(authClientProperties, redisTemplate));
registration.setName("authFilter");
registration.addUrlPatterns("/*");
log.info("Register AuthFilter,url=/*");
return registration;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/HiAuthClientController.java
================================================
package cn.hiauth.client;
import cn.hiauth.client.api.TokenVo;
import cn.hiauth.client.api.UserPwdUpdateDto;
import cn.hutool.core.codec.Base64;
import cn.webestar.scms.commons.Assert;
import cn.webestar.scms.commons.R;
import cn.webestar.scms.commons.SysCode;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Controller
@RequestMapping("/")
public class HiAuthClientController {
@Autowired
private HiAuthClientProviderProperties authClientProviderProperties;
@Autowired
private HiAuthClientRegistrationProperties authClientRegistrationProperties;
@Autowired
private HiAuthClientProperties authClientProperties;
@Autowired
private RestTemplate restTemplate;
@Autowired(required = false)
private SecurityService securityService;
@GetMapping("/oauth2/login")
public String login(HttpServletRequest request) {
String authUrl = authClientProviderProperties.getAuthorizationUri() +
"?response_type=code" +
"&client_id=" + authClientRegistrationProperties.getClientId() +
"&scope=" + String.join(" ", authClientRegistrationProperties.getScope()) +
"&redirect_uri=" + authClientRegistrationProperties.getRedirectUri();
return "redirect:" + authUrl;
}
@GetMapping("/oauth2/logout")
public String logout(HttpServletRequest request) {
String redirectUri = authClientProperties.getAuthSuccessRedirectUri();
if (!StringUtils.hasText(redirectUri)) {
redirectUri = authClientRegistrationProperties.getRedirectUri();
}
String logoutUrl = authClientProviderProperties.getIssuerUri() + "/unpapi/logoutWithRedirect?redirect_uri=" + redirectUri;
return "redirect:" + logoutUrl;
}
@ResponseBody
@GetMapping(value = "/oauth2/token")
public R getTokenJson(HttpServletRequest request, @RequestParam("code") String code) {
SessionContext context = auth(code);
long expireIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), context.getExpire());
TokenVo vo = new TokenVo();
vo.setAccessToken(context.getAccessToken());
vo.setRefreshToken(context.getRefreshToken());
vo.setExpireIn((int) expireIn);
return R.success(vo);
}
@GetMapping(value = "/oauth2/token/redirect")
public String getTokenHtml(HttpServletRequest request, @RequestParam("code") String code) {
Assert.notNull(authClientProperties.getAuthSuccessRedirectUri(), SysCode.biz(1), "请先配置参数:hiauth.client.authSuccessRedirectUri");
String customAuthSuccessRedirectUri = request.getHeader("dev-auth-success-redirect-uri");
String authSuccessRedirectUri = customAuthSuccessRedirectUri != null ? customAuthSuccessRedirectUri : authClientProperties.getAuthSuccessRedirectUri();
try {
SessionContext context = auth(code);
log.debug("REDIRECT-URI:{}?accessToken={}", authSuccessRedirectUri, context.getAccessToken());
return "redirect:" + authSuccessRedirectUri + "?accessToken=" + context.getAccessToken();
} catch (HttpClientErrorException e) {
log.debug("权限不足,退出重新登陆。");
return logout(request);
}
}
private SessionContext auth(String code) throws HttpClientErrorException {
Assert.notEmpty(code, 300001, "code不能为空。");
Map, ?> tokenMap = getTokenByOauthServer(code);
assert tokenMap != null;
Assert.isTrue(tokenMap.containsKey("access_token"), 300002, "无法获取accessToken。");
String accessToken = (String) tokenMap.get("access_token");
String refreshToken = (String) tokenMap.get("refresh_token");
String scope = (String) tokenMap.get("scope");
Integer expireIn = (Integer) tokenMap.get("expires_in");
Map, ?> userinfoMap = getUserInfoByOauthServer(accessToken);
Long appId = Long.parseLong(userinfoMap.get("appId").toString());
Long cid = Long.parseLong(userinfoMap.get("cid").toString());
Long userId = Long.parseLong(userinfoMap.get("userId").toString());
Long empId = Long.parseLong(userinfoMap.get("empId").toString());
String username = (String) userinfoMap.get("username");
String phoneNum = (String) userinfoMap.get("phoneNum");
String avatarUrl = (String) userinfoMap.get("avatarUrl");
String name = (String) userinfoMap.get("name");
List> authorities = (List>) userinfoMap.get("authorities");
Boolean isCorpAdmin = null;
if (userinfoMap.containsKey("isCorpAdmin")) {
isCorpAdmin = (Boolean) userinfoMap.get("isCorpAdmin");
}
HiAuthToken token = new HiAuthToken();
token.setAccessToken(accessToken);
token.setRefreshToken(refreshToken);
token.setScope(scope);
token.setExpire(LocalDateTime.now().plusSeconds(expireIn));
//设置认证信息
Authentication auth = new Authentication();
auth.setAppId(appId);
auth.setCid(cid);
auth.setUserId(userId);
auth.setUsername(username);
auth.setPhoneNum(phoneNum);
auth.setAvatarUrl(avatarUrl);
auth.setEmpId(empId);
auth.setName(name);
auth.setAuthorities(authorities);
auth.setIsCorpAdmin(isCorpAdmin);
//设置用户扩展信息
if (securityService != null) {
SecurityUser principal = securityService.loadSecurityUser(auth);
auth.setPrincipal(principal);
}
SessionContext context = new SessionContext(null, authClientProperties.getCachePrefix(), authClientProperties.getCacheExpire());
context.setToken(token);
context.setAuth(auth);
return SessionContextHolder.auth(context);
}
@ResponseBody
@GetMapping(value = "/api/common/userinfo")
public R userinfo(HttpServletRequest request) {
Authentication auth = SessionContextHolder.getContext().getAuth();
return R.success(UserinfoVo.toVo(auth));
}
@ResponseBody
@PostMapping(value = "/api/common/updatePwd")
public Map, ?> updatePwd(@RequestBody UserPwdUpdateDto body) {
SessionContext context = SessionContextHolder.getContext();
HiAuthToken hiAuthToken = context.getToken();
return updatePwdByOauthServer(hiAuthToken.getAccessToken(), body.getRawPwd(), body.getNewPwd());
}
@ResponseBody
@PostMapping(value = "/api/common/myCorps")
public R> myCorps() {
Authentication auth = SessionContextHolder.getContext().getAuth();
List corps = securityService.loadUserCorps(auth.getUserId());
return R.success(corps);
}
@ResponseBody
@PostMapping(value = "/api/common/switchCorp")
public R switchCorp(@RequestParam("id") Long id) {
return R.success(securityService.switchCorp(id));
}
private Map, ?> getTokenByOauthServer(String code) {
String basicStr = authClientRegistrationProperties.getClientId() + ":" + authClientRegistrationProperties.getClientSecret();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Authorization", "Basic " + Base64.encode(basicStr.getBytes()));
MultiValueMap map = new LinkedMultiValueMap<>();
map.add("grant_type", "authorization_code");
map.add("code", code);
map.add("redirect_uri", authClientRegistrationProperties.getRedirectUri());
HttpEntity> request = new HttpEntity<>(map, headers);
return restTemplate.postForObject(authClientProviderProperties.getTokenUri(), request, Map.class);
}
private Map, ?> getUserInfoByOauthServer(String accessToken) throws HttpClientErrorException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Authorization", "Bearer " + accessToken);
MultiValueMap map = new LinkedMultiValueMap<>();
HttpEntity> request = new HttpEntity<>(map, headers);
return restTemplate.postForObject(authClientProviderProperties.getUserInfoUri(), request, Map.class);
}
private Map, ?> updatePwdByOauthServer(String accessToken, String rawPwd, String newPwd) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Authorization", "Bearer " + accessToken);
Map map = new HashMap<>(2);
map.put("rawPwd", rawPwd);
map.put("pwd", newPwd);
HttpEntity> request = new HttpEntity<>(map, headers);
return restTemplate.postForObject(authClientProviderProperties.getIssuerUri() + "/oauth2/user/updatePwd", request, Map.class);
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/HiAuthClientProperties.java
================================================
package cn.hiauth.client;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
import java.util.Set;
@Data
@ConfigurationProperties("hiauth.client")
public class HiAuthClientProperties implements Serializable {
/**
* 认证完成后跳转的页面
*/
private String authSuccessRedirectUri;
/**
* 缓存前缀
*/
private String cachePrefix = "hiauth";
/**
* 缓存过期时间(秒),默认值:10天
*/
private Integer cacheExpire = 60 * 60 * 24 * 10;
/**
* 是否开启检查权限
*/
private boolean checkPermission = true;
/**
* 无需登录也无需鉴权的接口(暂时未启用)
*/
private Set ignoreUris = Set.of("/unpapi/**");
/**
* 需要登录,并且需要拥有权限,才可访问的接口
*/
private Set authUris = Set.of("/api/**");
/**
* 需要登录,无需分配权限,就能访问的接口(需要时authUris的子路径)
*/
private Set ignorePermissionUris = Set.of("/api/common/**");
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/HiAuthClientProviderProperties.java
================================================
package cn.hiauth.client;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
@Data
@ConfigurationProperties("spring.security.oauth2.client.provider.hiauth-server")
public class HiAuthClientProviderProperties implements Serializable {
private String issuerUri;
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private String jwkSetUri;
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/HiAuthClientRegistrationProperties.java
================================================
package cn.hiauth.client;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.io.Serializable;
import java.util.Set;
@Data
@ConfigurationProperties("spring.security.oauth2.client.registration.hiauth-code")
public class HiAuthClientRegistrationProperties implements Serializable {
private String provider;
private String clientName;
private String clientId;
private String clientSecret;
private String clientAuthenticationMethod;
private String authorizationGrantType;
private String redirectUri;
private Set scope;
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/HiAuthClientRunner.java
================================================
package cn.hiauth.client;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* @author zgs
*/
@Slf4j
@Component
public class HiAuthClientRunner implements ApplicationRunner {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void run(ApplicationArguments args) {
SessionContextHolder.setRedisTemplate(redisTemplate);
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/api/TokenVo.java
================================================
package cn.hiauth.client.api;
public class TokenVo {
private String accessToken;
private Integer expireIn;
private String refreshToken;
private String scope;
public TokenVo() {
}
public TokenVo(String accessToken, String refreshToken, Integer expireIn, String scope) {
this.accessToken = accessToken;
this.expireIn = expireIn;
this.refreshToken = refreshToken;
this.scope = scope;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public Integer getExpireIn() {
return expireIn;
}
public void setExpireIn(Integer expireIn) {
this.expireIn = expireIn;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/java/cn/hiauth/client/api/UserPwdUpdateDto.java
================================================
package cn.hiauth.client.api;
import lombok.Data;
@Data
public class UserPwdUpdateDto {
private String newPwd;
private String rawPwd;
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/resources/META-INF/spring-configuration-metadata.json
================================================
{
"properties": {
"app.security.enable": {
"description": "是否启用安全拦截。默认值:false",
"type": "boolean"
},
"hiauth.client.cachePrefix": {
"description": "缓存前缀。默认值:hiauth",
"type": "string"
},
"hiauth.client.checkPermission": {
"description": "是否需要检查权限。默认值:hiauth",
"type": "boolean"
},
"hiauth.client.authSuccessRedirectUri": {
"description": "认证完成后跳转的页面。无默认值",
"type": "string"
},
"hiauth.client.authUris": {
"description": "需要登录,并且需要拥有权限,才可访问的接口。默认值:/api/**",
"type": "string[]"
},
"hiauth.client.ignoreUris": {
"description": "无需登录也无需鉴权的接口(暂时未启用)。默认值:/unpapi/**",
"type": "string[]"
},
"spring.security.oauth2.client.provider.hiauth-server": {
"description": "HiAuth授权服务端地址",
"type": "string"
},
"spring.security.oauth2.client.registration.hiauth-code": {
"description": "HiAuth客户端注册",
"type": "string"
}
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-boot-starter/src/main/resources/META-INF/spring.factories
================================================
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.hiauth.client.HiAuthClientAutoConfig
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/pom.xml
================================================
4.0.0
cn.hiauth
hiauth-client-spring-cloud-gateway-starter
1.0.8
jar
hiauth-client-spring-cloud-gateway-starter
hiauth-client-spring-cloud-gateway-starter
https://github.com/bestaone/hiauth
17
17
UTF-8
org.springframework.boot
spring-boot-dependencies
3.4.5
pom
import
org.springframework.cloud
spring-cloud-dependencies
2023.0.5
pom
import
org.springframework.boot
spring-boot-configuration-processor
compile
true
org.springframework.boot
spring-boot-autoconfigure
org.springframework.cloud
spring-cloud-gateway-server
compile
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.projectlombok
lombok
cn.hutool
hutool-all
5.8.38
cn.webestar.scms
commons
1.2.0
cn.hiauth
hiauth-client-commons
1.0.0
org.apache.maven.plugins
maven-compiler-plugin
3.13.0
17
17
UTF-8
org.apache.maven.plugins
maven-source-plugin
3.3.1
attach-sources
package
jar
org.apache.maven.plugins
maven-javadoc-plugin
3.3.0
17
UTF-8
-Xdoclint:none
attach-javadocs
jar
org.apache.maven.plugins
maven-gpg-plugin
3.2.0
sign-artifacts
verify
sign
org.sonatype.central
central-publishing-maven-plugin
0.4.0
true
ossrh
true
${project.groupId}:${project.artifactId}:${project.version}
https://gitee.com/bestaone/scms
scm:git:https://github.com/bestaone/scms.git
scm:git:https://github.com/bestaone/hiauth.git
zgs
bestaone@163.com
https://github.com/bestaone/hiauth
+8
The Apache Software License, Version 2.0
http://www.apache.org/licenses/LICENSE-2.0.txt
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/src/main/java/cn/hiauth/client/gateway/AuthGatewayFilterFactory.java
================================================
package cn.hiauth.client.gateway;
import cn.hiauth.client.*;
import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWT;
import cn.webestar.scms.commons.Assert;
import cn.webestar.scms.commons.CommonException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
/**
* @author zgs
*/
@Slf4j
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory {
private final AntPathMatcher matcher = new AntPathMatcher();
private final HiAuthClientGatewayProperties hiAuthClientGatewayProperties;
private final RedisTemplate redisTemplate;
public AuthGatewayFilterFactory(HiAuthClientGatewayProperties hiAuthClientGatewayProperties, RedisTemplate redisTemplate) {
super(Config.class);
this.hiAuthClientGatewayProperties = hiAuthClientGatewayProperties;
this.redisTemplate = redisTemplate;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
Client client = hiAuthClientGatewayProperties.getClients().get(config.getClientName());
try {
// 检查是否已认证
checkAuth(exchange, client.getCachePrefix());
//TODO 检查是否已授权
} catch (Exception e) {
return handleException(exchange, e);
}
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
})).then();
};
}
private void checkAuth(ServerWebExchange exchange, String cachePrefix) {
ServerHttpRequest request = exchange.getRequest();
final String url = request.getPath().pathWithinApplication().value();
final String method = exchange.getRequest().getMethod().name();
if (Constant.IGNORE_METHOD.equalsIgnoreCase(method) || !matcherAuthUrl(url)) {
return;
}
SessionContext context = getSessionContext(request, cachePrefix);
Assert.notNull(context, 10401, "request fail");
Assert.notNull(cachePrefix, 10401, "cachePrefix is null");
//Assert.isTrue(cachePrefix.equals(context.getCachePrefix()), 10401, "invalid token");
SessionContextHolder.setContext(context);
}
public boolean matcherAuthUrl(String uri) {
for (String authUrl : hiAuthClientGatewayProperties.getAuthUris()) {
if (matcher.match(authUrl, uri)) {
return true;
}
}
return false;
}
private SessionContext getSessionContext(ServerHttpRequest request, String cachePrefix) {
String authHeader = request.getHeaders().getFirst(Constant.TOKEN_HEADER);
Assert.notNull(authHeader, 10401, "miss token");
authHeader = URLDecoder.decode(authHeader, StandardCharsets.UTF_8);
Assert.isTrue(authHeader.startsWith(Constant.TOKEN_PREFIX), 10401, "miss bearer");
String accessToken = authHeader.substring(Constant.TOKEN_PREFIX.length()).trim();
JWT jwt = JwtUtils.parseToken(accessToken);
Assert.notNull(jwt, 10401, "invalid token");
String username = (String) jwt.getPayload(JwtUtils.SUB_KEY);
Assert.notNull(username, 10401, "invalid token");
String accessTokenKey = String.format(Constant.ACCESS_TOKEN_CACHE_KEY, cachePrefix, username, accessToken);
String json = redisTemplate.opsForValue().get(accessTokenKey);
Assert.notNull(json, 10401, "invalid token");
SessionContext context = JSONUtil.toBean(json, SessionContext.class);
Assert.notNull(context, 10401, "invalid token");
return context;
}
private Mono handleException(ServerWebExchange exchange, Throwable ex) {
// 设置响应状态码
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
Integer code = 50000;
String msg;
if (ex instanceof CommonException ce) {
code = ce.getCode();
msg = ex.getMessage();
} else {
msg = "系统异常";
}
byte[] bytes = String.format(Constant.RESULT_JSON, code, msg).getBytes();
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Mono.just(buffer));
}
@Override
public List shortcutFieldOrder() {
return Arrays.asList("clientName", "enabled");
}
@Data
public static class Config {
private String clientName;
private boolean enabled;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/src/main/java/cn/hiauth/client/gateway/HiAuthClientGatewayAutoConfig.java
================================================
package cn.hiauth.client.gateway;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.client.RestTemplate;
/**
* @author zgs
*/
@Slf4j
@Configuration
@EnableConfigurationProperties(HiAuthClientGatewayProperties.class)
public class HiAuthClientGatewayAutoConfig {
@Autowired
private HiAuthClientGatewayProperties hiAuthClientGatewayProperties;
@Bean
public AuthGatewayFilterFactory authGatewayFilterFactory(RedisTemplate redisTemplate) {
return new AuthGatewayFilterFactory(hiAuthClientGatewayProperties, redisTemplate);
}
@Bean
@ConditionalOnMissingBean(RestTemplate.class)
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
@ConditionalOnMissingBean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
log.info("[cache-spring-boot-starter]:Init RedisTemplate");
ObjectMapper om = new ObjectMapper();
// 支持 LocalDate、LocalDateTime的序列号
om.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(om, Object.class);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/src/main/java/cn/hiauth/client/gateway/HiAuthClientGatewayController.java
================================================
package cn.hiauth.client.gateway;
import cn.hiauth.client.*;
import cn.hutool.core.codec.Base64;
import cn.webestar.scms.commons.Assert;
import cn.webestar.scms.commons.R;
import cn.webestar.scms.commons.SysCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Controller
@RequestMapping("/")
public class HiAuthClientGatewayController {
@Autowired
private RestTemplate restTemplate;
@Autowired(required = false)
private SecurityService securityService;
@Autowired
private HiAuthClientGatewayProperties hiauthClientProperties;
@GetMapping("/unpapi/{clientName}/oauth2/login")
public Mono login(@PathVariable("clientName") String clientName, ServerWebExchange exchange) {
Client client = hiauthClientProperties.getClients().get(clientName);
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(hiauthClientProperties.getAuthorizationUri())
.queryParam("response_type", "code")
.queryParam("client_id", client.getClientId())
.queryParam("scope", String.join(" ", client.getScope()))
.queryParam("redirect_uri", client.getRedirectUri());
exchange.getResponse().setStatusCode(HttpStatus.TEMPORARY_REDIRECT);
exchange.getResponse().getHeaders().setLocation(uriBuilder.build().toUri());
return exchange.getResponse().setComplete();
}
@GetMapping(value = "/unpapi/{clientName}/oauth2/token/redirect")
public Mono getTokenHtml(@PathVariable("clientName") String clientName, @RequestParam("code") String code, ServerWebExchange exchange) {
Client client = hiauthClientProperties.getClients().get(clientName);
Assert.notNull(client.getAuthSuccessRedirectUri(), SysCode.biz(1), "请先配置参数:hiauth.client.authSuccessRedirectUri");
String customAuthSuccessRedirectUri = exchange.getRequest().getHeaders().getFirst("dev-auth-success-redirect-uri");
String authSuccessRedirectUri = customAuthSuccessRedirectUri != null ? customAuthSuccessRedirectUri : client.getAuthSuccessRedirectUri();
try {
SessionContext context = auth(clientName, client, code);
log.debug("REDIRECT-URI:{}?accessToken={}", authSuccessRedirectUri, context.getAccessToken());
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(authSuccessRedirectUri)
.queryParam("accessToken", context.getAccessToken());
exchange.getResponse().setStatusCode(HttpStatus.TEMPORARY_REDIRECT);
exchange.getResponse().getHeaders().setLocation(uriBuilder.build().toUri());
return exchange.getResponse().setComplete();
} catch (HttpClientErrorException e) {
log.debug("权限不足,退出重新登陆。");
return logout(clientName, exchange);
}
}
@GetMapping("/unpapi/{clientName}/oauth2/logout")
public Mono logout(@PathVariable("clientName") String clientName, ServerWebExchange exchange) {
Client client = hiauthClientProperties.getClients().get(clientName);
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(hiauthClientProperties.getIssuerUri() + "/unpapi/logoutWithRedirect")
.queryParam("redirect_uri", client.getAuthSuccessRedirectUri());
exchange.getResponse().setStatusCode(HttpStatus.TEMPORARY_REDIRECT);
exchange.getResponse().getHeaders().setLocation(uriBuilder.build().toUri());
return exchange.getResponse().setComplete();
}
@ResponseBody
@GetMapping(value = "/api/common/userinfo")
public R userinfo() {
Authentication auth = SessionContextHolder.getContext().getAuth();
return R.success(UserinfoVo.toVo(auth));
}
@ResponseBody
@PostMapping(value = "/api/common/updatePwd")
public Map, ?> updatePwd(@RequestBody UserPwdUpdateDto body) {
SessionContext context = SessionContextHolder.getContext();
HiAuthToken token = context.getToken();
return updatePwdByOauthServer(token.getAccessToken(), body.getRawPwd(), body.getNewPwd());
}
private SessionContext auth(String clientName, Client client, String code) throws HttpClientErrorException {
Assert.notEmpty(code, 300001, "code不能为空。");
Map, ?> tokenMap = getTokenByOauthServer(client, code);
assert tokenMap != null;
Assert.isTrue(tokenMap.containsKey("access_token"), 300002, "无法获取accessToken。");
String accessToken = (String) tokenMap.get("access_token");
String refreshToken = (String) tokenMap.get("refresh_token");
String scope = (String) tokenMap.get("scope");
Integer expireIn = (Integer) tokenMap.get("expires_in");
Map, ?> userinfoMap = getUserInfoByOauthServer(accessToken);
Long appId = Long.parseLong(userinfoMap.get("appId").toString());
Long cid = Long.parseLong(userinfoMap.get("cid").toString());
Long userId = Long.parseLong(userinfoMap.get("userId").toString());
Long empId = Long.parseLong(userinfoMap.get("empId").toString());
String username = (String) userinfoMap.get("username");
String phoneNum = (String) userinfoMap.get("phoneNum");
String avatarUrl = (String) userinfoMap.get("avatarUrl");
String name = (String) userinfoMap.get("name");
List> authorities = (List>) userinfoMap.get("authorities");
Boolean isCorpAdmin = null;
if (userinfoMap.containsKey("isCorpAdmin")) {
isCorpAdmin = (Boolean) userinfoMap.get("isCorpAdmin");
}
HiAuthToken token = new HiAuthToken();
token.setAccessToken(accessToken);
token.setRefreshToken(refreshToken);
token.setScope(scope);
token.setExpire(LocalDateTime.now().plusSeconds(expireIn));
//设置认证信息
Authentication auth = new Authentication();
auth.setAppId(appId);
auth.setCid(cid);
auth.setUserId(userId);
auth.setUsername(username);
auth.setPhoneNum(phoneNum);
auth.setAvatarUrl(avatarUrl);
auth.setEmpId(empId);
auth.setName(name);
auth.setAuthorities(authorities);
auth.setIsCorpAdmin(isCorpAdmin);
//设置用户扩展信息
if (securityService != null) {
SecurityUser principal = securityService.loadSecurityUser(auth);
auth.setPrincipal(principal);
}
SessionContext context = new SessionContext(clientName, client.getCachePrefix(), client.getCacheExpire());
context.setToken(token);
context.setAuth(auth);
return SessionContextHolder.auth(context);
}
private Map, ?> getTokenByOauthServer(Client client, String code) {
String basicStr = client.getClientId() + ":" + client.getClientSecret();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Authorization", "Basic " + Base64.encode(basicStr.getBytes()));
MultiValueMap map = new LinkedMultiValueMap<>();
map.add("grant_type", "authorization_code");
map.add("code", code);
map.add("redirect_uri", client.getRedirectUri());
HttpEntity> request = new HttpEntity<>(map, headers);
return restTemplate.postForObject(hiauthClientProperties.getTokenUri(), request, Map.class);
}
private Map, ?> getUserInfoByOauthServer(String accessToken) throws HttpClientErrorException {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add("Authorization", "Bearer " + accessToken);
MultiValueMap map = new LinkedMultiValueMap<>();
HttpEntity> request = new HttpEntity<>(map, headers);
return restTemplate.postForObject(hiauthClientProperties.getUserInfoUri(), request, Map.class);
}
private Map, ?> updatePwdByOauthServer(String accessToken, String rawPwd, String newPwd) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.add("Authorization", "Bearer " + accessToken);
Map map = new HashMap<>(2);
map.put("rawPwd", rawPwd);
map.put("pwd", newPwd);
HttpEntity> request = new HttpEntity<>(map, headers);
return restTemplate.postForObject(hiauthClientProperties.getIssuerUri() + "/oauth2/user/updatePwd", request, Map.class);
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/src/main/java/cn/hiauth/client/gateway/HiAuthClientGatewayProperties.java
================================================
package cn.hiauth.client.gateway;
import cn.hiauth.client.Client;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Data
@ConfigurationProperties("hiauth.client.gateway")
public class HiAuthClientGatewayProperties {
private String issuerUri;
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
private Map clients = new HashMap<>();
/**
* 无需登录也无需鉴权的接口(暂时未启用)
*/
private Set ignoreUris = Set.of("/unpapi/**");
/**
* 需要登录,并且需要拥有权限,才可访问的接口
*/
private Set authUris = Set.of("/api/**");
/**
* 需要登录,无需分配权限,就能访问的接口(需要时authUris的子路径)
*/
private Set ignorePermissionUris = Set.of("/api/common/**");
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/src/main/java/cn/hiauth/client/gateway/HiAuthClientGatewayRunner.java
================================================
package cn.hiauth.client.gateway;
import cn.hiauth.client.SessionContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* @author zgs
*/
@Slf4j
@Component
public class HiAuthClientGatewayRunner implements ApplicationRunner {
@Autowired
private RedisTemplate redisTemplate;
@Override
public void run(ApplicationArguments args) {
SessionContextHolder.setRedisTemplate(redisTemplate);
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/src/main/java/cn/hiauth/client/gateway/UserPwdUpdateDto.java
================================================
package cn.hiauth.client.gateway;
import lombok.Data;
@Data
public class UserPwdUpdateDto {
private String newPwd;
private String rawPwd;
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/src/main/resources/META-INF/spring-configuration-metadata.json
================================================
{
"properties": {
"hiauth.client.gateway.issuerUri": {
"description": "HiAuth认证服务地址。",
"type": "string"
},
"hiauth.client.gateway.authorizationUri": {
"description": "authorization端点地址",
"type": "string"
},
"hiauth.client.gateway.tokenUri": {
"description": "token端点地址",
"type": "string"
},
"hiauth.client.gateway.userInfoUri": {
"description": "userInfo端点地址",
"type": "string"
},
"hiauth.client.gateway.clients": {
"description": "客户端配置",
"type": "[]"
},
"hiauth.client.gateway.ignoreUris": {
"description": "无需登录也无需鉴权的接口(暂时未启用)。默认值:/unpapi/**",
"type": "string[]"
},
"hiauth.client.gateway.authUris": {
"description": "需要登录,并且需要拥有权限,才可访问的接口。默认值:/api/**",
"type": "string[]"
}
}
}
================================================
FILE: hiauth-client-starter/hiauth-client-spring-cloud-gateway-starter/src/main/resources/META-INF/spring.factories
================================================
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.hiauth.client.gateway.HiAuthClientGatewayAutoConfig
================================================
FILE: hiauth-client-starter/pom.xml
================================================
4.0.0
cn.hiauth
hiauth-client-starter
1.0.0-SNAPSHOT
pom
hiauth-client-commons
hiauth-client-spring-boot-starter
hiauth-client-session-spring-boot-starter
hiauth-client-spring-cloud-gateway-starter
hiauth-client-resource-spring-boot-starter
================================================
FILE: hiauth-front/.browserslistrc
================================================
> 1%
last 2 versions
not dead
not ie 11
================================================
FILE: hiauth-front/.changeset/README.md
================================================
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
================================================
FILE: hiauth-front/.changeset/config.json
================================================
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "vbenjs/vue-vben-admin" }
],
"commit": false,
"fixed": [["@vben-core/*", "@vben/*"]],
"snapshot": {
"prereleaseTemplate": "{tag}-{datetime}"
},
"privatePackages": { "version": true, "tag": true },
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
================================================
FILE: hiauth-front/.commitlintrc.js
================================================
export { default } from '@vben/commitlint-config';
================================================
FILE: hiauth-front/.dockerignore
================================================
node_modules
.git
.gitignore
*.md
dist
.turbo
dist.zip
================================================
FILE: hiauth-front/.editorconfig
================================================
root = true
[*]
charset=utf-8
end_of_line=lf
insert_final_newline=true
indent_style=space
indent_size=2
max_line_length = 100
trim_trailing_whitespace = true
quote_type = single
[*.{yml,yaml,json}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
================================================
FILE: hiauth-front/.gitattributes
================================================
# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings
# Automatically normalize line endings (to LF) for all text-based files.
* text=auto eol=lf
# Declare files that will always have CRLF line endings on checkout.
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary
================================================
FILE: hiauth-front/.gitconfig
================================================
[core]
ignorecase = false
================================================
FILE: hiauth-front/.gitignore
================================================
node_modules
.DS_Store
dist
dist-ssr
dist.zip
dist.tar
dist.war
.nitro
.output
*-dist.zip
*-dist.tar
*-dist.war
coverage
*.local
**/.vitepress/cache
.cache
.turbo
.temp
dev-dist
.stylelintcache
yarn.lock
package-lock.json
.VSCodeCounter
**/backend-mock/data
# local env files
.env.local
.env.*.local
.eslintcache
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
vite.config.mts.*
vite.config.mjs.*
vite.config.js.*
vite.config.ts.*
# Editor directories and files
.idea
# .vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.history
.cursor
================================================
FILE: hiauth-front/.gitpod.yml
================================================
ports:
- port: 5555
onOpen: open-preview
tasks:
- init: npm i -g corepack && pnpm install
command: pnpm run dev:play
================================================
FILE: hiauth-front/.node-version
================================================
22.1.0
================================================
FILE: hiauth-front/.npmrc
================================================
registry = "https://registry.npmmirror.com"
public-hoist-pattern[]=lefthook
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier
public-hoist-pattern[]=prettier-plugin-tailwindcss
public-hoist-pattern[]=stylelint
public-hoist-pattern[]=*postcss*
public-hoist-pattern[]=@commitlint/*
public-hoist-pattern[]=czg
strict-peer-dependencies=false
auto-install-peers=true
dedupe-peer-dependents=true
================================================
FILE: hiauth-front/.prettierignore
================================================
dist
dev-dist
.local
.output.js
node_modules
.nvmrc
coverage
CODEOWNERS
.nitro
.output
**/*.svg
**/*.sh
public
.npmrc
*-lock.yaml
================================================
FILE: hiauth-front/.prettierrc.mjs
================================================
export { default } from '@vben/prettier-config';
================================================
FILE: hiauth-front/.stylelintignore
================================================
dist
public
__tests__
coverage
================================================
FILE: hiauth-front/Dockerfile
================================================
FROM harbor.vking.fun/vking/nginx:1.25.5
VOLUME /tmp
# 设置语言
ENV LANG en_US.UTF-8
# 设置地理位置
ENV TZ=Asia/Shanghai
RUN mkdir -p /html && chmod a+rwx -R /html
ADD ./apps/web-auth/dist.zip front-auth.zip
RUN unzip front-auth.zip -d /html/front-auth
EXPOSE 80
EXPOSE 443
================================================
FILE: hiauth-front/LICENSE
================================================
MIT License
Copyright (c) 2024-present, Vben
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: hiauth-front/README.ja-JP.md
================================================
[](LICENSE)
Vue Vben Admin
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
**日本語** | [English](./README.md) | [中文](./README.zh-CN.md)
## 紹介
Vue Vben Adminは、最新の`vue3`、`vite`、`TypeScript`などの主流技術を使用して開発された、無料でオープンソースの中・後端テンプレートです。すぐに使える中・後端のフロントエンドソリューションとして、学習の参考にもなります。
## アップグレード通知
これは最新バージョン `5.0` であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
## 特徴
- **最新技術スタック**:Vue 3やViteなどの最先端フロントエンド技術で開発
- **TypeScript**:アプリケーション規模のJavaScriptのための言語
- **テーマ**:複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
- **国際化**:完全な内蔵国際化サポート
- **権限管理**:動的ルートベースの権限生成ソリューションを内蔵
## プレビュー
- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト
テストアカウント:vben/123456
### Gitpodを使用
Gitpod(GitHub用の無料オンライン開発環境)でプロジェクトを開き、すぐにコーディングを開始します。
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
## ドキュメント
[ドキュメント](https://doc.vben.pro/)
## インストールと使用
1. プロジェクトコードを取得
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
2. 依存関係のインストール
```bash
cd vue-vben-admin
npm i -g corepack
pnpm install
```
3. 実行
```bash
pnpm dev
```
4. ビルド
```bash
pnpm build
```
## 変更ログ
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 貢献方法
ご参加をお待ちしておりますするか、Pull Requestを送信してください。
**Pull Request プロセス:**
1. コードをフォーク
2. 自分のブランチを作成:`git checkout -b feat/xxxx`
3. 変更をコミット:`git commit -am 'feat(function): add xxxxx'`
4. ブランチをプッシュ:`git push origin feat/xxxx`
5. `pull request`を送信
## Git貢献提出規則
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 新機能の追加
- `fix` 問題/バグの修正
- `style` コードスタイルに関連し、実行結果に影響しない
- `perf` 最適化/パフォーマンス向上
- `refactor` リファクタリング
- `revert` 変更の取り消し
- `test` テスト関連
- `docs` ドキュメント/注釈
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
- `ci` 継続的インテグレーション
- `types` 型定義ファイルの変更
## ブラウザサポート
ローカル開発には `Chrome 80+` ブラウザを推奨します
モダンブラウザをサポートし、IEはサポートしません
| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
| :-: | :-: | :-: | :-: |
| 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
## メンテナー
[@Vben](https://github.com/anncwb)
## スター歴史
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## 寄付
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!

Paypal Me
## 貢献者
## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
## ライセンス
[MIT © Vben-2020](./LICENSE)
================================================
FILE: hiauth-front/README.md
================================================
[](LICENSE)
Vue Vben Admin
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
## Introduction
Vue Vben Admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite`, `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
## Upgrade Notice
This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
## Features
- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
- **TypeScript**: A language for application-scale JavaScript
- **Themes**: Multiple theme colors available with customizable options
- **Internationalization**: Comprehensive built-in internationalization support
- **Permissions**: Built-in solution for dynamic route-based permission generation
## Preview
- [Vben Admin](https://vben.pro/) - Full version Chinese site
Test Account: vben/123456
### Use Gitpod
Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
## Documentation
[Document](https://doc.vben.pro/)
## Install and Use
1. Get the project code
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
2. Install dependencies
```bash
cd vue-vben-admin
npm i -g corepack
pnpm install
```
3. Run
```bash
pnpm dev
```
4. Build
```bash
pnpm build
```
## Change Log
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## How to Contribute
You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
**Pull Request Process:**
1. Fork the code
2. Create your branch: `git checkout -b feat/xxxx`
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
4. Push your branch: `git push origin feat/xxxx`
5. Submit `pull request`
## Git Contribution Submission Specification
Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` Add new features
- `fix` Fix the problem/BUG
- `style` The code style is related and does not affect the running result
- `perf` Optimization/performance improvement
- `refactor` Refactor
- `revert` Undo edit
- `test` Test related
- `docs` Documentation/notes
- `chore` Dependency update/scaffolding configuration modification etc.
- `ci` Continuous integration
- `types` Type definition file changes
## Browser Support
The `Chrome 80+` browser is recommended for local development
Support modern browsers, not IE
| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
| :-: | :-: | :-: | :-: |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Maintainer
[@Vben](https://github.com/anncwb)
## Star History
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## Donate
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!

Paypal Me
## Contributors
## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
## License
[MIT © Vben-2020](./LICENSE)
================================================
FILE: hiauth-front/README.zh-CN.md
================================================
[](LICENSE)
Vue Vben Admin
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
**中文** | [English](./README.md) | [日本語](./README.ja-JP.md)
## 简介
Vue Vben Admin 是 Vue Vben Admin 的升级版本。作为一个免费开源的中后台模板,它采用了最新的 Vue 3、Vite、TypeScript 等主流技术开发,开箱即用,可用于中后台前端开发,也适合学习参考。
## 升级提示
该版本为最新版本 `5.0`,与其他版本不兼容,如果你是新项目,建议使用最新版本。如果你想查看旧版本,请使用 [v2 分支](https://github.com/vbenjs/vue-vben-admin/tree/v2)
## 特性
- **最新技术栈**:使用 Vue3/vite 等前端前沿技术开发
- **TypeScript**:应用程序级 JavaScript 的语言
- **主题**:提供多套主题色彩,可配置自定义主题
- **国际化**:内置完善的国际化方案
- **权限**:内置完善的动态路由权限生成方案
## 预览
- [Vben Admin](https://vben.pro/) - 完整版中文站点
测试账号:vben/123456
### 使用 Gitpod
在 Gitpod(适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码。
[](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
## 文档
[文档地址](https://doc.vben.pro/)
## 安装使用
1. 获取项目代码
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
2. 安装依赖
```bash
cd vue-vben-admin
npm i -g corepack
pnpm install
```
3. 运行
```bash
pnpm dev
```
4. 打包
```bash
pnpm build
```
## 更新日志
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 如何贡献
非常欢迎你的加入 或者提交一个 Pull Request。
**Pull Request 流程:**
1. Fork 代码
2. 创建自己的分支:`git checkout -b feature/xxxx`
3. 提交你的修改:`git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支:`git push origin feature/xxxx`
5. 提交 `pull request`
## Git 贡献提交规范
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `ci` 持续集成
- `types` 类型定义文件更改
## 浏览器支持
本地开发推荐使用 `Chrome 80+` 浏览器
支持现代浏览器,不支持 IE
| [ ](http://godban.github.io/browsers-support-badges/)Edge | [ ](http://godban.github.io/browsers-support-badges/)Firefox | [ ](http://godban.github.io/browsers-support-badges/)Chrome | [ ](http://godban.github.io/browsers-support-badges/)Safari |
| :-: | :-: | :-: | :-: |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 维护者
[@Vben](https://github.com/anncwb)
## Star 历史
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## 捐赠
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!

Paypal Me
## 贡献者
## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
## 许可证
[MIT © Vben-2020](./LICENSE)
================================================
FILE: hiauth-front/apps/backend-mock/README.md
================================================
# @vben/backend-mock
## Description
Vben Admin 数据 mock 服务,没有对接任何的数据库,所有数据都是模拟的,用于前端开发时提供数据支持。线上环境不再提供 mock 集成,可自行部署服务或者对接真实数据,由于 `mock.js` 等工具有一些限制,比如上传文件不行、无法模拟复杂的逻辑等,所以这里使用了真实的后端服务来实现。唯一麻烦的是本地需要同时启动后端服务和前端服务,但是这样可以更好的模拟真实环境。该服务不需要手动启动,已经集成在 vite 插件内,随应用一起启用。
## Running the app
```bash
# development
$ pnpm run start
# production mode
$ pnpm run build
```
================================================
FILE: hiauth-front/apps/backend-mock/api/auth/codes.ts
================================================
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_CODES } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const codes =
MOCK_CODES.find((item) => item.username === userinfo.username)?.codes ?? [];
return useResponseSuccess(codes);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/auth/login.post.ts
================================================
import { defineEventHandler, readBody, setResponseStatus } from 'h3';
import {
clearRefreshTokenCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, generateRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import {
forbiddenResponse,
useResponseError,
useResponseSuccess,
} from '~/utils/response';
export default defineEventHandler(async (event) => {
const { password, username } = await readBody(event);
if (!password || !username) {
setResponseStatus(event, 400);
return useResponseError(
'BadRequestException',
'Username and password are required',
);
}
const findUser = MOCK_USERS.find(
(item) => item.username === username && item.password === password,
);
if (!findUser) {
clearRefreshTokenCookie(event);
return forbiddenResponse(event, 'Username or password is incorrect.');
}
const accessToken = generateAccessToken(findUser);
const refreshToken = generateRefreshToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return useResponseSuccess({
...findUser,
accessToken,
});
});
================================================
FILE: hiauth-front/apps/backend-mock/api/auth/logout.post.ts
================================================
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
} from '~/utils/cookie-utils';
import { useResponseSuccess } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return useResponseSuccess('');
}
clearRefreshTokenCookie(event);
return useResponseSuccess('');
});
================================================
FILE: hiauth-front/apps/backend-mock/api/auth/refresh.post.ts
================================================
import { defineEventHandler } from 'h3';
import {
clearRefreshTokenCookie,
getRefreshTokenFromCookie,
setRefreshTokenCookie,
} from '~/utils/cookie-utils';
import { generateAccessToken, verifyRefreshToken } from '~/utils/jwt-utils';
import { MOCK_USERS } from '~/utils/mock-data';
import { forbiddenResponse } from '~/utils/response';
export default defineEventHandler(async (event) => {
const refreshToken = getRefreshTokenFromCookie(event);
if (!refreshToken) {
return forbiddenResponse(event);
}
clearRefreshTokenCookie(event);
const userinfo = verifyRefreshToken(refreshToken);
if (!userinfo) {
return forbiddenResponse(event);
}
const findUser = MOCK_USERS.find(
(item) => item.username === userinfo.username,
);
if (!findUser) {
return forbiddenResponse(event);
}
const accessToken = generateAccessToken(findUser);
setRefreshTokenCookie(event, refreshToken);
return accessToken;
});
================================================
FILE: hiauth-front/apps/backend-mock/api/demo/bigint.ts
================================================
import { eventHandler, setHeader } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const data = `
{
"code": 0,
"message": "success",
"data": [
{
"id": 123456789012345678901234567890123456789012345678901234567890,
"name": "John Doe",
"age": 30,
"email": "john-doe@demo.com"
},
{
"id": 987654321098765432109876543210987654321098765432109876543210,
"name": "Jane Smith",
"age": 25,
"email": "jane@demo.com"
}
]
}
`;
setHeader(event, 'Content-Type', 'application/json');
return data;
});
================================================
FILE: hiauth-front/apps/backend-mock/api/menu/all.ts
================================================
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENUS } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const menus =
MOCK_MENUS.find((item) => item.username === userinfo.username)?.menus ?? [];
return useResponseSuccess(menus);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/status.ts
================================================
import { eventHandler, getQuery, setResponseStatus } from 'h3';
import { useResponseError } from '~/utils/response';
export default eventHandler((event) => {
const { status } = getQuery(event);
setResponseStatus(event, Number(status));
return useResponseError(`${status}`);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/system/dept/.post.ts
================================================
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
return useResponseSuccess(null);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/system/dept/[id].delete.ts
================================================
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(1000);
return useResponseSuccess(null);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/system/dept/[id].put.ts
================================================
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
useResponseSuccess,
} from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(2000);
return useResponseSuccess(null);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/system/dept/list.ts
================================================
import { faker } from '@faker-js/faker';
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record = {
id: faker.string.uuid(),
pid: 0,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2021-01-01', to: '2022-12-31' }),
),
remark: faker.lorem.sentence(),
};
if (faker.datatype.boolean()) {
dataItem.children = Array.from(
{ length: faker.number.int({ min: 1, max: 5 }) },
() => ({
id: faker.string.uuid(),
pid: dataItem.id,
name: faker.commerce.department(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2023-01-01', to: '2023-12-31' }),
),
remark: faker.lorem.sentence(),
}),
);
}
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(10);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const listData = structuredClone(mockData);
return useResponseSuccess(listData);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/system/menu/list.ts
================================================
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(MOCK_MENU_LIST);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/system/menu/name-exists.ts
================================================
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const namesMap: Record = {};
function getNames(menus: any[]) {
menus.forEach((menu) => {
namesMap[menu.name] = String(menu.id);
if (menu.children) {
getNames(menu.children);
}
});
}
getNames(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, name } = getQuery(event);
return (name as string) in namesMap &&
(!id || namesMap[name as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/system/menu/path-exists.ts
================================================
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
const pathMap: Record = { '/': 0 };
function getPaths(menus: any[]) {
menus.forEach((menu) => {
pathMap[menu.path] = String(menu.id);
if (menu.children) {
getPaths(menu.children);
}
});
}
getPaths(MOCK_MENU_LIST);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const { id, path } = getQuery(event);
return (path as string) in pathMap &&
(!id || pathMap[path as string] !== String(id))
? useResponseSuccess(true)
: useResponseSuccess(false);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/system/role/list.ts
================================================
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { getMenuIds, MOCK_MENU_LIST } from '~/utils/mock-data';
import { unAuthorizedResponse, usePageResponseSuccess } from '~/utils/response';
const formatterCN = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
const menuIds = getMenuIds(MOCK_MENU_LIST);
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem: Record = {
id: faker.string.uuid(),
name: faker.commerce.product(),
status: faker.helpers.arrayElement([0, 1]),
createTime: formatterCN.format(
faker.date.between({ from: '2022-01-01', to: '2025-01-01' }),
),
permissions: faker.helpers.arrayElements(menuIds),
remark: faker.lorem.sentence(),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
const {
page = 1,
pageSize = 20,
name,
id,
remark,
startTime,
endTime,
status,
} = getQuery(event);
let listData = structuredClone(mockData);
if (name) {
listData = listData.filter((item) =>
item.name.toLowerCase().includes(String(name).toLowerCase()),
);
}
if (id) {
listData = listData.filter((item) =>
item.id.toLowerCase().includes(String(id).toLowerCase()),
);
}
if (remark) {
listData = listData.filter((item) =>
item.remark?.toLowerCase()?.includes(String(remark).toLowerCase()),
);
}
if (startTime) {
listData = listData.filter((item) => item.createTime >= startTime);
}
if (endTime) {
listData = listData.filter((item) => item.createTime <= endTime);
}
if (['0', '1'].includes(status as string)) {
listData = listData.filter((item) => item.status === Number(status));
}
return usePageResponseSuccess(page as string, pageSize as string, listData);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/table/list.ts
================================================
import { faker } from '@faker-js/faker';
import { eventHandler, getQuery } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import {
sleep,
unAuthorizedResponse,
usePageResponseSuccess,
} from '~/utils/response';
function generateMockDataList(count: number) {
const dataList = [];
for (let i = 0; i < count; i++) {
const dataItem = {
id: faker.string.uuid(),
imageUrl: faker.image.avatar(),
imageUrl2: faker.image.avatar(),
open: faker.datatype.boolean(),
status: faker.helpers.arrayElement(['success', 'error', 'warning']),
productName: faker.commerce.productName(),
price: faker.commerce.price(),
currency: faker.finance.currencyCode(),
quantity: faker.number.int({ min: 1, max: 100 }),
available: faker.datatype.boolean(),
category: faker.commerce.department(),
releaseDate: faker.date.past(),
rating: faker.number.float({ min: 1, max: 5 }),
description: faker.commerce.productDescription(),
weight: faker.number.float({ min: 0.1, max: 10 }),
color: faker.color.human(),
inProduction: faker.datatype.boolean(),
tags: Array.from({ length: 3 }, () => faker.commerce.productAdjective()),
};
dataList.push(dataItem);
}
return dataList;
}
const mockData = generateMockDataList(100);
export default eventHandler(async (event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
await sleep(600);
const { page, pageSize, sortBy, sortOrder } = getQuery(event);
// 规范化分页参数,处理 string[]
const pageRaw = Array.isArray(page) ? page[0] : page;
const pageSizeRaw = Array.isArray(pageSize) ? pageSize[0] : pageSize;
const pageNumber = Math.max(
1,
Number.parseInt(String(pageRaw ?? '1'), 10) || 1,
);
const pageSizeNumber = Math.min(
100,
Math.max(1, Number.parseInt(String(pageSizeRaw ?? '10'), 10) || 10),
);
const listData = structuredClone(mockData);
// 规范化 query 入参,兼容 string[]
const sortKeyRaw = Array.isArray(sortBy) ? sortBy[0] : sortBy;
const sortOrderRaw = Array.isArray(sortOrder) ? sortOrder[0] : sortOrder;
// 检查 sortBy 是否是 listData 元素的合法属性键
if (
typeof sortKeyRaw === 'string' &&
listData[0] &&
Object.prototype.hasOwnProperty.call(listData[0], sortKeyRaw)
) {
// 定义数组元素的类型
type ItemType = (typeof listData)[0];
const sortKey = sortKeyRaw as keyof ItemType; // 将 sortBy 断言为合法键
const isDesc = sortOrderRaw === 'desc';
listData.sort((a, b) => {
const aValue = a[sortKey] as unknown;
const bValue = b[sortKey] as unknown;
let result = 0;
if (typeof aValue === 'number' && typeof bValue === 'number') {
result = aValue - bValue;
} else if (aValue instanceof Date && bValue instanceof Date) {
result = aValue.getTime() - bValue.getTime();
} else if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
if (aValue === bValue) {
result = 0;
} else {
result = aValue ? 1 : -1;
}
} else {
const aStr = String(aValue);
const bStr = String(bValue);
const aNum = Number(aStr);
const bNum = Number(bStr);
result =
Number.isFinite(aNum) && Number.isFinite(bNum)
? aNum - bNum
: aStr.localeCompare(bStr, undefined, {
numeric: true,
sensitivity: 'base',
});
}
return isDesc ? -result : result;
});
}
return usePageResponseSuccess(
String(pageNumber),
String(pageSizeNumber),
listData,
);
});
================================================
FILE: hiauth-front/apps/backend-mock/api/test.get.ts
================================================
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test get handler');
================================================
FILE: hiauth-front/apps/backend-mock/api/test.post.ts
================================================
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => 'Test post handler');
================================================
FILE: hiauth-front/apps/backend-mock/api/upload.ts
================================================
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess({
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
});
// return useResponseError("test")
});
================================================
FILE: hiauth-front/apps/backend-mock/api/user/info.ts
================================================
import { eventHandler } from 'h3';
import { verifyAccessToken } from '~/utils/jwt-utils';
import { unAuthorizedResponse, useResponseSuccess } from '~/utils/response';
export default eventHandler((event) => {
const userinfo = verifyAccessToken(event);
if (!userinfo) {
return unAuthorizedResponse(event);
}
return useResponseSuccess(userinfo);
});
================================================
FILE: hiauth-front/apps/backend-mock/error.ts
================================================
import type { NitroErrorHandler } from 'nitropack';
const errorHandler: NitroErrorHandler = function (error, event) {
event.node.res.end(`[Error Handler] ${error.stack}`);
};
export default errorHandler;
================================================
FILE: hiauth-front/apps/backend-mock/middleware/1.api.ts
================================================
import { defineEventHandler } from 'h3';
import { forbiddenResponse, sleep } from '~/utils/response';
export default defineEventHandler(async (event) => {
event.node.res.setHeader(
'Access-Control-Allow-Origin',
event.headers.get('Origin') ?? '*',
);
if (event.method === 'OPTIONS') {
event.node.res.statusCode = 204;
event.node.res.statusMessage = 'No Content.';
return 'OK';
} else if (
['DELETE', 'PATCH', 'POST', 'PUT'].includes(event.method) &&
event.path.startsWith('/api/system/')
) {
await sleep(Math.floor(Math.random() * 2000));
return forbiddenResponse(event, '演示环境,禁止修改');
}
});
================================================
FILE: hiauth-front/apps/backend-mock/nitro.config.ts
================================================
import errorHandler from './error';
process.env.COMPATIBILITY_DATE = new Date().toISOString();
export default defineNitroConfig({
devErrorHandler: errorHandler,
errorHandler: '~/error',
routeRules: {
'/api/**': {
cors: true,
headers: {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Headers':
'Accept, Authorization, Content-Length, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-CSRF-TOKEN, X-Requested-With',
'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
'Access-Control-Allow-Origin': '*',
'Access-Control-Expose-Headers': '*',
},
},
},
});
================================================
FILE: hiauth-front/apps/backend-mock/package.json
================================================
{
"name": "@vben/backend-mock",
"version": "0.0.1",
"description": "",
"private": true,
"license": "MIT",
"author": "",
"scripts": {
"build": "nitro build",
"start": "nitro dev"
},
"dependencies": {
"@faker-js/faker": "catalog:",
"jsonwebtoken": "catalog:",
"nitropack": "catalog:"
},
"devDependencies": {
"@types/jsonwebtoken": "catalog:",
"h3": "catalog:"
}
}
================================================
FILE: hiauth-front/apps/backend-mock/routes/[...].ts
================================================
import { defineEventHandler } from 'h3';
export default defineEventHandler(() => {
return `
Hello Vben Admin
Mock service is starting
`;
});
================================================
FILE: hiauth-front/apps/backend-mock/tsconfig.build.json
================================================
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}
================================================
FILE: hiauth-front/apps/backend-mock/tsconfig.json
================================================
{
"extends": "./.nitro/types/tsconfig.json"
}
================================================
FILE: hiauth-front/apps/backend-mock/utils/cookie-utils.ts
================================================
import type { EventHandlerRequest, H3Event } from 'h3';
import { deleteCookie, getCookie, setCookie } from 'h3';
export function clearRefreshTokenCookie(event: H3Event) {
deleteCookie(event, 'jwt', {
httpOnly: true,
sameSite: 'none',
secure: true,
});
}
export function setRefreshTokenCookie(
event: H3Event,
refreshToken: string,
) {
setCookie(event, 'jwt', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60, // unit: seconds
sameSite: 'none',
secure: true,
});
}
export function getRefreshTokenFromCookie(event: H3Event) {
const refreshToken = getCookie(event, 'jwt');
return refreshToken;
}
================================================
FILE: hiauth-front/apps/backend-mock/utils/jwt-utils.ts
================================================
import type { EventHandlerRequest, H3Event } from 'h3';
import type { UserInfo } from './mock-data';
import { getHeader } from 'h3';
import jwt from 'jsonwebtoken';
import { MOCK_USERS } from './mock-data';
// TODO: Replace with your own secret key
const ACCESS_TOKEN_SECRET = 'access_token_secret';
const REFRESH_TOKEN_SECRET = 'refresh_token_secret';
export interface UserPayload extends UserInfo {
iat: number;
exp: number;
}
export function generateAccessToken(user: UserInfo) {
return jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '7d' });
}
export function generateRefreshToken(user: UserInfo) {
return jwt.sign(user, REFRESH_TOKEN_SECRET, {
expiresIn: '30d',
});
}
export function verifyAccessToken(
event: H3Event,
): null | Omit {
const authHeader = getHeader(event, 'Authorization');
if (!authHeader?.startsWith('Bearer')) {
return null;
}
const tokenParts = authHeader.split(' ');
if (tokenParts.length !== 2) {
return null;
}
const token = tokenParts[1] as string;
try {
const decoded = jwt.verify(
token,
ACCESS_TOKEN_SECRET,
) as unknown as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find((item) => item.username === username);
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}
export function verifyRefreshToken(
token: string,
): null | Omit {
try {
const decoded = jwt.verify(token, REFRESH_TOKEN_SECRET) as UserPayload;
const username = decoded.username;
const user = MOCK_USERS.find(
(item) => item.username === username,
) as UserInfo;
if (!user) {
return null;
}
const { password: _pwd, ...userinfo } = user;
return userinfo;
} catch {
return null;
}
}
================================================
FILE: hiauth-front/apps/backend-mock/utils/mock-data.ts
================================================
export interface UserInfo {
id: number;
password: string;
realName: string;
roles: string[];
username: string;
homePath?: string;
}
export const MOCK_USERS: UserInfo[] = [
{
id: 0,
password: '123456',
realName: 'Vben',
roles: ['super'],
username: 'vben',
},
{
id: 1,
password: '123456',
realName: 'Admin',
roles: ['admin'],
username: 'admin',
homePath: '/workspace',
},
{
id: 2,
password: '123456',
realName: 'Jack',
roles: ['user'],
username: 'jack',
homePath: '/analytics',
},
];
export const MOCK_CODES = [
// super
{
codes: ['AC_100100', 'AC_100110', 'AC_100120', 'AC_100010'],
username: 'vben',
},
{
// admin
codes: ['AC_100010', 'AC_100020', 'AC_100030'],
username: 'admin',
},
{
// user
codes: ['AC_1000001', 'AC_1000002'],
username: 'jack',
},
];
const dashboardMenus = [
{
meta: {
order: -1,
title: 'page.dashboard.title',
},
name: 'Dashboard',
path: '/dashboard',
redirect: '/analytics',
children: [
{
name: 'Analytics',
path: '/analytics',
component: '/dashboard/analytics/index',
meta: {
affixTab: true,
title: 'page.dashboard.analytics',
},
},
{
name: 'Workspace',
path: '/workspace',
component: '/dashboard/workspace/index',
meta: {
title: 'page.dashboard.workspace',
},
},
],
},
];
const createDemosMenus = (role: 'admin' | 'super' | 'user') => {
const roleWithMenus = {
admin: {
component: '/demos/access/admin-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.adminVisible',
},
name: 'AccessAdminVisibleDemo',
path: '/demos/access/admin-visible',
},
super: {
component: '/demos/access/super-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.superVisible',
},
name: 'AccessSuperVisibleDemo',
path: '/demos/access/super-visible',
},
user: {
component: '/demos/access/user-visible',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.userVisible',
},
name: 'AccessUserVisibleDemo',
path: '/demos/access/user-visible',
},
};
return [
{
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: 'demos.title',
},
name: 'Demos',
path: '/demos',
redirect: '/demos/access',
children: [
{
name: 'AccessDemos',
path: '/demosaccess',
meta: {
icon: 'mdi:cloud-key-outline',
title: 'demos.access.backendPermissions',
},
redirect: '/demos/access/page-control',
children: [
{
name: 'AccessPageControlDemo',
path: '/demos/access/page-control',
component: '/demos/access/index',
meta: {
icon: 'mdi:page-previous-outline',
title: 'demos.access.pageAccess',
},
},
{
name: 'AccessButtonControlDemo',
path: '/demos/access/button-control',
component: '/demos/access/button-control',
meta: {
icon: 'mdi:button-cursor',
title: 'demos.access.buttonControl',
},
},
{
name: 'AccessMenuVisible403Demo',
path: '/demos/access/menu-visible-403',
component: '/demos/access/menu-visible-403',
meta: {
authority: ['no-body'],
icon: 'mdi:button-cursor',
menuVisibleWithForbidden: true,
title: 'demos.access.menuVisible403',
},
},
roleWithMenus[role],
],
},
],
},
];
};
export const MOCK_MENUS = [
{
menus: [...dashboardMenus, ...createDemosMenus('super')],
username: 'vben',
},
{
menus: [...dashboardMenus, ...createDemosMenus('admin')],
username: 'admin',
},
{
menus: [...dashboardMenus, ...createDemosMenus('user')],
username: 'jack',
},
];
export const MOCK_MENU_LIST = [
{
id: 1,
name: 'Workspace',
status: 1,
type: 'menu',
icon: 'mdi:dashboard',
path: '/workspace',
component: '/dashboard/workspace/index',
meta: {
icon: 'carbon:workspace',
title: 'page.dashboard.workspace',
affixTab: true,
order: 0,
},
},
{
id: 2,
meta: {
icon: 'carbon:settings',
order: 9997,
title: 'system.title',
badge: 'new',
badgeType: 'normal',
badgeVariants: 'primary',
},
status: 1,
type: 'catalog',
name: 'System',
path: '/system',
children: [
{
id: 201,
pid: 2,
path: '/system/menu',
name: 'SystemMenu',
authCode: 'System:Menu:List',
status: 1,
type: 'menu',
meta: {
icon: 'carbon:menu',
title: 'system.menu.title',
},
component: '/system/menu/list',
children: [
{
id: 20_101,
pid: 201,
name: 'SystemMenuCreate',
status: 1,
type: 'button',
authCode: 'System:Menu:Create',
meta: { title: 'common.create' },
},
{
id: 20_102,
pid: 201,
name: 'SystemMenuEdit',
status: 1,
type: 'button',
authCode: 'System:Menu:Edit',
meta: { title: 'common.edit' },
},
{
id: 20_103,
pid: 201,
name: 'SystemMenuDelete',
status: 1,
type: 'button',
authCode: 'System:Menu:Delete',
meta: { title: 'common.delete' },
},
],
},
{
id: 202,
pid: 2,
path: '/system/dept',
name: 'SystemDept',
status: 1,
type: 'menu',
authCode: 'System:Dept:List',
meta: {
icon: 'carbon:container-services',
title: 'system.dept.title',
},
component: '/system/dept/list',
children: [
{
id: 20_401,
pid: 201,
name: 'SystemDeptCreate',
status: 1,
type: 'button',
authCode: 'System:Dept:Create',
meta: { title: 'common.create' },
},
{
id: 20_402,
pid: 201,
name: 'SystemDeptEdit',
status: 1,
type: 'button',
authCode: 'System:Dept:Edit',
meta: { title: 'common.edit' },
},
{
id: 20_403,
pid: 201,
name: 'SystemDeptDelete',
status: 1,
type: 'button',
authCode: 'System:Dept:Delete',
meta: { title: 'common.delete' },
},
],
},
],
},
{
id: 9,
meta: {
badgeType: 'dot',
order: 9998,
title: 'demos.vben.title',
icon: 'carbon:data-center',
},
name: 'Project',
path: '/vben-admin',
type: 'catalog',
status: 1,
children: [
{
id: 901,
pid: 9,
name: 'VbenDocument',
path: '/vben-admin/document',
component: 'IFrameView',
type: 'embedded',
status: 1,
meta: {
icon: 'carbon:book',
iframeSrc: 'https://doc.vben.pro',
title: 'demos.vben.document',
},
},
{
id: 902,
pid: 9,
name: 'VbenGithub',
path: '/vben-admin/github',
component: 'IFrameView',
type: 'link',
status: 1,
meta: {
icon: 'carbon:logo-github',
link: 'https://github.com/vbenjs/vue-vben-admin',
title: 'Github',
},
},
{
id: 903,
pid: 9,
name: 'VbenAntdv',
path: '/vben-admin/antdv',
component: 'IFrameView',
type: 'link',
status: 0,
meta: {
icon: 'carbon:hexagon-vertical-solid',
badgeType: 'dot',
link: 'https://ant.vben.pro',
title: 'demos.vben.antdv',
},
},
],
},
{
id: 10,
component: '_core/about/index',
type: 'menu',
status: 1,
meta: {
icon: 'lucide:copyright',
order: 9999,
title: 'demos.vben.about',
},
name: 'About',
path: '/about',
},
];
export function getMenuIds(menus: any[]) {
const ids: number[] = [];
menus.forEach((item) => {
ids.push(item.id);
if (item.children && item.children.length > 0) {
ids.push(...getMenuIds(item.children));
}
});
return ids;
}
================================================
FILE: hiauth-front/apps/backend-mock/utils/response.ts
================================================
import type { EventHandlerRequest, H3Event } from 'h3';
import { setResponseStatus } from 'h3';
export function useResponseSuccess(data: T) {
return {
code: 0,
data,
error: null,
message: 'ok',
};
}
export function usePageResponseSuccess(
page: number | string,
pageSize: number | string,
list: T[],
{ message = 'ok' } = {},
) {
const pageData = pagination(
Number.parseInt(`${page}`),
Number.parseInt(`${pageSize}`),
list,
);
return {
...useResponseSuccess({
items: pageData,
total: list.length,
}),
message,
};
}
export function useResponseError(message: string, error: any = null) {
return {
code: -1,
data: null,
error,
message,
};
}
export function forbiddenResponse(
event: H3Event,
message = 'Forbidden Exception',
) {
setResponseStatus(event, 403);
return useResponseError(message, message);
}
export function unAuthorizedResponse(event: H3Event) {
setResponseStatus(event, 401);
return useResponseError('Unauthorized Exception', 'Unauthorized Exception');
}
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function pagination(
pageNo: number,
pageSize: number,
array: T[],
): T[] {
const offset = (pageNo - 1) * Number(pageSize);
return offset + Number(pageSize) >= array.length
? array.slice(offset)
: array.slice(offset, offset + Number(pageSize));
}
================================================
FILE: hiauth-front/apps/web-antd/index.html
================================================
<%= VITE_APP_TITLE %>
================================================
FILE: hiauth-front/apps/web-antd/package.json
================================================
{
"name": "@vben/web-antd",
"version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-antd"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"dayjs": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
}
}
================================================
FILE: hiauth-front/apps/web-antd/postcss.config.mjs
================================================
export { default } from '@vben/tailwind-config/postcss';
================================================
FILE: hiauth-front/apps/web-antd/src/adapter/component/index.ts
================================================
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { notification } from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = (
component: T,
type: 'input' | 'select',
componentProps: Recordable = {},
) => {
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
slots,
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'addonAfter',
inputComponent: Input,
modelValueProp: 'value',
}),
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };
================================================
FILE: hiauth-front/apps/web-antd/src/adapter/form.ts
================================================
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
async function initSetupVbenForm() {
setupVbenForm({
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
// 输入项目必填国际化适配
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
}
const useVbenForm = useForm;
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema;
export type { VbenFormProps };
================================================
FILE: hiauth-front/apps/web-antd/src/adapter/vxe-table.ts
================================================
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
columnConfig: {
resizable: true,
},
minHeight: 180,
formConfig: {
// 全局禁用vxe-table的表单配置,使用formOptions
enabled: false,
},
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
size: 'small',
} as VxeTableGridOptions,
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
return h(Image, { src: row[column.field] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
Button,
{ size: 'small', type: 'link' },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';
================================================
FILE: hiauth-front/apps/web-antd/src/api/core/auth.ts
================================================
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post('/auth/login', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/**
* 获取用户权限码
*/
export async function getAccessCodesApi() {
return requestClient.get('/auth/codes');
}
================================================
FILE: hiauth-front/apps/web-antd/src/api/core/index.ts
================================================
export * from './auth';
export * from './menu';
export * from './user';
================================================
FILE: hiauth-front/apps/web-antd/src/api/core/menu.ts
================================================
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get('/menu/all');
}
================================================
FILE: hiauth-front/apps/web-antd/src/api/core/user.ts
================================================
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户信息
*/
export async function getUserInfoApi() {
return requestClient.get('/user/info');
}
================================================
FILE: hiauth-front/apps/web-antd/src/api/index.ts
================================================
export * from './core';
================================================
FILE: hiauth-front/apps/web-antd/src/api/request.ts
================================================
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
defaultResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({
...options,
baseURL,
});
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
});
// 处理返回的响应数据格式
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: 'code',
dataField: 'data',
successCode: 0,
}),
);
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? '';
// 如果没有错误信息,则会根据状态码进行提示
message.error(errorMessage || msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL, {
responseReturn: 'data',
});
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
================================================
FILE: hiauth-front/apps/web-antd/src/app.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/bootstrap.ts
================================================
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// zIndex: 1020,
// });
const app = createApp(App);
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
}
});
app.mount('#app');
}
export { bootstrap };
================================================
FILE: hiauth-front/apps/web-antd/src/layouts/auth.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/layouts/basic.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/layouts/index.ts
================================================
const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
export { AuthPageLayout, BasicLayout, IFrameView };
================================================
FILE: hiauth-front/apps/web-antd/src/locales/README.md
================================================
# locale
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
================================================
FILE: hiauth-front/apps/web-antd/src/locales/index.ts
================================================
import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue';
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import { ref } from 'vue';
import {
$t,
setupI18n as coreSetup,
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
const antdLocale = ref(antdDefaultLocale);
const modules = import.meta.glob('./langs/**/*.json');
const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/,
modules,
);
/**
* 加载应用特有的语言包
* 这里也可以改造为从服务端获取翻译数据
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
* 加载第三方组件库的语言包
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
}
/**
* 加载dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* 加载antd的语言包
* @param lang
*/
async function loadAntdLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'en-US': {
antdLocale.value = antdEnLocale;
break;
}
case 'zh-CN': {
antdLocale.value = antdDefaultLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, antdLocale, setupI18n };
================================================
FILE: hiauth-front/apps/web-antd/src/locales/langs/en-US/demos.json
================================================
{
"title": "Demos",
"antd": "Ant Design Vue",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
}
}
================================================
FILE: hiauth-front/apps/web-antd/src/locales/langs/en-US/page.json
================================================
{
"auth": {
"login": "Login",
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
},
"dashboard": {
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
}
}
================================================
FILE: hiauth-front/apps/web-antd/src/locales/langs/zh-CN/demos.json
================================================
{
"title": "演示",
"antd": "Ant Design Vue",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
}
}
================================================
FILE: hiauth-front/apps/web-antd/src/locales/langs/zh-CN/page.json
================================================
{
"auth": {
"login": "登录",
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
},
"dashboard": {
"title": "概览",
"analytics": "分析页",
"workspace": "工作台"
}
}
================================================
FILE: hiauth-front/apps/web-antd/src/main.ts
================================================
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();
================================================
FILE: hiauth-front/apps/web-antd/src/preferences.ts
================================================
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
* !!! 更改配置后请清空缓存,否则可能不生效
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
},
});
================================================
FILE: hiauth-front/apps/web-antd/src/router/access.ts
================================================
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
message.loading({
content: `${$t('common.loadingMenu')}...`,
duration: 1.5,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };
================================================
FILE: hiauth-front/apps/web-antd/src/router/guard.ts
================================================
import type { Router } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
* 通用守卫配置
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set();
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
loadedPaths.add(to.path);
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
});
}
/**
* 权限访问守卫配置
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
preferences.app.defaultHomePath,
);
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === preferences.app.defaultHomePath
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示,但是访问会被重定向到403
routes: accessRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ??
(to.path === preferences.app.defaultHomePath
? userInfo.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };
================================================
FILE: hiauth-front/apps/web-antd/src/router/index.ts
================================================
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };
================================================
FILE: hiauth-front/apps/web-antd/src/router/routes/core.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
/**
* 根路由
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
* 此路由必须存在,且不应修改
*/
{
component: BasicLayout,
meta: {
hideInBreadcrumb: true,
title: 'Root',
},
name: 'Root',
path: '/',
redirect: preferences.app.defaultHomePath,
children: [],
},
{
component: AuthPageLayout,
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.auth.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.auth.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.auth.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.auth.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };
================================================
FILE: hiauth-front/apps/web-antd/src/router/routes/index.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由、外部路由和404兜底路由组成
* 无需走权限验证(会一直显示在菜单中) */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
export { accessRoutes, coreRouteNames, routes };
================================================
FILE: hiauth-front/apps/web-antd/src/router/routes/modules/dashboard.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/dashboard',
children: [
{
name: 'Analytics',
path: '/analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-antd/src/router/routes/modules/demos.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('demos.title'),
},
name: 'Demos',
path: '/demos',
children: [
{
meta: {
title: $t('demos.antd'),
},
name: 'AntDesignDemos',
path: '/demos/ant-design',
component: () => import('#/views/demos/antd/index.vue'),
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-antd/src/router/routes/modules/vben.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
} from '@vben/constants';
import { IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
badgeType: 'dot',
icon: VBEN_LOGO_URL,
order: 9998,
title: $t('demos.vben.title'),
},
name: 'VbenProject',
path: '/vben-admin',
children: [
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: VBEN_DOC_URL,
title: $t('demos.vben.document'),
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'mdi:github',
link: VBEN_GITHUB_URL,
title: 'Github',
},
},
{
name: 'VbenNaive',
path: '/vben-admin/naive',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: 'logos:naiveui',
link: VBEN_NAIVE_PREVIEW_URL,
title: $t('demos.vben.naive-ui'),
},
},
{
name: 'VbenElementPlus',
path: '/vben-admin/ele',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: 'logos:element',
link: VBEN_ELE_PREVIEW_URL,
title: $t('demos.vben.element-plus'),
},
},
],
},
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('demos.vben.about'),
order: 9999,
},
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-antd/src/store/auth.ts
================================================
import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const loginLoading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param params 登录表单数据
*/
async function authLogin(
params: Recordable,
onSuccess?: () => Promise | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken } = await loginApi(params);
// 如果成功获取到 accessToken
if (accessToken) {
accessStore.setAccessToken(accessToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(),
getAccessCodesApi(),
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(
userInfo.homePath || preferences.app.defaultHomePath,
);
}
if (userInfo?.realName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
async function logout(redirect: boolean = true) {
try {
await logoutApi();
} catch {
// 不做任何处理
}
resetAllStores();
accessStore.setLoginExpired(false);
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
loginLoading,
logout,
};
});
================================================
FILE: hiauth-front/apps/web-antd/src/store/index.ts
================================================
export * from './auth';
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/README.md
================================================
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/about/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/authentication/code-login.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/authentication/forget-password.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/authentication/login.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/authentication/qrcode-login.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/authentication/register.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/fallback/coming-soon.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/fallback/forbidden.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/fallback/internal-error.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/fallback/not-found.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/_core/fallback/offline.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/dashboard/analytics/analytics-trends.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/dashboard/analytics/analytics-visits-data.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/dashboard/analytics/analytics-visits-sales.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/dashboard/analytics/analytics-visits-source.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/dashboard/analytics/analytics-visits.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/dashboard/analytics/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-antd/src/views/dashboard/workspace/index.vue
================================================
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
今日晴,20℃ - 32℃!
================================================
FILE: hiauth-front/apps/web-antd/src/views/demos/antd/index.vue
================================================
Default
Primary
Info
Error
信息
错误
警告
成功
信息
错误
警告
成功
================================================
FILE: hiauth-front/apps/web-antd/tailwind.config.mjs
================================================
export { default } from '@vben/tailwind-config';
================================================
FILE: hiauth-front/apps/web-antd/tsconfig.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
================================================
FILE: hiauth-front/apps/web-antd/tsconfig.node.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": false
},
"include": ["vite.config.mts"]
}
================================================
FILE: hiauth-front/apps/web-antd/vite.config.mts
================================================
import { defineConfig } from '@vben/vite-config';
export default defineConfig(async () => {
return {
application: {},
vite: {
server: {
proxy: {
'/api': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://localhost:5320/api',
ws: true,
},
},
},
},
};
});
================================================
FILE: hiauth-front/apps/web-auth/index.html
================================================
<%= VITE_APP_TITLE %>
================================================
FILE: hiauth-front/apps/web-auth/package.json
================================================
{
"name": "@vben/web-auth",
"version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-auth"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"ant-design-vue": "catalog:",
"dayjs": "catalog:",
"jsencrypt": "^3.3.2",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
}
}
================================================
FILE: hiauth-front/apps/web-auth/postcss.config.mjs
================================================
export { default } from '@vben/tailwind-config/postcss';
================================================
FILE: hiauth-front/apps/web-auth/src/adapter/component/index.ts
================================================
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { notification } from 'ant-design-vue';
const AutoComplete = defineAsyncComponent(
() => import('ant-design-vue/es/auto-complete'),
);
const Button = defineAsyncComponent(() => import('ant-design-vue/es/button'));
const Checkbox = defineAsyncComponent(
() => import('ant-design-vue/es/checkbox'),
);
const CheckboxGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/checkbox').then((res) => res.CheckboxGroup),
);
const DatePicker = defineAsyncComponent(
() => import('ant-design-vue/es/date-picker'),
);
const Divider = defineAsyncComponent(() => import('ant-design-vue/es/divider'));
const Input = defineAsyncComponent(() => import('ant-design-vue/es/input'));
const InputNumber = defineAsyncComponent(
() => import('ant-design-vue/es/input-number'),
);
const InputPassword = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.InputPassword),
);
const Mentions = defineAsyncComponent(
() => import('ant-design-vue/es/mentions'),
);
const Radio = defineAsyncComponent(() => import('ant-design-vue/es/radio'));
const RadioGroup = defineAsyncComponent(() =>
import('ant-design-vue/es/radio').then((res) => res.RadioGroup),
);
const RangePicker = defineAsyncComponent(() =>
import('ant-design-vue/es/date-picker').then((res) => res.RangePicker),
);
const Rate = defineAsyncComponent(() => import('ant-design-vue/es/rate'));
const Select = defineAsyncComponent(() => import('ant-design-vue/es/select'));
const Space = defineAsyncComponent(() => import('ant-design-vue/es/space'));
const Switch = defineAsyncComponent(() => import('ant-design-vue/es/switch'));
const Textarea = defineAsyncComponent(() =>
import('ant-design-vue/es/input').then((res) => res.Textarea),
);
const TimePicker = defineAsyncComponent(
() => import('ant-design-vue/es/time-picker'),
);
const TreeSelect = defineAsyncComponent(
() => import('ant-design-vue/es/tree-select'),
);
const Upload = defineAsyncComponent(() => import('ant-design-vue/es/upload'));
const withDefaultPlaceholder = (
component: T,
type: 'input' | 'select',
componentProps: Recordable = {},
) => {
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
slots,
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: Select,
loadingSlot: 'suffixIcon',
visibleEvent: 'onDropdownVisibleChange',
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: TreeSelect,
fieldNames: { label: 'label', value: 'value', children: 'children' },
loadingSlot: 'suffixIcon',
modelPropName: 'value',
optionsPropName: 'treeData',
visibleEvent: 'onVisibleChange',
},
),
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'addonAfter',
inputComponent: Input,
modelValueProp: 'value',
}),
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };
================================================
FILE: hiauth-front/apps/web-auth/src/adapter/form.ts
================================================
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
async function initSetupVbenForm() {
setupVbenForm({
config: {
// ant design vue组件库默认都是 v-model:value
baseModelPropName: 'value',
// 一些组件是 v-model:checked 或者 v-model:fileList
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
// 输入项目必填国际化适配
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
// 选择项目必填国际化适配
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
}
const useVbenForm = useForm;
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema;
export type { VbenFormProps };
================================================
FILE: hiauth-front/apps/web-auth/src/adapter/vxe-table.ts
================================================
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
columnConfig: {
resizable: true,
},
minHeight: 180,
formConfig: {
// 全局禁用vxe-table的表单配置,使用formOptions
enabled: false,
},
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
size: 'small',
} as VxeTableGridOptions,
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
return h(Image, { src: row[column.field] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
Button,
{ size: 'small', type: 'link' },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/app.ts
================================================
import type { LimitDto, PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface AppPageDto extends PageDto {
name?: string;
}
export interface AppLimitDto extends LimitDto {
name?: string;
}
export interface AppCreateDto {
name?: string;
icon?: string;
remark?: string;
}
export interface AppUpdateDto {
id: string;
name?: string;
icon?: string;
remark?: string;
}
export interface AppVo {
id: string;
name: string;
icon: string;
createTime: string;
remark: string;
}
export const pageAppApi = async (params: AppPageDto) => {
return requestClient.post>('/api/common/appMgr/page', params);
};
export const getAppByIdApi = async (id: string) => {
return requestClient.post(`/api/common/appMgr/findById?id=${id}`);
};
// 新增用户
export const createAppApi = async (params: AppCreateDto) => {
return requestClient.post('/api/common/appMgr/create', params);
};
export const updateAppApi = async (params: AppUpdateDto) => {
return requestClient.post('/api/common/appMgr/update', params);
};
export const deleteAppsApi = async (params: { ids: string[] }) => {
return requestClient.post('/api/common/appMgr/delete', params);
};
export const limitAppApi = async (params: AppLimitDto) => {
return requestClient.post('/api/common/appMgr/limitList', params);
};
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/appClient.ts
================================================
import type { AppVo } from '#/api/core/app';
import type { LimitDto, PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface AppClientPageDto extends PageDto {
cid?: string;
keyword?: string;
}
export interface AppClientLimitDto extends LimitDto {
cid?: string;
name?: string;
}
export interface AppClientCreateDto {
appIds: string[];
corpId?: string;
}
export interface AppClientUpdateDto {
id: string;
clientId?: string;
clientSecret?: string;
clientName?: string;
redirectUris?: string;
scopes?: string;
accessTokenTimeToLive?: number;
}
export interface AppClientDeleteDto {
appIds: string[];
corpId: string;
}
export interface AppClientVo {
id: string;
cid: string;
appId: string;
name: string;
icon: string;
createTime: string;
remark: string;
clientId: string;
clientSecret: string;
clientName: string;
redirectUris: string[];
scopes: string;
accessTokenTimeToLive: number;
}
export const pageAppClientApi = (params: AppClientPageDto) => {
return requestClient.post>(
`/api/corpSpace/appClientMgr/page`,
params,
);
};
export const getAppClientByIdApi = async (id: string) => {
return requestClient.post(
`/api/corpSpace/appClientMgr/findById?id=${id}`,
);
};
// 新增用户
export const addAppClientsApi = async (params: AppClientCreateDto) => {
return requestClient.post(
`/api/corpSpace/appClientMgr/add`,
params,
);
};
export const createAppClientApi = async (params: AppClientCreateDto) => {
return requestClient.post(
`/api/corpSpace/appClientMgr/create`,
params,
);
};
export const updateAppClientApi = async (params: AppClientUpdateDto) => {
return requestClient.post(
`/api/corpSpace/appClientMgr/update`,
params,
);
};
export const deleteAppClientsApi = async (params: { ids: string[] }) => {
return requestClient.post(`/api/corpSpace/appClientMgr/delete`, params);
};
export const limitHaveAppApi = async (params: AppClientLimitDto) => {
return requestClient.post(
`/api/corpSpace/appClientMgr/limitHaveApp`,
params,
);
};
export const limitNotHaveAppApi = async (params: AppClientLimitDto) => {
return requestClient.post(
`/api/corpSpace/appClientMgr/limitNotHaveApp`,
params,
);
};
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/appResource.ts
================================================
import type { PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface AppResourcePageDto extends PageDto {
appId: string;
pid?: string;
keyword?: string;
code?: string;
url?: string;
api?: string;
name?: string;
type?: number;
}
export interface AppResourceCreateDto {
appId: string;
pid?: string;
code?: string;
url?: string;
api?: string;
name?: string;
type?: number;
remark?: string;
extend?: string;
}
export interface AppResourceUpdateDto {
id: string;
pid?: string;
code?: string;
url?: string;
api?: string;
name?: string;
type?: number;
remark?: string;
extend?: string;
}
export interface FindAppResourceIdsByRoleAndAppDto {
appId: string;
roleId: string;
}
export interface AppResourceVo {
id: string;
appId: string;
pid: string;
code: string;
url: string;
api: string;
name: string;
type: number;
remark: string;
extend: string;
}
export const AppResourceType = { 0: 'xx', 1: '菜单', 2: '页面', 3: '功能' };
export const AppResourceTypeOpt = [
{ label: 'xx', value: 0 },
{ label: '菜单', value: 1 },
{ label: '页面', value: 2 },
{ label: '功能', value: 3 },
];
export const pageAppResourceApi = (params: AppResourcePageDto) => {
return requestClient.post>(
`/api/common/appResourceMgr/page`,
params,
);
};
export const getAppResourceByIdApi = async (id: string) => {
return requestClient.post(
`/api/common/appResourceMgr/findById?id=${id}`,
);
};
export const createAppResourceApi = (params: AppResourceCreateDto) => {
return requestClient.post(
`/api/common/appResourceMgr/create`,
params,
);
};
export const updateAppResourceApi = (params: AppResourceUpdateDto) => {
return requestClient.post(
`/api/common/appResourceMgr/update`,
params,
);
};
export const deleteAppResourcesApi = (params: { ids: string[] }) => {
return requestClient.post(`/api/common/appResourceMgr/delete`, params);
};
export const appResourceTreeApi = (appId: string) => {
return requestClient.post(
`/api/common/appResourceMgr/tree?appId=${appId}`,
{},
);
};
export const findAppResourceIdsByRoleAndAppApi = (
params: FindAppResourceIdsByRoleAndAppDto,
) => {
return requestClient.post(
`/api/common/appResourceMgr/findAppResourceIdsByRoleAndApp`,
params,
);
};
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/auth.ts
================================================
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
}
export interface SmsCodeDto {
type: number;
requestId: string;
phoneNum: string;
}
export interface PhoneNumLoginDto {
requestId: string;
phoneNum: string;
smsCode: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
export interface RegisterDto {
corpName: string;
username: string;
password: string;
phoneNum: string;
smsCode: string;
}
/**
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post('/unpapi/login/account', data);
}
export async function phoneNumLoginApi(data: AuthApi.PhoneNumLoginDto) {
return requestClient.post(
'/unpapi/login/phoneNum',
data,
);
}
export async function getSmsCodeApi(data: AuthApi.SmsCodeDto) {
return requestClient.post('/unpapi/captcha/smsCode', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return baseRequestClient.get('/api/logout', {
withCredentials: true,
});
}
/**
* 获取用户权限码
*/
export async function getAccessCodesApi() {
return requestClient.post('/api/common/codes');
}
export async function registerApi(data: RegisterDto) {
return requestClient.post('/unpapi/register', data);
}
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/common.ts
================================================
import { requestClient } from '#/api/request';
export interface R {
code: string;
msg: string;
data: T;
}
export interface PageVo {
records: T[];
pageNum: number;
pageSize: number;
totalCount: number;
}
export interface PageDto {
pageNum: number;
pageSize: number;
}
export interface LimitDto {
offset: number;
limit: number;
}
export interface FileUrlVo {
fileUrl: string;
}
export interface MetadataVo {
usernamePlaceholder: string;
passwordPlaceholder: string;
encryptType: string;
publicKey: string;
}
export const YesOrNoUseNum = { 0: '否', 1: '是' };
export const YesOrNoUseNumOpt = [
{ label: '否', value: 0 },
{ label: '是', value: 1 },
];
export const YesOrNoUseBool = { false: '否', true: '是' };
export const YesOrNoUseBoolOpt = [
{ label: '否', value: false },
{ label: '是', value: true },
];
export const EnableStatusUseNum = { 0: '禁用', 1: '启用' };
export const EnableStatusUseNumOpt = [
{ label: '禁用', value: 0 },
{ label: '启用', value: 1 },
];
export const EnableStatusUseBool = { false: '禁用', true: '启用' };
export const EnableStatusUseBoolOpt = [
{ label: '禁用', value: false },
{ label: '启用', value: true },
];
export async function getMetadataApi() {
return requestClient.post('/unpapi/metadata', {});
}
export async function uploadImgApi(file: File) {
return await requestClient.upload('/api/common/file/uploadImg', { file });
}
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/corp.ts
================================================
import type { LimitDto, PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface CorpPageDto extends PageDto {
keyword?: string;
}
export interface CorpLimitDto extends LimitDto {
keyword?: string;
}
export interface CorpCreateDto {
name?: string;
status?: number;
}
export interface CorpUpdateDto {
id: string;
name?: string;
status?: number;
}
export interface CorpVo {
id: string;
name: string;
appCount: number;
depCount: number;
empCount: number;
}
export const listCorpApi = () => {
return requestClient.post('/api/adminSpace/corpMgr/listCorp');
};
export const pageCorpApi = (params: CorpPageDto) => {
return requestClient.post>(
'/api/adminSpace/corpMgr/page',
params,
);
};
export const limitCorpApi = (params: CorpLimitDto) => {
return requestClient.post('/api/adminSpace/corpMgr/limit', params);
};
export const getCorpByIdApi = async (id: string) => {
return requestClient.post(
`/api/adminSpace/corpMgr/findById?id=${id}`,
);
};
export const createCorpApi = (params: CorpCreateDto) => {
return requestClient.post('/api/adminSpace/corpMgr/create', params);
};
export const updateCorpApi = (params: CorpUpdateDto) => {
return requestClient.post('/api/adminSpace/corpMgr/update', params);
};
export const deleteCorpsApi = (params: { ids: string[] }) => {
return requestClient.post('/api/adminSpace/corpMgr/delete', params);
};
export const intoCorpSpaceApi = (cid: string) => {
return requestClient.post(
`/api/adminSpace/corpMgr/intoCorpSpace?cid=${cid}`,
);
};
export const intoAdminSpaceApi = () => {
return requestClient.post('/api/adminSpace/corpMgr/intoAdminSpace');
};
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/dep.ts
================================================
import type { LimitDto, PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface DepPageDto extends PageDto {
cid?: string;
keyword?: string;
no?: string;
name?: string;
status?: number;
}
export interface DepLimitDto extends LimitDto {
cid?: string;
keyword?: string;
}
export interface DepCreateDto {
cid?: string;
sort?: number;
no?: string;
name?: string;
status?: number;
remark?: string;
}
export interface DepUpdateDto {
id: string;
sort?: number;
no?: string;
name?: string;
status?: number;
remark?: string;
}
export interface DepVo {
id: string;
cid: string;
sort: number;
no: string;
name: string;
createTime: string;
status: number;
remark: string;
}
export const pageDepApi = (params: DepPageDto) => {
return requestClient.post>(
'/api/corpSpace/depMgr/page',
params,
);
};
export const limitDepApi = (params: DepLimitDto) => {
return requestClient.post('/api/corpSpace/depMgr/limit', params);
};
export const getDepByIdApi = async (id: string) => {
return requestClient.post(`/api/corpSpace/depMgr/findById?id=${id}`);
};
export const createDepApi = (params: DepCreateDto) => {
return requestClient.post('/api/corpSpace/depMgr/create', params);
};
export const updateDepApi = (params: DepUpdateDto) => {
return requestClient.post('/api/corpSpace/depMgr/update', params);
};
export const deleteDepsApi = (params: { ids: string[] }) => {
return requestClient.post('/api/corpSpace/depMgr/delete', params);
};
export const depTreeApi = () => {
return requestClient.post('/api/corpSpace/depMgr/depTree', {});
};
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/dict.ts
================================================
import type { LimitDto, PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface DictPageDto extends PageDto {
keyword: string | undefined;
code?: string;
name?: string;
isRoot: boolean;
}
export interface DictLimitDto extends LimitDto {
keyword?: string;
pcode?: string;
}
export interface DictCreateDto {
sort?: number;
code?: string;
pcode?: string;
name?: string;
value?: string;
}
export interface DictUpdateDto {
id: string;
sort?: number;
code?: string;
pcode?: string;
name?: string;
value?: string;
isEnable?: boolean;
}
export interface DictVo {
id: string;
sort: number;
code: string;
pCode: string;
name: string;
value: string;
isEnable: boolean;
createTime: string;
hasChild: boolean;
}
export async function pageDictApi(params: DictPageDto) {
return requestClient.post>(
'/api/corpSpace/dictMgr/page',
params,
);
}
export async function getDictByIdApi(id: string) {
return requestClient.post(`/api/corpSpace/dictMgr/findById?id=${id}`);
}
// 新增用户
export async function createDictApi(params: DictCreateDto) {
return requestClient.post('/api/corpSpace/dictMgr/create', params);
}
export async function updateDictApi(params: DictUpdateDto) {
return requestClient.post('/api/corpSpace/dictMgr/update', params);
}
export async function deleteDictApi(params: { ids: string[] }) {
return requestClient.post('/api/corpSpace/dictMgr/delete', params);
}
export async function findDictApi(params: DictLimitDto) {
return requestClient.post(
'/api/corpSpace/dictMgr/findDict',
params,
);
}
export async function findSubDictApi(params: DictLimitDto) {
return requestClient.post(
'/api/corpSpace/dictMgr/findSubDict',
params,
);
}
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/emp.ts
================================================
import type { UserLimitDto, UserVo } from '#/api';
import type { PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface EmpPageDto extends PageDto {
cid?: string;
depIds?: string[];
keyword?: string;
name?: string;
}
export interface EmpCreateDto {
cid?: string;
userId?: string;
no?: string;
name?: string;
email?: string;
isCorpAdmin?: boolean;
depIds?: string[];
roleIds?: string[];
}
export interface EmpUpdateDto {
id: string;
userId?: string;
no?: string;
name?: string;
email?: string;
isCorpAdmin?: boolean;
depIds?: string[];
roleIds?: string[];
}
export interface EmpVo {
id: string;
cid: string;
userId: string;
no: string;
name: string;
email: string;
createTime: string;
isCorpAdmin: boolean;
depIds: string[];
}
export const pageEmpApi = (params: EmpPageDto) => {
return requestClient.post>(
'/api/corpSpace/empMgr/page',
params,
);
};
export const getEmpByIdApi = async (id: string) => {
return requestClient.post(`/api/corpSpace/empMgr/findById?id=${id}`);
};
// 新增用户
export const createEmpApi = (params: EmpCreateDto) => {
return requestClient.post('/api/corpSpace/empMgr/create', params);
};
export const updateEmpApi = (params: EmpUpdateDto) => {
return requestClient.post('/api/corpSpace/empMgr/update', params);
};
export const deleteEmpsApi = (params: { ids: string[] }) => {
return requestClient.post('/api/corpSpace/empMgr/delete', params);
};
export const findUserApi = (params: UserLimitDto) => {
return requestClient.post('/api/corpSpace/empMgr/findUser', params);
};
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/index.ts
================================================
export * from './auth';
export * from './dict';
export * from './menu';
export * from './user';
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/menu.ts
================================================
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get('/menu/all');
}
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/role.ts
================================================
import type { LimitDto, PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface RolePageDto extends PageDto {
cid?: string;
name?: string;
}
export interface RoleLimitDto extends LimitDto {
keyword?: string;
}
export interface RoleCreateDto {
cid?: string;
name?: string;
remark?: string;
isEnable?: boolean;
}
export interface RoleUpdateDto {
id: string;
name?: string;
remark?: string;
isEnable?: boolean;
}
export interface RoleAuthDto {
appId: string;
roleId: string | undefined;
appResourceIds: string[];
}
export interface RoleVo {
id: string;
cid: string;
name: string;
createTime: string;
remark: string;
isEnable: boolean;
}
export const pageRoleApi = (params: RolePageDto) => {
return requestClient.post>(
'/api/corpSpace/roleMgr/page',
params,
);
};
export const limitRoleApi = (params: RoleLimitDto) => {
return requestClient.post('/api/corpSpace/roleMgr/limit', params);
};
export const getRoleByIdApi = async (id: string) => {
return requestClient.post(`/api/corpSpace/roleMgr/findById?id=${id}`);
};
// 新增用户
export const createRoleApi = (params: RoleCreateDto) => {
return requestClient.post('/api/corpSpace/roleMgr/create', params);
};
export const updateRoleApi = (params: RoleUpdateDto) => {
return requestClient.post('/api/corpSpace/roleMgr/update', params);
};
export const deleteRolesApi = (params: { ids: string[] }) => {
return requestClient.post('/api/corpSpace/roleMgr/delete', params);
};
export const authRoleApi = (params: RoleAuthDto) => {
return requestClient.post('/api/corpSpace/roleMgr/auth', params);
};
================================================
FILE: hiauth-front/apps/web-auth/src/api/core/user.ts
================================================
import type { UserInfo } from '@vben/types';
import type { LimitDto, PageDto, PageVo } from '#/api/core/common';
import { requestClient } from '#/api/request';
export interface UserPageDto extends PageDto {
keyword: string;
name: string;
username: string;
phoneNum: string;
gender: number;
status: number;
regtime: string[];
isSysAdmin: boolean;
}
export interface UserLimitDto extends LimitDto {
keyword?: string;
}
export interface UserCreateDto {
name?: string;
avatar?: string;
username?: string;
phoneNum?: string;
gender?: number;
status?: number;
isSysAdmin?: boolean;
}
export interface UserUpdateDto {
id: string;
name?: string;
avatar?: string;
username?: string;
phoneNum?: string;
gender?: number;
status?: number;
isSysAdmin?: boolean;
}
export interface UserVo {
id: string;
name: string;
avatar: string;
username: string;
phoneNum: string;
gender: number;
status: number;
regtime: string;
lastLoginTime: string;
isSysAdmin: boolean;
}
/**
* @description:用户性别
*/
export const GenderType = { 0: '未知', 1: '男', 2: '女' };
export const GenderTypeOpt = [
{ label: '未知', value: 0 },
{ label: '男', value: 1 },
{ label: '女', value: 2 },
];
/**
* 获取用户信息
*/
export async function getUserInfoApi() {
return requestClient.post('/api/common/userInfo');
}
export async function pageUserApi(params: UserPageDto) {
return requestClient.post>(
'/api/adminSpace/userMgr/page',
params,
);
}
export async function getUserByIdApi(id: string) {
return requestClient.post(
`/api/adminSpace/userMgr/findById?id=${id}`,
);
}
// 新增用户
export async function createUserApi(params: UserCreateDto) {
return requestClient.post('/api/adminSpace/userMgr/create', params);
}
export async function updateUserApi(params: UserUpdateDto) {
return requestClient.post('/api/adminSpace/userMgr/update', params);
}
export async function deleteUsersApi(params: { ids: string[] }) {
return requestClient.post('/api/adminSpace/userMgr/delete', params);
}
================================================
FILE: hiauth-front/apps/web-auth/src/api/index.ts
================================================
export * from './core';
================================================
FILE: hiauth-front/apps/web-auth/src/api/request.ts
================================================
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
defaultResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from 'ant-design-vue';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({
...options,
baseURL,
});
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
});
// 处理返回的响应数据格式
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: 'code',
dataField: 'data',
successCode: 10_000,
}),
);
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? '';
// 如果没有错误信息,则会根据状态码进行提示
message.error(errorMessage || msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL, {
responseReturn: 'data',
});
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
================================================
FILE: hiauth-front/apps/web-auth/src/app.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/bootstrap.ts
================================================
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui/es/loading';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/antd';
import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// zIndex: 1020,
// });
const app = createApp(App);
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
}
});
app.mount('#app');
}
export { bootstrap };
================================================
FILE: hiauth-front/apps/web-auth/src/common/config.ts
================================================
import type { Context } from './types';
const defaultContext: Context = {
app: {
showGoAdminSpaceBut: false,
},
};
export { defaultContext };
================================================
FILE: hiauth-front/apps/web-auth/src/common/constants.ts
================================================
const ROLE_SYS_ADMIN = 'sysAdmin';
const ROLE_CORP_ADMIN = 'corpAdmin';
enum ACTION {
ADD = 'ADD',
EDIT = 'EDIT',
VIEW = 'VIEW',
}
export { ACTION, ROLE_CORP_ADMIN, ROLE_SYS_ADMIN };
================================================
FILE: hiauth-front/apps/web-auth/src/common/context.ts
================================================
import type { DeepPartial } from '@vben/types';
import type { Context } from './types';
import { markRaw, reactive, readonly } from 'vue';
import { merge, StorageManager } from '@vben/utils';
import { useDebounceFn } from '@vueuse/core';
import { defaultContext } from './config';
const STORAGE_KEY = 'app-context';
class ContextManager {
private cache: null | StorageManager = new StorageManager({ prefix: 'auth' });
private initialContext: Context = defaultContext;
private saveContext: (preference: Context) => void;
private state: Context = reactive({
...this.loadContext(),
});
constructor() {
// 避免频繁的操作缓存
this.saveContext = useDebounceFn(
(context: Context) => this._saveContext(context),
150,
);
}
public getContext() {
return readonly(this.state);
}
resetContext() {
// 将状态重置为初始偏好设置
Object.assign(this.state, this.initialContext);
// 保存重置后的偏好设置
this.saveContext(this.state);
// 从存储中移除偏好设置项
[STORAGE_KEY].forEach((key) => {
this.cache?.removeItem(key);
});
this.updateContext(this.state);
}
public updateContext(updates: DeepPartial) {
const mergedState = merge({}, updates, markRaw(this.state));
Object.assign(this.state, mergedState);
this.saveContext(this.state);
}
private _saveContext(preference: Context) {
this.cache?.setItem(STORAGE_KEY, preference);
}
private loadCachedContext() {
return this.cache?.getItem(STORAGE_KEY);
}
private loadContext(): Context {
return this.loadCachedContext() || { ...defaultContext };
}
}
const contextManager = new ContextManager();
export { ContextManager, contextManager };
================================================
FILE: hiauth-front/apps/web-auth/src/common/index.ts
================================================
import type { Context } from './types';
import { contextManager } from './context';
// 偏好设置(带有层级关系)
const context: Context = contextManager.getContext.apply(contextManager);
// 更新偏好设置
const updateContext = contextManager.updateContext.bind(contextManager);
// 重置偏好设置
const resetContext = contextManager.resetContext.bind(contextManager);
export { context, contextManager, resetContext, updateContext };
export type * from './types';
================================================
FILE: hiauth-front/apps/web-auth/src/common/types.ts
================================================
interface AppContext {
/** 是否显示去管理员空间按钮 */
showGoAdminSpaceBut: boolean;
}
interface Context {
/** 全局配置 */
app: AppContext;
}
export type { AppContext, Context };
================================================
FILE: hiauth-front/apps/web-auth/src/layouts/auth.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/layouts/basic.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/layouts/change-corp.vue
================================================
租户列表
================================================
FILE: hiauth-front/apps/web-auth/src/layouts/go-admin-space-button.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/layouts/index.ts
================================================
const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
export { AuthPageLayout, BasicLayout, IFrameView };
================================================
FILE: hiauth-front/apps/web-auth/src/locales/README.md
================================================
# locale
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
================================================
FILE: hiauth-front/apps/web-auth/src/locales/index.ts
================================================
import type { Locale } from 'ant-design-vue/es/locale';
import type { App } from 'vue';
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import { ref } from 'vue';
import {
$t,
setupI18n as coreSetup,
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import antdEnLocale from 'ant-design-vue/es/locale/en_US';
import antdDefaultLocale from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
const antdLocale = ref(antdDefaultLocale);
const modules = import.meta.glob('./langs/**/*.json');
const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/,
modules,
);
/**
* 加载应用特有的语言包
* 这里也可以改造为从服务端获取翻译数据
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
* 加载第三方组件库的语言包
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadAntdLocale(lang), loadDayjsLocale(lang)]);
}
/**
* 加载dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* 加载antd的语言包
* @param lang
*/
async function loadAntdLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'en-US': {
antdLocale.value = antdEnLocale;
break;
}
case 'zh-CN': {
antdLocale.value = antdDefaultLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, antdLocale, setupI18n };
================================================
FILE: hiauth-front/apps/web-auth/src/locales/langs/en-US/demos.json
================================================
{
"title": "Demos",
"antd": "Ant Design Vue",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
}
}
================================================
FILE: hiauth-front/apps/web-auth/src/locales/langs/en-US/page.json
================================================
{
"auth": {
"login": "Login",
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
},
"dashboard": {
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
}
}
================================================
FILE: hiauth-front/apps/web-auth/src/locales/langs/zh-CN/demos.json
================================================
{
"title": "演示",
"antd": "Ant Design Vue",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
}
}
================================================
FILE: hiauth-front/apps/web-auth/src/locales/langs/zh-CN/page.json
================================================
{
"auth": {
"login": "登录",
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
},
"dashboard": {
"title": "概览",
"analytics": "分析页",
"workspace": "工作台"
}
}
================================================
FILE: hiauth-front/apps/web-auth/src/main.ts
================================================
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();
================================================
FILE: hiauth-front/apps/web-auth/src/preferences.ts
================================================
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
* !!! 更改配置后请清空缓存,否则可能不生效
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
},
copyright: {
companyName: 'Earven',
},
theme: {
mode: 'light',
},
});
================================================
FILE: hiauth-front/apps/web-auth/src/router/access.ts
================================================
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from 'ant-design-vue';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
message.loading({
content: `${$t('common.loadingMenu')}...`,
duration: 1.5,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };
================================================
FILE: hiauth-front/apps/web-auth/src/router/guard.ts
================================================
import type { Router } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
* 通用守卫配置
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set();
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
loadedPaths.add(to.path);
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
});
}
/**
* 权限访问守卫配置
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
preferences.app.defaultHomePath,
);
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === preferences.app.defaultHomePath
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示,但是访问会被重定向到403
routes: accessRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ??
(to.path === preferences.app.defaultHomePath
? userInfo.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };
================================================
FILE: hiauth-front/apps/web-auth/src/router/index.ts
================================================
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };
================================================
FILE: hiauth-front/apps/web-auth/src/router/routes/core.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
/**
* 根路由
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
* 此路由必须存在,且不应修改
*/
{
component: BasicLayout,
meta: {
hideInBreadcrumb: true,
title: 'Root',
},
name: 'Root',
path: '/',
redirect: preferences.app.defaultHomePath,
children: [],
},
{
component: AuthPageLayout,
props: {
pageTitle: '统一认证授权系统',
pageDescription: '应用管理、组织管理、账号管理、权限管理、统一授权',
},
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.auth.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.auth.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.auth.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.auth.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };
================================================
FILE: hiauth-front/apps/web-auth/src/router/routes/index.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由、外部路由和404兜底路由组成
* 无需走权限验证(会一直显示在菜单中) */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
export { accessRoutes, coreRouteNames, routes };
================================================
FILE: hiauth-front/apps/web-auth/src/router/routes/modules/adminSpace.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { ROLE_SYS_ADMIN } from '#/common/constants';
import { BasicLayout } from '#/layouts';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
name: 'corpMgr',
path: '/adminSpace/corpMgr',
meta: {
title: '租户管理',
icon: 'ant-design:solution-outlined',
keepAlive: true,
order: 1000,
authority: [ROLE_SYS_ADMIN],
},
children: [
{
name: 'corpList',
path: '/adminSpace/corpMgr/list',
meta: {
title: '租户列表',
icon: 'ant-design:bars-outlined',
},
component: () => import('#/views/adminSpace/corpMgr/index.vue'),
},
],
},
{
component: BasicLayout,
name: 'userMgr',
path: '/adminSpace/userMgr',
meta: {
title: '用户管理',
icon: 'ant-design:user-outlined',
keepAlive: true,
order: 1000,
authority: [ROLE_SYS_ADMIN],
},
children: [
{
name: 'userList',
path: '/adminSpace/userMgr/list',
meta: {
title: '用户列表',
icon: 'ant-design:bars-outlined',
},
component: () => import('#/views/adminSpace/userMgr/index.vue'),
},
],
},
{
component: BasicLayout,
name: 'appMgr',
path: '/adminSpace/appMgr',
meta: {
title: '应用管理',
icon: 'ant-design:appstore-twotone',
keepAlive: true,
order: 1000,
authority: [ROLE_SYS_ADMIN],
},
children: [
{
name: 'appList',
path: '/adminSpace/appMgr/list',
meta: {
title: '应用列表',
icon: 'ant-design:bars-outlined',
},
component: () => import('#/views/common/appMgr/index.vue'),
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-auth/src/router/routes/modules/common.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { BasicLayout } from '#/layouts';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
name: 'appResourceMgr',
path: '/common/appResourceMgr',
meta: {
title: '应用配置',
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
hideInMenu: true,
},
children: [
{
name: 'appResourceList',
path: '/common/appResourceMgr/list/:id',
meta: {
title: '配置列表',
isHide: true,
},
component: () => import('#/views/common/appResourceMgr/index.vue'),
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-auth/src/router/routes/modules/corpSpace.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { ROLE_CORP_ADMIN } from '#/common/constants';
import { BasicLayout } from '#/layouts';
const routes: RouteRecordRaw[] = [
{
component: BasicLayout,
name: 'depMgr',
path: '/corpSpace/depMgr',
meta: {
title: '部门管理',
icon: 'ant-design:bank-twotone',
keepAlive: true,
order: 1000,
authority: [ROLE_CORP_ADMIN],
},
children: [
{
name: 'depList',
path: '/corpSpace/depMgr/list',
meta: {
title: '部门列表',
icon: 'ant-design:bars-outlined',
},
component: () => import('#/views/corpSpace/depMgr/index.vue'),
},
],
},
{
component: BasicLayout,
name: 'empMgr',
path: '/corpSpace/empMgr',
meta: {
title: '员工管理',
icon: 'mdi:account-hard-hat-outline',
keepAlive: true,
order: 1000,
authority: [ROLE_CORP_ADMIN],
},
children: [
{
name: 'empList',
path: '/corpSpace/empMgr/list',
meta: {
title: '员工列表',
icon: 'ant-design:bars-outlined',
},
component: () => import('#/views/corpSpace/empMgr/index.vue'),
},
],
},
{
component: BasicLayout,
name: 'roleMgr',
path: '/corpSpace/roleMgr',
meta: {
title: '角色管理',
icon: 'ant-design:team-outlined',
keepAlive: true,
order: 1000,
authority: [ROLE_CORP_ADMIN],
},
children: [
{
name: 'roleList',
path: '/corpSpace/roleMgr/list',
meta: {
title: '角色列表',
icon: 'ant-design:bars-outlined',
},
component: () => import('#/views/corpSpace/roleMgr/index.vue'),
},
],
},
{
component: BasicLayout,
name: 'corpAppMgr',
path: '/corpSpace/corpAppMgr',
meta: {
title: '应用管理',
icon: 'ant-design:appstore-twotone',
keepAlive: true,
order: 1000,
authority: [ROLE_CORP_ADMIN],
},
children: [
{
name: 'corpAppList',
path: '/corpSpace/corpAppMgr/list',
meta: {
title: '应用列表',
icon: 'ant-design:bars-outlined',
},
component: () => import('#/views/common/appMgr/index.vue'),
},
{
name: 'appClientList',
path: '/corpSpace/appClientMgr/list',
meta: {
title: '客户端列表',
icon: 'ant-design:bars-outlined',
},
component: () => import('#/views/corpSpace/appClientMgr/index.vue'),
},
],
},
{
component: BasicLayout,
name: 'sysMgr',
path: '/corpSpace/sysMgr',
meta: {
title: '系统管理',
icon: 'ant-design:setting-outlined',
keepAlive: true,
order: 1000,
authority: [ROLE_CORP_ADMIN],
},
children: [
{
name: 'dictMgr',
path: '/corpSpace/sysMgr/dictMgr/list',
meta: {
title: '字典管理',
icon: 'material-symbols:book-3-outline',
},
component: () => import('#/views/corpSpace/sysMgr/dictMgr/index.vue'),
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-auth/src/router/routes/modules/dashboard.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/dashboard',
children: [
{
name: 'Analytics',
path: '/analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: false,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-auth/src/store/auth.ts
================================================
import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { notification } from 'ant-design-vue';
import { defineStore } from 'pinia';
import {
getAccessCodesApi,
getUserInfoApi,
loginApi,
logoutApi,
phoneNumLoginApi,
} from '#/api';
import { updateContext } from '#/common';
import { ROLE_CORP_ADMIN } from '#/common/constants';
import { $t } from '#/locales';
import { useContentStore } from '#/store/content';
import { encrypt } from '#/utils/rsa';
const apiUrl = import.meta.env.VITE_GLOB_API_URL;
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const contentStore = useContentStore();
const loginLoading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param params 登录表单数据
* @param onSuccess
*/
async function authLogin(
params: Recordable,
onSuccess?: () => Promise | void,
) {
const ePwd =
contentStore.encryptType === 'RSA'
? encrypt(contentStore.publicKey, params.password)
: params.password;
const { accessToken } = await loginApi({ ...params, password: ePwd });
return changeSpace(accessToken, onSuccess);
}
async function smsCodeLogin(
params: Recordable,
onSuccess?: () => Promise | void,
) {
const { accessToken } = await phoneNumLoginApi({
requestId: params.phoneNumber,
phoneNum: params.phoneNumber,
smsCode: params.code,
});
return changeSpace(accessToken, onSuccess);
}
async function changeSpace(
accessToken: string,
onSuccess?: () => Promise | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
// 如果成功获取到 accessToken
if (accessToken) {
accessStore.setAccessToken(accessToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(),
getAccessCodesApi(),
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
const { roles } = userInfo;
const isCorpSpace: boolean =
roles !== undefined &&
roles.some((role) => {
return ROLE_CORP_ADMIN.includes(role);
});
updateContext({ app: { showGoAdminSpaceBut: isCorpSpace } });
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(
userInfo.homePath || preferences.app.defaultHomePath,
);
}
if (userInfo?.realName) {
notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
duration: 3,
message: $t('authentication.loginSuccess'),
});
}
}
} finally {
loginLoading.value = false;
}
return { userInfo };
}
async function logout(redirect: boolean = true) {
try {
await logoutApi();
} catch {
// 不做任何处理
}
resetAllStores();
accessStore.setLoginExpired(false);
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
async function fetchUserInfo() {
const userInfo: UserInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
userInfo.avatar = apiUrl + userInfo.avatar;
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
smsCodeLogin,
changeSpace,
fetchUserInfo,
loginLoading,
logout,
};
});
================================================
FILE: hiauth-front/apps/web-auth/src/store/content.ts
================================================
import { acceptHMRUpdate, defineStore } from 'pinia';
interface ContentState {
/**
* 加密公钥,和后台传输敏感数据时使用
*/
publicKey: string;
/**
* 加密公钥的加密算法,默认为 RSA
*/
encryptType: string;
}
/**
* @zh_CN 上下文信息相关
*/
export const useContentStore = defineStore('core-content', {
actions: {
setEncrypt(publicKey: string, encryptType: string) {
this.publicKey = publicKey;
this.encryptType = encryptType;
},
},
state: (): ContentState => ({
publicKey: '',
encryptType: 'RSA',
}),
});
// 解决热更新问题
const hot = import.meta.hot;
if (hot) {
hot.accept(acceptHMRUpdate(useContentStore, hot));
}
================================================
FILE: hiauth-front/apps/web-auth/src/store/index.ts
================================================
export * from './auth';
================================================
FILE: hiauth-front/apps/web-auth/src/utils/rsa.ts
================================================
import JSEncrypt from 'jsencrypt';
const encryptor = new JSEncrypt();
export function encrypt(publicKey: string, content: string) {
encryptor.setPublicKey(publicKey);
return encryptor.encrypt(content);
}
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/README.md
================================================
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/about/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/authentication/code-login.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/authentication/forget-password.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/authentication/login.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/authentication/qrcode-login.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/authentication/register.vue
================================================
注册账号
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/fallback/coming-soon.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/fallback/forbidden.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/fallback/internal-error.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/fallback/not-found.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/_core/fallback/offline.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/adminSpace/corpMgr/corp-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/adminSpace/corpMgr/index.vue
================================================
企业状态:{{ item.status ? '启用' : '禁用' }}
部门数量:{{ item.depCount }}个
用户数量:{{ item.depCount }}个
del(item)"
>
创建企业
================================================
FILE: hiauth-front/apps/web-auth/src/views/adminSpace/userMgr/index.vue
================================================
{{ GenderType[row.gender as keyof typeof GenderType] }}
{{
YesOrNoUseBool[
row.isSysAdmin?.toString() as keyof typeof YesOrNoUseBool
]
}}
{{
EnableStatusUseNum[row.status as keyof typeof EnableStatusUseNum]
}}
编辑
重置密码
delUser(row)"
>
删除
================================================
FILE: hiauth-front/apps/web-auth/src/views/adminSpace/userMgr/user-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/common/appMgr/app-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/common/appMgr/index.vue
================================================
编辑
配置
delApp(row)"
>
删除
================================================
FILE: hiauth-front/apps/web-auth/src/views/common/appResourceMgr/app-resource-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/common/appResourceMgr/index.vue
================================================
{{ AppResourceType[row.type as keyof typeof AppResourceType] }}
编辑
delAppResource(row)"
>
删除
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/appClientMgr/app-client-add-modal.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/appClientMgr/app-client-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/appClientMgr/index.vue
================================================
编辑
配置
delAppClient(row)"
>
删除
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/depMgr/dep-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/depMgr/index.vue
================================================
{{
EnableStatusUseNum[row.status as keyof typeof EnableStatusUseNum]
}}
编辑
delDep(row)"
>
删除
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/empMgr/emp-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/empMgr/index.vue
================================================
{{
YesOrNoUseBool[
row.isCorpAdmin?.toString() as keyof typeof YesOrNoUseBool
]
}}
编辑
delEmp(row)"
>
删除
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/roleMgr/auth-modal.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/roleMgr/index.vue
================================================
{{
EnableStatusUseBool[
row.isEnable?.toString() as keyof typeof EnableStatusUseBool
]
}}
编辑
授权
delRole(row)"
>
删除
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/roleMgr/role-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/sysMgr/dictMgr/dict-drawer.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/corpSpace/sysMgr/dictMgr/index.vue
================================================
{{
EnableStatusUseBool[
row.isEnable?.toString() as keyof typeof EnableStatusUseBool
]
}}
添加
编辑
delDict(row)"
>
删除
================================================
FILE: hiauth-front/apps/web-auth/src/views/dashboard/analytics/analytics-trends.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/dashboard/analytics/analytics-visits-data.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/dashboard/analytics/analytics-visits-sales.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/dashboard/analytics/analytics-visits-source.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/dashboard/analytics/analytics-visits.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/dashboard/analytics/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-auth/src/views/dashboard/workspace/index.vue
================================================
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
今日晴,20℃ - 32℃!
================================================
FILE: hiauth-front/apps/web-auth/src/views/demos/antd/index.vue
================================================
Default
Primary
Info
Error
信息
错误
警告
成功
信息
错误
警告
成功
================================================
FILE: hiauth-front/apps/web-auth/tailwind.config.mjs
================================================
export { default } from '@vben/tailwind-config';
================================================
FILE: hiauth-front/apps/web-auth/tsconfig.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
================================================
FILE: hiauth-front/apps/web-auth/tsconfig.node.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": false
},
"include": ["vite.config.mts"]
}
================================================
FILE: hiauth-front/apps/web-auth/vite.config.mts
================================================
import { defineConfig } from '@vben/vite-config';
export default defineConfig(async () => {
return {
application: {},
vite: {
server: {
proxy: {
'/gateway': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/gateway/, ''),
target: 'http://127.0.0.1:8080',
ws: true,
},
},
},
},
};
});
================================================
FILE: hiauth-front/apps/web-ele/index.html
================================================
<%= VITE_APP_TITLE %>
================================================
FILE: hiauth-front/apps/web-ele/package.json
================================================
{
"name": "@vben/web-ele",
"version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-ele"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"dayjs": "catalog:",
"element-plus": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
},
"devDependencies": {
"unplugin-element-plus": "catalog:"
}
}
================================================
FILE: hiauth-front/apps/web-ele/postcss.config.mjs
================================================
export { default } from '@vben/tailwind-config/postcss';
================================================
FILE: hiauth-front/apps/web-ele/src/adapter/component/index.ts
================================================
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { ElNotification } from 'element-plus';
const ElButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/button/index'),
import('element-plus/es/components/button/style/css'),
]).then(([res]) => res.ElButton),
);
const ElCheckbox = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox/style/css'),
]).then(([res]) => res.ElCheckbox),
);
const ElCheckboxButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox-button/style/css'),
]).then(([res]) => res.ElCheckboxButton),
);
const ElCheckboxGroup = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/checkbox/index'),
import('element-plus/es/components/checkbox-group/style/css'),
]).then(([res]) => res.ElCheckboxGroup),
);
const ElDatePicker = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/date-picker/index'),
import('element-plus/es/components/date-picker/style/css'),
]).then(([res]) => res.ElDatePicker),
);
const ElDivider = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/divider/index'),
import('element-plus/es/components/divider/style/css'),
]).then(([res]) => res.ElDivider),
);
const ElInput = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/input/index'),
import('element-plus/es/components/input/style/css'),
]).then(([res]) => res.ElInput),
);
const ElInputNumber = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/input-number/index'),
import('element-plus/es/components/input-number/style/css'),
]).then(([res]) => res.ElInputNumber),
);
const ElRadio = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio/style/css'),
]).then(([res]) => res.ElRadio),
);
const ElRadioButton = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio-button/style/css'),
]).then(([res]) => res.ElRadioButton),
);
const ElRadioGroup = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/radio/index'),
import('element-plus/es/components/radio-group/style/css'),
]).then(([res]) => res.ElRadioGroup),
);
const ElSelectV2 = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/select-v2/index'),
import('element-plus/es/components/select-v2/style/css'),
]).then(([res]) => res.ElSelectV2),
);
const ElSpace = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/space/index'),
import('element-plus/es/components/space/style/css'),
]).then(([res]) => res.ElSpace),
);
const ElSwitch = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/switch/index'),
import('element-plus/es/components/switch/style/css'),
]).then(([res]) => res.ElSwitch),
);
const ElTimePicker = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/time-picker/index'),
import('element-plus/es/components/time-picker/style/css'),
]).then(([res]) => res.ElTimePicker),
);
const ElTreeSelect = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/tree-select/index'),
import('element-plus/es/components/tree-select/style/css'),
]).then(([res]) => res.ElTreeSelect),
);
const ElUpload = defineAsyncComponent(() =>
Promise.all([
import('element-plus/es/components/upload/index'),
import('element-plus/es/components/upload/style/css'),
]).then(([res]) => res.ElUpload),
);
const withDefaultPlaceholder = (
component: T,
type: 'input' | 'select',
componentProps: Recordable = {},
) => {
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
slots,
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'RadioGroup'
| 'Select'
| 'Space'
| 'Switch'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: ElSelectV2,
loadingSlot: 'loading',
visibleEvent: 'onVisibleChange',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: ElTreeSelect,
props: { label: 'label', children: 'children' },
nodeKey: 'value',
loadingSlot: 'loading',
optionsPropName: 'data',
visibleEvent: 'onVisibleChange',
},
),
Checkbox: ElCheckbox,
CheckboxGroup: (props, { attrs, slots }) => {
let defaultSlot;
if (Reflect.has(slots, 'default')) {
defaultSlot = slots.default;
} else {
const { options, isButton } = attrs;
if (Array.isArray(options)) {
defaultSlot = () =>
options.map((option) =>
h(isButton ? ElCheckboxButton : ElCheckbox, option),
);
}
}
return h(
ElCheckboxGroup,
{ ...props, ...attrs },
{ ...slots, default: defaultSlot },
);
},
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'info' }, slots);
},
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(ElButton, { ...props, attrs, type: 'primary' }, slots);
},
Divider: ElDivider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'append',
modelValueProp: 'model-value',
inputComponent: ElInput,
}),
Input: withDefaultPlaceholder(ElInput, 'input'),
InputNumber: withDefaultPlaceholder(ElInputNumber, 'input'),
RadioGroup: (props, { attrs, slots }) => {
let defaultSlot;
if (Reflect.has(slots, 'default')) {
defaultSlot = slots.default;
} else {
const { options } = attrs;
if (Array.isArray(options)) {
defaultSlot = () =>
options.map((option) =>
h(attrs.isButton ? ElRadioButton : ElRadio, option),
);
}
}
return h(
ElRadioGroup,
{ ...props, ...attrs },
{ ...slots, default: defaultSlot },
);
},
Select: (props, { attrs, slots }) => {
return h(ElSelectV2, { ...props, attrs }, slots);
},
Space: ElSpace,
Switch: ElSwitch,
TimePicker: (props, { attrs, slots }) => {
const { name, id, isRange } = props;
const extraProps: Recordable = {};
if (isRange) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElTimePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
DatePicker: (props, { attrs, slots }) => {
const { name, id, type } = props;
const extraProps: Recordable = {};
if (type && type.includes('range')) {
if (name && !Array.isArray(name)) {
extraProps.name = [name, `${name}_end`];
}
if (id && !Array.isArray(id)) {
extraProps.id = [id, `${id}_end`];
}
}
return h(
ElDatePicker,
{
...props,
...attrs,
...extraProps,
},
slots,
);
},
TreeSelect: withDefaultPlaceholder(ElTreeSelect, 'select'),
Upload: ElUpload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
ElNotification({
title,
message: content,
position: 'bottom-right',
duration: 0,
type: 'success',
});
},
});
}
export { initComponentAdapter };
================================================
FILE: hiauth-front/apps/web-ele/src/adapter/form.ts
================================================
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
async function initSetupVbenForm() {
setupVbenForm({
config: {
modelPropNameMap: {
Upload: 'fileList',
CheckboxGroup: 'model-value',
},
},
defineRules: {
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
}
const useVbenForm = useForm;
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema;
export type { VbenFormProps };
================================================
FILE: hiauth-front/apps/web-ele/src/adapter/vxe-table.ts
================================================
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { ElButton, ElImage } from 'element-plus';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
columnConfig: {
resizable: true,
},
minHeight: 180,
formConfig: {
// 全局禁用vxe-table的表单配置,使用formOptions
enabled: false,
},
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
size: 'small',
} as VxeTableGridOptions,
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
const src = row[column.field];
return h(ElImage, { src, previewSrcList: [src] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
ElButton,
{ size: 'small', link: true },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';
================================================
FILE: hiauth-front/apps/web-ele/src/api/core/auth.ts
================================================
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post('/auth/login', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/**
* 获取用户权限码
*/
export async function getAccessCodesApi() {
return requestClient.get('/auth/codes');
}
================================================
FILE: hiauth-front/apps/web-ele/src/api/core/index.ts
================================================
export * from './auth';
export * from './menu';
export * from './user';
================================================
FILE: hiauth-front/apps/web-ele/src/api/core/menu.ts
================================================
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get('/menu/all');
}
================================================
FILE: hiauth-front/apps/web-ele/src/api/core/user.ts
================================================
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户信息
*/
export async function getUserInfoApi() {
return requestClient.get('/user/info');
}
================================================
FILE: hiauth-front/apps/web-ele/src/api/index.ts
================================================
export * from './core';
================================================
FILE: hiauth-front/apps/web-ele/src/api/request.ts
================================================
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
defaultResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { ElMessage } from 'element-plus';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({
...options,
baseURL,
});
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
});
// 处理返回的响应数据格式
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: 'code',
dataField: 'data',
successCode: 0,
}),
);
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? '';
// 如果没有错误信息,则会根据状态码进行提示
ElMessage.error(errorMessage || msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL, {
responseReturn: 'data',
});
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
================================================
FILE: hiauth-front/apps/web-ele/src/app.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/bootstrap.ts
================================================
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/ele';
import { useTitle } from '@vueuse/core';
import { ElLoading } from 'element-plus';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// zIndex: 2000,
// });
const app = createApp(App);
// 注册Element Plus提供的v-loading指令
app.directive('loading', ElLoading.directive);
// 注册Vben提供的v-loading和v-spinning指令
registerLoadingDirective(app, {
loading: false, // Vben提供的v-loading指令和Element Plus提供的v-loading指令二选一即可,此处false表示不注册Vben提供的v-loading指令
spinning: 'spinning',
});
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
}
});
app.mount('#app');
}
export { bootstrap };
================================================
FILE: hiauth-front/apps/web-ele/src/layouts/auth.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/layouts/basic.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/layouts/index.ts
================================================
const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
export { AuthPageLayout, BasicLayout, IFrameView };
================================================
FILE: hiauth-front/apps/web-ele/src/locales/README.md
================================================
# locale
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
================================================
FILE: hiauth-front/apps/web-ele/src/locales/index.ts
================================================
import type { Language } from 'element-plus/es/locale';
import type { App } from 'vue';
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import { ref } from 'vue';
import {
$t,
setupI18n as coreSetup,
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
import dayjs from 'dayjs';
import enLocale from 'element-plus/es/locale/lang/en';
import defaultLocale from 'element-plus/es/locale/lang/zh-cn';
const elementLocale = ref(defaultLocale);
const modules = import.meta.glob('./langs/**/*.json');
const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/,
modules,
);
/**
* 加载应用特有的语言包
* 这里也可以改造为从服务端获取翻译数据
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const [appLocaleMessages] = await Promise.all([
localesMap[lang]?.(),
loadThirdPartyMessage(lang),
]);
return appLocaleMessages?.default;
}
/**
* 加载第三方组件库的语言包
* @param lang
*/
async function loadThirdPartyMessage(lang: SupportedLanguagesType) {
await Promise.all([loadElementLocale(lang), loadDayjsLocale(lang)]);
}
/**
* 加载dayjs的语言包
* @param lang
*/
async function loadDayjsLocale(lang: SupportedLanguagesType) {
let locale;
switch (lang) {
case 'en-US': {
locale = await import('dayjs/locale/en');
break;
}
case 'zh-CN': {
locale = await import('dayjs/locale/zh-cn');
break;
}
// 默认使用英语
default: {
locale = await import('dayjs/locale/en');
}
}
if (locale) {
dayjs.locale(locale);
} else {
console.error(`Failed to load dayjs locale for ${lang}`);
}
}
/**
* 加载element-plus的语言包
* @param lang
*/
async function loadElementLocale(lang: SupportedLanguagesType) {
switch (lang) {
case 'en-US': {
elementLocale.value = enLocale;
break;
}
case 'zh-CN': {
elementLocale.value = defaultLocale;
break;
}
}
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, elementLocale, setupI18n };
================================================
FILE: hiauth-front/apps/web-ele/src/locales/langs/en-US/demos.json
================================================
{
"title": "Demos",
"elementPlus": "Element Plus",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
}
}
================================================
FILE: hiauth-front/apps/web-ele/src/locales/langs/en-US/page.json
================================================
{
"auth": {
"login": "Login",
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
},
"dashboard": {
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
}
}
================================================
FILE: hiauth-front/apps/web-ele/src/locales/langs/zh-CN/demos.json
================================================
{
"title": "演示",
"elementPlus": "Element Plus",
"form": "表单演示",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
}
}
================================================
FILE: hiauth-front/apps/web-ele/src/locales/langs/zh-CN/page.json
================================================
{
"auth": {
"login": "登录",
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
},
"dashboard": {
"title": "概览",
"analytics": "分析页",
"workspace": "工作台"
}
}
================================================
FILE: hiauth-front/apps/web-ele/src/main.ts
================================================
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();
================================================
FILE: hiauth-front/apps/web-ele/src/preferences.ts
================================================
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
* !!! 更改配置后请清空缓存,否则可能不生效
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
},
});
================================================
FILE: hiauth-front/apps/web-ele/src/router/access.ts
================================================
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { ElMessage } from 'element-plus';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
ElMessage({
duration: 1500,
message: `${$t('common.loadingMenu')}...`,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };
================================================
FILE: hiauth-front/apps/web-ele/src/router/guard.ts
================================================
import type { Router } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
* 通用守卫配置
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set();
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
loadedPaths.add(to.path);
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
});
}
/**
* 权限访问守卫配置
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
preferences.app.defaultHomePath,
);
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === preferences.app.defaultHomePath
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示,但是访问会被重定向到403
routes: accessRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ??
(to.path === preferences.app.defaultHomePath
? userInfo.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };
================================================
FILE: hiauth-front/apps/web-ele/src/router/index.ts
================================================
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };
================================================
FILE: hiauth-front/apps/web-ele/src/router/routes/core.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
/**
* 根路由
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
* 此路由必须存在,且不应修改
*/
{
component: BasicLayout,
meta: {
hideInBreadcrumb: true,
title: 'Root',
},
name: 'Root',
path: '/',
redirect: preferences.app.defaultHomePath,
children: [],
},
{
component: AuthPageLayout,
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.auth.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.auth.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.auth.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.auth.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };
================================================
FILE: hiauth-front/apps/web-ele/src/router/routes/index.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由、外部路由和404兜底路由组成
* 无需走权限验证(会一直显示在菜单中) */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
export { accessRoutes, coreRouteNames, routes };
================================================
FILE: hiauth-front/apps/web-ele/src/router/routes/modules/dashboard.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/dashboard',
children: [
{
name: 'Analytics',
path: '/analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-ele/src/router/routes/modules/demos.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('demos.title'),
},
name: 'Demos',
path: '/demos',
children: [
{
meta: {
title: $t('demos.elementPlus'),
},
name: 'NaiveDemos',
path: '/demos/element',
component: () => import('#/views/demos/element/index.vue'),
},
{
meta: {
title: $t('demos.form'),
},
name: 'BasicForm',
path: '/demos/form',
component: () => import('#/views/demos/form/basic.vue'),
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-ele/src/router/routes/modules/vben.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_ANT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
VBEN_NAIVE_PREVIEW_URL,
} from '@vben/constants';
import { SvgAntdvLogoIcon } from '@vben/icons';
import { IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
badgeType: 'dot',
icon: VBEN_LOGO_URL,
order: 9998,
title: $t('demos.vben.title'),
},
name: 'VbenProject',
path: '/vben-admin',
children: [
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: VBEN_DOC_URL,
title: $t('demos.vben.document'),
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'mdi:github',
link: VBEN_GITHUB_URL,
title: 'Github',
},
},
{
name: 'VbenNaive',
path: '/vben-admin/naive',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: 'logos:naiveui',
link: VBEN_NAIVE_PREVIEW_URL,
title: $t('demos.vben.naive-ui'),
},
},
{
name: 'VbenAntd',
path: '/vben-admin/antd',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: SvgAntdvLogoIcon,
link: VBEN_ANT_PREVIEW_URL,
title: $t('demos.vben.antdv'),
},
},
],
},
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('demos.vben.about'),
order: 9999,
},
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-ele/src/store/auth.ts
================================================
import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { ElNotification } from 'element-plus';
import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const loginLoading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param params 登录表单数据
*/
async function authLogin(
params: Recordable,
onSuccess?: () => Promise | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken } = await loginApi(params);
// 如果成功获取到 accessToken
if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(),
getAccessCodesApi(),
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(
userInfo.homePath || preferences.app.defaultHomePath,
);
}
if (userInfo?.realName) {
ElNotification({
message: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
title: $t('authentication.loginSuccess'),
type: 'success',
});
}
}
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
async function logout(redirect: boolean = true) {
try {
await logoutApi();
} catch {
// 不做任何处理
}
resetAllStores();
accessStore.setLoginExpired(false);
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
loginLoading,
logout,
};
});
================================================
FILE: hiauth-front/apps/web-ele/src/store/index.ts
================================================
export * from './auth';
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/README.md
================================================
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/about/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/authentication/code-login.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/authentication/forget-password.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/authentication/login.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/authentication/qrcode-login.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/authentication/register.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/fallback/coming-soon.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/fallback/forbidden.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/fallback/internal-error.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/fallback/not-found.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/_core/fallback/offline.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/dashboard/analytics/analytics-trends.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/dashboard/analytics/analytics-visits-data.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/dashboard/analytics/analytics-visits-sales.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/dashboard/analytics/analytics-visits-source.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/dashboard/analytics/analytics-visits.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/dashboard/analytics/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-ele/src/views/dashboard/workspace/index.vue
================================================
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
今日晴,20℃ - 32℃!
================================================
FILE: hiauth-front/apps/web-ele/src/views/demos/element/index.vue
================================================
按钮
Text
Default
Primary
Info
Success
Warning
Error
Message
信息
错误
警告
成功
Notification
信息
错误
警告
成功
Segmented
V-Loading
一些演示的内容
================================================
FILE: hiauth-front/apps/web-ele/src/views/demos/form/basic.vue
================================================
基础表单演示
设置表单值
打开抽屉
================================================
FILE: hiauth-front/apps/web-ele/tailwind.config.mjs
================================================
export { default } from '@vben/tailwind-config';
================================================
FILE: hiauth-front/apps/web-ele/tsconfig.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
================================================
FILE: hiauth-front/apps/web-ele/tsconfig.node.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": false
},
"include": ["vite.config.mts"]
}
================================================
FILE: hiauth-front/apps/web-ele/vite.config.mts
================================================
import { defineConfig } from '@vben/vite-config';
import ElementPlus from 'unplugin-element-plus/vite';
export default defineConfig(async () => {
return {
application: {},
vite: {
plugins: [
ElementPlus({
format: 'esm',
}),
],
server: {
proxy: {
'/api': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://localhost:5320/api',
ws: true,
},
},
},
},
};
});
================================================
FILE: hiauth-front/apps/web-naive/index.html
================================================
<%= VITE_APP_TITLE %>
================================================
FILE: hiauth-front/apps/web-naive/package.json
================================================
{
"name": "@vben/web-naive",
"version": "5.5.9",
"homepage": "https://vben.pro",
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
"repository": {
"type": "git",
"url": "git+https://github.com/vbenjs/vue-vben-admin.git",
"directory": "apps/web-naive"
},
"license": "MIT",
"author": {
"name": "vben",
"email": "ann.vben@gmail.com",
"url": "https://github.com/anncwb"
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck"
},
"imports": {
"#/*": "./src/*"
},
"dependencies": {
"@vben/access": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/constants": "workspace:*",
"@vben/hooks": "workspace:*",
"@vben/icons": "workspace:*",
"@vben/layouts": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/preferences": "workspace:*",
"@vben/request": "workspace:*",
"@vben/stores": "workspace:*",
"@vben/styles": "workspace:*",
"@vben/types": "workspace:*",
"@vben/utils": "workspace:*",
"@vueuse/core": "catalog:",
"naive-ui": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
}
}
================================================
FILE: hiauth-front/apps/web-naive/postcss.config.mjs
================================================
export { default } from '@vben/tailwind-config/postcss';
================================================
FILE: hiauth-front/apps/web-naive/src/adapter/component/index.ts
================================================
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { Component } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { defineAsyncComponent, defineComponent, h, ref } from 'vue';
import { ApiComponent, globalShareState, IconPicker } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { message } from '#/adapter/naive';
const NButton = defineAsyncComponent(() =>
import('naive-ui/es/button').then((res) => res.NButton),
);
const NCheckbox = defineAsyncComponent(() =>
import('naive-ui/es/checkbox').then((res) => res.NCheckbox),
);
const NCheckboxGroup = defineAsyncComponent(() =>
import('naive-ui/es/checkbox').then((res) => res.NCheckboxGroup),
);
const NDatePicker = defineAsyncComponent(() =>
import('naive-ui/es/date-picker').then((res) => res.NDatePicker),
);
const NDivider = defineAsyncComponent(() =>
import('naive-ui/es/divider').then((res) => res.NDivider),
);
const NInput = defineAsyncComponent(() =>
import('naive-ui/es/input').then((res) => res.NInput),
);
const NInputNumber = defineAsyncComponent(() =>
import('naive-ui/es/input-number').then((res) => res.NInputNumber),
);
const NRadio = defineAsyncComponent(() =>
import('naive-ui/es/radio').then((res) => res.NRadio),
);
const NRadioButton = defineAsyncComponent(() =>
import('naive-ui/es/radio').then((res) => res.NRadioButton),
);
const NRadioGroup = defineAsyncComponent(() =>
import('naive-ui/es/radio').then((res) => res.NRadioGroup),
);
const NSelect = defineAsyncComponent(() =>
import('naive-ui/es/select').then((res) => res.NSelect),
);
const NSpace = defineAsyncComponent(() =>
import('naive-ui/es/space').then((res) => res.NSpace),
);
const NSwitch = defineAsyncComponent(() =>
import('naive-ui/es/switch').then((res) => res.NSwitch),
);
const NTimePicker = defineAsyncComponent(() =>
import('naive-ui/es/time-picker').then((res) => res.NTimePicker),
);
const NTreeSelect = defineAsyncComponent(() =>
import('naive-ui/es/tree-select').then((res) => res.NTreeSelect),
);
const NUpload = defineAsyncComponent(() =>
import('naive-ui/es/upload').then((res) => res.NUpload),
);
const withDefaultPlaceholder = (
component: T,
type: 'input' | 'select',
componentProps: Recordable = {},
) => {
return defineComponent({
name: component.name,
inheritAttrs: false,
setup: (props: any, { attrs, expose, slots }) => {
const placeholder =
props?.placeholder ||
attrs?.placeholder ||
$t(`ui.placeholder.${type}`);
// 透传组件暴露的方法
const innerRef = ref();
expose(
new Proxy(
{},
{
get: (_target, key) => innerRef.value?.[key],
has: (_target, key) => key in (innerRef.value || {}),
},
),
);
return () =>
h(
component,
{ ...componentProps, placeholder, ...props, ...attrs, ref: innerRef },
slots,
);
},
});
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'ApiSelect'
| 'ApiTreeSelect'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'Divider'
| 'IconPicker'
| 'Input'
| 'InputNumber'
| 'RadioGroup'
| 'Select'
| 'Space'
| 'Switch'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
ApiSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiSelect',
},
'select',
{
component: NSelect,
modelPropName: 'value',
},
),
ApiTreeSelect: withDefaultPlaceholder(
{
...ApiComponent,
name: 'ApiTreeSelect',
},
'select',
{
component: NTreeSelect,
nodeKey: 'value',
loadingSlot: 'arrow',
keyField: 'value',
modelPropName: 'value',
optionsPropName: 'options',
visibleEvent: 'onVisibleChange',
},
),
Checkbox: NCheckbox,
CheckboxGroup: (props, { attrs, slots }) => {
let defaultSlot;
if (Reflect.has(slots, 'default')) {
defaultSlot = slots.default;
} else {
const { options } = attrs;
if (Array.isArray(options)) {
defaultSlot = () => options.map((option) => h(NCheckbox, option));
}
}
return h(
NCheckboxGroup,
{ ...props, ...attrs },
{ default: defaultSlot },
);
},
DatePicker: NDatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(NButton, { ...props, attrs, type: 'default' }, slots);
},
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(NButton, { ...props, attrs, type: 'primary' }, slots);
},
Divider: NDivider,
IconPicker: withDefaultPlaceholder(IconPicker, 'select', {
iconSlot: 'suffix',
inputComponent: NInput,
}),
Input: withDefaultPlaceholder(NInput, 'input'),
InputNumber: withDefaultPlaceholder(NInputNumber, 'input'),
RadioGroup: (props, { attrs, slots }) => {
let defaultSlot;
if (Reflect.has(slots, 'default')) {
defaultSlot = slots.default;
} else {
const { options } = attrs;
if (Array.isArray(options)) {
defaultSlot = () =>
options.map((option) =>
h(attrs.isButton ? NRadioButton : NRadio, option),
);
}
}
const groupRender = h(
NRadioGroup,
{ ...props, ...attrs },
{ default: defaultSlot },
);
return attrs.isButton
? h(NSpace, { vertical: true }, () => groupRender)
: groupRender;
},
Select: withDefaultPlaceholder(NSelect, 'select'),
Space: NSpace,
Switch: NSwitch,
TimePicker: NTimePicker,
TreeSelect: withDefaultPlaceholder(NTreeSelect, 'select'),
Upload: NUpload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
message.success(content || title, {
duration: 0,
});
},
});
}
export { initComponentAdapter };
================================================
FILE: hiauth-front/apps/web-naive/src/adapter/form.ts
================================================
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
async function initSetupVbenForm() {
setupVbenForm({
config: {
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
emptyStateValue: null,
baseModelPropName: 'value',
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Upload: 'fileList',
},
},
defineRules: {
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
}
const useVbenForm = useForm;
export { initSetupVbenForm, useVbenForm, z };
export type VbenFormSchema = FormSchema;
export type { VbenFormProps };
================================================
FILE: hiauth-front/apps/web-naive/src/adapter/naive.ts
================================================
import { computed } from 'vue';
import { preferences } from '@vben/preferences';
import '@vben/styles';
import { createDiscreteApi, darkTheme, lightTheme } from 'naive-ui';
const themeOverridesProviderProps = computed(() => ({
themeOverrides: preferences.theme.mode === 'light' ? lightTheme : darkTheme,
}));
const themeProviderProps = computed(() => ({
theme: preferences.theme.mode === 'light' ? lightTheme : darkTheme,
}));
export const { dialog, loadingBar, message, modal, notification } =
createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar', 'modal'],
{
configProviderProps: themeProviderProps,
loadingBarProviderProps: themeOverridesProviderProps,
messageProviderProps: themeOverridesProviderProps,
notificationProviderProps: themeOverridesProviderProps,
},
);
================================================
FILE: hiauth-front/apps/web-naive/src/adapter/vxe-table.ts
================================================
import type { VxeTableGridOptions } from '@vben/plugins/vxe-table';
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { NButton, NImage } from 'naive-ui';
import { useVbenForm } from './form';
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
columnConfig: {
resizable: true,
},
minHeight: 180,
formConfig: {
// 全局禁用vxe-table的表单配置,使用formOptions
enabled: false,
},
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
size: 'small',
} as VxeTableGridOptions,
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
return h(NImage, { src: row[column.field] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
NButton,
{ size: 'small', type: 'primary', quaternary: true },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';
================================================
FILE: hiauth-front/apps/web-naive/src/api/core/auth.ts
================================================
import { baseRequestClient, requestClient } from '#/api/request';
export namespace AuthApi {
/** 登录接口参数 */
export interface LoginParams {
password?: string;
username?: string;
}
/** 登录接口返回值 */
export interface LoginResult {
accessToken: string;
}
export interface RefreshTokenResult {
data: string;
status: number;
}
}
/**
* 登录
*/
export async function loginApi(data: AuthApi.LoginParams) {
return requestClient.post('/auth/login', data);
}
/**
* 刷新accessToken
*/
export async function refreshTokenApi() {
return baseRequestClient.post('/auth/refresh', {
withCredentials: true,
});
}
/**
* 退出登录
*/
export async function logoutApi() {
return baseRequestClient.post('/auth/logout', {
withCredentials: true,
});
}
/**
* 获取用户权限码
*/
export async function getAccessCodesApi() {
return requestClient.get('/auth/codes');
}
================================================
FILE: hiauth-front/apps/web-naive/src/api/core/index.ts
================================================
export * from './auth';
export * from './menu';
export * from './user';
================================================
FILE: hiauth-front/apps/web-naive/src/api/core/menu.ts
================================================
import type { RouteRecordStringComponent } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户所有菜单
*/
export async function getAllMenusApi() {
return requestClient.get('/menu/all');
}
================================================
FILE: hiauth-front/apps/web-naive/src/api/core/user.ts
================================================
import type { UserInfo } from '@vben/types';
import { requestClient } from '#/api/request';
/**
* 获取用户信息
*/
export async function getUserInfoApi() {
return requestClient.get('/user/info');
}
================================================
FILE: hiauth-front/apps/web-naive/src/api/index.ts
================================================
export * from './core';
================================================
FILE: hiauth-front/apps/web-naive/src/api/request.ts
================================================
/**
* 该文件可自行根据业务逻辑进行调整
*/
import type { RequestClientOptions } from '@vben/request';
import { useAppConfig } from '@vben/hooks';
import { preferences } from '@vben/preferences';
import {
authenticateResponseInterceptor,
defaultResponseInterceptor,
errorMessageResponseInterceptor,
RequestClient,
} from '@vben/request';
import { useAccessStore } from '@vben/stores';
import { message } from '#/adapter/naive';
import { useAuthStore } from '#/store';
import { refreshTokenApi } from './core';
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
function createRequestClient(baseURL: string, options?: RequestClientOptions) {
const client = new RequestClient({
...options,
baseURL,
});
/**
* 重新认证逻辑
*/
async function doReAuthenticate() {
console.warn('Access token or refresh token is invalid or expired. ');
const accessStore = useAccessStore();
const authStore = useAuthStore();
accessStore.setAccessToken(null);
if (
preferences.app.loginExpiredMode === 'modal' &&
accessStore.isAccessChecked
) {
accessStore.setLoginExpired(true);
} else {
await authStore.logout();
}
}
/**
* 刷新token逻辑
*/
async function doRefreshToken() {
const accessStore = useAccessStore();
const resp = await refreshTokenApi();
const newToken = resp.data;
accessStore.setAccessToken(newToken);
return newToken;
}
function formatToken(token: null | string) {
return token ? `Bearer ${token}` : null;
}
// 请求头处理
client.addRequestInterceptor({
fulfilled: async (config) => {
const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken);
config.headers['Accept-Language'] = preferences.app.locale;
return config;
},
});
// 处理返回的响应数据格式
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: 'code',
dataField: 'data',
successCode: 0,
}),
);
// token过期的处理
client.addResponseInterceptor(
authenticateResponseInterceptor({
client,
doReAuthenticate,
doRefreshToken,
enableRefreshToken: preferences.app.enableRefreshToken,
formatToken,
}),
);
// 通用的错误处理,如果没有进入上面的错误处理逻辑,就会进入这里
client.addResponseInterceptor(
errorMessageResponseInterceptor((msg: string, error) => {
// 这里可以根据业务进行定制,你可以拿到 error 内的信息进行定制化处理,根据不同的 code 做不同的提示,而不是直接使用 message.error 提示 msg
// 当前mock接口返回的错误字段是 error 或者 message
const responseData = error?.response?.data ?? {};
const errorMessage = responseData?.error ?? responseData?.message ?? '';
// 如果没有错误信息,则会根据状态码进行提示
message.error(errorMessage || msg);
}),
);
return client;
}
export const requestClient = createRequestClient(apiURL, {
responseReturn: 'data',
});
export const baseRequestClient = new RequestClient({ baseURL: apiURL });
================================================
FILE: hiauth-front/apps/web-naive/src/app.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/bootstrap.ts
================================================
import { createApp, watchEffect } from 'vue';
import { registerAccessDirective } from '@vben/access';
import { registerLoadingDirective } from '@vben/common-ui';
import { preferences } from '@vben/preferences';
import { initStores } from '@vben/stores';
import '@vben/styles';
import '@vben/styles/naive';
import { useTitle } from '@vueuse/core';
import { $t, setupI18n } from '#/locales';
import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form';
import App from './app.vue';
import { router } from './router';
async function bootstrap(namespace: string) {
// 初始化组件适配器
await initComponentAdapter();
// 初始化表单组件
await initSetupVbenForm();
// // 设置弹窗的默认配置
// setDefaultModalProps({
// fullscreenButton: false,
// });
// // 设置抽屉的默认配置
// setDefaultDrawerProps({
// // zIndex: 2000,
// });
const app = createApp(App);
// 注册v-loading指令
registerLoadingDirective(app, {
loading: 'loading', // 在这里可以自定义指令名称,也可以明确提供false表示不注册这个指令
spinning: 'spinning',
});
// 国际化 i18n 配置
await setupI18n(app);
// 配置 pinia-tore
await initStores(app, { namespace });
// 安装权限指令
registerAccessDirective(app);
// 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy');
initTippy(app);
// 配置路由及路由守卫
app.use(router);
// 配置Motion插件
const { MotionPlugin } = await import('@vben/plugins/motion');
app.use(MotionPlugin);
// 动态更新标题
watchEffect(() => {
if (preferences.app.dynamicTitle) {
const routeTitle = router.currentRoute.value.meta?.title;
const pageTitle =
(routeTitle ? `${$t(routeTitle)} - ` : '') + preferences.app.name;
useTitle(pageTitle);
}
});
app.mount('#app');
}
export { bootstrap };
================================================
FILE: hiauth-front/apps/web-naive/src/layouts/auth.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/layouts/basic.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/layouts/index.ts
================================================
const BasicLayout = () => import('./basic.vue');
const AuthPageLayout = () => import('./auth.vue');
const IFrameView = () => import('@vben/layouts').then((m) => m.IFrameView);
export { AuthPageLayout, BasicLayout, IFrameView };
================================================
FILE: hiauth-front/apps/web-naive/src/locales/README.md
================================================
# locale
每个app使用的国际化可能不同,这里用于扩展国际化的功能,例如扩展 dayjs、antd组件库的多语言切换,以及app本身的国际化文件。
================================================
FILE: hiauth-front/apps/web-naive/src/locales/index.ts
================================================
import type { App } from 'vue';
import type { LocaleSetupOptions, SupportedLanguagesType } from '@vben/locales';
import {
$t,
setupI18n as coreSetup,
loadLocalesMapFromDir,
} from '@vben/locales';
import { preferences } from '@vben/preferences';
const modules = import.meta.glob('./langs/**/*.json');
const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/,
modules,
);
/**
* 加载应用特有的语言包
* 这里也可以改造为从服务端获取翻译数据
* @param lang
*/
async function loadMessages(lang: SupportedLanguagesType) {
const appLocaleMessages = await localesMap[lang]?.();
return appLocaleMessages?.default;
}
async function setupI18n(app: App, options: LocaleSetupOptions = {}) {
await coreSetup(app, {
defaultLocale: preferences.app.locale,
loadMessages,
missingWarn: !import.meta.env.PROD,
...options,
});
}
export { $t, setupI18n };
================================================
FILE: hiauth-front/apps/web-naive/src/locales/langs/en-US/demos.json
================================================
{
"title": "Demos",
"naive": "Naive UI",
"table": "Table",
"form": "Form",
"vben": {
"title": "Project",
"about": "About",
"document": "Document",
"antdv": "Ant Design Vue Version",
"naive-ui": "Naive UI Version",
"element-plus": "Element Plus Version"
}
}
================================================
FILE: hiauth-front/apps/web-naive/src/locales/langs/en-US/page.json
================================================
{
"auth": {
"login": "Login",
"register": "Register",
"codeLogin": "Code Login",
"qrcodeLogin": "Qr Code Login",
"forgetPassword": "Forget Password"
},
"dashboard": {
"title": "Dashboard",
"analytics": "Analytics",
"workspace": "Workspace"
}
}
================================================
FILE: hiauth-front/apps/web-naive/src/locales/langs/zh-CN/demos.json
================================================
{
"title": "演示",
"naive": "Naive UI",
"table": "Table",
"form": "表单",
"vben": {
"title": "项目",
"about": "关于",
"document": "文档",
"antdv": "Ant Design Vue 版本",
"naive-ui": "Naive UI 版本",
"element-plus": "Element Plus 版本"
}
}
================================================
FILE: hiauth-front/apps/web-naive/src/locales/langs/zh-CN/page.json
================================================
{
"auth": {
"login": "登录",
"register": "注册",
"codeLogin": "验证码登录",
"qrcodeLogin": "二维码登录",
"forgetPassword": "忘记密码"
},
"dashboard": {
"title": "概览",
"analytics": "分析页",
"workspace": "工作台"
}
}
================================================
FILE: hiauth-front/apps/web-naive/src/main.ts
================================================
import { initPreferences } from '@vben/preferences';
import { unmountGlobalLoading } from '@vben/utils';
import { overridesPreferences } from './preferences';
/**
* 应用初始化完成之后再进行页面加载渲染
*/
async function initApplication() {
// name用于指定项目唯一标识
// 用于区分不同项目的偏好设置以及存储数据的key前缀以及其他一些需要隔离的数据
const env = import.meta.env.PROD ? 'prod' : 'dev';
const appVersion = import.meta.env.VITE_APP_VERSION;
const namespace = `${import.meta.env.VITE_APP_NAMESPACE}-${appVersion}-${env}`;
// app偏好设置初始化
await initPreferences({
namespace,
overrides: overridesPreferences,
});
// 启动应用并挂载
// vue应用主要逻辑及视图
const { bootstrap } = await import('./bootstrap');
await bootstrap(namespace);
// 移除并销毁loading
unmountGlobalLoading();
}
initApplication();
================================================
FILE: hiauth-front/apps/web-naive/src/preferences.ts
================================================
import { defineOverridesPreferences } from '@vben/preferences';
/**
* @description 项目配置文件
* 只需要覆盖项目中的一部分配置,不需要的配置不用覆盖,会自动使用默认配置
* !!! 更改配置后请清空缓存,否则可能不生效
*/
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
name: import.meta.env.VITE_APP_TITLE,
},
});
================================================
FILE: hiauth-front/apps/web-naive/src/router/access.ts
================================================
import type {
ComponentRecordType,
GenerateMenuAndRoutesOptions,
} from '@vben/types';
import { generateAccessible } from '@vben/access';
import { preferences } from '@vben/preferences';
import { message } from '#/adapter/naive';
import { getAllMenusApi } from '#/api';
import { BasicLayout, IFrameView } from '#/layouts';
import { $t } from '#/locales';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
const layoutMap: ComponentRecordType = {
BasicLayout,
IFrameView,
};
return await generateAccessible(preferences.app.accessMode, {
...options,
fetchMenuListAsync: async () => {
message.loading(`${$t('common.loadingMenu')}...`, {
duration: 1.5,
});
return await getAllMenusApi();
},
// 可以指定没有权限跳转403页面
forbiddenComponent,
// 如果 route.meta.menuVisibleWithForbidden = true
layoutMap,
pageMap,
});
}
export { generateAccess };
================================================
FILE: hiauth-front/apps/web-naive/src/router/guard.ts
================================================
import type { Router } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';
import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';
import { generateAccess } from './access';
/**
* 通用守卫配置
* @param router
*/
function setupCommonGuard(router: Router) {
// 记录已经加载的页面
const loadedPaths = new Set();
router.beforeEach((to) => {
to.meta.loaded = loadedPaths.has(to.path);
// 页面加载进度条
if (!to.meta.loaded && preferences.transition.progress) {
startProgress();
}
return true;
});
router.afterEach((to) => {
// 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行
loadedPaths.add(to.path);
// 关闭页面加载进度条
if (preferences.transition.progress) {
stopProgress();
}
});
}
/**
* 权限访问守卫配置
* @param router
*/
function setupAccessGuard(router: Router) {
router.beforeEach(async (to, from) => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const authStore = useAuthStore();
// 基本路由,这些路由不需要进入权限拦截
if (coreRouteNames.includes(to.name as string)) {
if (to.path === LOGIN_PATH && accessStore.accessToken) {
return decodeURIComponent(
(to.query?.redirect as string) ||
userStore.userInfo?.homePath ||
preferences.app.defaultHomePath,
);
}
return true;
}
// accessToken 检查
if (!accessStore.accessToken) {
// 明确声明忽略权限访问权限,则可以访问
if (to.meta.ignoreAccess) {
return true;
}
// 没有访问权限,跳转登录页面
if (to.fullPath !== LOGIN_PATH) {
return {
path: LOGIN_PATH,
// 如不需要,直接删除 query
query:
to.fullPath === preferences.app.defaultHomePath
? {}
: { redirect: encodeURIComponent(to.fullPath) },
// 携带当前跳转的页面,登录后重新跳转该页面
replace: true,
};
}
return to;
}
// 是否已经生成过动态路由
if (accessStore.isAccessChecked) {
return true;
}
// 生成路由表
// 当前登录用户拥有的角色标识列表
const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
const userRoles = userInfo.roles ?? [];
// 生成菜单和路由
const { accessibleMenus, accessibleRoutes } = await generateAccess({
roles: userRoles,
router,
// 则会在菜单中显示,但是访问会被重定向到403
routes: accessRoutes,
});
// 保存菜单信息和路由信息
accessStore.setAccessMenus(accessibleMenus);
accessStore.setAccessRoutes(accessibleRoutes);
accessStore.setIsAccessChecked(true);
const redirectPath = (from.query.redirect ??
(to.path === preferences.app.defaultHomePath
? userInfo.homePath || preferences.app.defaultHomePath
: to.fullPath)) as string;
return {
...router.resolve(decodeURIComponent(redirectPath)),
replace: true,
};
});
}
/**
* 项目守卫配置
* @param router
*/
function createRouterGuard(router: Router) {
/** 通用 */
setupCommonGuard(router);
/** 权限访问 */
setupAccessGuard(router);
}
export { createRouterGuard };
================================================
FILE: hiauth-front/apps/web-naive/src/router/index.ts
================================================
import {
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import { resetStaticRoutes } from '@vben/utils';
import { createRouterGuard } from './guard';
import { routes } from './routes';
/**
* @zh_CN 创建vue-router实例
*/
const router = createRouter({
history:
import.meta.env.VITE_ROUTER_HISTORY === 'hash'
? createWebHashHistory(import.meta.env.VITE_BASE)
: createWebHistory(import.meta.env.VITE_BASE),
// 应该添加到路由的初始路由列表。
routes,
scrollBehavior: (to, _from, savedPosition) => {
if (savedPosition) {
return savedPosition;
}
return to.hash ? { behavior: 'smooth', el: to.hash } : { left: 0, top: 0 };
},
// 是否应该禁止尾部斜杠。
// strict: true,
});
const resetRoutes = () => resetStaticRoutes(router, routes);
// 创建路由守卫
createRouterGuard(router);
export { resetRoutes, router };
================================================
FILE: hiauth-front/apps/web-naive/src/router/routes/core.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { $t } from '#/locales';
const BasicLayout = () => import('#/layouts/basic.vue');
const AuthPageLayout = () => import('#/layouts/auth.vue');
/** 全局404页面 */
const fallbackNotFoundRoute: RouteRecordRaw = {
component: () => import('#/views/_core/fallback/not-found.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: '404',
},
name: 'FallbackNotFound',
path: '/:path(.*)*',
};
/** 基本路由,这些路由是必须存在的 */
const coreRoutes: RouteRecordRaw[] = [
/**
* 根路由
* 使用基础布局,作为所有页面的父级容器,子级就不必配置BasicLayout。
* 此路由必须存在,且不应修改
*/
{
component: BasicLayout,
meta: {
hideInBreadcrumb: true,
title: 'Root',
},
name: 'Root',
path: '/',
redirect: preferences.app.defaultHomePath,
children: [],
},
{
component: AuthPageLayout,
meta: {
hideInTab: true,
title: 'Authentication',
},
name: 'Authentication',
path: '/auth',
redirect: LOGIN_PATH,
children: [
{
name: 'Login',
path: 'login',
component: () => import('#/views/_core/authentication/login.vue'),
meta: {
title: $t('page.auth.login'),
},
},
{
name: 'CodeLogin',
path: 'code-login',
component: () => import('#/views/_core/authentication/code-login.vue'),
meta: {
title: $t('page.auth.codeLogin'),
},
},
{
name: 'QrCodeLogin',
path: 'qrcode-login',
component: () =>
import('#/views/_core/authentication/qrcode-login.vue'),
meta: {
title: $t('page.auth.qrcodeLogin'),
},
},
{
name: 'ForgetPassword',
path: 'forget-password',
component: () =>
import('#/views/_core/authentication/forget-password.vue'),
meta: {
title: $t('page.auth.forgetPassword'),
},
},
{
name: 'Register',
path: 'register',
component: () => import('#/views/_core/authentication/register.vue'),
meta: {
title: $t('page.auth.register'),
},
},
],
},
];
export { coreRoutes, fallbackNotFoundRoute };
================================================
FILE: hiauth-front/apps/web-naive/src/router/routes/index.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
eager: true,
});
// 有需要可以自行打开注释,并创建文件夹
// const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true });
// const staticRouteFiles = import.meta.glob('./static/**/*.ts', { eager: true });
/** 动态路由 */
const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules(dynamicRouteFiles);
/** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */
// const externalRoutes: RouteRecordRaw[] = mergeRouteModules(externalRouteFiles);
// const staticRoutes: RouteRecordRaw[] = mergeRouteModules(staticRouteFiles);
const staticRoutes: RouteRecordRaw[] = [];
const externalRoutes: RouteRecordRaw[] = [];
/** 路由列表,由基本路由、外部路由和404兜底路由组成
* 无需走权限验证(会一直显示在菜单中) */
const routes: RouteRecordRaw[] = [
...coreRoutes,
...externalRoutes,
fallbackNotFoundRoute,
];
/** 基本路由列表,这些路由不需要进入权限拦截 */
const coreRouteNames = traverseTreeValues(coreRoutes, (route) => route.name);
/** 有权限校验的路由列表,包含动态路由和静态路由 */
const accessRoutes = [...dynamicRoutes, ...staticRoutes];
export { accessRoutes, coreRouteNames, routes };
================================================
FILE: hiauth-front/apps/web-naive/src/router/routes/modules/dashboard.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'lucide:layout-dashboard',
order: -1,
title: $t('page.dashboard.title'),
},
name: 'Dashboard',
path: '/dashboard',
children: [
{
name: 'Analytics',
path: '/analytics',
component: () => import('#/views/dashboard/analytics/index.vue'),
meta: {
affixTab: true,
icon: 'lucide:area-chart',
title: $t('page.dashboard.analytics'),
},
},
{
name: 'Workspace',
path: '/workspace',
component: () => import('#/views/dashboard/workspace/index.vue'),
meta: {
icon: 'carbon:workspace',
title: $t('page.dashboard.workspace'),
},
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-naive/src/router/routes/modules/demos.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
icon: 'ic:baseline-view-in-ar',
keepAlive: true,
order: 1000,
title: $t('demos.title'),
},
name: 'Demos',
path: '/demos',
children: [
{
meta: {
title: $t('demos.naive'),
},
name: 'NaiveDemos',
path: '/demos/naive',
component: () => import('#/views/demos/naive/index.vue'),
},
{
meta: {
title: $t('demos.table'),
},
name: 'Table',
path: '/demos/table',
component: () => import('#/views/demos/table/index.vue'),
},
{
meta: {
title: $t('demos.form'),
},
name: 'Form',
path: '/demos/form',
component: () => import('#/views/demos/form/basic.vue'),
},
],
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-naive/src/router/routes/modules/vben.ts
================================================
import type { RouteRecordRaw } from 'vue-router';
import {
VBEN_ANT_PREVIEW_URL,
VBEN_DOC_URL,
VBEN_ELE_PREVIEW_URL,
VBEN_GITHUB_URL,
VBEN_LOGO_URL,
} from '@vben/constants';
import { SvgAntdvLogoIcon } from '@vben/icons';
import { IFrameView } from '#/layouts';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
meta: {
badgeType: 'dot',
icon: VBEN_LOGO_URL,
order: 9998,
title: $t('demos.vben.title'),
},
name: 'VbenProject',
path: '/vben-admin',
children: [
{
name: 'VbenDocument',
path: '/vben-admin/document',
component: IFrameView,
meta: {
icon: 'lucide:book-open-text',
link: VBEN_DOC_URL,
title: $t('demos.vben.document'),
},
},
{
name: 'VbenGithub',
path: '/vben-admin/github',
component: IFrameView,
meta: {
icon: 'mdi:github',
link: VBEN_GITHUB_URL,
title: 'Github',
},
},
{
name: 'VbenAntd',
path: '/vben-admin/antd',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: SvgAntdvLogoIcon,
link: VBEN_ANT_PREVIEW_URL,
title: $t('demos.vben.antdv'),
},
},
{
name: 'VbenElementPlus',
path: '/vben-admin/ele',
component: IFrameView,
meta: {
badgeType: 'dot',
icon: 'logos:element',
link: VBEN_ELE_PREVIEW_URL,
title: $t('demos.vben.element-plus'),
},
},
],
},
{
name: 'VbenAbout',
path: '/vben-admin/about',
component: () => import('#/views/_core/about/index.vue'),
meta: {
icon: 'lucide:copyright',
title: $t('demos.vben.about'),
order: 9999,
},
},
];
export default routes;
================================================
FILE: hiauth-front/apps/web-naive/src/store/auth.ts
================================================
import type { Recordable, UserInfo } from '@vben/types';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
import { defineStore } from 'pinia';
import { notification } from '#/adapter/naive';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales';
export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore();
const userStore = useUserStore();
const router = useRouter();
const loginLoading = ref(false);
/**
* 异步处理登录操作
* Asynchronously handle the login process
* @param params 登录表单数据
*/
async function authLogin(
params: Recordable,
onSuccess?: () => Promise | void,
) {
// 异步处理用户登录操作并获取 accessToken
let userInfo: null | UserInfo = null;
try {
loginLoading.value = true;
const { accessToken } = await loginApi(params);
// 如果成功获取到 accessToken
if (accessToken) {
// 将 accessToken 存储到 accessStore 中
accessStore.setAccessToken(accessToken);
// 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(),
getAccessCodesApi(),
]);
userInfo = fetchUserInfoResult;
userStore.setUserInfo(userInfo);
accessStore.setAccessCodes(accessCodes);
if (accessStore.loginExpired) {
accessStore.setLoginExpired(false);
} else {
onSuccess
? await onSuccess?.()
: await router.push(
userInfo.homePath || preferences.app.defaultHomePath,
);
}
if (userInfo?.realName) {
notification.success({
content: $t('authentication.loginSuccess'),
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`,
duration: 3000,
});
}
}
} finally {
loginLoading.value = false;
}
return {
userInfo,
};
}
async function logout(redirect: boolean = true) {
try {
await logoutApi();
} catch {
// 不做任何处理
}
resetAllStores();
accessStore.setLoginExpired(false);
// 回登录页带上当前路由地址
await router.replace({
path: LOGIN_PATH,
query: redirect
? {
redirect: encodeURIComponent(router.currentRoute.value.fullPath),
}
: {},
});
}
async function fetchUserInfo() {
let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo);
return userInfo;
}
function $reset() {
loginLoading.value = false;
}
return {
$reset,
authLogin,
fetchUserInfo,
loginLoading,
logout,
};
});
================================================
FILE: hiauth-front/apps/web-naive/src/store/index.ts
================================================
export * from './auth';
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/README.md
================================================
# \_core
此目录包含应用程序正常运行所需的基本视图。这些视图是应用程序布局中使用的视图。
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/about/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/authentication/code-login.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/authentication/forget-password.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/authentication/login.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/authentication/qrcode-login.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/authentication/register.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/fallback/coming-soon.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/fallback/forbidden.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/fallback/internal-error.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/fallback/not-found.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/_core/fallback/offline.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/dashboard/analytics/analytics-trends.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/dashboard/analytics/analytics-visits-data.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/dashboard/analytics/analytics-visits-sales.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/dashboard/analytics/analytics-visits-source.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/dashboard/analytics/analytics-visits.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/dashboard/analytics/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/dashboard/workspace/index.vue
================================================
早安, {{ userStore.userInfo?.realName }}, 开始您一天的工作吧!
今日晴,20℃ - 32℃!
================================================
FILE: hiauth-front/apps/web-naive/src/views/demos/form/basic.vue
================================================
设置表单值
打开弹窗
================================================
FILE: hiauth-front/apps/web-naive/src/views/demos/form/modal.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/src/views/demos/naive/index.vue
================================================
Default
Tertiary
Primary
Info
Success
Warning
Error
错误
警告
成功
加载中
错误
警告
成功
加载中
================================================
FILE: hiauth-front/apps/web-naive/src/views/demos/table/index.vue
================================================
================================================
FILE: hiauth-front/apps/web-naive/tailwind.config.mjs
================================================
export { default } from '@vben/tailwind-config';
================================================
FILE: hiauth-front/apps/web-naive/tsconfig.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web-app.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"#/*": ["./src/*"]
}
},
"references": [{ "path": "./tsconfig.node.json" }],
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
================================================
FILE: hiauth-front/apps/web-naive/tsconfig.node.json
================================================
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/node.json",
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"noEmit": false
},
"include": ["vite.config.mts"]
}
================================================
FILE: hiauth-front/apps/web-naive/vite.config.mts
================================================
import { defineConfig } from '@vben/vite-config';
export default defineConfig(async () => {
return {
application: {},
vite: {
server: {
proxy: {
'/api': {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
// mock代理目标地址
target: 'http://localhost:5320/api',
ws: true,
},
},
},
},
};
});
================================================
FILE: hiauth-front/changlist.txt
================================================
本目录,除了如下内容,其他全部删除:
1、changlist.txt;
2、deploy.yaml;
3、Dockerfile;
4、apps;(单独处理)
官方源码,剔除的内容:
1、.git目录;
2、.github目录;
3、.idea目录;
4、apps目录;(单独处理)
需要手动合并的
1、./package.json
- "build:auth": "pnpm run build --filter=@vben/web-auth"
- "dev:auth": "pnpm -F @vben/web-auth run dev",
2、./vben-admin.code-workspace
- 添加 web-auth
3、./apps/web-auth/package.json
- "jsencrypt": "^3.3.2"
================================================
FILE: hiauth-front/cspell.json
================================================
{
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
"version": "0.2",
"language": "en,en-US",
"allowCompoundWords": true,
"words": [
"acmr",
"antd",
"antdv",
"astro",
"brotli",
"clsx",
"defu",
"demi",
"echarts",
"ependencies",
"esno",
"etag",
"execa",
"iconify",
"iconoir",
"intlify",
"lockb",
"lucide",
"minh",
"minw",
"mkdist",
"mockjs",
"naiveui",
"nocheck",
"noopener",
"noreferrer",
"nprogress",
"nuxt",
"pinia",
"prefixs",
"publint",
"qrcode",
"shadcn",
"sonner",
"sortablejs",
"styl",
"taze",
"ui-kit",
"uicons",
"unplugin",
"unref",
"vben",
"vbenjs",
"vite",
"vitejs",
"vitepress",
"vnode",
"vueuse",
"yxxx"
],
"ignorePaths": [
"**/node_modules/**",
"**/dist/**",
"**/*-dist/**",
"**/icons/**",
"pnpm-lock.yaml",
"**/*.log",
"**/*.test.ts",
"**/*.spec.ts",
"**/__tests__/**"
]
}
================================================
FILE: hiauth-front/deploy.yaml
================================================
kind: Deployment
apiVersion: apps/v1
metadata:
name: ingress
namespace: $NAMESPACE
spec:
selector:
matchLabels:
app: ingress
replicas: $APP_REPLICAS
template:
metadata:
labels:
app: ingress
spec:
imagePullSecrets:
- name: harborsecret
containers:
- name: ingress
image: $IMAGE_NAME
imagePullPolicy: 'Always'
ports:
- containerPort: 80
- containerPort: 443
resources:
requests:
memory: 1Gi
cpu: 1
limits:
memory: 2Gi
cpu: 2
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/
lifecycle:
postStart:
exec:
command:
[
'/bin/sh',
'-c',
'cp -rf /etc/nginx/..data/nginx.conf /usr/local/nginx/conf/ && /usr/local/nginx/sbin/nginx -s reload',
]
preStop:
exec:
command:
- sh
- '-c'
- sleep 5 && kill -SIGQUIT 1
volumes:
- name: nginx-config
configMap:
name: ingress.conf
---
apiVersion: v1
kind: Service
metadata:
name: ingress-svc
namespace: platform
spec:
ports:
- name: http
nodePort: 30280
port: 80
protocol: TCP
targetPort: 80
selector:
app: ingress
type: LoadBalancer
================================================
FILE: hiauth-front/docs/.vitepress/components/demo-preview.vue
================================================
ERROR:
The preview directory does not exist. Please check the 'dir'
parameter.
================================================
FILE: hiauth-front/docs/.vitepress/components/index.ts
================================================
export { default as DemoPreview } from './demo-preview.vue';
================================================
FILE: hiauth-front/docs/.vitepress/components/preview-group.vue
================================================
{{ tab.label }}
{{ open ? 'Collapse code' : 'Expand code' }}
================================================
FILE: hiauth-front/docs/.vitepress/config/en.mts
================================================
import type { DefaultTheme } from 'vitepress';
import { defineConfig } from 'vitepress';
import { version } from '../../../package.json';
export const en = defineConfig({
description: 'Vben Admin & Enterprise level management system framework',
lang: 'en-US',
themeConfig: {
darkModeSwitchLabel: 'Theme',
darkModeSwitchTitle: 'Switch to Dark Mode',
docFooter: {
next: 'Next Page',
prev: 'Previous Page',
},
editLink: {
pattern:
'https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path',
text: 'Edit this page on GitHub',
},
footer: {
copyright: `Copyright © 2020-${new Date().getFullYear()} Vben`,
message: 'Released under the MIT License.',
},
langMenuLabel: 'Language',
lastUpdated: {
formatOptions: {
dateStyle: 'short',
timeStyle: 'medium',
},
text: 'Last updated on',
},
lightModeSwitchTitle: 'Switch to Light Mode',
nav: nav(),
outline: {
label: 'Navigate',
},
returnToTopLabel: 'Back to top',
sidebar: {
'/en/commercial/': {
base: '/en/commercial/',
items: sidebarCommercial(),
},
'/en/guide/': { base: '/en/guide/', items: sidebarGuide() },
},
},
});
function sidebarGuide(): DefaultTheme.SidebarItem[] {
return [
{
collapsed: false,
text: 'Introduction',
items: [
{
link: 'introduction/vben',
text: 'About Vben Admin',
},
{
link: 'introduction/why',
text: 'Why Choose Us?',
},
{ link: 'introduction/quick-start', text: 'Quick Start' },
{ link: 'introduction/thin', text: 'Lite Version' },
],
},
{
text: 'Basics',
items: [
{ link: 'essentials/concept', text: 'Basic Concepts' },
{ link: 'essentials/development', text: 'Local Development' },
{ link: 'essentials/route', text: 'Routing and Menu' },
{ link: 'essentials/settings', text: 'Configuration' },
{ link: 'essentials/icons', text: 'Icons' },
{ link: 'essentials/styles', text: 'Styles' },
{ link: 'essentials/external-module', text: 'External Modules' },
{ link: 'essentials/build', text: 'Build and Deployment' },
{ link: 'essentials/server', text: 'Server Interaction and Data Mock' },
],
},
{
text: 'Advanced',
items: [
{ link: 'in-depth/login', text: 'Login' },
{ link: 'in-depth/theme', text: 'Theme' },
{ link: 'in-depth/access', text: 'Access Control' },
{ link: 'in-depth/locale', text: 'Internationalization' },
{ link: 'in-depth/features', text: 'Common Features' },
{ link: 'in-depth/check-updates', text: 'Check Updates' },
{ link: 'in-depth/loading', text: 'Global Loading' },
{ link: 'in-depth/ui-framework', text: 'UI Framework Switching' },
],
},
{
text: 'Engineering',
items: [
{ link: 'project/standard', text: 'Standards' },
{ link: 'project/cli', text: 'CLI' },
{ link: 'project/dir', text: 'Directory Explanation' },
{ link: 'project/test', text: 'Unit Testing' },
{ link: 'project/tailwindcss', text: 'Tailwind CSS' },
{ link: 'project/changeset', text: 'Changeset' },
{ link: 'project/vite', text: 'Vite Config' },
],
},
{
text: 'Others',
items: [
{ link: 'other/project-update', text: 'Project Update' },
{ link: 'other/remove-code', text: 'Remove Code' },
{ link: 'other/faq', text: 'FAQ' },
],
},
];
}
function sidebarCommercial(): DefaultTheme.SidebarItem[] {
return [
{
link: 'community',
text: 'Community',
},
{
link: 'technical-support',
text: 'Technical-support',
},
{
link: 'customized',
text: 'Customized',
},
];
}
function nav(): DefaultTheme.NavItem[] {
return [
{
activeMatch: '^/en/(guide|components)/',
text: 'Doc',
items: [
{
activeMatch: '^/en/guide/',
link: '/en/guide/introduction/vben',
text: 'Guide',
},
// {
// activeMatch: '^/en/components/',
// link: '/en/components/introduction',
// text: 'Components',
// },
{
text: 'Historical Versions',
items: [
{
link: 'https://doc.vvbin.cn',
text: '2.x Version Documentation',
},
],
},
],
},
{
text: 'Demo',
items: [
{
text: 'Vben Admin',
items: [
{
link: 'https://www.vben.pro',
text: 'Demo Version',
},
{
link: 'https://ant.vben.pro',
text: 'Ant Design Vue Version',
},
{
link: 'https://naive.vben.pro',
text: 'Naive Version',
},
{
link: 'https://ele.vben.pro',
text: 'Element Plus Version',
},
],
},
{
text: 'Others',
items: [
{
link: 'https://vben.vvbin.cn',
text: 'Vben Admin 2.x',
},
],
},
],
},
{
text: version,
items: [
{
link: 'https://github.com/vbenjs/vue-vben-admin/releases',
text: 'Changelog',
},
{
link: 'https://github.com/orgs/vbenjs/projects/5',
text: 'Roadmap',
},
{
link: 'https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md',
text: 'Contribution',
},
],
},
{
link: '/commercial/technical-support',
text: '🦄 Tech Support',
},
{
link: '/sponsor/personal',
text: '✨ Sponsor',
},
{
link: '/commercial/community',
text: '👨👦👦 Community',
},
// {
// link: '/friend-links/',
// text: '🤝 Friend Links',
// },
];
}
================================================
FILE: hiauth-front/docs/.vitepress/config/index.mts
================================================
import { withPwa } from '@vite-pwa/vitepress';
import { defineConfigWithTheme } from 'vitepress';
import { en } from './en.mts';
import { shared } from './shared.mts';
import { zh } from './zh.mts';
export default withPwa(
defineConfigWithTheme({
...shared,
locales: {
en: {
label: 'English',
lang: 'en',
link: '/en/',
...en,
},
root: {
label: '简体中文',
lang: 'zh-CN',
...zh,
},
},
}),
);
================================================
FILE: hiauth-front/docs/.vitepress/config/plugins/demo-preview.ts
================================================
import type { MarkdownEnv, MarkdownRenderer } from 'vitepress';
import crypto from 'node:crypto';
import { readdirSync } from 'node:fs';
import { join } from 'node:path';
export const rawPathRegexp =
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/strict
/^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/;
function rawPathToToken(rawPath: string) {
const [
filepath = '',
extension = '',
region = '',
lines = '',
lang = '',
rawTitle = '',
] = (rawPathRegexp.exec(rawPath) || []).slice(1);
const title = rawTitle || filepath.split('/').pop() || '';
return { extension, filepath, lang, lines, region, title };
}
export const demoPreviewPlugin = (md: MarkdownRenderer) => {
md.core.ruler.after('inline', 'demo-preview', (state) => {
const insertComponentImport = (importString: string) => {
const index = state.tokens.findIndex(
(i) => i.type === 'html_block' && i.content.match(/\n`;
state.tokens.splice(0, 0, importComponent);
} else {
if (state.tokens[index]) {
const content = state.tokens[index].content;
state.tokens[index].content = content.replace(
'',
`${importString}\n`,
);
}
}
};
// Define the regular expression to match the desired pattern
const regex = /]*\sdir="([^"]*)"/g;
// Iterate through the Markdown content and replace the pattern
state.src = state.src.replaceAll(regex, (_match, dir) => {
const componentDir = join(process.cwd(), 'src', dir).replaceAll(
'\\',
'/',
);
let childFiles: string[] = [];
let dirExists = true;
try {
childFiles =
readdirSync(componentDir, {
encoding: 'utf8',
recursive: false,
withFileTypes: false,
}) || [];
} catch {
dirExists = false;
}
if (!dirExists) {
return '';
}
const uniqueWord = generateContentHash(componentDir);
const ComponentName = `DemoComponent_${uniqueWord}`;
insertComponentImport(
`import ${ComponentName} from '${componentDir}/index.vue'`,
);
const { path: _path } = state.env as MarkdownEnv;
const index = state.tokens.findIndex((i) => i.content.match(regex));
if (!state.tokens[index]) {
return '';
}
const firstString = 'index.vue';
childFiles = childFiles.sort((a, b) => {
if (a === firstString) return -1;
if (b === firstString) return 1;
return a.localeCompare(b, 'en', { sensitivity: 'base' });
});
state.tokens[index].content =
`<${ComponentName}/>
`;
const _dummyToken = new state.Token('', '', 0);
const tokenArray: Array = [];
childFiles.forEach((filename) => {
// const slotName = filename.replace(extname(filename), '');
const templateStart = new state.Token('html_inline', '', 0);
templateStart.content = ``;
tokenArray.push(templateStart);
const resolvedPath = join(componentDir, filename);
const { extension, filepath, lang, lines, title } =
rawPathToToken(resolvedPath);
// Add code tokens for each line
const token = new state.Token('fence', 'code', 0);
token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${
title ? `[${title}]` : ''
}`;
token.content = `<<< ${filepath}`;
(token as any).src = [resolvedPath];
tokenArray.push(token);
const templateEnd = new state.Token('html_inline', '', 0);
templateEnd.content = ' ';
tokenArray.push(templateEnd);
});
const endTag = new state.Token('html_inline', '', 0);
endTag.content = ' ';
tokenArray.push(endTag);
state.tokens.splice(index + 1, 0, ...tokenArray);
// console.log(
// state.md.renderer.render(state.tokens, state?.options ?? [], state.env),
// );
return '';
});
});
};
function generateContentHash(input: string, length: number = 10): string {
// 使用 SHA-256 生成哈希值
const hash = crypto.createHash('sha256').update(input).digest('hex');
// 将哈希值转换为 Base36 编码,并取指定长度的字符作为结果
return Number.parseInt(hash, 16).toString(36).slice(0, length);
}
================================================
FILE: hiauth-front/docs/.vitepress/config/shared.mts
================================================
import type { PwaOptions } from '@vite-pwa/vitepress';
import type { HeadConfig } from 'vitepress';
import { resolve } from 'node:path';
import {
viteArchiverPlugin,
viteVxeTableImportsPlugin,
} from '@vben/vite-config';
import {
GitChangelog,
GitChangelogMarkdownSection,
} from '@nolebase/vitepress-plugin-git-changelog/vite';
import tailwind from 'tailwindcss';
import { defineConfig, postcssIsolateStyles } from 'vitepress';
import {
groupIconMdPlugin,
groupIconVitePlugin,
} from 'vitepress-plugin-group-icons';
import { demoPreviewPlugin } from './plugins/demo-preview';
import { search as zhSearch } from './zh.mts';
export const shared = defineConfig({
appearance: 'dark',
head: head(),
markdown: {
preConfig(md) {
md.use(demoPreviewPlugin);
md.use(groupIconMdPlugin);
},
},
pwa: pwa(),
srcDir: 'src',
themeConfig: {
i18nRouting: true,
logo: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
search: {
options: {
locales: {
...zhSearch,
},
},
provider: 'local',
},
siteTitle: 'Vben Admin',
socialLinks: [
{ icon: 'github', link: 'https://github.com/vbenjs/vue-vben-admin' },
],
},
title: 'Vben Admin',
vite: {
build: {
chunkSizeWarningLimit: Infinity,
minify: 'terser',
},
css: {
postcss: {
plugins: [
tailwind(),
postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] }),
],
},
preprocessorOptions: {
scss: {
api: 'modern',
},
},
},
json: {
stringify: true,
},
plugins: [
GitChangelog({
mapAuthors: [
{
mapByNameAliases: ['Vben'],
name: 'vben',
username: 'anncwb',
},
{
name: 'vince',
username: 'vince292007',
},
{
name: 'Li Kui',
username: 'likui628',
},
],
repoURL: () => 'https://github.com/vbenjs/vue-vben-admin',
}),
GitChangelogMarkdownSection(),
viteArchiverPlugin({ outputDir: '.vitepress' }),
groupIconVitePlugin(),
await viteVxeTableImportsPlugin(),
],
server: {
fs: {
allow: ['../..'],
},
host: true,
port: 6173,
},
ssr: {
external: ['@vue/repl'],
},
},
});
function head(): HeadConfig[] {
return [
['meta', { content: 'Vbenjs Team', name: 'author' }],
[
'meta',
{
content: 'vben, vitejs, vite, shacdn-ui, vue',
name: 'keywords',
},
],
['link', { href: '/favicon.ico', rel: 'icon', type: 'image/svg+xml' }],
[
'meta',
{
content:
'width=device-width,initial-scale=1,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no',
name: 'viewport',
},
],
['meta', { content: 'vben admin docs', name: 'keywords' }],
['link', { href: '/favicon.ico', rel: 'icon' }],
// [
// 'script',
// {
// src: 'https://cdn.tailwindcss.com',
// },
// ],
];
}
function pwa(): PwaOptions {
return {
includeManifestIcons: false,
manifest: {
description:
'Vben Admin is a modern admin dashboard template based on Vue 3. ',
icons: [
{
sizes: '192x192',
src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-192.png',
type: 'image/png',
},
{
sizes: '512x512',
src: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/pwa-icon-512.png',
type: 'image/png',
},
],
id: '/',
name: 'Vben Admin Doc',
short_name: 'vben_admin_doc',
theme_color: '#ffffff',
},
outDir: resolve(process.cwd(), '.vitepress/dist'),
registerType: 'autoUpdate',
workbox: {
globPatterns: ['**/*.{css,js,html,svg,png,ico,txt,woff2}'],
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
},
};
}
================================================
FILE: hiauth-front/docs/.vitepress/config/zh.mts
================================================
import type { DefaultTheme } from 'vitepress';
import { defineConfig } from 'vitepress';
import { version } from '../../../package.json';
export const zh = defineConfig({
description: 'Vben Admin & 企业级管理系统框架',
lang: 'zh-Hans',
themeConfig: {
darkModeSwitchLabel: '主题',
darkModeSwitchTitle: '切换到深色模式',
docFooter: {
next: '下一页',
prev: '上一页',
},
editLink: {
pattern:
'https://github.com/vbenjs/vue-vben-admin/edit/main/docs/src/:path',
text: '在 GitHub 上编辑此页面',
},
footer: {
copyright: `Copyright © 2020-${new Date().getFullYear()} Vben`,
message: '基于 MIT 许可发布.',
},
langMenuLabel: '多语言',
lastUpdated: {
formatOptions: {
dateStyle: 'short',
timeStyle: 'medium',
},
text: '最后更新于',
},
lightModeSwitchTitle: '切换到浅色模式',
nav: nav(),
outline: {
label: '页面导航',
},
returnToTopLabel: '回到顶部',
sidebar: {
'/commercial/': { base: '/commercial/', items: sidebarCommercial() },
'/components/': { base: '/components/', items: sidebarComponents() },
'/guide/': { base: '/guide/', items: sidebarGuide() },
},
sidebarMenuLabel: '菜单',
},
});
function sidebarGuide(): DefaultTheme.SidebarItem[] {
return [
{
collapsed: false,
text: '简介',
items: [
{
link: 'introduction/vben',
text: '关于 Vben Admin',
},
{
link: 'introduction/why',
text: '为什么选择我们?',
},
{ link: 'introduction/quick-start', text: '快速开始' },
{ link: 'introduction/thin', text: '精简版本' },
{
base: '/',
link: 'components/introduction',
text: '组件文档',
},
],
},
{
text: '基础',
items: [
{ link: 'essentials/concept', text: '基础概念' },
{ link: 'essentials/development', text: '本地开发' },
{ link: 'essentials/route', text: '路由和菜单' },
{ link: 'essentials/settings', text: '配置' },
{ link: 'essentials/icons', text: '图标' },
{ link: 'essentials/styles', text: '样式' },
{ link: 'essentials/external-module', text: '外部模块' },
{ link: 'essentials/build', text: '构建与部署' },
{ link: 'essentials/server', text: '服务端交互与数据Mock' },
],
},
{
text: '深入',
items: [
{ link: 'in-depth/login', text: '登录' },
// { link: 'in-depth/layout', text: '布局' },
{ link: 'in-depth/theme', text: '主题' },
{ link: 'in-depth/access', text: '权限' },
{ link: 'in-depth/locale', text: '国际化' },
{ link: 'in-depth/features', text: '常用功能' },
{ link: 'in-depth/check-updates', text: '检查更新' },
{ link: 'in-depth/loading', text: '全局loading' },
{ link: 'in-depth/ui-framework', text: '组件库切换' },
],
},
{
text: '工程',
items: [
{ link: 'project/standard', text: '规范' },
{ link: 'project/cli', text: 'CLI' },
{ link: 'project/dir', text: '目录说明' },
{ link: 'project/test', text: '单元测试' },
{ link: 'project/tailwindcss', text: 'Tailwind CSS' },
{ link: 'project/changeset', text: 'Changeset' },
{ link: 'project/vite', text: 'Vite Config' },
],
},
{
text: '其他',
items: [
{ link: 'other/project-update', text: '项目更新' },
{ link: 'other/remove-code', text: '移除代码' },
{ link: 'other/faq', text: '常见问题' },
],
},
];
}
function sidebarCommercial(): DefaultTheme.SidebarItem[] {
return [
{
link: 'community',
text: '交流群',
},
{
link: 'technical-support',
text: '技术支持',
},
{
link: 'customized',
text: '定制开发',
},
];
}
function sidebarComponents(): DefaultTheme.SidebarItem[] {
return [
{
text: '组件',
items: [
{
link: 'introduction',
text: '介绍',
},
],
},
{
collapsed: false,
text: '布局组件',
items: [
{
link: 'layout-ui/page',
text: 'Page 页面',
},
],
},
{
collapsed: false,
text: '通用组件',
items: [
{
link: 'common-ui/vben-api-component',
text: 'ApiComponent Api组件包装器',
},
{
link: 'common-ui/vben-alert',
text: 'Alert 轻量提示框',
},
{
link: 'common-ui/vben-modal',
text: 'Modal 模态框',
},
{
link: 'common-ui/vben-drawer',
text: 'Drawer 抽屉',
},
{
link: 'common-ui/vben-form',
text: 'Form 表单',
},
{
link: 'common-ui/vben-vxe-table',
text: 'Vxe Table 表格',
},
{
link: 'common-ui/vben-count-to-animator',
text: 'CountToAnimator 数字动画',
},
{
link: 'common-ui/vben-ellipsis-text',
text: 'EllipsisText 省略文本',
},
],
},
];
}
function nav(): DefaultTheme.NavItem[] {
return [
{
activeMatch: '^/(guide|components)/',
text: '文档',
items: [
{
activeMatch: '^/guide/',
link: '/guide/introduction/vben',
text: '指南',
},
{
activeMatch: '^/components/',
link: '/components/introduction',
text: '组件',
},
{
text: '历史版本',
items: [
{
link: 'https://doc.vvbin.cn',
text: '2.x版本文档',
},
],
},
],
},
{
text: '演示',
items: [
{
text: 'Vben Admin',
items: [
{
link: 'https://www.vben.pro',
text: '演示版本',
},
{
link: 'https://ant.vben.pro',
text: 'Ant Design Vue 版本',
},
{
link: 'https://naive.vben.pro',
text: 'Naive 版本',
},
{
link: 'https://ele.vben.pro',
text: 'Element Plus版本',
},
],
},
{
text: '其他',
items: [
{
link: 'https://vben.vvbin.cn',
text: 'Vben Admin 2.x',
},
],
},
],
},
{
text: version,
items: [
{
link: 'https://github.com/vbenjs/vue-vben-admin/releases',
text: '更新日志',
},
{
link: 'https://github.com/orgs/vbenjs/projects/5',
text: '路线图',
},
{
link: 'https://github.com/vbenjs/vue-vben-admin/blob/main/.github/contributing.md',
text: '贡献',
},
],
},
{
link: '/commercial/technical-support',
text: '🦄 技术支持',
},
{
link: '/sponsor/personal',
text: '✨ 赞助',
},
{
link: '/commercial/community',
text: '👨👦👦 交流群',
// items: [
// {
// link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc',
// text: 'QQ频道',
// },
// {
// link: 'https://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=mjZmlhgVzzUxvdxllB6C1vHpX8O8QRL0&authKey=DBdFbBwERmfaKY95JvRWqLCJIRGJAmKyZbrpzZ41EKDMZ5SR6MfbjOBaaNRN73fr&noverify=0&group_code=4286109',
// text: 'QQ群',
// },
// {
// link: 'https://discord.gg/VU62jTecad',
// text: 'Discord',
// },
// ],
},
// {
// link: '/friend-links/',
// text: '🤝 友情链接',
// },
];
}
export const search: DefaultTheme.AlgoliaSearchOptions['locales'] = {
root: {
placeholder: '搜索文档',
translations: {
button: {
buttonAriaLabel: '搜索文档',
buttonText: '搜索文档',
},
modal: {
errorScreen: {
helpText: '你可能需要检查你的网络连接',
titleText: '无法获取结果',
},
footer: {
closeText: '关闭',
navigateText: '切换',
searchByText: '搜索提供者',
selectText: '选择',
},
noResultsScreen: {
noResultsText: '无法找到相关结果',
reportMissingResultsLinkText: '点击反馈',
reportMissingResultsText: '你认为该查询应该有结果?',
suggestedQueryText: '你可以尝试查询',
},
searchBox: {
cancelButtonAriaLabel: '取消',
cancelButtonText: '取消',
resetButtonAriaLabel: '清除查询条件',
resetButtonTitle: '清除查询条件',
},
startScreen: {
favoriteSearchesTitle: '收藏',
noRecentSearchesText: '没有搜索历史',
recentSearchesTitle: '搜索历史',
removeFavoriteSearchButtonTitle: '从收藏中移除',
removeRecentSearchButtonTitle: '从搜索历史中移除',
saveRecentSearchButtonTitle: '保存至搜索历史',
},
},
},
},
};
================================================
FILE: hiauth-front/docs/.vitepress/theme/components/site-layout.vue
================================================
================================================
FILE: hiauth-front/docs/.vitepress/theme/components/vben-contributors.vue
================================================
Contributors
================================================
FILE: hiauth-front/docs/.vitepress/theme/index.ts
================================================
// https://vitepress.dev/guide/custom-theme
import type { EnhanceAppContext, Theme } from 'vitepress';
import { NolebaseGitChangelogPlugin } from '@nolebase/vitepress-plugin-git-changelog/client';
import DefaultTheme from 'vitepress/theme';
import { DemoPreview } from '../components';
import SiteLayout from './components/site-layout.vue';
import VbenContributors from './components/vben-contributors.vue';
import { initHmPlugin } from './plugins/hm';
import './styles';
import 'virtual:group-icons.css';
import '@nolebase/vitepress-plugin-git-changelog/client/style.css';
export default {
async enhanceApp(ctx: EnhanceAppContext) {
const { app } = ctx;
app.component('VbenContributors', VbenContributors);
app.component('DemoPreview', DemoPreview);
app.use(NolebaseGitChangelogPlugin);
// 百度统计
initHmPlugin();
},
extends: DefaultTheme,
Layout: SiteLayout,
} satisfies Theme;
================================================
FILE: hiauth-front/docs/.vitepress/theme/plugins/hm.ts
================================================
import { inBrowser } from 'vitepress';
const SITE_ID = '2e443a834727c065877c01d89921545e';
declare global {
interface Window {
_hmt: any;
}
}
function registerAnalytics() {
window._hmt = window._hmt || [];
const script = document.createElement('script');
script.innerHTML = `var _hmt = _hmt || [];
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?${SITE_ID}";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})()`;
document.querySelector('head')?.append(script);
}
export function initHmPlugin() {
if (inBrowser && import.meta.env.PROD) {
registerAnalytics();
}
}
================================================
FILE: hiauth-front/docs/.vitepress/theme/styles/base.css
================================================
html.dark {
color-scheme: dark;
}
.dark .VPContent {
/* background-color: #14161a; */
}
.form-valid-error p {
margin: 0;
}
/* 顶部导航栏选中项样式 */
.VPNavBarMenuLink,
.VPNavBarMenuGroup {
border-bottom: 1px solid transparent;
}
.VPNavBarMenuLink.active,
.VPNavBarMenuGroup.active {
border-bottom-color: var(--vp-c-brand-1);
}
================================================
FILE: hiauth-front/docs/.vitepress/theme/styles/index.ts
================================================
import '@vben/styles';
import './variables.css';
import './base.css';
================================================
FILE: hiauth-front/docs/.vitepress/theme/styles/variables.css
================================================
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
*
* Each colors have exact same color scale system with 3 levels of solid
* colors with different brightness, and 1 soft color.
*
* - `XXX-1`: The most solid color used mainly for colored text. It must
* satisfy the contrast ratio against when used on top of `XXX-soft`.
*
* - `XXX-2`: The color used mainly for hover state of the button.
*
* - `XXX-3`: The color for solid background, such as bg color of the button.
* It must satisfy the contrast ratio with pure white (#ffffff) text on
* top of it.
*
* - `XXX-soft`: The color used for subtle background such as custom container
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
* on top of it.
*
* The soft color must be semi transparent alpha channel. This is crucial
* because it allows adding multiple "soft" colors on top of each other
* to create a accent, such as when having inline code block inside
* custom containers.
*
* - `default`: The color used purely for subtle indication without any
* special meanings attched to it such as bg color for menu hover state.
*
* - `brand`: Used for primary brand colors, such as link text, button with
* brand theme, etc.
*
* - `tip`: Used to indicate useful information. The default theme uses the
* brand color for this by default.
*
* - `warning`: Used to indicate warning to the users. Used in custom
* container, badges, etc.
*
* - `danger`: Used to show error, or dangerous message to the users. Used
* in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root {
/* --vp-c-indigo-1: #4f69fd; */
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);
--vp-c-brand-1: var(--vp-c-indigo-1);
--vp-c-brand-2: var(--vp-c-indigo-2);
--vp-c-brand-3: var(--vp-c-indigo-3);
--vp-c-brand-soft: var(--vp-c-indigo-soft);
--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
--vp-c-tip-3: var(--vp-c-brand-3);
--vp-c-tip-soft: var(--vp-c-brand-soft);
--vp-c-warning-1: var(--vp-c-yellow-1);
--vp-c-warning-2: var(--vp-c-yellow-2);
--vp-c-warning-3: var(--vp-c-yellow-3);
--vp-c-warning-soft: var(--vp-c-yellow-soft);
--vp-c-danger-1: var(--vp-c-red-1);
--vp-c-danger-2: var(--vp-c-red-2);
--vp-c-danger-3: var(--vp-c-red-3);
--vp-c-danger-soft: var(--vp-c-red-soft);
/**
* Component: Button
* -------------------------------------------------------------------------- */
--vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand-3);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
--vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-c-brand-1);
/**
* Component: Home
* -------------------------------------------------------------------------- */
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: linear-gradient(
120deg,
var(--vp-c-indigo-1) 30%,
#18cefe
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#18cefe 50%,
#c279ed 50%
);
--vp-home-hero-image-filter: blur(44px);
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
--vp-custom-block-tip-border: transparent;
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
/**
* modal zIndex
*/
--popup-z-index: 1000;
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}
================================================
FILE: hiauth-front/docs/package.json
================================================
{
"name": "@vben/docs",
"version": "5.5.9",
"private": true,
"scripts": {
"build": "vitepress build",
"dev": "vitepress dev",
"docs:preview": "vitepress preview"
},
"imports": {
"#/*": {
"node": "./src/_env/node/*",
"default": "./src/_env/*"
}
},
"dependencies": {
"@vben-core/shadcn-ui": "workspace:*",
"@vben/common-ui": "workspace:*",
"@vben/locales": "workspace:*",
"@vben/plugins": "workspace:*",
"@vben/styles": "workspace:*",
"ant-design-vue": "catalog:",
"lucide-vue-next": "catalog:",
"medium-zoom": "catalog:",
"radix-vue": "catalog:",
"vitepress-plugin-group-icons": "catalog:"
},
"devDependencies": {
"@nolebase/vitepress-plugin-git-changelog": "catalog:",
"@vben/vite-config": "workspace:*",
"@vite-pwa/vitepress": "catalog:",
"vitepress": "catalog:",
"vue": "catalog:"
}
}
================================================
FILE: hiauth-front/docs/src/_env/adapter/component.ts
================================================
/**
* 通用组件共同的使用的基础组件,原先放在 adapter/form 内部,限制了使用范围,这里提取出来,方便其他地方使用
* 可用于 vben-form、vben-modal、vben-drawer 等组件使用,
*/
import type { Component, SetupContext } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import { h } from 'vue';
import { globalShareState } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'ant-design-vue';
const withDefaultPlaceholder = (
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit) => {
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
};
// 这里需要自行根据业务组件库进行适配,需要用到的组件都需要在这里类型说明
export type ComponentType =
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial> = {
// 如果你的组件体积比较大,可以使用异步加载
// Button: () =>
// import('xxx').then((res) => res.Button),
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
// 自定义默认按钮
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
// 自定义主要按钮
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
};
// 将组件注册到全局共享状态中
globalShareState.setComponents(components);
// 定义全局共享状态中的消息提示
globalShareState.defineMessage({
// 复制成功消息提示
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };
================================================
FILE: hiauth-front/docs/src/_env/adapter/form.ts
================================================
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { initComponentAdapter } from './component';
initComponentAdapter();
setupVbenForm({
config: {
baseModelPropName: 'value',
// naive-ui组件的空值为null,不能是undefined,否则重置表单时不生效
emptyStateValue: null,
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
defineRules: {
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
const useVbenForm = useForm;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema;
export type { VbenFormProps };
================================================
FILE: hiauth-front/docs/src/_env/adapter/vxe-table.ts
================================================
import { h } from 'vue';
import { setupVbenVxeTable, useVbenVxeGrid } from '@vben/plugins/vxe-table';
import { Button, Image } from 'ant-design-vue';
import { useVbenForm } from './form';
if (!import.meta.env.SSR) {
setupVbenVxeTable({
configVxeTable: (vxeUI) => {
vxeUI.setConfig({
grid: {
align: 'center',
border: false,
columnConfig: {
resizable: true,
},
formConfig: {
// 全局禁用vxe-table的表单配置,使用formOptions
enabled: false,
},
minHeight: 180,
proxyConfig: {
autoLoad: true,
response: {
result: 'items',
total: 'total',
list: 'items',
},
showActiveMsg: true,
showResponseMsg: false,
},
round: true,
showOverflow: true,
size: 'small',
},
});
// 表格配置项可以用 cellRender: { name: 'CellImage' },
vxeUI.renderer.add('CellImage', {
renderTableDefault(_renderOpts, params) {
const { column, row } = params;
return h(Image, { src: row[column.field] });
},
});
// 表格配置项可以用 cellRender: { name: 'CellLink' },
vxeUI.renderer.add('CellLink', {
renderTableDefault(renderOpts) {
const { props } = renderOpts;
return h(
Button,
{ size: 'small', type: 'link' },
{ default: () => props?.text },
);
},
});
// 这里可以自行扩展 vxe-table 的全局配置,比如自定义格式化
// vxeUI.formats.add
},
useVbenForm,
});
}
export { useVbenVxeGrid };
export type * from '@vben/plugins/vxe-table';
================================================
FILE: hiauth-front/docs/src/_env/node/adapter/form.ts
================================================
export const useVbenForm = () => {};
export const z = {};
export type VbenFormSchema = any;
export type VbenFormProps = any;
================================================
FILE: hiauth-front/docs/src/_env/node/adapter/vxe-table.ts
================================================
export type * from '@vben/plugins/vxe-table';
export const useVbenVxeGrid = () => {};
================================================
FILE: hiauth-front/docs/src/commercial/community.md
================================================
# 社区交流
社区交流群主要是为了方便大家交流,提问,解答问题,分享经验等。偏自助方式,如果你有问题,可以通过以下方式加入社区交流群:
- [QQ频道](https://pd.qq.com/s/16p8lvvob):推荐!!!主要提供问题解答,分享经验等。
- QQ群:[大群](https://qm.qq.com/q/MEmHoCLbG0),[1群](https://qm.qq.com/q/YacMHPYAMu)、[2群](https://qm.qq.com/q/ajVKZvFICk)、[3群](https://qm.qq.com/q/36zdwThP2E),[4群](https://qm.qq.com/q/sCzSlm3504),[5群](https://qm.qq.com/q/ya9XrtbS6s),主要的使用者交流群。
- [Discord](https://discord.com/invite/VU62jTecad): 主要提供问题解答,分享经验等。
::: tip
免费QQ群人数上限200,将会不定期清理。推荐加入QQ频道进行交流
:::
## 微信群
作者主要通过微信群提供帮助,如果你有问题,可以通过以下方式加入微信群。
通过微信联系作者,注明加群来意:
::: tip
因为微信群人数有限制,加微信群要求:
- 通过[赞助](../sponsor/personal.md)任意金额。
- 发送赞助`截图`,备注`加入微信群`即可。
:::
================================================
FILE: hiauth-front/docs/src/commercial/customized.md
================================================
# 定制开发
我们提供基于 Vben Admin 的技术支持服务及定制开发,基本需求我们都可以满足。
详细需求可添加作者了解,并注明来意:
- 通过邮箱联系开发者: [ann.vben@gmail.com](mailto:ann.vben@gmail.com)
- 通过微信联系开发者:
我们会在第一时间回复您,定制费用根据需求而定。
================================================
FILE: hiauth-front/docs/src/commercial/technical-support.md
================================================
# 技术支持
## 问题反馈
在使用项目的过程中,如果遇到问题,你可以先详细阅读本文档,未找到解决方案时,可以通过以下方式获取技术支持:
- 通过 [GitHub Issues](https://github.com/vbenjs/vue-vben-admin/issues)
- 通过 [GitHub Discussions](https://github.com/vbenjs/vue-vben-admin/discussions)
================================================
FILE: hiauth-front/docs/src/components/common-ui/vben-alert.md
================================================
---
outline: deep
---
# Vben Alert 轻量提示框
框架提供的一些用于轻量提示的弹窗,仅使用js代码即可快速动态创建提示而不需要在template写任何代码。
::: info 应用场景
Alert提供的功能与Modal类似,但只适用于简单应用场景。例如临时性、动态地弹出模态确认框、输入框等。如果对弹窗有更复杂的需求,请使用VbenModal
:::
::: tip 注意
Alert提供的快捷方法alert、confirm、prompt动态创建的弹窗在已打开的情况下,不支持HMR(热更新),代码变更后需要关闭这些弹窗后重新打开。
:::
::: tip README
下方示例代码中的,存在一些主题色未适配、样式缺失的问题,这些问题只在文档内会出现,实际使用并不会有这些问题,可忽略,不必纠结。
:::
## 基础用法
使用 `alert` 创建只有一个确认按钮的提示框。
使用 `confirm` 创建有确认和取消按钮的提示框。
使用 `prompt` 创建有确认和取消按钮、接受用户输入的提示框。
## useAlertContext
当弹窗的content、footer、icon使用自定义组件时,在这些组件中可以使用 `useAlertContext` 获取当前弹窗的上下文对象,用来主动控制弹窗。
::: tip 注意
`useAlertContext`只能用在setup或者函数式组件中。
:::
### Methods
| 方法 | 描述 | 类型 | 版本要求 |
| --------- | ------------------ | -------- | -------- |
| doConfirm | 调用弹窗的确认操作 | ()=>void | >5.5.4 |
| doCancel | 调用弹窗的取消操作 | ()=>void | >5.5.4 |
## 类型说明
```ts
/** 预置的图标类型 */
export type IconType = 'error' | 'info' | 'question' | 'success' | 'warning';
export type BeforeCloseScope = {
/** 是否为点击确认按钮触发的关闭 */
isConfirm: boolean;
};
/**
* alert 属性
*/
export type AlertProps = {
/** 关闭前的回调,如果返回false,则终止关闭 */
beforeClose?: (
scope: BeforeCloseScope,
) => boolean | Promise | undefined;
/** 边框 */
bordered?: boolean;
/** 按钮对齐方式 */
buttonAlign?: 'center' | 'end' | 'start';
/** 取消按钮的标题 */
cancelText?: string;
/** 是否居中显示 */
centered?: boolean;
/** 确认按钮的标题 */
confirmText?: string;
/** 弹窗容器的额外样式 */
containerClass?: string;
/** 弹窗提示内容 */
content: Component | string;
/** 弹窗内容的额外样式 */
contentClass?: string;
/** 执行beforeClose回调期间,在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */
icon?: Component | IconType;
/**
* 弹窗遮罩模糊效果
*/
overlayBlur?: number;
/** 是否显示取消按钮 */
showCancel?: boolean;
/** 弹窗标题 */
title?: string;
};
/** prompt 属性 */
export type PromptProps = {
/** 关闭前的回调,如果返回false,则终止关闭 */
beforeClose?: (scope: {
isConfirm: boolean;
value: T | undefined;
}) => boolean | Promise