Skip to content

Commit

Permalink
feat: add author pages for theme-side
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Dec 12, 2022
1 parent d060788 commit 6f5ac7d
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/main/java/run/halo/app/core/extension/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ public static class UserStatus {

private Instant lastLoginAt;

private String permalink;

private List<LoginHistory> loginHistories;

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,113 @@
package run.halo.app.core.extension.reconciler;

import java.util.HashSet;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import run.halo.app.content.permalinks.ExtensionLocator;
import run.halo.app.core.extension.User;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.controller.Controller;
import run.halo.app.extension.controller.ControllerBuilder;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.extension.controller.Reconciler.Request;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.infra.utils.PathUtils;
import run.halo.app.theme.router.PermalinkIndexDeleteCommand;
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;

@Slf4j
@Component
@AllArgsConstructor
public class UserReconciler implements Reconciler<Request> {

private static final String FINALIZER_NAME = "user-protection";
private final ExtensionClient client;

public UserReconciler(ExtensionClient client) {
this.client = client;
}
private final ApplicationEventPublisher eventPublisher;
private final ExternalUrlSupplier externalUrlSupplier;

@Override
public Result reconcile(Request request) {
//TODO Add reconciliation logic here for User extension.
client.fetch(User.class, request.name()).ifPresent(user -> {
if (user.getMetadata().getDeletionTimestamp() != null) {
cleanUpResourcesAndRemoveFinalizer(request.name());
return;
}

addFinalizerIfNecessary(user);
updatePermalink(request.name());
});
return new Result(false, null);
}

private void updatePermalink(String name) {
client.fetch(User.class, name).ifPresent(user -> {
if (AnonymousUserConst.isAnonymousUser(name)) {
// anonymous user is not allowed to have permalink
return;
}

final User oldUser = JsonUtils.deepCopy(user);
if (user.getStatus() == null) {
user.setStatus(new User.UserStatus());
}
User.UserStatus status = user.getStatus();
status.setPermalink(getUserPermalink(user));

ExtensionLocator extensionLocator = getExtensionLocator(name);
eventPublisher.publishEvent(
new PermalinkIndexUpdateCommand(this, extensionLocator, status.getPermalink()));

if (!user.equals(oldUser)) {
client.update(user);
}
});
}

private static ExtensionLocator getExtensionLocator(String name) {
return new ExtensionLocator(GroupVersionKind.fromExtension(User.class), name,
name);
}

private String getUserPermalink(User user) {
return externalUrlSupplier.get()
.resolve(PathUtils.combinePath("authors", user.getMetadata().getName()))
.normalize().toString();
}

private void addFinalizerIfNecessary(User oldUser) {
Set<String> finalizers = oldUser.getMetadata().getFinalizers();
if (finalizers != null && finalizers.contains(FINALIZER_NAME)) {
return;
}
client.fetch(User.class, oldUser.getMetadata().getName())
.ifPresent(user -> {
Set<String> newFinalizers = user.getMetadata().getFinalizers();
if (newFinalizers == null) {
newFinalizers = new HashSet<>();
user.getMetadata().setFinalizers(newFinalizers);
}
newFinalizers.add(FINALIZER_NAME);
client.update(user);
});
}

private void cleanUpResourcesAndRemoveFinalizer(String userName) {
client.fetch(User.class, userName).ifPresent(user -> {
eventPublisher.publishEvent(
new PermalinkIndexDeleteCommand(this, getExtensionLocator(userName)));

if (user.getMetadata().getFinalizers() != null) {
user.getMetadata().getFinalizers().remove(FINALIZER_NAME);
}
client.update(user);
});
}

@Override
public Controller setupWith(ControllerBuilder builder) {
return builder
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/run/halo/app/infra/AnonymousUserConst.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ public interface AnonymousUserConst {
String PRINCIPAL = "anonymousUser";

String Role = "anonymous";

static boolean isAnonymousUser(String principal) {
return PRINCIPAL.equals(principal);
}
}
4 changes: 3 additions & 1 deletion src/main/java/run/halo/app/theme/DefaultTemplateEnum.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public enum DefaultTemplateEnum {

TAGS("tags"),

SINGLE_PAGE("page");
SINGLE_PAGE("page"),

AUTHOR("author");

private final String value;

Expand Down
40 changes: 40 additions & 0 deletions src/main/java/run/halo/app/theme/finders/vo/UserVo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package run.halo.app.theme.finders.vo;

import java.util.List;
import lombok.Builder;
import lombok.Value;
import org.apache.commons.lang3.ObjectUtils;
import run.halo.app.core.extension.User;
import run.halo.app.extension.MetadataOperator;
import run.halo.app.infra.utils.JsonUtils;

@Value
@Builder
public class UserVo {
MetadataOperator metadata;

User.UserSpec spec;

User.UserStatus status;

/**
* Converts to {@link UserVo} from {@link User}.
*
* @param user user extension
* @return user value object.
*/
public static UserVo from(User user) {
User.UserStatus statusCopy =
JsonUtils.deepCopy(ObjectUtils.defaultIfNull(user.getStatus(), new User.UserStatus()));
statusCopy.setLoginHistories(List.of());
statusCopy.setLastLoginAt(null);

User.UserSpec userSpecCopy = JsonUtils.deepCopy(user.getSpec());
userSpecCopy.setPassword("[PROTECTED]");
return UserVo.builder()
.metadata(user.getMetadata())
.spec(userSpecCopy)
.status(statusCopy)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private SystemSetting.ThemeRouteRules getPermalinkRules() {
public String getPattern(DefaultTemplateEnum defaultTemplateEnum) {
SystemSetting.ThemeRouteRules permalinkRules = getPermalinkRules();
return switch (defaultTemplateEnum) {
case INDEX, SINGLE_PAGE -> null;
case INDEX, SINGLE_PAGE, AUTHOR -> null;
case POST -> permalinkRules.getPost();
case ARCHIVES -> permalinkRules.getArchives();
case CATEGORY, CATEGORIES -> permalinkRules.getCategories();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package run.halo.app.theme.router.strategy;

import java.util.Map;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.User;
import run.halo.app.extension.GroupVersionKind;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.vo.UserVo;

/**
* Author route strategy.
*
* @author guqing
* @since 2.0.1
*/
@Component
@AllArgsConstructor
public class AuthorRouteStrategy implements DetailsPageRouteHandlerStrategy {

private final ReactiveExtensionClient client;

@Override
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
String name) {
return request -> ServerResponse.ok()
.render(DefaultTemplateEnum.AUTHOR.getValue(),
Map.of("name", name,
"author", getByName(name),
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.AUTHOR.getValue()
)
);
}

private Mono<UserVo> getByName(String name) {
return client.fetch(User.class, name)
.map(UserVo::from);
}

@Override
public boolean supports(GroupVersionKind gvk) {
return GroupVersionKind.fromExtension(User.class).equals(gvk);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package run.halo.app.core.extension.reconciler;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationEventPublisher;
import run.halo.app.core.extension.User;
import run.halo.app.extension.ExtensionClient;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.controller.Reconciler;
import run.halo.app.infra.AnonymousUserConst;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.theme.router.PermalinkIndexUpdateCommand;

/**
* Tests for {@link UserReconciler}.
*
* @author guqing
* @since 2.0.1
*/
@ExtendWith(MockitoExtension.class)
class UserReconcilerTest {
@Mock
private ApplicationEventPublisher eventPublisher;

@Mock
private ExternalUrlSupplier externalUrlSupplier;

@Mock
private ExtensionClient client;

@InjectMocks
private UserReconciler userReconciler;

@Test
void permalinkForFakeUser() throws URISyntaxException {
when(externalUrlSupplier.get()).thenReturn(new URI("http://localhost:8090"));

when(client.fetch(eq(User.class), eq("fake-user")))
.thenReturn(Optional.of(user("fake-user")));
userReconciler.reconcile(new Reconciler.Request("fake-user"));
verify(client, times(1)).update(any(User.class));
verify(eventPublisher, times(1)).publishEvent(any(PermalinkIndexUpdateCommand.class));

ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(client, times(1)).update(captor.capture());
assertThat(captor.getValue().getStatus().getPermalink())
.isEqualTo("http://localhost:8090/authors/fake-user");
}

@Test
void permalinkForAnonymousUser() {
when(client.fetch(eq(User.class), eq(AnonymousUserConst.PRINCIPAL)))
.thenReturn(Optional.of(user(AnonymousUserConst.PRINCIPAL)));
userReconciler.reconcile(new Reconciler.Request(AnonymousUserConst.PRINCIPAL));
verify(client, times(0)).update(any(User.class));
verify(eventPublisher, times(0)).publishEvent(any(PermalinkIndexUpdateCommand.class));
}

User user(String name) {
User user = new User();
user.setMetadata(new Metadata());
user.getMetadata().setName(name);
user.getMetadata().setFinalizers(Set.of("user-protection"));
return user;
}
}
Loading

0 comments on commit 6f5ac7d

Please sign in to comment.