mirror of https://github.com/halo-dev/halo
refactor: auth provider sorting logic for better maintainability and clarity (#6846)
* refactor: auth provider sorting logic for better maintainability and clarity * Refine UI * chore: remove other auth type * Remove other auth providers --------- Co-authored-by: Ryan Wang <i@ryanc.cc>pull/6860/head v2.20.0
parent
4059f159b2
commit
82498dcedf
|
@ -16034,6 +16034,7 @@
|
||||||
},
|
},
|
||||||
"AuthProviderSpec": {
|
"AuthProviderSpec": {
|
||||||
"required": [
|
"required": [
|
||||||
|
"authType",
|
||||||
"authenticationUrl",
|
"authenticationUrl",
|
||||||
"displayName"
|
"displayName"
|
||||||
],
|
],
|
||||||
|
@ -16072,10 +16073,6 @@
|
||||||
"method": {
|
"method": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"priority": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"rememberMeSupport": {
|
"rememberMeSupport": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -18297,6 +18294,13 @@
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"authType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"FORM",
|
||||||
|
"OAUTH2"
|
||||||
|
]
|
||||||
|
},
|
||||||
"authenticationUrl": {
|
"authenticationUrl": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -18324,6 +18328,10 @@
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"priority": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
"privileged": {
|
"privileged": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3568,6 +3568,7 @@
|
||||||
},
|
},
|
||||||
"AuthProviderSpec": {
|
"AuthProviderSpec": {
|
||||||
"required": [
|
"required": [
|
||||||
|
"authType",
|
||||||
"authenticationUrl",
|
"authenticationUrl",
|
||||||
"displayName"
|
"displayName"
|
||||||
],
|
],
|
||||||
|
@ -3606,10 +3607,6 @@
|
||||||
"method": {
|
"method": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"priority": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"rememberMeSupport": {
|
"rememberMeSupport": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -4435,6 +4432,13 @@
|
||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"authType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"FORM",
|
||||||
|
"OAUTH2"
|
||||||
|
]
|
||||||
|
},
|
||||||
"authenticationUrl": {
|
"authenticationUrl": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -4462,6 +4466,10 @@
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"priority": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
},
|
||||||
"privileged": {
|
"privileged": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
@ -8666,6 +8666,7 @@
|
||||||
},
|
},
|
||||||
"AuthProviderSpec": {
|
"AuthProviderSpec": {
|
||||||
"required": [
|
"required": [
|
||||||
|
"authType",
|
||||||
"authenticationUrl",
|
"authenticationUrl",
|
||||||
"displayName"
|
"displayName"
|
||||||
],
|
],
|
||||||
|
@ -8704,10 +8705,6 @@
|
||||||
"method": {
|
"method": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"priority": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32"
|
|
||||||
},
|
|
||||||
"rememberMeSupport": {
|
"rememberMeSupport": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,9 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import org.springframework.lang.NonNull;
|
||||||
import run.halo.app.extension.AbstractExtension;
|
import run.halo.app.extension.AbstractExtension;
|
||||||
import run.halo.app.extension.GVK;
|
import run.halo.app.extension.GVK;
|
||||||
|
|
||||||
|
@ -55,19 +57,23 @@ public class AuthProvider extends AbstractExtension {
|
||||||
/**
|
/**
|
||||||
* Auth type: form or oauth2.
|
* Auth type: form or oauth2.
|
||||||
*/
|
*/
|
||||||
private AuthType authType;
|
@Getter(onMethod_ = @NonNull)
|
||||||
|
@Schema(requiredMode = REQUIRED)
|
||||||
|
private AuthType authType = AuthType.OAUTH2;
|
||||||
|
|
||||||
private String bindingUrl;
|
private String bindingUrl;
|
||||||
|
|
||||||
private String unbindUrl;
|
private String unbindUrl;
|
||||||
|
|
||||||
private int priority;
|
|
||||||
|
|
||||||
@Schema(requiredMode = NOT_REQUIRED)
|
@Schema(requiredMode = NOT_REQUIRED)
|
||||||
private SettingRef settingRef;
|
private SettingRef settingRef;
|
||||||
|
|
||||||
@Schema(requiredMode = NOT_REQUIRED)
|
@Schema(requiredMode = NOT_REQUIRED)
|
||||||
private ConfigMapRef configMapRef;
|
private ConfigMapRef configMapRef;
|
||||||
|
|
||||||
|
public void setAuthType(AuthType authType) {
|
||||||
|
this.authType = (authType == null ? AuthType.OAUTH2 : authType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
@ -89,7 +95,6 @@ public class AuthProvider extends AbstractExtension {
|
||||||
|
|
||||||
public enum AuthType {
|
public enum AuthType {
|
||||||
FORM,
|
FORM,
|
||||||
OAUTH2,
|
OAUTH2
|
||||||
;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ public class Unstructured implements Extension {
|
||||||
return new UnstructuredMetadata();
|
return new UnstructuredMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
@EqualsAndHashCode(exclude = "version")
|
@EqualsAndHashCode(exclude = "tatersion")
|
||||||
class UnstructuredMetadata implements MetadataOperator {
|
class UnstructuredMetadata implements MetadataOperator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,8 +2,9 @@ package run.halo.app.infra;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.List;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
import org.springframework.boot.convert.ApplicationConversionService;
|
import org.springframework.boot.convert.ApplicationConversionService;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
@ -113,7 +114,15 @@ public class SystemSetting {
|
||||||
@Data
|
@Data
|
||||||
public static class AuthProvider {
|
public static class AuthProvider {
|
||||||
public static final String GROUP = "authProvider";
|
public static final String GROUP = "authProvider";
|
||||||
private Set<String> enabled;
|
private List<AuthProviderState> states;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public static class AuthProviderState {
|
||||||
|
private String name;
|
||||||
|
private boolean enabled;
|
||||||
|
private int priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,6 +19,9 @@ public interface AuthProviderService {
|
||||||
|
|
||||||
Mono<List<ListedAuthProvider>> listAll();
|
Mono<List<ListedAuthProvider>> listAll();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of enabled AuthProviders sorted by priority.
|
||||||
|
*/
|
||||||
Flux<AuthProvider> getEnabledProviders();
|
Flux<AuthProvider> getEnabledProviders();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,31 @@
|
||||||
package run.halo.app.security;
|
package run.halo.app.security;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.function.Function;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.scheduler.Schedulers;
|
||||||
|
import reactor.util.retry.Retry;
|
||||||
import run.halo.app.core.extension.AuthProvider;
|
import run.halo.app.core.extension.AuthProvider;
|
||||||
import run.halo.app.core.extension.UserConnection;
|
import run.halo.app.core.extension.UserConnection;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.ExtensionUtil;
|
import run.halo.app.extension.ExtensionUtil;
|
||||||
import run.halo.app.extension.ListOptions;
|
import run.halo.app.extension.ListOptions;
|
||||||
import run.halo.app.extension.Metadata;
|
|
||||||
import run.halo.app.extension.MetadataUtil;
|
import run.halo.app.extension.MetadataUtil;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.index.query.QueryFactory;
|
import run.halo.app.extension.index.query.QueryFactory;
|
||||||
|
@ -42,17 +46,17 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
||||||
@Override
|
@Override
|
||||||
public Mono<AuthProvider> enable(String name) {
|
public Mono<AuthProvider> enable(String name) {
|
||||||
return client.get(AuthProvider.class, name)
|
return client.get(AuthProvider.class, name)
|
||||||
.flatMap(authProvider -> updateAuthProviderEnabled(enabled -> enabled.add(name))
|
.flatMap(authProvider -> updateAuthProviderEnabled(name, true)
|
||||||
.thenReturn(authProvider)
|
.thenReturn(authProvider)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<AuthProvider> disable(String name) {
|
public Mono<AuthProvider> disable(String name) {
|
||||||
// privileged auth provider cannot be disabled
|
|
||||||
return client.get(AuthProvider.class, name)
|
return client.get(AuthProvider.class, name)
|
||||||
|
// privileged auth provider cannot be disabled
|
||||||
.filter(authProvider -> !privileged(authProvider))
|
.filter(authProvider -> !privileged(authProvider))
|
||||||
.flatMap(authProvider -> updateAuthProviderEnabled(enabled -> enabled.remove(name))
|
.flatMap(authProvider -> updateAuthProviderEnabled(name, false)
|
||||||
.thenReturn(authProvider)
|
.thenReturn(authProvider)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -62,50 +66,91 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
||||||
var listOptions = ListOptions.builder()
|
var listOptions = ListOptions.builder()
|
||||||
.andQuery(ExtensionUtil.notDeleting())
|
.andQuery(ExtensionUtil.notDeleting())
|
||||||
.build();
|
.build();
|
||||||
return client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())
|
var allProvidersMono =
|
||||||
.map(this::convertTo)
|
client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())
|
||||||
.collectList()
|
.map(this::convertTo)
|
||||||
.flatMap(providers -> listMyConnections()
|
|
||||||
.map(connection -> connection.getSpec().getRegistrationId())
|
|
||||||
.collectList()
|
.collectList()
|
||||||
.map(connectedNames -> providers.stream()
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
.peek(provider -> {
|
|
||||||
boolean isBound = connectedNames.contains(provider.getName());
|
var boundProvidersMono = listMyConnections()
|
||||||
provider.setIsBound(isBound);
|
.map(connection -> connection.getSpec().getRegistrationId())
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
|
||||||
|
return Mono.zip(allProvidersMono, boundProvidersMono, fetchProviderStates())
|
||||||
|
.map(tuple3 -> {
|
||||||
|
var allProviders = tuple3.getT1();
|
||||||
|
var boundProviderNames = tuple3.getT2();
|
||||||
|
var stateMap = tuple3.getT3().stream()
|
||||||
|
.collect(Collectors.toMap(SystemSetting.AuthProviderState::getName,
|
||||||
|
Function.identity()));
|
||||||
|
return allProviders.stream()
|
||||||
|
.peek(authProvider -> {
|
||||||
|
authProvider.setIsBound(
|
||||||
|
boundProviderNames.contains(authProvider.getName()));
|
||||||
|
authProvider.setEnabled(false);
|
||||||
|
// set enabled state and priority
|
||||||
|
var state = stateMap.get(authProvider.getName());
|
||||||
|
if (state != null) {
|
||||||
|
authProvider.setEnabled(state.isEnabled());
|
||||||
|
authProvider.setPriority(state.getPriority());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList())
|
.sorted(Comparator.comparingInt(ListedAuthProvider::getPriority)
|
||||||
)
|
.thenComparing(ListedAuthProvider::getName))
|
||||||
.defaultIfEmpty(providers)
|
.toList();
|
||||||
)
|
});
|
||||||
.flatMap(providers -> fetchEnabledAuthProviders()
|
|
||||||
.map(names -> providers.stream()
|
|
||||||
.peek(provider -> {
|
|
||||||
boolean enabled = names.contains(provider.getName());
|
|
||||||
provider.setEnabled(enabled);
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList())
|
|
||||||
)
|
|
||||||
.defaultIfEmpty(providers)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<AuthProvider> getEnabledProviders() {
|
public Flux<AuthProvider> getEnabledProviders() {
|
||||||
return fetchEnabledAuthProviders().flatMapMany(enabledNames -> {
|
return fetchProviderStates().flatMapMany(states -> {
|
||||||
|
var namePriorityMap = states.stream()
|
||||||
|
// filter enabled providers
|
||||||
|
.filter(SystemSetting.AuthProviderState::isEnabled)
|
||||||
|
.collect(Collectors.toMap(SystemSetting.AuthProviderState::getName,
|
||||||
|
SystemSetting.AuthProviderState::getPriority));
|
||||||
|
|
||||||
var listOptions = ListOptions.builder()
|
var listOptions = ListOptions.builder()
|
||||||
.andQuery(QueryFactory.in("metadata.name", enabledNames))
|
.andQuery(QueryFactory.in("metadata.name", namePriorityMap.keySet()))
|
||||||
.andQuery(ExtensionUtil.notDeleting())
|
.andQuery(ExtensionUtil.notDeleting())
|
||||||
.build();
|
.build();
|
||||||
return client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort());
|
return client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())
|
||||||
|
.map(provider -> new AuthProviderWithPriority()
|
||||||
|
.setAuthProvider(provider)
|
||||||
|
.setPriority(namePriorityMap.getOrDefault(
|
||||||
|
provider.getMetadata().getName(), 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.sort(AuthProviderWithPriority::compareTo)
|
||||||
|
.map(AuthProviderWithPriority::getAuthProvider);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Set<String>> fetchEnabledAuthProviders() {
|
@Data
|
||||||
return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
|
@Accessors(chain = true)
|
||||||
.map(configMap -> {
|
static class AuthProviderWithPriority implements Comparable<AuthProviderWithPriority> {
|
||||||
SystemSetting.AuthProvider authProvider = getAuthProvider(configMap);
|
private AuthProvider authProvider;
|
||||||
return authProvider.getEnabled();
|
private int priority;
|
||||||
});
|
|
||||||
|
public String getName() {
|
||||||
|
return authProvider.getMetadata().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NonNull AuthProviderWithPriority o) {
|
||||||
|
return Comparator.comparingInt(AuthProviderWithPriority::getPriority)
|
||||||
|
.thenComparing(AuthProviderWithPriority::getName)
|
||||||
|
.compare(this, o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<List<SystemSetting.AuthProviderState>> fetchProviderStates() {
|
||||||
|
return fetchSystemConfigMap()
|
||||||
|
.map(AuthProviderServiceImpl::getAuthProviderConfig)
|
||||||
|
.map(SystemSetting.AuthProvider::getStates)
|
||||||
|
.defaultIfEmpty(List.of())
|
||||||
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
}
|
}
|
||||||
|
|
||||||
Flux<UserConnection> listMyConnections() {
|
Flux<UserConnection> listMyConnections() {
|
||||||
|
@ -121,32 +166,6 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Comparator<AuthProvider> defaultComparator() {
|
|
||||||
return Comparator.comparing((AuthProvider item) -> item.getSpec().getPriority())
|
|
||||||
.thenComparing(item -> item.getMetadata().getName())
|
|
||||||
.thenComparing(item -> item.getMetadata().getCreationTimestamp());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<ConfigMap> updateAuthProviderEnabled(Consumer<Set<String>> consumer) {
|
|
||||||
return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
|
|
||||||
.switchIfEmpty(Mono.defer(() -> {
|
|
||||||
ConfigMap configMap = new ConfigMap();
|
|
||||||
configMap.setMetadata(new Metadata());
|
|
||||||
configMap.getMetadata().setName(SystemSetting.SYSTEM_CONFIG);
|
|
||||||
configMap.setData(new HashMap<>());
|
|
||||||
return client.create(configMap);
|
|
||||||
}))
|
|
||||||
.flatMap(configMap -> {
|
|
||||||
SystemSetting.AuthProvider authProvider = getAuthProvider(configMap);
|
|
||||||
consumer.accept(authProvider.getEnabled());
|
|
||||||
|
|
||||||
final Map<String, String> data = configMap.getData();
|
|
||||||
data.put(SystemSetting.AuthProvider.GROUP,
|
|
||||||
JsonUtils.objectToJson(authProvider));
|
|
||||||
return client.update(configMap);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private ListedAuthProvider convertTo(AuthProvider authProvider) {
|
private ListedAuthProvider convertTo(AuthProvider authProvider) {
|
||||||
return ListedAuthProvider.builder()
|
return ListedAuthProvider.builder()
|
||||||
.name(authProvider.getMetadata().getName())
|
.name(authProvider.getMetadata().getName())
|
||||||
|
@ -159,6 +178,7 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
||||||
.bindingUrl(authProvider.getSpec().getBindingUrl())
|
.bindingUrl(authProvider.getSpec().getBindingUrl())
|
||||||
.unbindingUrl(authProvider.getSpec().getUnbindUrl())
|
.unbindingUrl(authProvider.getSpec().getUnbindUrl())
|
||||||
.supportsBinding(supportsBinding(authProvider))
|
.supportsBinding(supportsBinding(authProvider))
|
||||||
|
.authType(authProvider.getSpec().getAuthType())
|
||||||
.isBound(false)
|
.isBound(false)
|
||||||
.enabled(false)
|
.enabled(false)
|
||||||
.privileged(privileged(authProvider))
|
.privileged(privileged(authProvider))
|
||||||
|
@ -176,7 +196,7 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static SystemSetting.AuthProvider getAuthProvider(ConfigMap configMap) {
|
private static SystemSetting.AuthProvider getAuthProviderConfig(ConfigMap configMap) {
|
||||||
if (configMap.getData() == null) {
|
if (configMap.getData() == null) {
|
||||||
configMap.setData(new HashMap<>());
|
configMap.setData(new HashMap<>());
|
||||||
}
|
}
|
||||||
|
@ -190,10 +210,41 @@ public class AuthProviderServiceImpl implements AuthProviderService {
|
||||||
authProvider =
|
authProvider =
|
||||||
JsonUtils.jsonToObject(providerGroup, SystemSetting.AuthProvider.class);
|
JsonUtils.jsonToObject(providerGroup, SystemSetting.AuthProvider.class);
|
||||||
}
|
}
|
||||||
|
if (authProvider.getStates() == null) {
|
||||||
if (authProvider.getEnabled() == null) {
|
authProvider.setStates(new ArrayList<>());
|
||||||
authProvider.setEnabled(new HashSet<>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return authProvider;
|
return authProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<ConfigMap> updateAuthProviderEnabled(String name, boolean enabled) {
|
||||||
|
return Mono.defer(() -> fetchSystemConfigMap()
|
||||||
|
.flatMap(configMap -> {
|
||||||
|
var providerConfig = getAuthProviderConfig(configMap);
|
||||||
|
var stateToFoundOpt = providerConfig.getStates()
|
||||||
|
.stream()
|
||||||
|
.filter(state -> state.getName().equals(name))
|
||||||
|
.findFirst();
|
||||||
|
if (stateToFoundOpt.isEmpty()) {
|
||||||
|
var state = new SystemSetting.AuthProviderState()
|
||||||
|
.setName(name)
|
||||||
|
.setEnabled(enabled);
|
||||||
|
providerConfig.getStates().add(state);
|
||||||
|
} else {
|
||||||
|
stateToFoundOpt.get().setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
configMap.getData().put(SystemSetting.AuthProvider.GROUP,
|
||||||
|
JsonUtils.objectToJson(providerConfig));
|
||||||
|
|
||||||
|
return client.update(configMap);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.retryWhen(Retry.backoff(5, Duration.ofMillis(100))
|
||||||
|
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||||
|
}
|
||||||
|
|
||||||
|
Mono<ConfigMap> fetchSystemConfigMap() {
|
||||||
|
return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import run.halo.app.core.extension.AuthProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A listed value object for {@link run.halo.app.core.extension.AuthProvider}.
|
* A listed value object for {@link run.halo.app.core.extension.AuthProvider}.
|
||||||
|
@ -35,11 +36,15 @@ public class ListedAuthProvider {
|
||||||
|
|
||||||
String unbindingUrl;
|
String unbindingUrl;
|
||||||
|
|
||||||
|
AuthProvider.AuthType authType;
|
||||||
|
|
||||||
Boolean isBound;
|
Boolean isBound;
|
||||||
|
|
||||||
Boolean enabled;
|
Boolean enabled;
|
||||||
|
|
||||||
|
int priority;
|
||||||
|
|
||||||
Boolean supportsBinding;
|
Boolean supportsBinding;
|
||||||
|
|
||||||
Boolean privileged;
|
Boolean privileged;
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,14 +61,21 @@ class PreAuthLoginEndpoint {
|
||||||
var publicKey = cryptoService.readPublicKey()
|
var publicKey = cryptoService.readPublicKey()
|
||||||
.map(key -> Base64.getEncoder().encodeToString(key));
|
.map(key -> Base64.getEncoder().encodeToString(key));
|
||||||
var globalInfo = globalInfoService.getGlobalInfo().cache();
|
var globalInfo = globalInfoService.getGlobalInfo().cache();
|
||||||
var loginMethod = request.queryParam("method").orElse("local");
|
|
||||||
var authProviders = authProviderService.getEnabledProviders().cache();
|
var authProviders = authProviderService.getEnabledProviders().cache();
|
||||||
var authProvider = authProviders
|
|
||||||
.filter(ap -> Objects.equals(loginMethod, ap.getMetadata().getName()))
|
var allFormProviders = authProviders
|
||||||
.next()
|
.filter(ap -> AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
|
||||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException(
|
.cache();
|
||||||
"Invalid login method " + loginMethod)
|
|
||||||
))
|
var authProvider = Mono.justOrEmpty(request.queryParam("method"))
|
||||||
|
.flatMap(method -> allFormProviders
|
||||||
|
.filter(ap -> Objects.equals(method, ap.getMetadata().getName()))
|
||||||
|
.next()
|
||||||
|
.switchIfEmpty(Mono.error(
|
||||||
|
() -> new ServerWebInputException("Invalid login method " + method))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.switchIfEmpty(allFormProviders.next())
|
||||||
.cache();
|
.cache();
|
||||||
|
|
||||||
var fragmentTemplateName = authProvider.map(ap -> {
|
var fragmentTemplateName = authProvider.map(ap -> {
|
||||||
|
@ -83,9 +90,12 @@ class PreAuthLoginEndpoint {
|
||||||
var socialAuthProviders = authProviders
|
var socialAuthProviders = authProviders
|
||||||
.filter(ap -> !AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
|
.filter(ap -> !AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
|
||||||
.cache();
|
.cache();
|
||||||
var formAuthProviders = authProviders
|
var formAuthProviders = allFormProviders
|
||||||
.filter(ap -> AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
|
.filterWhen(ap -> authProvider
|
||||||
.filter(ap -> !Objects.equals(loginMethod, ap.getMetadata().getName()))
|
.map(provider -> !Objects.equals(provider.getMetadata().getName(),
|
||||||
|
ap.getMetadata().getName())
|
||||||
|
)
|
||||||
|
)
|
||||||
.cache();
|
.cache();
|
||||||
|
|
||||||
return serverRequestCache.saveRequest(exchange).then(Mono.defer(() ->
|
return serverRequestCache.saveRequest(exchange).then(Mono.defer(() ->
|
||||||
|
|
|
@ -9,7 +9,6 @@ import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Set;
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
@ -48,7 +47,7 @@ class AuthProviderServiceImplTest {
|
||||||
AuthProviderServiceImpl authProviderService;
|
AuthProviderServiceImpl authProviderService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testEnable() {
|
void testEnable() throws JSONException {
|
||||||
// Create a test auth provider
|
// Create a test auth provider
|
||||||
AuthProvider authProvider = createAuthProvider("github");
|
AuthProvider authProvider = createAuthProvider("github");
|
||||||
when(client.get(eq(AuthProvider.class), eq("github"))).thenReturn(Mono.just(authProvider));
|
when(client.get(eq(AuthProvider.class), eq("github"))).thenReturn(Mono.just(authProvider));
|
||||||
|
@ -56,10 +55,7 @@ class AuthProviderServiceImplTest {
|
||||||
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
||||||
when(client.update(captor.capture())).thenReturn(Mono.empty());
|
when(client.update(captor.capture())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
ConfigMap configMap = new ConfigMap();
|
pileSystemConfigMap();
|
||||||
configMap.setData(new HashMap<>());
|
|
||||||
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
|
||||||
.thenReturn(Mono.just(configMap));
|
|
||||||
|
|
||||||
// Call the method being tested
|
// Call the method being tested
|
||||||
authProviderService.enable("github")
|
authProviderService.enable("github")
|
||||||
|
@ -68,51 +64,59 @@ class AuthProviderServiceImplTest {
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
ConfigMap value = captor.getValue();
|
ConfigMap value = captor.getValue();
|
||||||
String providerSettingStr = value.getData().get(SystemSetting.AuthProvider.GROUP);
|
JSONAssert.assertEquals("""
|
||||||
Set<String> enabled =
|
{
|
||||||
JsonUtils.jsonToObject(providerSettingStr, SystemSetting.AuthProvider.class)
|
"states": [
|
||||||
.getEnabled();
|
{
|
||||||
assertThat(enabled).containsExactly("github");
|
"name": "github",
|
||||||
|
"enabled": true,
|
||||||
|
"priority": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
value.getData().get(SystemSetting.AuthProvider.GROUP),
|
||||||
|
true);
|
||||||
// Verify the result
|
// Verify the result
|
||||||
verify(client).get(AuthProvider.class, "github");
|
verify(client).get(AuthProvider.class, "github");
|
||||||
verify(client).fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testDisable() {
|
void testDisable() throws JSONException {
|
||||||
// Create a test auth provider
|
// Create a test auth provider
|
||||||
AuthProvider authProvider = createAuthProvider("github");
|
AuthProvider authProvider = createAuthProvider("github");
|
||||||
when(client.get(eq(AuthProvider.class), eq("github"))).thenReturn(Mono.just(authProvider));
|
when(client.get(eq(AuthProvider.class), eq("github"))).thenReturn(Mono.just(authProvider));
|
||||||
|
|
||||||
AuthProvider local = createAuthProvider("local");
|
AuthProvider local = createAuthProvider("local");
|
||||||
local.getMetadata().getLabels().put(AuthProvider.PRIVILEGED_LABEL, "true");
|
local.getMetadata().getLabels().put(AuthProvider.PRIVILEGED_LABEL, "true");
|
||||||
// when(client.list(eq(AuthProvider.class), any(), any())).thenReturn(Flux.just(local));
|
|
||||||
|
|
||||||
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
ArgumentCaptor<ConfigMap> captor = ArgumentCaptor.forClass(ConfigMap.class);
|
||||||
when(client.update(captor.capture())).thenReturn(Mono.empty());
|
when(client.update(captor.capture())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
ConfigMap configMap = new ConfigMap();
|
pileSystemConfigMap();
|
||||||
configMap.setData(new HashMap<>());
|
|
||||||
configMap.getData().put(SystemSetting.AuthProvider.GROUP, "{\"enabled\":[\"github\"]}");
|
|
||||||
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
|
||||||
.thenReturn(Mono.just(configMap));
|
|
||||||
|
|
||||||
// Call the method being tested
|
// Call the method being tested
|
||||||
Mono<AuthProvider> result = authProviderService.disable("github");
|
Mono<AuthProvider> result = authProviderService.disable("github");
|
||||||
|
|
||||||
assertEquals(authProvider, result.block());
|
assertEquals(authProvider, result.block());
|
||||||
ConfigMap value = captor.getValue();
|
ConfigMap value = captor.getValue();
|
||||||
String providerSettingStr = value.getData().get(SystemSetting.AuthProvider.GROUP);
|
JSONAssert.assertEquals("""
|
||||||
Set<String> enabled =
|
{
|
||||||
JsonUtils.jsonToObject(providerSettingStr, SystemSetting.AuthProvider.class)
|
"states": [
|
||||||
.getEnabled();
|
{
|
||||||
assertThat(enabled).isEmpty();
|
"name": "github",
|
||||||
|
"enabled": false,
|
||||||
|
"priority": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
value.getData().get(SystemSetting.AuthProvider.GROUP),
|
||||||
|
true);
|
||||||
// Verify the result
|
// Verify the result
|
||||||
verify(client).get(AuthProvider.class, "github");
|
verify(client).get(AuthProvider.class, "github");
|
||||||
verify(client).fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser(username = "admin")
|
@WithMockUser(username = "admin")
|
||||||
void listAll() {
|
void listAll() {
|
||||||
|
@ -129,11 +133,7 @@ class AuthProviderServiceImplTest {
|
||||||
when(client.listAll(same(UserConnection.class), any(ListOptions.class), any(Sort.class)))
|
when(client.listAll(same(UserConnection.class), any(ListOptions.class), any(Sort.class)))
|
||||||
.thenReturn(Flux.empty());
|
.thenReturn(Flux.empty());
|
||||||
|
|
||||||
ConfigMap configMap = new ConfigMap();
|
pileSystemConfigMap();
|
||||||
configMap.setData(new HashMap<>());
|
|
||||||
configMap.getData().put(SystemSetting.AuthProvider.GROUP, "{\"enabled\":[\"github\"]}");
|
|
||||||
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
|
||||||
.thenReturn(Mono.just(configMap));
|
|
||||||
|
|
||||||
authProviderService.listAll()
|
authProviderService.listAll()
|
||||||
.as(StepVerifier::create)
|
.as(StepVerifier::create)
|
||||||
|
@ -142,29 +142,36 @@ class AuthProviderServiceImplTest {
|
||||||
try {
|
try {
|
||||||
JSONAssert.assertEquals("""
|
JSONAssert.assertEquals("""
|
||||||
[{
|
[{
|
||||||
"name": "github",
|
"name": "gitee",
|
||||||
"displayName": "github",
|
"displayName": "gitee",
|
||||||
"bindingUrl": "fake-binding-url",
|
"authType": "OAUTH2",
|
||||||
"enabled": true,
|
"isBound": false,
|
||||||
"isBound": false,
|
"enabled": false,
|
||||||
"supportsBinding": false,
|
"priority": 0,
|
||||||
"privileged": false
|
"supportsBinding": false,
|
||||||
}, {
|
"privileged": false
|
||||||
"name": "gitlab",
|
},
|
||||||
"displayName": "gitlab",
|
{
|
||||||
"bindingUrl": "fake-binding-url",
|
"name": "github",
|
||||||
"enabled": false,
|
"displayName": "github",
|
||||||
"isBound": false,
|
"bindingUrl": "fake-binding-url",
|
||||||
"supportsBinding": false,
|
"authType": "OAUTH2",
|
||||||
"privileged": false
|
"isBound": false,
|
||||||
},{
|
"enabled": false,
|
||||||
|
"priority": 0,
|
||||||
"name": "gitee",
|
"supportsBinding": false,
|
||||||
"displayName": "gitee",
|
"privileged": false
|
||||||
"enabled": false,
|
},
|
||||||
"isBound": false,
|
{
|
||||||
"supportsBinding": false,
|
"name": "gitlab",
|
||||||
"privileged": false
|
"displayName": "gitlab",
|
||||||
|
"bindingUrl": "fake-binding-url",
|
||||||
|
"authType": "OAUTH2",
|
||||||
|
"isBound": false,
|
||||||
|
"enabled": false,
|
||||||
|
"priority": 0,
|
||||||
|
"supportsBinding": false,
|
||||||
|
"privileged": false
|
||||||
}]
|
}]
|
||||||
""",
|
""",
|
||||||
JsonUtils.objectToJson(result),
|
JsonUtils.objectToJson(result),
|
||||||
|
@ -183,6 +190,14 @@ class AuthProviderServiceImplTest {
|
||||||
authProvider.getMetadata().setLabels(new HashMap<>());
|
authProvider.getMetadata().setLabels(new HashMap<>());
|
||||||
authProvider.setSpec(new AuthProvider.AuthProviderSpec());
|
authProvider.setSpec(new AuthProvider.AuthProviderSpec());
|
||||||
authProvider.getSpec().setDisplayName(name);
|
authProvider.getSpec().setDisplayName(name);
|
||||||
|
authProvider.getSpec().setAuthType(AuthProvider.AuthType.OAUTH2);
|
||||||
return authProvider;
|
return authProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void pileSystemConfigMap() {
|
||||||
|
ConfigMap configMap = new ConfigMap();
|
||||||
|
configMap.setData(new HashMap<>());
|
||||||
|
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
|
||||||
|
.thenReturn(Mono.just(configMap));
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,22 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { AuthProvider, ListedAuthProvider } from "@halo-dev/api-client";
|
import type { ListedAuthProvider } from "@halo-dev/api-client";
|
||||||
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
|
import {
|
||||||
|
AuthProviderSpecAuthTypeEnum,
|
||||||
|
consoleApiClient,
|
||||||
|
} from "@halo-dev/api-client";
|
||||||
import {
|
import {
|
||||||
IconLockPasswordLine,
|
IconLockPasswordLine,
|
||||||
VCard,
|
|
||||||
VLoading,
|
VLoading,
|
||||||
VPageHeader,
|
VPageHeader,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
import { useQuery } from "@tanstack/vue-query";
|
import { useQuery } from "@tanstack/vue-query";
|
||||||
import Fuse from "fuse.js";
|
import { ref } from "vue";
|
||||||
import { computed, ref } from "vue";
|
import AuthProvidersSection from "./components/AuthProvidersSection.vue";
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
|
||||||
import AuthProviderListItem from "./components/AuthProviderListItem.vue";
|
|
||||||
|
|
||||||
const authProviders = ref<ListedAuthProvider[]>([]);
|
const SYSTEM_CONFIGMAP_AUTH_PROVIDER = "authProvider";
|
||||||
|
|
||||||
|
const formAuthProviders = ref<ListedAuthProvider[]>([]);
|
||||||
|
const oauth2AuthProviders = ref<ListedAuthProvider[]>([]);
|
||||||
|
|
||||||
const { isLoading, refetch } = useQuery<ListedAuthProvider[]>({
|
const { isLoading, refetch } = useQuery<ListedAuthProvider[]>({
|
||||||
queryKey: ["auth-providers"],
|
queryKey: ["auth-providers"],
|
||||||
|
@ -23,26 +26,17 @@ const { isLoading, refetch } = useQuery<ListedAuthProvider[]>({
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
authProviders.value = data;
|
formAuthProviders.value = data.filter(
|
||||||
fuse = new Fuse(data, {
|
(authProvider) =>
|
||||||
keys: ["name", "displayName"],
|
authProvider.authType === AuthProviderSpecAuthTypeEnum.Form
|
||||||
useExtendedSearch: true,
|
);
|
||||||
threshold: 0.2,
|
oauth2AuthProviders.value = data.filter(
|
||||||
});
|
(authProvider) =>
|
||||||
|
authProvider.authType === AuthProviderSpecAuthTypeEnum.Oauth2
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const keyword = ref("");
|
|
||||||
let fuse: Fuse<ListedAuthProvider> | undefined = undefined;
|
|
||||||
|
|
||||||
const searchResults = computed(() => {
|
|
||||||
if (!fuse || !keyword.value) {
|
|
||||||
return authProviders.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fuse?.search(keyword.value).map((item) => item.item);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Drag and drop
|
// Drag and drop
|
||||||
const updating = ref(false);
|
const updating = ref(false);
|
||||||
|
|
||||||
|
@ -50,29 +44,23 @@ async function onSortUpdate() {
|
||||||
try {
|
try {
|
||||||
updating.value = true;
|
updating.value = true;
|
||||||
|
|
||||||
const { data: rawAuthProviders } =
|
const allAuthProviders = [
|
||||||
await coreApiClient.auth.authProvider.listAuthProvider();
|
...formAuthProviders.value,
|
||||||
|
...oauth2AuthProviders.value,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
const authProviderNames = authProviders.value.map((item) => item.name);
|
await consoleApiClient.configMap.system.updateSystemConfigByGroup({
|
||||||
|
group: SYSTEM_CONFIGMAP_AUTH_PROVIDER,
|
||||||
const sortedAuthProviders = authProviderNames
|
body: {
|
||||||
.map((name) => {
|
states: allAuthProviders.map((authProvider, index) => {
|
||||||
const authProvider = rawAuthProviders.items.find(
|
return {
|
||||||
(item) => item.metadata.name === name
|
name: authProvider.name,
|
||||||
);
|
enabled: authProvider.enabled,
|
||||||
if (authProvider) {
|
priority: index,
|
||||||
authProvider.spec.priority = authProviderNames.indexOf(name);
|
};
|
||||||
}
|
}),
|
||||||
return authProvider;
|
},
|
||||||
})
|
});
|
||||||
.filter(Boolean) as AuthProvider[];
|
|
||||||
|
|
||||||
for (const authProvider of sortedAuthProviders) {
|
|
||||||
await coreApiClient.auth.authProvider.updateAuthProvider({
|
|
||||||
name: authProvider.metadata.name,
|
|
||||||
authProvider: authProvider,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
await refetch();
|
await refetch();
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
|
@ -87,46 +75,25 @@ async function onSortUpdate() {
|
||||||
</template>
|
</template>
|
||||||
</VPageHeader>
|
</VPageHeader>
|
||||||
|
|
||||||
<div class="m-0 md:m-4">
|
<div class="m-0 space-y-5 md:m-4">
|
||||||
<VCard :body-class="['!p-0']">
|
<VLoading v-if="isLoading" />
|
||||||
<template #header>
|
<TransitionGroup v-else appear name="fade">
|
||||||
<div class="block w-full bg-gray-50 px-4 py-3">
|
<AuthProvidersSection
|
||||||
<div
|
:key="AuthProviderSpecAuthTypeEnum.Form"
|
||||||
class="relative flex flex-col items-start sm:flex-row sm:items-center"
|
v-model="formAuthProviders"
|
||||||
>
|
:title="$t('core.identity_authentication.list.types.form')"
|
||||||
<div class="flex w-full flex-1 sm:w-auto">
|
:loading="updating"
|
||||||
<FormKit
|
@update="onSortUpdate"
|
||||||
v-model="keyword"
|
/>
|
||||||
:placeholder="$t('core.common.placeholder.search')"
|
|
||||||
type="text"
|
<AuthProvidersSection
|
||||||
></FormKit>
|
v-if="oauth2AuthProviders.length"
|
||||||
</div>
|
:key="AuthProviderSpecAuthTypeEnum.Oauth2"
|
||||||
</div>
|
v-model="oauth2AuthProviders"
|
||||||
</div>
|
:title="$t('core.identity_authentication.list.types.oauth2')"
|
||||||
</template>
|
:loading="updating"
|
||||||
<VLoading v-if="isLoading" />
|
@update="onSortUpdate"
|
||||||
<Transition v-else appear name="fade">
|
/>
|
||||||
<VueDraggable
|
</TransitionGroup>
|
||||||
v-model="authProviders"
|
|
||||||
ghost-class="opacity-50"
|
|
||||||
handle=".drag-element"
|
|
||||||
class="box-border h-full w-full divide-y divide-gray-100"
|
|
||||||
:class="{
|
|
||||||
'cursor-progress opacity-60': updating,
|
|
||||||
}"
|
|
||||||
role="list"
|
|
||||||
tag="ul"
|
|
||||||
:disabled="updating"
|
|
||||||
@update="onSortUpdate"
|
|
||||||
>
|
|
||||||
<li v-for="(authProvider, index) in searchResults" :key="index">
|
|
||||||
<AuthProviderListItem
|
|
||||||
:auth-provider="authProvider"
|
|
||||||
@reload="refetch"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</VueDraggable>
|
|
||||||
</Transition>
|
|
||||||
</VCard>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ListedAuthProvider } from "@halo-dev/api-client";
|
||||||
|
import { VCard } from "@halo-dev/components";
|
||||||
|
import { useQueryClient } from "@tanstack/vue-query";
|
||||||
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
|
import AuthProviderListItem from "./AuthProviderListItem.vue";
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const modelValue = defineModel<ListedAuthProvider[]>({ default: [] });
|
||||||
|
|
||||||
|
const { loading = false } = defineProps<{
|
||||||
|
loading?: boolean;
|
||||||
|
title: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function onReload() {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["auth-providers"] });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VCard :title="title" :body-class="['!p-0']">
|
||||||
|
<VueDraggable
|
||||||
|
v-model="modelValue"
|
||||||
|
ghost-class="opacity-50"
|
||||||
|
handle=".drag-element"
|
||||||
|
class="box-border h-full w-full divide-y divide-gray-100"
|
||||||
|
:class="{
|
||||||
|
'cursor-progress opacity-60': loading,
|
||||||
|
}"
|
||||||
|
role="list"
|
||||||
|
tag="ul"
|
||||||
|
:disabled="loading"
|
||||||
|
@update="emit('update')"
|
||||||
|
>
|
||||||
|
<li v-for="authProvider in modelValue" :key="authProvider.name">
|
||||||
|
<AuthProviderListItem
|
||||||
|
:auth-provider="authProvider"
|
||||||
|
@reload="onReload"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</VueDraggable>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
|
@ -16,6 +16,7 @@ export default definePlugin({
|
||||||
meta: {
|
meta: {
|
||||||
title: "core.identity_authentication.title",
|
title: "core.identity_authentication.title",
|
||||||
searchable: true,
|
searchable: true,
|
||||||
|
permissions: ["*"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -24,6 +25,7 @@ export default definePlugin({
|
||||||
component: AuthProviderDetail,
|
component: AuthProviderDetail,
|
||||||
meta: {
|
meta: {
|
||||||
title: "core.identity_authentication.detail.title",
|
title: "core.identity_authentication.detail.title",
|
||||||
|
permissions: ["*"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -292,12 +292,14 @@ function onGrantPermissionModalClose() {
|
||||||
</template>
|
</template>
|
||||||
{{ $t("core.user.actions.roles") }}
|
{{ $t("core.user.actions.roles") }}
|
||||||
</VButton>
|
</VButton>
|
||||||
<VButton :route="{ name: 'AuthProviders' }" size="sm" type="default">
|
<HasPermission :permissions="['*']">
|
||||||
<template #icon>
|
<VButton :route="{ name: 'AuthProviders' }" size="sm" type="default">
|
||||||
<IconLockPasswordLine class="h-full w-full" />
|
<template #icon>
|
||||||
</template>
|
<IconLockPasswordLine class="h-full w-full" />
|
||||||
{{ $t("core.user.actions.identity_authentication") }}
|
</template>
|
||||||
</VButton>
|
{{ $t("core.user.actions.identity_authentication") }}
|
||||||
|
</VButton>
|
||||||
|
</HasPermission>
|
||||||
<VButton
|
<VButton
|
||||||
v-permission="['system:users:manage']"
|
v-permission="['system:users:manage']"
|
||||||
type="secondary"
|
type="secondary"
|
||||||
|
|
|
@ -31,7 +31,7 @@ export interface AuthProviderSpec {
|
||||||
* @type {string}
|
* @type {string}
|
||||||
* @memberof AuthProviderSpec
|
* @memberof AuthProviderSpec
|
||||||
*/
|
*/
|
||||||
'authType'?: AuthProviderSpecAuthTypeEnum;
|
'authType': AuthProviderSpecAuthTypeEnum;
|
||||||
/**
|
/**
|
||||||
* Authentication url of the auth provider
|
* Authentication url of the auth provider
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -80,12 +80,6 @@ export interface AuthProviderSpec {
|
||||||
* @memberof AuthProviderSpec
|
* @memberof AuthProviderSpec
|
||||||
*/
|
*/
|
||||||
'method'?: string;
|
'method'?: string;
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
* @memberof AuthProviderSpec
|
|
||||||
*/
|
|
||||||
'priority'?: number;
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
|
|
@ -20,6 +20,12 @@
|
||||||
* @interface ListedAuthProvider
|
* @interface ListedAuthProvider
|
||||||
*/
|
*/
|
||||||
export interface ListedAuthProvider {
|
export interface ListedAuthProvider {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ListedAuthProvider
|
||||||
|
*/
|
||||||
|
'authType'?: ListedAuthProviderAuthTypeEnum;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
|
@ -74,6 +80,12 @@ export interface ListedAuthProvider {
|
||||||
* @memberof ListedAuthProvider
|
* @memberof ListedAuthProvider
|
||||||
*/
|
*/
|
||||||
'name': string;
|
'name': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof ListedAuthProvider
|
||||||
|
*/
|
||||||
|
'priority'?: number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -100,3 +112,11 @@ export interface ListedAuthProvider {
|
||||||
'website'?: string;
|
'website'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ListedAuthProviderAuthTypeEnum = {
|
||||||
|
Form: 'FORM',
|
||||||
|
Oauth2: 'OAUTH2'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ListedAuthProviderAuthTypeEnum = typeof ListedAuthProviderAuthTypeEnum[keyof typeof ListedAuthProviderAuthTypeEnum];
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1140,6 +1140,10 @@ core:
|
||||||
local: Login with credentials
|
local: Login with credentials
|
||||||
description:
|
description:
|
||||||
local: Default login method built into Halo
|
local: Default login method built into Halo
|
||||||
|
list:
|
||||||
|
types:
|
||||||
|
form: Basic Authentication Method
|
||||||
|
oauth2: Third-party Authentication Method
|
||||||
detail:
|
detail:
|
||||||
title: Identity authentication detail
|
title: Identity authentication detail
|
||||||
fields:
|
fields:
|
||||||
|
|
|
@ -1068,6 +1068,10 @@ core:
|
||||||
local: 账号密码登录
|
local: 账号密码登录
|
||||||
description:
|
description:
|
||||||
local: Halo 内置的默认登录方式
|
local: Halo 内置的默认登录方式
|
||||||
|
list:
|
||||||
|
types:
|
||||||
|
form: 基础认证方式
|
||||||
|
oauth2: 三方认证方式
|
||||||
detail:
|
detail:
|
||||||
title: 身份认证详情
|
title: 身份认证详情
|
||||||
fields:
|
fields:
|
||||||
|
|
|
@ -1045,6 +1045,10 @@ core:
|
||||||
local: 帳號密碼登入
|
local: 帳號密碼登入
|
||||||
description:
|
description:
|
||||||
local: Halo 內建的默認登入方式
|
local: Halo 內建的默認登入方式
|
||||||
|
list:
|
||||||
|
types:
|
||||||
|
form: 基礎認證方式
|
||||||
|
oauth2: 三方認證方式
|
||||||
detail:
|
detail:
|
||||||
title: 身份認證詳情
|
title: 身份認證詳情
|
||||||
fields:
|
fields:
|
||||||
|
|
Loading…
Reference in New Issue