feat: init api (#2)

This commit is contained in:
vran
2022-01-24 22:58:47 +08:00
committed by GitHub
parent 643d182d5f
commit 61e5708196
205 changed files with 17366 additions and 59 deletions

10
api/Dockerfile Normal file
View File

@@ -0,0 +1,10 @@
FROM openjdk:11.0.13-jre
ARG service_name_folder
WORKDIR /app
ADD databasir.jar /app
EXPOSE 8080
#-Ddatabasir.datasource.username=${databasir.datasource.username}
#-Ddatabasir.datasource.password=${databasir.datasource.password}
#-Ddatabasir.datasource.url=${databasir.datasource.url}
ENTRYPOINT ["sh", "-c","java ${JAVA_OPTS} -jar /app/databasir.jar"]

41
api/build.gradle Normal file
View File

@@ -0,0 +1,41 @@
plugins {
id 'io.spring.dependency-management'
id 'org.springframework.boot' apply false
}
bootJar {
archiveBaseName = 'databasir'
archiveVersion = ''
enabled = true
}
bootBuildImage {
imageName = "${project.group}/databasir:${project.version}"
publish = false
}
dependencies {
implementation project(":common")
implementation project(":plugin")
implementation project(":core")
implementation project(":dao")
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.flywaydb:flyway-core'
}
/**
* Docker
*/
task copyDockerfile(type: Copy) {
from("Dockerfile")
into("build/libs")
}
bootJar.finalizedBy copyDockerfile
assemble.finalizedBy copyDockerfile

View File

@@ -0,0 +1,13 @@
package com.databasir;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration;
@SpringBootApplication(exclude = {R2dbcAutoConfiguration.class})
public class DatabasirApplication {
public static void main(String[] args) {
SpringApplication.run(DatabasirApplication.class, args);
}
}

View File

@@ -0,0 +1,44 @@
package com.databasir.api;
import com.databasir.common.JsonData;
import com.databasir.core.domain.document.data.DatabaseDocumentResponse;
import com.databasir.core.domain.document.data.DatabaseDocumentVersionResponse;
import com.databasir.core.domain.document.service.DocumentService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@Validated
public class DocumentController {
private final DocumentService documentService;
@PostMapping(Routes.Document.SYNC_ONE)
public JsonData<Void> sync(@PathVariable Integer projectId) {
documentService.syncByProjectId(projectId);
return JsonData.ok();
}
@GetMapping(Routes.Document.GET_ONE)
public JsonData<DatabaseDocumentResponse> getByProjectId(@PathVariable Integer projectId,
@RequestParam(required = false) Long version) {
return documentService.getOneByProjectId(projectId, version)
.map(JsonData::ok)
.orElseGet(JsonData::ok);
}
@GetMapping(Routes.Document.LIST_VERSIONS)
public JsonData<Page<DatabaseDocumentVersionResponse>> getVersionsByProjectId(@PathVariable Integer projectId,
@PageableDefault(sort = "id", direction = Sort.Direction.DESC)
Pageable page) {
return JsonData.ok(documentService.getVersionsBySchemaSourceId(projectId, page));
}
}

View File

@@ -0,0 +1,106 @@
package com.databasir.api;
import com.databasir.api.validator.UserOperationValidator;
import com.databasir.common.JsonData;
import com.databasir.core.domain.group.data.*;
import com.databasir.core.domain.group.service.GroupService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.Arrays;
import java.util.List;
@RestController
@RequiredArgsConstructor
@Validated
public class GroupController {
private final GroupService groupService;
private final UserOperationValidator userOperationValidator;
@PostMapping(Routes.Group.CREATE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
public JsonData<Void> create(@RequestBody @Valid GroupCreateRequest request) {
groupService.create(request);
return JsonData.ok();
}
@PatchMapping(Routes.Group.UPDATE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER'.concat('?groupId='.concat(#request.id)))")
public JsonData<Void> update(@RequestBody @Valid GroupUpdateRequest request) {
groupService.update(request);
return JsonData.ok();
}
@GetMapping(Routes.Group.LIST)
public JsonData<Page<GroupPageResponse>> list(@PageableDefault(sort = "id", direction = Sort.Direction.DESC)
Pageable page,
GroupPageCondition condition) {
return JsonData.ok(groupService.list(page, condition));
}
@DeleteMapping(Routes.Group.DELETE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER'.concat('?groupId='.concat(#groupId)))")
public JsonData<Void> deleteById(@PathVariable Integer groupId) {
groupService.delete(groupId);
return JsonData.ok();
}
@GetMapping(Routes.Group.GET_ONE)
public JsonData<GroupResponse> getOne(@PathVariable Integer groupId) {
return JsonData.ok(groupService.get(groupId));
}
@GetMapping(Routes.Group.MEMBERS)
public JsonData<Page<GroupMemberPageResponse>> listGroupMembers(@PathVariable Integer groupId,
@PageableDefault(sort = "user_role.create_at", direction = Sort.Direction.DESC)
Pageable pageable,
GroupMemberPageCondition condition) {
return JsonData.ok(groupService.listGroupMembers(groupId, pageable, condition));
}
@PostMapping(Routes.Group.ADD_MEMBER)
@PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER'.concat('?groupId='.concat(#groupId)))")
public JsonData<Void> addGroupMember(@PathVariable Integer groupId,
@RequestBody @Valid GroupMemberCreateRequest request) {
userOperationValidator.forbiddenIfUpdateSelfRole(request.getUserId());
List<String> groupRoles = Arrays.asList("GROUP_OWNER", "GROUP_MEMBER");
if (!groupRoles.contains(request.getRole())) {
throw new IllegalArgumentException("role should be GROUP_OWNER or GROUP_MEMBER");
}
groupService.addMember(groupId, request);
return JsonData.ok();
}
@DeleteMapping(Routes.Group.DELETE_MEMBER)
@PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER'.concat('?groupId='.concat(#groupId)))")
public JsonData<Void> removeGroupMember(@PathVariable Integer groupId,
@PathVariable Integer userId) {
userOperationValidator.forbiddenIfUpdateSelfRole(userId);
groupService.removeMember(groupId, userId);
return JsonData.ok();
}
@PatchMapping(Routes.Group.UPDATE_MEMBER)
@PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER'.concat('?groupId='.concat(#groupId)))")
public JsonData<Void> updateGroupMemberRole(@PathVariable Integer groupId,
@PathVariable Integer userId,
@RequestBody GroupMemberRoleUpdateRequest request) {
userOperationValidator.forbiddenIfUpdateSelfRole(userId);
List<String> groupRoles = Arrays.asList("GROUP_OWNER", "GROUP_MEMBER");
if (!groupRoles.contains(request.getRole())) {
throw new IllegalArgumentException("role should be GROUP_OWNER or GROUP_MEMBER");
}
groupService.changeMemberRole(groupId, userId, request.getRole());
return JsonData.ok();
}
}

View File

@@ -0,0 +1,55 @@
package com.databasir.api;
import com.databasir.common.DatabasirException;
import com.databasir.common.JsonData;
import com.databasir.common.exception.InvalidTokenException;
import com.databasir.core.domain.DomainErrors;
import com.databasir.core.domain.login.data.AccessTokenRefreshRequest;
import com.databasir.core.domain.login.data.AccessTokenRefreshResponse;
import com.databasir.core.domain.login.service.LoginService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.util.Objects;
@RestController
@RequiredArgsConstructor
@Validated
@Slf4j
public class LoginController {
private final AuthenticationManager authenticationManager;
private final LoginService loginService;
@GetMapping(Routes.Login.LOGOUT)
public JsonData<Void> logout() {
SecurityContextHolder.clearContext();
return JsonData.ok();
}
@PostMapping(Routes.Login.REFRESH_ACCESS_TOKEN)
public JsonData<AccessTokenRefreshResponse> refreshAccessTokens(@RequestBody @Valid AccessTokenRefreshRequest request,
HttpServletResponse response) {
try {
return JsonData.ok(loginService.refreshAccessTokens(request));
} catch (DatabasirException e) {
if (Objects.equals(e.getErrCode(), DomainErrors.ACCESS_TOKEN_REFRESH_INVALID.getErrCode())) {
throw new InvalidTokenException(DomainErrors.ACCESS_TOKEN_REFRESH_INVALID);
}
if (Objects.equals(e.getErrCode(), DomainErrors.REFRESH_TOKEN_EXPIRED.getErrCode())) {
throw new InvalidTokenException(DomainErrors.REFRESH_TOKEN_EXPIRED);
}
throw e;
}
}
}

View File

@@ -0,0 +1,58 @@
package com.databasir.api;
import com.databasir.common.JsonData;
import com.databasir.core.domain.project.data.*;
import com.databasir.core.domain.project.service.ProjectService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequiredArgsConstructor
@Validated
public class ProjectController {
private final ProjectService projectService;
@PostMapping(Routes.GroupProject.CREATE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER?groupId='+#request.groupId, 'GROUP_MEMBER?groupId='+#request.groupId)")
public JsonData<Void> create(@RequestBody @Valid ProjectCreateRequest request) {
projectService.create(request);
return JsonData.ok();
}
@PatchMapping(Routes.GroupProject.UPDATE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER?groupId='+#groupId, 'GROUP_MEMBER?groupId='+#groupId)")
public JsonData<Void> update(@RequestBody @Valid ProjectUpdateRequest request,
@PathVariable Integer groupId) {
projectService.update(groupId, request);
return JsonData.ok();
}
@DeleteMapping(Routes.GroupProject.DELETE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER?groupId='+#groupId, 'GROUP_MEMBER?groupId='+#groupId)")
public JsonData<Void> delete(@PathVariable Integer groupId,
@PathVariable Integer projectId) {
projectService.delete(projectId);
return JsonData.ok();
}
@GetMapping(Routes.GroupProject.GET_ONE)
public JsonData<ProjectDetailResponse> getOne(@PathVariable Integer projectId) {
return JsonData.ok(projectService.getOne(projectId));
}
@GetMapping(Routes.GroupProject.LIST)
public JsonData<Page<ProjectSimpleResponse>> list(@PageableDefault(sort = "id", direction = Sort.Direction.DESC)
Pageable page,
ProjectListCondition condition) {
return JsonData.ok(projectService.list(page, condition));
}
}

View File

@@ -0,0 +1,84 @@
package com.databasir.api;
public interface Routes {
String BASE = "/api/v1.0";
interface User {
String LIST = BASE + "/users";
String GET_ONE = BASE + "/users/{userId}";
String ENABLE = BASE + "/users/{userId}/enable";
String DISABLE = BASE + "/users/{userId}/disable";
String CREATE = BASE + "/users";
String UPDATE_PASSWORD = BASE + "/users/{userId}/password";
String UPDATE_NICKNAME = BASE + "/users/{userId}/nickname";
String RENEW_PASSWORD = BASE + "/users/{userId}/renew_password";
String ADD_OR_REMOVE_SYS_OWNER = BASE + "/users/{userId}/sys_owners";
}
interface Group {
String LIST = BASE + "/groups";
String GET_ONE = BASE + "/groups/{groupId}";
String CREATE = BASE + "/groups";
String UPDATE = BASE + "/groups";
String DELETE = BASE + "/groups/{groupId}";
String MEMBERS = GET_ONE + "/members";
String DELETE_MEMBER = GET_ONE + "/members/{userId}";
String ADD_MEMBER = GET_ONE + "/members";
String UPDATE_MEMBER = GET_ONE + "/members/{userId}";
}
interface GroupProject {
String LIST = BASE + "/projects";
String GET_ONE = BASE + "/projects/{projectId}";
String CREATE = BASE + "/projects";
String UPDATE = BASE + "/groups/{groupId}/projects";
String DELETE = BASE + "/groups/{groupId}/projects/{projectId}";
}
interface Document {
String GET_ONE = BASE + "/projects/{projectId}/documents";
String SYNC_ONE = BASE + "/projects/{projectId}/documents";
String LIST_VERSIONS = BASE + "/projects/{projectId}/document_versions";
}
interface Setting {
String GET_SYS_EMAIL = BASE + "/settings/sys_email";
String UPDATE_SYS_EMAIL = BASE + "/settings/sys_email";
}
interface Login {
String LOGOUT = "/logout";
String REFRESH_ACCESS_TOKEN = "/access_tokens";
}
}

View File

@@ -0,0 +1,35 @@
package com.databasir.api;
import com.databasir.common.JsonData;
import com.databasir.core.domain.system.data.SystemEmailResponse;
import com.databasir.core.domain.system.data.SystemEmailUpdateRequest;
import com.databasir.core.domain.system.service.SystemService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequiredArgsConstructor
@Validated
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
public class SettingController {
private final SystemService systemService;
@GetMapping(Routes.Setting.GET_SYS_EMAIL)
public JsonData<SystemEmailResponse> getSystemEmailSetting() {
return systemService.getEmailSetting()
.map(JsonData::ok)
.orElseGet(JsonData::ok);
}
@PostMapping(Routes.Setting.UPDATE_SYS_EMAIL)
public JsonData<Void> updateSystemEmailSetting(@RequestBody @Valid SystemEmailUpdateRequest request) {
systemService.updateEmailSetting(request);
return JsonData.ok();
}
}

View File

@@ -0,0 +1,105 @@
package com.databasir.api;
import com.databasir.api.validator.UserOperationValidator;
import com.databasir.common.JsonData;
import com.databasir.common.exception.Forbidden;
import com.databasir.core.domain.user.data.*;
import com.databasir.core.domain.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequiredArgsConstructor
@Validated
public class UserController {
private final UserService userService;
private final UserOperationValidator userOperationValidator;
@GetMapping(Routes.User.LIST)
public JsonData<Page<UserPageResponse>> list(@PageableDefault(sort = "id", direction = Sort.Direction.DESC)
Pageable pageable,
UserPageCondition condition) {
return JsonData.ok(userService.list(pageable, condition));
}
@PostMapping(Routes.User.DISABLE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
public JsonData<Void> disableUser(@PathVariable Integer userId) {
userService.switchEnableStatus(userId, false);
return JsonData.ok();
}
@PostMapping(Routes.User.ENABLE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
public JsonData<Void> enableUser(@PathVariable Integer userId) {
userService.switchEnableStatus(userId, true);
return JsonData.ok();
}
@PostMapping(Routes.User.CREATE)
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
public JsonData<Void> create(@RequestBody @Valid UserCreateRequest request) {
userService.create(request);
return JsonData.ok();
}
@GetMapping(Routes.User.GET_ONE)
public JsonData<UserDetailResponse> getOne(@PathVariable Integer userId) {
return JsonData.ok(userService.get(userId));
}
@PostMapping(Routes.User.RENEW_PASSWORD)
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
public JsonData<Void> renewPassword(@PathVariable Integer userId) {
userService.renewPassword(userId);
return JsonData.ok();
}
@PostMapping(Routes.User.ADD_OR_REMOVE_SYS_OWNER)
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
public JsonData<Void> addSysOwner(@PathVariable Integer userId) {
userOperationValidator.forbiddenIfUpdateSelfRole(userId);
userService.addSysOwnerTo(userId);
return JsonData.ok();
}
@DeleteMapping(Routes.User.ADD_OR_REMOVE_SYS_OWNER)
@PreAuthorize("hasAnyAuthority('SYS_OWNER')")
public JsonData<Void> removeSysOwner(@PathVariable Integer userId) {
userOperationValidator.forbiddenIfUpdateSelfRole(userId);
userService.removeSysOwnerFrom(userId);
return JsonData.ok();
}
@PostMapping(Routes.User.UPDATE_PASSWORD)
public JsonData<Void> updatePassword(@PathVariable Integer userId,
@RequestBody @Valid UserPasswordUpdateRequest request) {
if (userOperationValidator.isMyself(userId)) {
userService.updatePassword(userId, request);
return JsonData.ok();
} else {
throw new Forbidden();
}
}
@PostMapping(Routes.User.UPDATE_NICKNAME)
public JsonData<Void> updateNickname(@PathVariable Integer userId,
@RequestBody @Valid UserNicknameUpdateRequest request) {
if (userOperationValidator.isMyself(userId)) {
userService.updateNickname(userId, request);
return JsonData.ok();
} else {
throw new Forbidden();
}
}
}

View File

@@ -0,0 +1,234 @@
package com.databasir.api.advice;
import com.databasir.common.DatabasirException;
import com.databasir.common.JsonData;
import com.databasir.common.SystemException;
import com.databasir.common.exception.Forbidden;
import com.databasir.common.exception.InvalidTokenException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Set;
@RestControllerAdvice
@Slf4j
public class DatabasirExceptionAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler({ConstraintViolationException.class})
public ResponseEntity<Object> handleConstraintViolationException(
ConstraintViolationException constraintViolationException, WebRequest request) {
String errorMsg = "";
String path = getPath(request);
Set<ConstraintViolation<?>> violations = constraintViolationException.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
errorMsg = item.getMessage();
log.warn("ConstraintViolationException, request: {}, exception: {}, invalid value: {}",
path, errorMsg, item.getInvalidValue());
break;
}
return handleNon200Response(errorMsg, HttpStatus.BAD_REQUEST, path);
}
@ExceptionHandler({InvalidTokenException.class})
protected ResponseEntity<Object> handleInvalidTokenException(InvalidTokenException ex, WebRequest request) {
String path = getPath(request);
log.warn("handle InvalidTokenException " + path + ", " + ex);
JsonData<Object> data = JsonData.error(ex.getErrCode(), ex.getErrMessage());
return handleNon200Response(ex.getMessage(), HttpStatus.UNAUTHORIZED, path, data);
}
@ExceptionHandler({IllegalArgumentException.class})
protected ResponseEntity<Object> handleIllegalArgument(IllegalArgumentException ex, WebRequest request) {
String path = getPath(request);
log.warn("handle illegalArgument " + path, ex);
return handleNon200Response(ex.getMessage(), HttpStatus.BAD_REQUEST, path);
}
@ExceptionHandler({AccessDeniedException.class})
protected ResponseEntity<Object> handleAccessDeniedException(AccessDeniedException ex, WebRequest request) {
String path = getPath(request);
log.warn("AccessDeniedException, request: {}, exception: {}", path, ex.getMessage());
return handleNon200Response(ex.getMessage(), HttpStatus.FORBIDDEN, path);
}
@ExceptionHandler({Forbidden.class})
protected ResponseEntity<Object> handleForbiddenException(Forbidden ex, WebRequest request) {
String path = getPath(request);
log.warn("Forbidden, request: {}, exception: {}", path, ex.getMessage());
return handleNon200Response(ex.getMessage(), HttpStatus.FORBIDDEN, path);
}
@ExceptionHandler(value = DatabasirException.class)
public ResponseEntity<Object> handleBusinessException(
DatabasirException databasirException, WebRequest request) {
String path = getPath(request);
JsonData<Void> body = JsonData.error(databasirException.getErrCode(), databasirException.getErrMessage());
if (databasirException.getCause() == null) {
log.warn("BusinessException, request: {}, exception: {}", path, databasirException.getErrMessage());
} else {
log.warn("BusinessException, request: " + path, databasirException);
}
return ResponseEntity.ok().body(body);
}
@ExceptionHandler({SystemException.class})
public ResponseEntity<Object> handleSystemException(SystemException systemException, WebRequest request) {
String path = getPath(request);
if (systemException.getCause() != null) {
log.error("SystemException, request: " + path
+ ", exception: " + systemException.getMessage() + ", caused by:", systemException.getCause());
} else {
log.error("SystemException, request: " + path + ", exception: " + systemException.getMessage());
}
return handleNon200Response(systemException.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, path);
}
@ExceptionHandler({Exception.class})
public ResponseEntity<Object> handleUnspecificException(Exception ex, WebRequest request) {
String path = getPath(request);
String errorMsg = ex.getMessage();
log.error("Unspecific exception, request: " + path + ", exception: " + errorMsg + ":", ex);
return handleNon200Response(errorMsg, HttpStatus.INTERNAL_SERVER_ERROR, path);
}
@Override
public ResponseEntity<Object> handleBindException(
BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String errorMsg = buildMessages(ex.getBindingResult());
log.warn("BindException, request: {}, exception: {}", getPath(request), errorMsg);
return handleOverriddenException(ex, headers, status, request, errorMsg);
}
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String errorMsg = buildMessages(ex.getBindingResult());
log.warn("MethodArgumentNotValidException, request: {}, exception: {}", getPath(request), errorMsg);
return handleOverriddenException(ex, headers, status, request, errorMsg);
}
@Override
public ResponseEntity<Object> handleTypeMismatch(
TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("TypeMismatchException, request: {}, exception: {}", getPath(request), ex.getMessage());
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
}
@Override
public ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("MissingServletRequestParameterException, request: {}, exception: {}",
getPath(request), ex.getMessage());
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
}
@Override
public ResponseEntity<Object> handleMissingServletRequestPart(
MissingServletRequestPartException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("MissingServletRequestPartException, request: {}, exception: {}", getPath(request), ex.getMessage());
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
}
@Override
public ResponseEntity<Object> handleHttpMessageNotReadable(
HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String errorMsg = ex.getMostSpecificCause().getMessage();
log.warn("HttpMessageNotReadableException, request: {}, exception: {}", getPath(request), errorMsg);
return handleOverriddenException(ex, headers, status, request, errorMsg);
}
@Override
public ResponseEntity<Object> handleServletRequestBindingException(
ServletRequestBindingException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("ServletRequestBindingException, request: {}, exception: {}", getPath(request), ex.getMessage());
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
}
@Override
public ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String errorMsg = ex.getMessage();
log.warn("HttpRequestMethodNotSupportedException, request: {}, exception: {}", getPath(request), errorMsg);
return handleOverriddenException(ex, headers, status, request, ex.getMessage());
}
@ExceptionHandler({BadCredentialsException.class})
protected ResponseEntity<Object> handleBadCredentialsException(BadCredentialsException ex, WebRequest request) {
String path = getPath(request);
JsonData<Void> body = JsonData.error("-1", "用户名或密码错误");
log.warn("BadCredentialsException, request: {}, exception: {}", path, ex.getMessage());
return ResponseEntity.ok().body(body);
}
private String buildMessages(BindingResult result) {
StringBuilder resultBuilder = new StringBuilder();
List<ObjectError> errors = result.getAllErrors();
for (ObjectError error : errors) {
if (error instanceof FieldError) {
FieldError fieldError = (FieldError) error;
String fieldName = fieldError.getField();
String fieldErrMsg = fieldError.getDefaultMessage();
resultBuilder.append(fieldName).append(" ").append(fieldErrMsg);
}
}
return resultBuilder.toString();
}
private ResponseEntity<Object> handleNon200Response(String errorMsg, HttpStatus httpStatus, String path) {
return ResponseEntity.status(httpStatus).body(null);
}
private ResponseEntity<Object> handleNon200Response(String errorMsg,
HttpStatus httpStatus,
String path,
Object body) {
return ResponseEntity.status(httpStatus).body(body);
}
private ResponseEntity<Object> handleOverriddenException(
Exception ex, HttpHeaders headers, HttpStatus status, WebRequest request, String errorMsg) {
return handleExceptionInternal(ex, null, headers, status, request);
}
private String getPath(WebRequest request) {
String description = request.getDescription(false);
return description.startsWith("uri=") ? description.substring(4) : description;
}
}

View File

@@ -0,0 +1,75 @@
package com.databasir.api.config;
import com.databasir.api.Routes;
import com.databasir.api.config.security.*;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final DatabasirUserDetailService databasirUserDetailService;
private final DatabasirAuthenticationEntryPoint databasirAuthenticationEntryPoint;
private final DatabasirJwtTokenFilter databasirJwtTokenFilter;
private final DatabasirAuthenticationFailureHandler databasirAuthenticationFailureHandler;
private final DatabasirAuthenticationSuccessHandler databasirAuthenticationSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http.csrf().disable();
http.cors();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.loginProcessingUrl("/login")
.failureHandler(databasirAuthenticationFailureHandler)
.successHandler(databasirAuthenticationSuccessHandler)
.and()
.authorizeRequests()
.antMatchers("/login", Routes.Login.REFRESH_ACCESS_TOKEN).permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(databasirAuthenticationEntryPoint);
http.addFilterBefore(
databasirJwtTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(databasirUserDetailService)
.passwordEncoder(bCryptPasswordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,18 @@
package com.databasir.api.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableSpringDataWebSupport
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "DELETE", "PATCH", "PUT");
}
}

View File

@@ -0,0 +1,26 @@
package com.databasir.api.config.security;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Service
@RequiredArgsConstructor
@Slf4j
public class DatabasirAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.warn("验证未通过. 提示信息 - {}", authException.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}

View File

@@ -0,0 +1,49 @@
package com.databasir.api.config.security;
import com.databasir.common.JsonData;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Component
@RequiredArgsConstructor
public class DatabasirAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
if (exception instanceof BadCredentialsException) {
JsonData<Void> data = JsonData.error("-1", "用户名或密码错误");
String jsonString = objectMapper.writeValueAsString(data);
response.setStatus(HttpStatus.OK.value());
response.getOutputStream().write(jsonString.getBytes(StandardCharsets.UTF_8));
} else if (exception instanceof DisabledException) {
JsonData<Void> data = JsonData.error("-1", "用户已禁用");
String jsonString = objectMapper.writeValueAsString(data);
response.setStatus(HttpStatus.OK.value());
response.getOutputStream().write(jsonString.getBytes(StandardCharsets.UTF_8));
} else {
JsonData<Void> data = JsonData.error("-1", "未登录或未授权用户");
String jsonString = objectMapper.writeValueAsString(data);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getOutputStream().write(jsonString.getBytes(StandardCharsets.UTF_8));
}
}
}

View File

@@ -0,0 +1,61 @@
package com.databasir.api.config.security;
import com.databasir.common.JsonData;
import com.databasir.core.domain.login.data.LoginKeyResponse;
import com.databasir.core.domain.login.service.LoginService;
import com.databasir.core.domain.user.data.UserLoginResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.util.List;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class DatabasirAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper objectMapper;
private final LoginService loginService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
DatabasirUserDetails user = (DatabasirUserDetails) authentication.getPrincipal();
List<UserLoginResponse.RoleResponse> roles = user.getRoles()
.stream()
.map(ur -> {
UserLoginResponse.RoleResponse data = new UserLoginResponse.RoleResponse();
data.setRole(ur.getRole());
data.setGroupId(ur.getGroupId());
return data;
})
.collect(Collectors.toList());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
UserLoginResponse data = new UserLoginResponse();
data.setId(user.getUserPojo().getId());
data.setNickname(user.getUserPojo().getNickname());
data.setEmail(user.getUserPojo().getEmail());
data.setUsername(user.getUsername());
LoginKeyResponse loginKey = loginService.generate(user.getUserPojo().getId());
data.setAccessToken(loginKey.getAccessToken());
long expireAt = loginKey.getAccessTokenExpireAt().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
data.setAccessTokenExpireAt(expireAt);
data.setRefreshToken(loginKey.getRefreshToken());
data.setRoles(roles);
objectMapper.writeValue(response.getWriter(), JsonData.ok(data));
}
}

