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
guqing 2024-10-14 10:53:24 +08:00 committed by GitHub
parent 4059f159b2
commit 82498dcedf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 411 additions and 255 deletions

View File

@ -16034,6 +16034,7 @@
},
"AuthProviderSpec": {
"required": [
"authType",
"authenticationUrl",
"displayName"
],
@ -16072,10 +16073,6 @@
"method": {
"type": "string"
},
"priority": {
"type": "integer",
"format": "int32"
},
"rememberMeSupport": {
"type": "boolean"
},
@ -18297,6 +18294,13 @@
],
"type": "object",
"properties": {
"authType": {
"type": "string",
"enum": [
"FORM",
"OAUTH2"
]
},
"authenticationUrl": {
"type": "string"
},
@ -18324,6 +18328,10 @@
"name": {
"type": "string"
},
"priority": {
"type": "integer",
"format": "int32"
},
"privileged": {
"type": "boolean"
},

View File

@ -3568,6 +3568,7 @@
},
"AuthProviderSpec": {
"required": [
"authType",
"authenticationUrl",
"displayName"
],
@ -3606,10 +3607,6 @@
"method": {
"type": "string"
},
"priority": {
"type": "integer",
"format": "int32"
},
"rememberMeSupport": {
"type": "boolean"
},
@ -4435,6 +4432,13 @@
],
"type": "object",
"properties": {
"authType": {
"type": "string",
"enum": [
"FORM",
"OAUTH2"
]
},
"authenticationUrl": {
"type": "string"
},
@ -4462,6 +4466,10 @@
"name": {
"type": "string"
},
"priority": {
"type": "integer",
"format": "int32"
},
"privileged": {
"type": "boolean"
},

View File

@ -8666,6 +8666,7 @@
},
"AuthProviderSpec": {
"required": [
"authType",
"authenticationUrl",
"displayName"
],
@ -8704,10 +8705,6 @@
"method": {
"type": "string"
},
"priority": {
"type": "integer",
"format": "int32"
},
"rememberMeSupport": {
"type": "boolean"
},

View File

@ -6,7 +6,9 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import org.springframework.lang.NonNull;
import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK;
@ -55,19 +57,23 @@ public class AuthProvider extends AbstractExtension {
/**
* Auth type: form or oauth2.
*/
private AuthType authType;
@Getter(onMethod_ = @NonNull)
@Schema(requiredMode = REQUIRED)
private AuthType authType = AuthType.OAUTH2;
private String bindingUrl;
private String unbindUrl;
private int priority;
@Schema(requiredMode = NOT_REQUIRED)
private SettingRef settingRef;
@Schema(requiredMode = NOT_REQUIRED)
private ConfigMapRef configMapRef;
public void setAuthType(AuthType authType) {
this.authType = (authType == null ? AuthType.OAUTH2 : authType);
}
}
@Data
@ -89,7 +95,6 @@ public class AuthProvider extends AbstractExtension {
public enum AuthType {
FORM,
OAUTH2,
;
OAUTH2
}
}

View File

@ -72,7 +72,7 @@ public class Unstructured implements Extension {
return new UnstructuredMetadata();
}
@EqualsAndHashCode(exclude = "version")
@EqualsAndHashCode(exclude = "tatersion")
class UnstructuredMetadata implements MetadataOperator {
@Override

View File

@ -2,8 +2,9 @@ package run.halo.app.infra;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.List;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.boot.convert.ApplicationConversionService;
import run.halo.app.extension.ConfigMap;
import run.halo.app.infra.utils.JsonUtils;
@ -113,7 +114,15 @@ public class SystemSetting {
@Data
public static class 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;
}
/**

View File

@ -19,6 +19,9 @@ public interface AuthProviderService {
Mono<List<ListedAuthProvider>> listAll();
/**
* Return a list of enabled AuthProviders sorted by priority.
*/
Flux<AuthProvider> getEnabledProviders();
}

View File

@ -1,27 +1,31 @@
package run.halo.app.security;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
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.UserConnection;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ExtensionUtil;
import run.halo.app.extension.ListOptions;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.MetadataUtil;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.index.query.QueryFactory;
@ -42,17 +46,17 @@ public class AuthProviderServiceImpl implements AuthProviderService {
@Override
public Mono<AuthProvider> enable(String name) {
return client.get(AuthProvider.class, name)
.flatMap(authProvider -> updateAuthProviderEnabled(enabled -> enabled.add(name))
.flatMap(authProvider -> updateAuthProviderEnabled(name, true)
.thenReturn(authProvider)
);
}
@Override
public Mono<AuthProvider> disable(String name) {
// privileged auth provider cannot be disabled
return client.get(AuthProvider.class, name)
// privileged auth provider cannot be disabled
.filter(authProvider -> !privileged(authProvider))
.flatMap(authProvider -> updateAuthProviderEnabled(enabled -> enabled.remove(name))
.flatMap(authProvider -> updateAuthProviderEnabled(name, false)
.thenReturn(authProvider)
);
}
@ -62,50 +66,91 @@ public class AuthProviderServiceImpl implements AuthProviderService {
var listOptions = ListOptions.builder()
.andQuery(ExtensionUtil.notDeleting())
.build();
return client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())
.map(this::convertTo)
.collectList()
.flatMap(providers -> listMyConnections()
.map(connection -> connection.getSpec().getRegistrationId())
var allProvidersMono =
client.listAll(AuthProvider.class, listOptions, ExtensionUtil.defaultSort())
.map(this::convertTo)
.collectList()
.map(connectedNames -> providers.stream()
.peek(provider -> {
boolean isBound = connectedNames.contains(provider.getName());
provider.setIsBound(isBound);
.subscribeOn(Schedulers.boundedElastic());
var boundProvidersMono = listMyConnections()
.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())
)
.defaultIfEmpty(providers)
)
.flatMap(providers -> fetchEnabledAuthProviders()
.map(names -> providers.stream()
.peek(provider -> {
boolean enabled = names.contains(provider.getName());
provider.setEnabled(enabled);
})
.collect(Collectors.toList())
)
.defaultIfEmpty(providers)
);
.sorted(Comparator.comparingInt(ListedAuthProvider::getPriority)
.thenComparing(ListedAuthProvider::getName))
.toList();
});
}
@Override
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()
.andQuery(QueryFactory.in("metadata.name", enabledNames))
.andQuery(QueryFactory.in("metadata.name", namePriorityMap.keySet()))
.andQuery(ExtensionUtil.notDeleting())
.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() {
return client.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG)
.map(configMap -> {
SystemSetting.AuthProvider authProvider = getAuthProvider(configMap);
return authProvider.getEnabled();
});
@Data
@Accessors(chain = true)
static class AuthProviderWithPriority implements Comparable<AuthProviderWithPriority> {
private AuthProvider authProvider;
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() {
@ -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) {
return ListedAuthProvider.builder()
.name(authProvider.getMetadata().getName())
@ -159,6 +178,7 @@ public class AuthProviderServiceImpl implements AuthProviderService {
.bindingUrl(authProvider.getSpec().getBindingUrl())
.unbindingUrl(authProvider.getSpec().getUnbindUrl())
.supportsBinding(supportsBinding(authProvider))
.authType(authProvider.getSpec().getAuthType())
.isBound(false)
.enabled(false)
.privileged(privileged(authProvider))
@ -176,7 +196,7 @@ public class AuthProviderServiceImpl implements AuthProviderService {
}
@NonNull
private static SystemSetting.AuthProvider getAuthProvider(ConfigMap configMap) {
private static SystemSetting.AuthProvider getAuthProviderConfig(ConfigMap configMap) {
if (configMap.getData() == null) {
configMap.setData(new HashMap<>());
}
@ -190,10 +210,41 @@ public class AuthProviderServiceImpl implements AuthProviderService {
authProvider =
JsonUtils.jsonToObject(providerGroup, SystemSetting.AuthProvider.class);
}
if (authProvider.getEnabled() == null) {
authProvider.setEnabled(new HashSet<>());
if (authProvider.getStates() == null) {
authProvider.setStates(new ArrayList<>());
}
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);
}
}

View File

@ -5,6 +5,7 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import 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;
AuthProvider.AuthType authType;
Boolean isBound;
Boolean enabled;
int priority;
Boolean supportsBinding;
Boolean privileged;
}

View File

@ -61,14 +61,21 @@ class PreAuthLoginEndpoint {
var publicKey = cryptoService.readPublicKey()
.map(key -> Base64.getEncoder().encodeToString(key));
var globalInfo = globalInfoService.getGlobalInfo().cache();
var loginMethod = request.queryParam("method").orElse("local");
var authProviders = authProviderService.getEnabledProviders().cache();
var authProvider = authProviders
.filter(ap -> Objects.equals(loginMethod, ap.getMetadata().getName()))
.next()
.switchIfEmpty(Mono.error(() -> new ServerWebInputException(
"Invalid login method " + loginMethod)
))
var allFormProviders = authProviders
.filter(ap -> AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
.cache();
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();
var fragmentTemplateName = authProvider.map(ap -> {
@ -83,9 +90,12 @@ class PreAuthLoginEndpoint {
var socialAuthProviders = authProviders
.filter(ap -> !AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
.cache();
var formAuthProviders = authProviders
.filter(ap -> AuthProvider.AuthType.FORM.equals(ap.getSpec().getAuthType()))
.filter(ap -> !Objects.equals(loginMethod, ap.getMetadata().getName()))
var formAuthProviders = allFormProviders
.filterWhen(ap -> authProvider
.map(provider -> !Objects.equals(provider.getMetadata().getName(),
ap.getMetadata().getName())
)
)
.cache();
return serverRequestCache.saveRequest(exchange).then(Mono.defer(() ->

View File

@ -9,7 +9,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Set;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -48,7 +47,7 @@ class AuthProviderServiceImplTest {
AuthProviderServiceImpl authProviderService;
@Test
void testEnable() {
void testEnable() throws JSONException {
// Create a test auth provider
AuthProvider authProvider = createAuthProvider("github");
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);
when(client.update(captor.capture())).thenReturn(Mono.empty());
ConfigMap configMap = new ConfigMap();
configMap.setData(new HashMap<>());
when(client.fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG)))
.thenReturn(Mono.just(configMap));
pileSystemConfigMap();
// Call the method being tested
authProviderService.enable("github")
@ -68,51 +64,59 @@ class AuthProviderServiceImplTest {
.verifyComplete();
ConfigMap value = captor.getValue();
String providerSettingStr = value.getData().get(SystemSetting.AuthProvider.GROUP);
Set<String> enabled =
JsonUtils.jsonToObject(providerSettingStr, SystemSetting.AuthProvider.class)
.getEnabled();
assertThat(enabled).containsExactly("github");
JSONAssert.assertEquals("""
{
"states": [
{
"name": "github",
"enabled": true,
"priority": 0
}
]
}
""",
value.getData().get(SystemSetting.AuthProvider.GROUP),
true);
// Verify the result
verify(client).get(AuthProvider.class, "github");
verify(client).fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG));
}
@Test
void testDisable() {
void testDisable() throws JSONException {
// Create a test auth provider
AuthProvider authProvider = createAuthProvider("github");
when(client.get(eq(AuthProvider.class), eq("github"))).thenReturn(Mono.just(authProvider));
AuthProvider local = createAuthProvider("local");
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);
when(client.update(captor.capture())).thenReturn(Mono.empty());
ConfigMap configMap = new ConfigMap();
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));
pileSystemConfigMap();
// Call the method being tested
Mono<AuthProvider> result = authProviderService.disable("github");
assertEquals(authProvider, result.block());
ConfigMap value = captor.getValue();
String providerSettingStr = value.getData().get(SystemSetting.AuthProvider.GROUP);
Set<String> enabled =
JsonUtils.jsonToObject(providerSettingStr, SystemSetting.AuthProvider.class)
.getEnabled();
assertThat(enabled).isEmpty();
JSONAssert.assertEquals("""
{
"states": [
{
"name": "github",
"enabled": false,
"priority": 0
}
]
}
""",
value.getData().get(SystemSetting.AuthProvider.GROUP),
true);
// Verify the result
verify(client).get(AuthProvider.class, "github");
verify(client).fetch(eq(ConfigMap.class), eq(SystemSetting.SYSTEM_CONFIG));
}
@Test
@WithMockUser(username = "admin")
void listAll() {
@ -129,11 +133,7 @@ class AuthProviderServiceImplTest {
when(client.listAll(same(UserConnection.class), any(ListOptions.class), any(Sort.class)))
.thenReturn(Flux.empty());
ConfigMap configMap = new ConfigMap();
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));
pileSystemConfigMap();
authProviderService.listAll()
.as(StepVerifier::create)
@ -142,29 +142,36 @@ class AuthProviderServiceImplTest {
try {
JSONAssert.assertEquals("""
[{
"name": "github",
"displayName": "github",
"bindingUrl": "fake-binding-url",
"enabled": true,
"isBound": false,
"supportsBinding": false,
"privileged": false
}, {
"name": "gitlab",
"displayName": "gitlab",
"bindingUrl": "fake-binding-url",
"enabled": false,
"isBound": false,
"supportsBinding": false,
"privileged": false
},{
"name": "gitee",
"displayName": "gitee",
"enabled": false,
"isBound": false,
"supportsBinding": false,
"privileged": false
"name": "gitee",
"displayName": "gitee",
"authType": "OAUTH2",
"isBound": false,
"enabled": false,
"priority": 0,
"supportsBinding": false,
"privileged": false
},
{
"name": "github",
"displayName": "github",
"bindingUrl": "fake-binding-url",
"authType": "OAUTH2",
"isBound": false,
"enabled": false,
"priority": 0,
"supportsBinding": false,
"privileged": false
},
{
"name": "gitlab",
"displayName": "gitlab",
"bindingUrl": "fake-binding-url",
"authType": "OAUTH2",
"isBound": false,
"enabled": false,
"priority": 0,
"supportsBinding": false,
"privileged": false
}]
""",
JsonUtils.objectToJson(result),
@ -183,6 +190,14 @@ class AuthProviderServiceImplTest {
authProvider.getMetadata().setLabels(new HashMap<>());
authProvider.setSpec(new AuthProvider.AuthProviderSpec());
authProvider.getSpec().setDisplayName(name);
authProvider.getSpec().setAuthType(AuthProvider.AuthType.OAUTH2);
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));
}
}

View File

@ -1,19 +1,22 @@
<script lang="ts" setup>
import type { AuthProvider, ListedAuthProvider } from "@halo-dev/api-client";
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
import type { ListedAuthProvider } from "@halo-dev/api-client";
import {
AuthProviderSpecAuthTypeEnum,
consoleApiClient,
} from "@halo-dev/api-client";
import {
IconLockPasswordLine,
VCard,
VLoading,
VPageHeader,
} from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query";
import Fuse from "fuse.js";
import { computed, ref } from "vue";
import { VueDraggable } from "vue-draggable-plus";
import AuthProviderListItem from "./components/AuthProviderListItem.vue";
import { ref } from "vue";
import AuthProvidersSection from "./components/AuthProvidersSection.vue";
const authProviders = ref<ListedAuthProvider[]>([]);
const SYSTEM_CONFIGMAP_AUTH_PROVIDER = "authProvider";
const formAuthProviders = ref<ListedAuthProvider[]>([]);
const oauth2AuthProviders = ref<ListedAuthProvider[]>([]);
const { isLoading, refetch } = useQuery<ListedAuthProvider[]>({
queryKey: ["auth-providers"],
@ -23,26 +26,17 @@ const { isLoading, refetch } = useQuery<ListedAuthProvider[]>({
return data;
},
onSuccess(data) {
authProviders.value = data;
fuse = new Fuse(data, {
keys: ["name", "displayName"],
useExtendedSearch: true,
threshold: 0.2,
});
formAuthProviders.value = data.filter(
(authProvider) =>
authProvider.authType === AuthProviderSpecAuthTypeEnum.Form
);
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
const updating = ref(false);
@ -50,29 +44,23 @@ async function onSortUpdate() {
try {
updating.value = true;
const { data: rawAuthProviders } =
await coreApiClient.auth.authProvider.listAuthProvider();
const allAuthProviders = [
...formAuthProviders.value,
...oauth2AuthProviders.value,
].filter(Boolean);
const authProviderNames = authProviders.value.map((item) => item.name);
const sortedAuthProviders = authProviderNames
.map((name) => {
const authProvider = rawAuthProviders.items.find(
(item) => item.metadata.name === name
);
if (authProvider) {
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,
});
}
await consoleApiClient.configMap.system.updateSystemConfigByGroup({
group: SYSTEM_CONFIGMAP_AUTH_PROVIDER,
body: {
states: allAuthProviders.map((authProvider, index) => {
return {
name: authProvider.name,
enabled: authProvider.enabled,
priority: index,
};
}),
},
});
} finally {
await refetch();
updating.value = false;
@ -87,46 +75,25 @@ async function onSortUpdate() {
</template>
</VPageHeader>
<div class="m-0 md:m-4">
<VCard :body-class="['!p-0']">
<template #header>
<div class="block w-full bg-gray-50 px-4 py-3">
<div
class="relative flex flex-col items-start sm:flex-row sm:items-center"
>
<div class="flex w-full flex-1 sm:w-auto">
<FormKit
v-model="keyword"
:placeholder="$t('core.common.placeholder.search')"
type="text"
></FormKit>
</div>
</div>
</div>
</template>
<VLoading v-if="isLoading" />
<Transition v-else appear name="fade">
<VueDraggable
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 class="m-0 space-y-5 md:m-4">
<VLoading v-if="isLoading" />
<TransitionGroup v-else appear name="fade">
<AuthProvidersSection
:key="AuthProviderSpecAuthTypeEnum.Form"
v-model="formAuthProviders"
:title="$t('core.identity_authentication.list.types.form')"
:loading="updating"
@update="onSortUpdate"
/>
<AuthProvidersSection
v-if="oauth2AuthProviders.length"
:key="AuthProviderSpecAuthTypeEnum.Oauth2"
v-model="oauth2AuthProviders"
:title="$t('core.identity_authentication.list.types.oauth2')"
:loading="updating"
@update="onSortUpdate"
/>
</TransitionGroup>
</div>
</template>

View File

@ -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>

View File

@ -16,6 +16,7 @@ export default definePlugin({
meta: {
title: "core.identity_authentication.title",
searchable: true,
permissions: ["*"],
},
},
{
@ -24,6 +25,7 @@ export default definePlugin({
component: AuthProviderDetail,
meta: {
title: "core.identity_authentication.detail.title",
permissions: ["*"],
},
},
],

View File

@ -292,12 +292,14 @@ function onGrantPermissionModalClose() {
</template>
{{ $t("core.user.actions.roles") }}
</VButton>
<VButton :route="{ name: 'AuthProviders' }" size="sm" type="default">
<template #icon>
<IconLockPasswordLine class="h-full w-full" />
</template>
{{ $t("core.user.actions.identity_authentication") }}
</VButton>
<HasPermission :permissions="['*']">
<VButton :route="{ name: 'AuthProviders' }" size="sm" type="default">
<template #icon>
<IconLockPasswordLine class="h-full w-full" />
</template>
{{ $t("core.user.actions.identity_authentication") }}
</VButton>
</HasPermission>
<VButton
v-permission="['system:users:manage']"
type="secondary"

View File

@ -31,7 +31,7 @@ export interface AuthProviderSpec {
* @type {string}
* @memberof AuthProviderSpec
*/
'authType'?: AuthProviderSpecAuthTypeEnum;
'authType': AuthProviderSpecAuthTypeEnum;
/**
* Authentication url of the auth provider
* @type {string}
@ -80,12 +80,6 @@ export interface AuthProviderSpec {
* @memberof AuthProviderSpec
*/
'method'?: string;
/**
*
* @type {number}
* @memberof AuthProviderSpec
*/
'priority'?: number;
/**
*
* @type {boolean}

View File

@ -20,6 +20,12 @@
* @interface ListedAuthProvider
*/
export interface ListedAuthProvider {
/**
*
* @type {string}
* @memberof ListedAuthProvider
*/
'authType'?: ListedAuthProviderAuthTypeEnum;
/**
*
* @type {string}
@ -74,6 +80,12 @@ export interface ListedAuthProvider {
* @memberof ListedAuthProvider
*/
'name': string;
/**
*
* @type {number}
* @memberof ListedAuthProvider
*/
'priority'?: number;
/**
*
* @type {boolean}
@ -100,3 +112,11 @@ export interface ListedAuthProvider {
'website'?: string;
}
export const ListedAuthProviderAuthTypeEnum = {
Form: 'FORM',
Oauth2: 'OAUTH2'
} as const;
export type ListedAuthProviderAuthTypeEnum = typeof ListedAuthProviderAuthTypeEnum[keyof typeof ListedAuthProviderAuthTypeEnum];

View File

@ -1140,6 +1140,10 @@ core:
local: Login with credentials
description:
local: Default login method built into Halo
list:
types:
form: Basic Authentication Method
oauth2: Third-party Authentication Method
detail:
title: Identity authentication detail
fields:

View File

@ -1068,6 +1068,10 @@ core:
local: 账号密码登录
description:
local: Halo 内置的默认登录方式
list:
types:
form: 基础认证方式
oauth2: 三方认证方式
detail:
title: 身份认证详情
fields:

View File

@ -1045,6 +1045,10 @@ core:
local: 帳號密碼登入
description:
local: Halo 內建的默認登入方式
list:
types:
form: 基礎認證方式
oauth2: 三方認證方式
detail:
title: 身份認證詳情
fields: