mirror of
https://gitee.com/dromara/MaxKey.git
synced 2025-12-08 01:48:33 +08:00
Social SignOn
This commit is contained in:
parent
36ea37aff2
commit
63f33facac
@ -64,13 +64,13 @@ public class AbstractSocialSignOnEndpoint {
|
|||||||
@Autowired
|
@Autowired
|
||||||
ApplicationConfig applicationConfig;
|
ApplicationConfig applicationConfig;
|
||||||
|
|
||||||
protected AuthRequest buildAuthRequest(String instId,String provider){
|
protected AuthRequest buildAuthRequest(String instId,String provider,String baseUrl){
|
||||||
try {
|
try {
|
||||||
SocialsProvider socialSignOnProvider = socialSignOnProviderService.get(instId,provider);
|
SocialsProvider socialSignOnProvider = socialSignOnProviderService.get(instId,provider);
|
||||||
_logger.debug("socialSignOn Provider : "+socialSignOnProvider);
|
_logger.debug("socialSignOn Provider : "+socialSignOnProvider);
|
||||||
|
|
||||||
if(socialSignOnProvider != null){
|
if(socialSignOnProvider != null){
|
||||||
authRequest = socialSignOnProviderService.getAuthRequest(instId,provider,WebContext.getBaseUri());
|
authRequest = socialSignOnProviderService.getAuthRequest(instId,provider,baseUrl);
|
||||||
return authRequest;
|
return authRequest;
|
||||||
}
|
}
|
||||||
}catch(Exception e) {
|
}catch(Exception e) {
|
||||||
@ -79,7 +79,7 @@ public class AbstractSocialSignOnEndpoint {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SocialsAssociate authCallback(String instId,String provider) throws Exception {
|
protected SocialsAssociate authCallback(String instId,String provider,String baseUrl) throws Exception {
|
||||||
SocialsAssociate socialsAssociate = null;
|
SocialsAssociate socialsAssociate = null;
|
||||||
AuthCallback authCallback=new AuthCallback();
|
AuthCallback authCallback=new AuthCallback();
|
||||||
authCallback.setCode(WebContext.getRequest().getParameter("code"));
|
authCallback.setCode(WebContext.getRequest().getParameter("code"));
|
||||||
@ -97,7 +97,7 @@ public class AbstractSocialSignOnEndpoint {
|
|||||||
authCallback.getState());
|
authCallback.getState());
|
||||||
|
|
||||||
if(authRequest == null) {//if authRequest is null renew one
|
if(authRequest == null) {//if authRequest is null renew one
|
||||||
authRequest=socialSignOnProviderService.getAuthRequest(instId,provider,WebContext.getBaseUri());
|
authRequest=socialSignOnProviderService.getAuthRequest(instId,provider,baseUrl);
|
||||||
_logger.debug("session authRequest is null , renew one");
|
_logger.debug("session authRequest is null , renew one");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright [2020] [MaxKey of copyright http://www.maxkey.top]
|
* Copyright [2022] [MaxKey of copyright http://www.maxkey.top]
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -25,7 +25,6 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import org.maxkey.authn.LoginCredential;
|
import org.maxkey.authn.LoginCredential;
|
||||||
import org.maxkey.authn.annotation.CurrentUser;
|
import org.maxkey.authn.annotation.CurrentUser;
|
||||||
import org.maxkey.authn.jwt.AuthJwt;
|
import org.maxkey.authn.jwt.AuthJwt;
|
||||||
import org.maxkey.authn.web.AuthorizationUtils;
|
|
||||||
import org.maxkey.constants.ConstsLoginType;
|
import org.maxkey.constants.ConstsLoginType;
|
||||||
import org.maxkey.entity.Message;
|
import org.maxkey.entity.Message;
|
||||||
import org.maxkey.entity.SocialsAssociate;
|
import org.maxkey.entity.SocialsAssociate;
|
||||||
@ -38,6 +37,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
@ -54,23 +54,34 @@ public class SocialSignOnEndpoint extends AbstractSocialSignOnEndpoint{
|
|||||||
|
|
||||||
@RequestMapping(value={"/authorize/{provider}"}, method = RequestMethod.GET)
|
@RequestMapping(value={"/authorize/{provider}"}, method = RequestMethod.GET)
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public ResponseEntity<?> authorize(HttpServletRequest request,
|
public ResponseEntity<?> authorize( HttpServletRequest request,
|
||||||
@PathVariable String provider
|
@PathVariable String provider,
|
||||||
|
@RequestHeader("Origin") String originURL
|
||||||
) {
|
) {
|
||||||
_logger.trace("SocialSignOn provider : " + provider);
|
_logger.trace("SocialSignOn provider : " + provider);
|
||||||
String instId = WebContext.getInst().getId();
|
String instId = WebContext.getInst().getId();
|
||||||
String authorizationUrl = buildAuthRequest(instId,provider).authorize(authTokenService.genRandomJwt());
|
String authorizationUrl =
|
||||||
|
buildAuthRequest(
|
||||||
|
instId,
|
||||||
|
provider,
|
||||||
|
originURL + applicationConfig.getFrontendUri()
|
||||||
|
).authorize(authTokenService.genRandomJwt());
|
||||||
|
|
||||||
_logger.trace("authorize SocialSignOn : " + authorizationUrl);
|
_logger.trace("authorize SocialSignOn : " + authorizationUrl);
|
||||||
return new Message<Object>((Object)authorizationUrl).buildResponse();
|
return new Message<Object>((Object)authorizationUrl).buildResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value={"/scanqrcode/{provider}"}, method = RequestMethod.GET)
|
@RequestMapping(value={"/scanqrcode/{provider}"}, method = RequestMethod.GET)
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public ResponseEntity<?> scanQRCode(
|
public ResponseEntity<?> scanQRCode(HttpServletRequest request,
|
||||||
HttpServletRequest request,
|
@PathVariable("provider") String provider,
|
||||||
@PathVariable("provider") String provider) {
|
@RequestHeader("Origin") String originURL) {
|
||||||
String instId = WebContext.getInst().getId();
|
String instId = WebContext.getInst().getId();
|
||||||
AuthRequest authRequest = buildAuthRequest(instId,provider);
|
AuthRequest authRequest =
|
||||||
|
buildAuthRequest(
|
||||||
|
instId,
|
||||||
|
provider,
|
||||||
|
originURL + applicationConfig.getFrontendUri());
|
||||||
|
|
||||||
if(authRequest == null ) {
|
if(authRequest == null ) {
|
||||||
_logger.error("build authRequest fail .");
|
_logger.error("build authRequest fail .");
|
||||||
@ -82,17 +93,21 @@ public class SocialSignOnEndpoint extends AbstractSocialSignOnEndpoint{
|
|||||||
SocialsProvider scanQrProvider = new SocialsProvider(socialSignOnProvider);
|
SocialsProvider scanQrProvider = new SocialsProvider(socialSignOnProvider);
|
||||||
scanQrProvider.setState(state);
|
scanQrProvider.setState(state);
|
||||||
scanQrProvider.setRedirectUri(
|
scanQrProvider.setRedirectUri(
|
||||||
socialSignOnProviderService.getRedirectUri(WebContext.getBaseUri(), provider));
|
socialSignOnProviderService.getRedirectUri(
|
||||||
|
originURL + applicationConfig.getFrontendUri(), provider));
|
||||||
|
|
||||||
return new Message<SocialsProvider>(scanQrProvider).buildResponse();
|
return new Message<SocialsProvider>(scanQrProvider).buildResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@RequestMapping(value={"/bind/{provider}"}, method = RequestMethod.GET)
|
@RequestMapping(value={"/bind/{provider}"}, method = RequestMethod.GET)
|
||||||
public ResponseEntity<?> bind(@PathVariable String provider,@CurrentUser UserInfo userInfo) {
|
public ResponseEntity<?> bind(@PathVariable String provider,
|
||||||
|
@RequestHeader("Origin") String originURL,
|
||||||
|
@CurrentUser UserInfo userInfo) {
|
||||||
//auth call back may exception
|
//auth call back may exception
|
||||||
try {
|
try {
|
||||||
SocialsAssociate socialsAssociate = this.authCallback(userInfo.getInstId(),provider);
|
SocialsAssociate socialsAssociate =
|
||||||
|
this.authCallback(userInfo.getInstId(),provider,originURL + applicationConfig.getFrontendUri());
|
||||||
socialsAssociate.setSocialUserInfo(accountJsonString);
|
socialsAssociate.setSocialUserInfo(accountJsonString);
|
||||||
socialsAssociate.setUserId(userInfo.getId());
|
socialsAssociate.setUserId(userInfo.getId());
|
||||||
socialsAssociate.setUsername(userInfo.getUsername());
|
socialsAssociate.setUsername(userInfo.getUsername());
|
||||||
@ -111,11 +126,13 @@ public class SocialSignOnEndpoint extends AbstractSocialSignOnEndpoint{
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value={"/callback/{provider}"}, method = RequestMethod.GET)
|
@RequestMapping(value={"/callback/{provider}"}, method = RequestMethod.GET)
|
||||||
public ResponseEntity<?> callback(@PathVariable String provider) {
|
public ResponseEntity<?> callback(@PathVariable String provider,
|
||||||
|
@RequestHeader("Origin") String originURL) {
|
||||||
//auth call back may exception
|
//auth call back may exception
|
||||||
try {
|
try {
|
||||||
String instId = WebContext.getInst().getId();
|
String instId = WebContext.getInst().getId();
|
||||||
SocialsAssociate socialsAssociate = this.authCallback(instId,provider);
|
SocialsAssociate socialsAssociate =
|
||||||
|
this.authCallback(instId,provider,originURL + applicationConfig.getFrontendUri());
|
||||||
|
|
||||||
socialsAssociate=this.socialsAssociateService.get(socialsAssociate);
|
socialsAssociate=this.socialsAssociateService.get(socialsAssociate);
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "maxkey",
|
"name": "maxkey",
|
||||||
"version": "3.5.0",
|
"version": "3.5.0",
|
||||||
"description": "Leading-Edge IAM Identity and Access Management",
|
"description": "Leading-Edge IAM Identity and Access Management",
|
||||||
"author": "MaxKey <maxkeysupport@163.com>",
|
"author": "MaxKey <support@maxsso.net>",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitee.com/dromara/MaxKey"
|
"url": "https://gitee.com/dromara/MaxKey"
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HttpErrorResponse,
|
HttpErrorResponse,
|
||||||
HttpEvent,
|
HttpEvent,
|
||||||
|
|||||||
@ -1,24 +1,21 @@
|
|||||||
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
|
<form nz-form [formGroup]="form" (ngSubmit)="submit()" role="form">
|
||||||
<nz-radio-group
|
<nz-radio-group [(ngModel)]="loginType" [ngModelOptions]="{ standalone: true }" nzSize="large" *ngIf="switchTab"
|
||||||
nzButtonStyle="solid"
|
style="margin-bottom: 8px; width: 100%">
|
||||||
[(ngModel)]="loginType"
|
<label nz-radio-button nzValue="normal" style="width: 50%; text-align: center">
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
style="margin-bottom: 8px; width: 100%"
|
|
||||||
nzSize="large"
|
|
||||||
>
|
|
||||||
<label nz-radio-button nzValue="normal" style="width: 33.3%; text-align: center">
|
|
||||||
<i nz-icon nzType="user" nzTheme="outline"></i>
|
<i nz-icon nzType="user" nzTheme="outline"></i>
|
||||||
{{ 'mxk.login.tab-credentials' | i18n }}</label
|
{{ 'mxk.login.tab-credentials' | i18n }}
|
||||||
>
|
</label>
|
||||||
<label nz-radio-button nzValue="mobile" style="width: 33.3%; text-align: center"
|
<label nz-radio-button nzValue="mobile" style="width: 50%; text-align: center" class="d-none">
|
||||||
><i nz-icon nzType="mobile" nzTheme="outline"></i>{{ 'mxk.login.tab-mobile' | i18n }}</label
|
<i nz-icon nzType="mobile" nzTheme="outline"></i>
|
||||||
>
|
{{ 'mxk.login.tab-mobile' | i18n }}
|
||||||
<label nz-radio-button nzValue="qrscan" style="width: 33.3%; text-align: center" (click)="getQrCode()">
|
</label>
|
||||||
<i nz-icon nzType="qrcode" nzTheme="outline"></i>{{ 'mxk.login.tab-qrscan' | i18n }}</label
|
<label nz-radio-button nzValue="qrscan" style="width: 50%; text-align: center" (click)="getQrCode()">
|
||||||
>
|
<i nz-icon nzType="qrcode" nzTheme="outline"></i>{{ 'mxk.login.tab-qrscan' | i18n }}
|
||||||
|
</label>
|
||||||
</nz-radio-group>
|
</nz-radio-group>
|
||||||
<div nz-row *ngIf="loginType == 'normal'">
|
<div nz-row *ngIf="loginType == 'normal'">
|
||||||
<nz-alert style="width: 100%" *ngIf="error" [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true" class="mb-lg"></nz-alert>
|
<nz-alert style="width: 100%" *ngIf="error" [nzType]="'error'" [nzMessage]="error" [nzShowIcon]="true"
|
||||||
|
class="mb-lg"></nz-alert>
|
||||||
<nz-form-item style="width: 100%">
|
<nz-form-item style="width: 100%">
|
||||||
<nz-form-control nzErrorTip="">
|
<nz-form-control nzErrorTip="">
|
||||||
<nz-input-group nzSize="large" nzPrefixIcon="user">
|
<nz-input-group nzSize="large" nzPrefixIcon="user">
|
||||||
@ -29,15 +26,12 @@
|
|||||||
<nz-form-item style="width: 100%">
|
<nz-form-item style="width: 100%">
|
||||||
<nz-form-control nzErrorTip="">
|
<nz-form-control nzErrorTip="">
|
||||||
<nz-input-group [nzSuffix]="suffixTemplate" nzSize="large" nzPrefixIcon="key">
|
<nz-input-group [nzSuffix]="suffixTemplate" nzSize="large" nzPrefixIcon="key">
|
||||||
<input
|
<input [type]="passwordVisible ? 'text' : 'password'" nz-input
|
||||||
[type]="passwordVisible ? 'text' : 'password'"
|
placeholder="{{ 'mxk.login.text.password' | i18n }}" formControlName="password" />
|
||||||
nz-input
|
|
||||||
placeholder="{{ 'mxk.login.text.password' | i18n }}"
|
|
||||||
formControlName="password"
|
|
||||||
/>
|
|
||||||
</nz-input-group>
|
</nz-input-group>
|
||||||
<ng-template #suffixTemplate>
|
<ng-template #suffixTemplate>
|
||||||
<i nz-icon [nzType]="passwordVisible ? 'eye-invisible' : 'eye'" (click)="passwordVisible = !passwordVisible"></i>
|
<i nz-icon [nzType]="passwordVisible ? 'eye-invisible' : 'eye'"
|
||||||
|
(click)="passwordVisible = !passwordVisible"></i>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</nz-form-control>
|
</nz-form-control>
|
||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
@ -74,7 +68,8 @@
|
|||||||
<input nz-input formControlName="otpCaptcha" placeholder="{{ 'mxk.login.text.captcha' | i18n }}" />
|
<input nz-input formControlName="otpCaptcha" placeholder="{{ 'mxk.login.text.captcha' | i18n }}" />
|
||||||
</nz-input-group>
|
</nz-input-group>
|
||||||
<ng-template #suffixSendOtpCodeButton>
|
<ng-template #suffixSendOtpCodeButton>
|
||||||
<button type="button" nz-button nzSize="large" (click)="sendOtpCode()" [disabled]="count > 0" nzBlock [nzLoading]="loading">
|
<button type="button" nz-button nzSize="large" (click)="sendOtpCode()" [disabled]="count > 0" nzBlock
|
||||||
|
[nzLoading]="loading">
|
||||||
{{ count ? count + 's' : ('app.register.get-verification-code' | i18n) }}
|
{{ count ? count + 's' : ('app.register.get-verification-code' | i18n) }}
|
||||||
</button>
|
</button>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
@ -89,8 +84,8 @@
|
|||||||
<label nz-checkbox formControlName="remember">{{ 'mxk.login.remember-me' | i18n }}</label>
|
<label nz-checkbox formControlName="remember">{{ 'mxk.login.remember-me' | i18n }}</label>
|
||||||
</nz-col>
|
</nz-col>
|
||||||
<nz-col [nzSpan]="12" class="text-right">
|
<nz-col [nzSpan]="12" class="text-right">
|
||||||
<a class="forgot" routerLink="/passport/forgot">{{ 'mxk.login.forgot-password' | i18n }}</a></nz-col
|
<a class="forgot" routerLink="/passport/forgot">{{ 'mxk.login.forgot-password' | i18n }}</a>
|
||||||
>
|
</nz-col>
|
||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
<nz-form-item *ngIf="loginType == 'normal' || loginType == 'mobile'">
|
<nz-form-item *ngIf="loginType == 'normal' || loginType == 'mobile'">
|
||||||
<button nz-button type="submit" nzType="primary" nzSize="large" [nzLoading]="loading" nzBlock>
|
<button nz-button type="submit" nzType="primary" nzSize="large" [nzLoading]="loading" nzBlock>
|
||||||
@ -98,13 +93,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</nz-form-item>
|
</nz-form-item>
|
||||||
</form>
|
</form>
|
||||||
<div class="other">
|
<div class="other" *ngIf="loginType == 'normal'">
|
||||||
{{ 'app.login.sign-in-with' | i18n }}
|
{{ 'app.login.sign-in-with' | i18n }}
|
||||||
|
|
||||||
<ng-container *ngFor="let provd of socials.providers">
|
<ng-container *ngFor="let provd of socials.providers">
|
||||||
<i nz-tooltip nzTooltipTitle="{{ provd.providerName }}" (click)="socialauth(provd.provider)" nz-icon class="icon">
|
<i nz-tooltip nzTooltipTitle="{{ provd.providerName }}" (click)="socialauth(provd.provider)" nz-icon class="icon">
|
||||||
<img src="{{ provd.icon }}" style="width: 32px" />
|
<img src="{{ provd.icon }}" style="width: 32px" />
|
||||||
</i>
|
</i>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<a class="register" routerLink="/passport/register">{{ 'mxk.login.signup' | i18n }}</a>
|
<a class="register d-none" routerLink="/passport/register">{{ 'mxk.login.signup' | i18n }}</a>
|
||||||
</div>
|
</div>
|
||||||
@ -22,6 +22,13 @@
|
|||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-tab{
|
||||||
|
color: #000;
|
||||||
|
background: unset;
|
||||||
|
border-color: unset;
|
||||||
|
border-right-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
color: rgb(0 0 0 / 20%);
|
color: rgb(0 0 0 / 20%);
|
||||||
|
|||||||
@ -50,13 +50,13 @@ export class UserLoginComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
error = '';
|
error = '';
|
||||||
|
switchTab = true;
|
||||||
loginType = 'normal';
|
loginType = 'normal';
|
||||||
loading = false;
|
loading = false;
|
||||||
passwordVisible = false;
|
passwordVisible = false;
|
||||||
imageCaptcha = '';
|
imageCaptcha = '';
|
||||||
captchaType = '';
|
captchaType = '';
|
||||||
state = '';
|
state = '';
|
||||||
|
|
||||||
count = 0;
|
count = 0;
|
||||||
interval$: any;
|
interval$: any;
|
||||||
|
|
||||||
@ -279,6 +279,7 @@ export class UserLoginComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// #region social
|
// #region social
|
||||||
socialauth(provider: string): void {
|
socialauth(provider: string): void {
|
||||||
|
this.authnService.clearUser();
|
||||||
this.socialsProviderService.authorize(provider).subscribe(res => {
|
this.socialsProviderService.authorize(provider).subscribe(res => {
|
||||||
//console.log(res.data);
|
//console.log(res.data);
|
||||||
window.location.href = res.data;
|
window.location.href = res.data;
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
import { NzSafeAny } from 'ng-zorro-antd/core/types';
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
export function set2String(set: Set<String>): string {
|
export function set2String(set: Set<String>): string {
|
||||||
let setValues = '';
|
let setValues = '';
|
||||||
set.forEach(value => {
|
set.forEach(value => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "maxkey",
|
"name": "maxkey",
|
||||||
"version": "3.5.0",
|
"version": "3.5.0",
|
||||||
"description": "Leading-Edge IAM Identity and Access Management",
|
"description": "Leading-Edge IAM Identity and Access Management",
|
||||||
"author": "MaxKey <maxkeysupport@163.com>",
|
"author": "MaxKey <support@maxsso.net>",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitee.com/dromara/MaxKey"
|
"url": "https://gitee.com/dromara/MaxKey"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user