View File

@@ -0,0 +1,56 @@
package com.databasir.api.config.security;
import com.databasir.core.infrastructure.jwt.JwtTokens;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class DatabasirJwtTokenFilter extends OncePerRequestFilter {
private static final String SPACE = " ";
private final JwtTokens jwtTokens;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (!StringUtils.hasText(header) || !header.startsWith(JwtTokens.TOKEN_PREFIX)) {
filterChain.doFilter(request, response);
return;
}
final String token = header.split(SPACE)[1].trim();
if (!jwtTokens.verify(token)) {
filterChain.doFilter(request, response);
return;
}
String username = jwtTokens.getUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,34 @@
package com.databasir.api.config.security;
import com.databasir.dao.impl.LoginDao;
import com.databasir.dao.impl.UserDao;
import com.databasir.dao.impl.UserRoleDao;
import com.databasir.dao.tables.pojos.UserPojo;
import com.databasir.dao.tables.pojos.UserRolePojo;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
@RequiredArgsConstructor
public class DatabasirUserDetailService implements UserDetailsService {
private final UserDao userDao;
private final UserRoleDao userRoleDao;
private final LoginDao loginDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserPojo user = userDao.selectByEmailOrUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户名或密码错误"));
List<UserRolePojo> roles = userRoleDao.selectByUserIds(Collections.singletonList(user.getId()));
return new DatabasirUserDetails(user, roles);
}
}

