feat: add basic functions of demo mode (#569)

* feat: add basic functions of demo mode

* feat: add @disableApi annotation to some API that should be disabled

* feat: add DisableApi test case

* fix: change DisableApi annotation name to DisableOnConditation

* Fix test cases

* Add more strict test on failing case

Co-authored-by: John Niang <johnniang@foxmail.com>
pull/575/head
guqing 2020-02-14 22:08:38 +08:00 committed by GitHub
parent df3601a751
commit b78e2161e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 244 additions and 0 deletions

View File

@ -6,6 +6,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import run.halo.app.Application;
import run.halo.app.cache.lock.CacheLock;
import run.halo.app.model.annotation.DisableOnCondition;
import run.halo.app.model.dto.EnvironmentDTO;
import run.halo.app.model.dto.StatisticDTO;
import run.halo.app.model.params.LoginParam;
@ -66,6 +67,7 @@ public class AdminController {
}
@PutMapping("password/reset")
@DisableOnCondition
@ApiOperation("Resets password by verify code")
public void resetPassword(@RequestBody @Valid ResetPasswordParam param) {
adminService.resetPasswordByCode(param);
@ -104,12 +106,14 @@ public class AdminController {
}
@PutMapping("spring/application.yaml")
@DisableOnCondition
@ApiOperation("Updates application config content")
public void updateSpringApplicationConfig(@RequestParam(name = "content") String content) {
adminService.updateApplicationConfig(content);
}
@PostMapping(value = {"halo/restart", "spring/restart"})
@DisableOnCondition
@ApiOperation("Restarts halo server")
public void restartApplication() {
Application.restart();

View File

@ -6,6 +6,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import run.halo.app.model.annotation.DisableOnCondition;
import run.halo.app.model.dto.OptionDTO;
import run.halo.app.model.dto.OptionSimpleDTO;
import run.halo.app.model.entity.Option;
@ -43,6 +44,7 @@ public class OptionController {
}
@PostMapping("saving")
@DisableOnCondition
@ApiOperation("Saves options")
public void saveOptions(@Valid @RequestBody List<OptionParam> optionParams) {
optionService.save(optionParams);
@ -73,12 +75,14 @@ public class OptionController {
}
@PostMapping
@DisableOnCondition
@ApiOperation("Creates option")
public void createBy(@RequestBody @Valid OptionParam optionParam) {
optionService.save(optionParam);
}
@PutMapping("{optionId:\\d+}")
@DisableOnCondition
@ApiOperation("Updates option")
public void updateBy(@PathVariable("optionId") Integer optionId,
@RequestBody @Valid OptionParam optionParam) {
@ -86,12 +90,14 @@ public class OptionController {
}
@DeleteMapping("{optionId:\\d+}")
@DisableOnCondition
@ApiOperation("Deletes option")
public void deletePermanently(@PathVariable("optionId") Integer optionId) {
optionService.removePermanently(optionId);
}
@PostMapping("map_view/saving")
@DisableOnCondition
@ApiOperation("Saves options by option map")
public void saveOptionsWithMapView(@RequestBody Map<String, Object> optionMap) {
optionService.save(optionMap);

View File

@ -2,6 +2,7 @@ package run.halo.app.controller.admin.api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import run.halo.app.model.annotation.DisableOnCondition;
import run.halo.app.model.dto.UserDTO;
import run.halo.app.model.entity.User;
import run.halo.app.model.params.PasswordParam;
@ -49,6 +50,7 @@ public class UserController {
}
@PutMapping("profiles/password")
@DisableOnCondition
@ApiOperation("Updates user's password")
public BaseResponse updatePassword(@RequestBody @Valid PasswordParam passwordParam, User user) {
userService.updatePassword(passwordParam.getOldPassword(), passwordParam.getNewPassword(), user.getId());

View File

@ -0,0 +1,45 @@
package run.halo.app.handler.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import run.halo.app.config.properties.HaloProperties;
import run.halo.app.exception.ForbiddenException;
import run.halo.app.model.annotation.DisableOnCondition;
import run.halo.app.model.enums.Mode;
/**
* DisableApi
*
* @author guqing
* @date 2020-02-14 14:08
*/
@Aspect
@Slf4j
@Component
public class DisableOnConditionAspect {
private final HaloProperties haloProperties;
public DisableOnConditionAspect(HaloProperties haloProperties) {
this.haloProperties = haloProperties;
}
@Pointcut("@annotation(run.halo.app.model.annotation.DisableOnCondition)")
public void pointcut() {
}
@Around("pointcut() && @annotation(disableApi)")
public Object around(ProceedingJoinPoint joinPoint,
DisableOnCondition disableApi) throws Throwable {
Mode mode = disableApi.mode();
if (haloProperties.getMode().equals(mode)) {
throw new ForbiddenException("禁止访问");
}
return joinPoint.proceed();
}
}

View File

@ -0,0 +1,22 @@
package run.halo.app.model.annotation;
import org.springframework.core.annotation.AliasFor;
import run.halo.app.model.enums.Mode;
import java.lang.annotation.*;
/**
* 访api
* @author guqing
* @date 2020-02-14 13:48
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DisableOnCondition {
@AliasFor("mode")
Mode value() default Mode.DEMO;
@AliasFor("value")
Mode mode() default Mode.DEMO;
}

View File

@ -13,6 +13,7 @@ import org.springframework.lang.Nullable;
public enum Mode {
PRODUCTION,
DEVELOPMENT,
DEMO,
TEST;
/**

View File

@ -0,0 +1,65 @@
server:
port: 8090
forward-headers-strategy: native
compression:
enabled: false
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
devtools:
add-properties: false
output:
ansi:
enabled: always
datasource:
type: com.zaxxer.hikari.HikariDataSource
# H2 database configuration.
driver-class-name: org.h2.Driver
url: jdbc:h2:file:~/.halo/db/halo
username: admin
password: 123456
# MySQL database configuration.
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://127.0.0.1:3306/halodb?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
# username: root
# password: 123456
h2:
console:
settings:
web-allow-others: false
path: /h2-console
enabled: false
jpa:
hibernate:
ddl-auto: update
show-sql: false
open-in-view: false
flyway:
enabled: false
servlet:
multipart:
max-file-size: 10240MB
max-request-size: 10240MB
management:
endpoints:
web:
base-path: /api/admin/actuator
exposure:
include: ['httptrace', 'metrics','env','logfile','health']
logging:
level:
run.halo.app: INFO
file:
path: ${user.home}/.halo/logs
halo:
download-timeout: 5m
doc-disabled: false
production-env: false
auth-enabled: true
mode: demo
workDir: ${user.home}/halo-demo/
cache: level

View File

@ -0,0 +1,29 @@
package run.halo.app.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import run.halo.app.model.annotation.DisableOnCondition;
import run.halo.app.model.support.BaseResponse;
/**
* DisableApi
*
* @author guqing
* @date 2020-02-14 17:51
*/
@RestController
@RequestMapping("/api/admin/test/disableOnCondition")
public class DisableOnConditionController {
@GetMapping("/no")
@DisableOnCondition
public BaseResponse<String> blockAccess() {
return BaseResponse.ok("测试静止访问");
}
@GetMapping("/yes")
public BaseResponse<String> ableAccess() {
return BaseResponse.ok("成功");
}
}

View File

@ -0,0 +1,70 @@
package run.halo.app.handler.aspect;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.util.NestedServletException;
import run.halo.app.exception.ForbiddenException;
import run.halo.app.model.properties.PrimaryProperties;
import run.halo.app.service.OptionService;
import static org.hamcrest.core.Is.is;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* @author guqing
* @date 2020-02-14 17:06
*/
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "halo.auth-enabled=false")
@ActiveProfiles("demo")
@AutoConfigureMockMvc
class DisableOnConditionAspectTest {
@Autowired
MockMvc mvc;
@Autowired
OptionService optionService;
static final String REQUEST_URI = "/api/admin/test/disableOnCondition";
@BeforeEach
void setUp() {
optionService.saveProperty(PrimaryProperties.IS_INSTALLED, "true");
}
@Test()
void blockAccessTest() throws Exception {
Throwable t = null;
try {
mvc.perform(get(REQUEST_URI + "/no"))
.andDo(print())
.andReturn();
} catch (NestedServletException nse) {
t = nse;
}
Assertions.assertNotNull(t);
final Throwable rootException = t;
Assertions.assertThrows(ForbiddenException.class, () -> {
throw rootException.getCause();
});
}
@Test
void ableAccessTest() throws Exception {
mvc.perform(get(REQUEST_URI + "/yes"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.status", is(HttpStatus.OK.value())));
}
}