View File

@@ -0,0 +1,67 @@
package com.databasir.api.config.security;
import com.databasir.dao.tables.pojos.UserPojo;
import com.databasir.dao.tables.pojos.UserRolePojo;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public class DatabasirUserDetails implements UserDetails {
@Getter
private final UserPojo userPojo;
@Getter
private final List<UserRolePojo> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> {
String expression = role.getRole();
if (role.getGroupId() != null) {
expression += "?groupId=" + role.getGroupId();
}
return new SimpleGrantedAuthority(expression);
})
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return userPojo.getPassword();
}
@Override
public String getUsername() {
return userPojo.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return userPojo.getEnabled();
}
}

View File

@@ -0,0 +1,27 @@
package com.databasir.api.validator;
import com.databasir.api.config.security.DatabasirUserDetails;
import com.databasir.core.domain.DomainErrors;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
public class UserOperationValidator {
public void forbiddenIfUpdateSelfRole(Integer userId) {
DatabasirUserDetails principal = (DatabasirUserDetails) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
if (principal.getUserPojo().getId().equals(userId)) {
throw DomainErrors.CANNOT_UPDATE_SELF_ROLE.exception();
}
}
public boolean isMyself(Integer userId) {
DatabasirUserDetails principal = (DatabasirUserDetails) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
return principal.getUserPojo().getId().equals(userId);
}
}

View File

@@ -0,0 +1,11 @@
server.port=8080
logging.level.org.jooq=DEBUG
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/databasir
spring.jooq.sql-dialect=mysql
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.locations=classpath:db/migration

View File

@@ -0,0 +1,12 @@
server.port=8080
# datasource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=${databasir.datasource.username}
spring.datasource.password=${databasir.datasource.password}
spring.datasource.url=jdbc:mysql://${databasir.datasource.url}/${databasir.datasource.database-name:databasir}
# jooq
spring.jooq.sql-dialect=mysql
# flyway
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.locations=classpath:db/migration