Merge pull request 'feat(auth): add user profile centre and admin role system' (#12) from feature/user-profile-and-roles into develop
Reviewed-on: https://onixbyte.dev/onixbyte/delta-force-guide-server/pulls/12 Reviewed-by: Siu Jam Oh <jamo.siu@gmail.com>
This commit is contained in:
commit
581634f33a
22 files changed
+985
-8
No files matched your search
@@ -3,12 +3,16 @@ package com.onixbyte.deltaforceguide.config;
|
||||
import com.onixbyte.deltaforceguide.interceptor.GitHubWebhookInterceptor;
|
||||
import com.onixbyte.deltaforceguide.interceptor.TrafficInterceptor;
|
||||
import com.onixbyte.deltaforceguide.properties.AppProperties;
|
||||
import com.onixbyte.deltaforceguide.security.resolver.CurrentUserArgumentResolver;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(AppProperties.class)
|
||||
public class AppConfig implements WebMvcConfigurer {
|
||||
@@ -17,13 +21,17 @@ public class AppConfig implements WebMvcConfigurer {
|
||||
|
||||
private final GitHubWebhookInterceptor gitHubWebhookInterceptor;
|
||||
|
||||
private final CurrentUserArgumentResolver currentUserArgumentResolver;
|
||||
|
||||
@Autowired
|
||||
public AppConfig(
|
||||
TrafficInterceptor trafficInterceptor,
|
||||
GitHubWebhookInterceptor gitHubWebhookInterceptor
|
||||
GitHubWebhookInterceptor gitHubWebhookInterceptor,
|
||||
CurrentUserArgumentResolver currentUserArgumentResolver
|
||||
) {
|
||||
this.trafficInterceptor = trafficInterceptor;
|
||||
this.gitHubWebhookInterceptor = gitHubWebhookInterceptor;
|
||||
this.currentUserArgumentResolver = currentUserArgumentResolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -32,4 +40,9 @@ public class AppConfig implements WebMvcConfigurer {
|
||||
registry.addInterceptor(gitHubWebhookInterceptor)
|
||||
.addPathPatterns("/webhooks/github");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
|
||||
resolvers.add(currentUserArgumentResolver);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
@@ -22,7 +23,6 @@ import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.onixbyte.deltaforceguide.controller;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.dto.ManageRoleRequest;
|
||||
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
|
||||
import com.onixbyte.deltaforceguide.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* REST controller for administrative user and role management.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Tag(name = "管理员", description = "管理员用户与角色管理")
|
||||
@RestController
|
||||
@RequestMapping("/admin")
|
||||
@Validated
|
||||
@RequiresAuth
|
||||
public class AdminController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public AdminController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Operation(description = "为用户分配角色(仅超级管理员可操作)")
|
||||
@PreAuthorize("hasRole('SUPER_USER')")
|
||||
@PutMapping("/users/{id}/roles")
|
||||
public ResponseEntity<Void> assignRole(
|
||||
@PathVariable @Positive(message = "用户ID必须为正数") Long id,
|
||||
@Valid @RequestBody ManageRoleRequest request
|
||||
) {
|
||||
userService.assignRole(id, request.role());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Operation(description = "移除用户角色(仅超级管理员可操作)")
|
||||
@PreAuthorize("hasRole('SUPER_USER')")
|
||||
@DeleteMapping("/users/{id}/roles/{role}")
|
||||
public ResponseEntity<Void> removeRole(
|
||||
@PathVariable @Positive(message = "用户ID必须为正数") Long id,
|
||||
@PathVariable String role
|
||||
) {
|
||||
userService.removeRole(id, role);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.onixbyte.deltaforceguide.controller;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.dto.BuildSummaryResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.ChangePasswordRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.UpdateProfileRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.UserProfileResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||
import com.onixbyte.deltaforceguide.security.annotation.RequiresAuth;
|
||||
import com.onixbyte.deltaforceguide.security.resolver.CurrentUser;
|
||||
import com.onixbyte.deltaforceguide.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* REST controller for the current user's profile management endpoints.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Tag(name = "用户个人中心", description = "当前用户的资料管理与密码修改")
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@Validated
|
||||
@RequiresAuth
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@Operation(description = "获取当前登录用户的个人资料")
|
||||
@GetMapping("/me")
|
||||
public UserProfileResponse profile(@CurrentUser User user) {
|
||||
return userService.getProfile(user);
|
||||
}
|
||||
|
||||
@Operation(description = "修改当前用户的个人资料(昵称与邮箱)")
|
||||
@PutMapping("/me")
|
||||
public UserProfileResponse updateProfile(
|
||||
@CurrentUser User user,
|
||||
@Valid @RequestBody UpdateProfileRequest request
|
||||
) {
|
||||
return userService.updateProfile(user, request);
|
||||
}
|
||||
|
||||
@Operation(description = "修改当前用户的登录密码")
|
||||
@PutMapping("/me/password")
|
||||
public ResponseEntity<Void> changePassword(
|
||||
@CurrentUser User user,
|
||||
@Valid @RequestBody ChangePasswordRequest request
|
||||
) {
|
||||
userService.changePassword(user, request);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Operation(description = "分页查询当前用户的改枪码列表")
|
||||
@GetMapping("/me/builds")
|
||||
public PageResponse<BuildSummaryResponse> builds(
|
||||
@CurrentUser User user,
|
||||
@RequestParam(defaultValue = "0") @Min(0) int page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) @Max(100) int size
|
||||
) {
|
||||
return userService.getBuilds(user, PageRequest.of(page, size));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Response record summarising a user's custom modification build.
|
||||
* This is a skeleton record whose {@code from()} factory will be fully implemented
|
||||
* when the CustomModification entity is introduced in a future issue.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
public record BuildSummaryResponse(
|
||||
Long id,
|
||||
String name,
|
||||
String code,
|
||||
String firearmName,
|
||||
List<String> tags
|
||||
) {
|
||||
// from() factory to be implemented when CustomModification entity lands
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* Request record for changing the current user's password.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
public record ChangePasswordRequest(
|
||||
@NotBlank(message = "旧密码不能为空")
|
||||
String oldPassword,
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@Size(min = 8, max = 128, message = "密码长度必须在8-128个字符之间")
|
||||
@Pattern(
|
||||
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).+$",
|
||||
message = "密码必须包含至少一个大写字母、一个小写字母和一个数字"
|
||||
)
|
||||
String newPassword
|
||||
) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* Request record for managing a user's role assignment.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
public record ManageRoleRequest(
|
||||
@NotBlank(message = "角色不能为空")
|
||||
String role
|
||||
) {}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* Request record for updating the current user's profile information.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
public record UpdateProfileRequest(
|
||||
@Size(min = 1, max = 64, message = "昵称长度必须在1-64个字符之间")
|
||||
String nickname,
|
||||
|
||||
@Email(message = "请输入正确的电子邮箱地址")
|
||||
@Size(max = 255, message = "电子邮箱长度不能超过255个字符")
|
||||
String email
|
||||
) {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.onixbyte.deltaforceguide.domain.dto;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Response record for the current user's full profile information.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
public record UserProfileResponse(
|
||||
Long id,
|
||||
String username,
|
||||
String nickname,
|
||||
String email,
|
||||
String avatarUrl,
|
||||
Boolean emailVerified,
|
||||
Boolean enabled,
|
||||
LocalDateTime createdAt,
|
||||
List<String> roles
|
||||
) {
|
||||
|
||||
/**
|
||||
* Creates a {@code UserProfileResponse} from a {@link User} entity and its assigned roles.
|
||||
*
|
||||
* @param user the user entity
|
||||
* @param roles the list of role names assigned to the user
|
||||
* @return the constructed response
|
||||
*/
|
||||
public static UserProfileResponse from(User user, List<String> roles) {
|
||||
return new UserProfileResponse(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getNickname() == null || user.getNickname().isBlank()
|
||||
? user.getUsername() : user.getNickname(),
|
||||
user.getEmail(),
|
||||
user.getAvatarUrl(),
|
||||
user.getEmailVerified(),
|
||||
user.getEnabled(),
|
||||
user.getCreatedAt(),
|
||||
roles
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,10 @@ import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -31,6 +33,24 @@ public class User {
|
||||
@Column(name = "email", nullable = false)
|
||||
private String email;
|
||||
|
||||
@Column(name = "nickname")
|
||||
private String nickname;
|
||||
|
||||
@Column(name = "avatar_url")
|
||||
private String avatarUrl;
|
||||
|
||||
@Column(name = "email_verified", nullable = false)
|
||||
private Boolean emailVerified = false;
|
||||
|
||||
@Column(name = "enabled", nullable = false)
|
||||
private Boolean enabled = true;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<UserCredential> credentials = new ArrayList<>();
|
||||
|
||||
@@ -58,6 +78,59 @@ public class User {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public void setNickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
}
|
||||
|
||||
public String getAvatarUrl() {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
public Boolean getEmailVerified() {
|
||||
return emailVerified;
|
||||
}
|
||||
|
||||
public void setEmailVerified(Boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
private void onUpdate() {
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public List<UserCredential> getCredentials() {
|
||||
return credentials;
|
||||
}
|
||||
@@ -85,6 +158,12 @@ public class User {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private String nickname;
|
||||
private String avatarUrl;
|
||||
private Boolean emailVerified;
|
||||
private Boolean enabled;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private List<UserCredential> credentials;
|
||||
|
||||
public Builder id(Long id) {
|
||||
@@ -102,6 +181,36 @@ public class User {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder nickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder avatarUrl(String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder emailVerified(Boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder enabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder createdAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder updatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder credentials(List<UserCredential> credentials) {
|
||||
this.credentials = credentials;
|
||||
return this;
|
||||
@@ -112,6 +221,13 @@ public class User {
|
||||
user.id = this.id;
|
||||
user.username = this.username;
|
||||
user.email = this.email;
|
||||
user.nickname = this.nickname == null || this.nickname.isBlank()
|
||||
? this.username : this.nickname;
|
||||
user.avatarUrl = this.avatarUrl;
|
||||
user.emailVerified = this.emailVerified != null && this.emailVerified;
|
||||
user.enabled = this.enabled == null || this.enabled;
|
||||
user.createdAt = this.createdAt;
|
||||
user.updatedAt = this.updatedAt;
|
||||
user.credentials = this.credentials == null ? new ArrayList<>() : this.credentials;
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.onixbyte.deltaforceguide.domain.entity;
|
||||
|
||||
import jakarta.persistence.AttributeOverride;
|
||||
import jakarta.persistence.AttributeOverrides;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.EmbeddedId;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.ForeignKey;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/**
|
||||
* Entity representing a role assignment linking a user to a security role.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "user_role")
|
||||
public class UserRole {
|
||||
|
||||
@EmbeddedId
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "userId", column = @Column(name = "user_id")),
|
||||
@AttributeOverride(name = "role", column = @Column(name = "role"))
|
||||
})
|
||||
private UserRoleId id = new UserRoleId();
|
||||
|
||||
@MapsId("userId")
|
||||
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||
@JoinColumn(name = "user_id", nullable = false, foreignKey = @ForeignKey(name = "fk_user_role_user"))
|
||||
private User user;
|
||||
|
||||
public UserRoleId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UserRoleId id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
|
||||
public void setUser(User user) {
|
||||
this.user = user;
|
||||
if (this.id == null) {
|
||||
this.id = new UserRoleId();
|
||||
}
|
||||
this.id.setUserId(user == null ? null : user.getId());
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return id == null ? null : id.getUserId();
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
if (this.id == null) {
|
||||
this.id = new UserRoleId();
|
||||
}
|
||||
this.id.setUserId(userId);
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return id == null ? null : id.getRole();
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
if (this.id == null) {
|
||||
this.id = new UserRoleId();
|
||||
}
|
||||
this.id.setRole(role);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private UserRoleId id;
|
||||
private User user;
|
||||
private Long userId;
|
||||
private String role;
|
||||
|
||||
public Builder id(UserRoleId id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder user(User user) {
|
||||
this.user = user;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder userId(Long userId) {
|
||||
this.userId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder role(String role) {
|
||||
this.role = role;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserRole build() {
|
||||
UserRole userRole = new UserRole();
|
||||
userRole.id = this.id == null ? new UserRoleId() : this.id;
|
||||
userRole.user = this.user;
|
||||
if (this.user != null) {
|
||||
userRole.id.setUserId(this.user.getId());
|
||||
}
|
||||
if (this.userId != null) {
|
||||
userRole.id.setUserId(this.userId);
|
||||
}
|
||||
if (this.role != null) {
|
||||
userRole.id.setRole(this.role);
|
||||
}
|
||||
return userRole;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.onixbyte.deltaforceguide.domain.entity;
|
||||
|
||||
import jakarta.persistence.Embeddable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Composite key for the UserRole entity, combining user ID and role name.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Embeddable
|
||||
public class UserRoleId implements Serializable {
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String role;
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
UserRoleId that = (UserRoleId) o;
|
||||
return Objects.equals(userId, that.userId) && Objects.equals(role, that.role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(userId, role);
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private Long userId;
|
||||
private String role;
|
||||
|
||||
public Builder userId(Long userId) {
|
||||
this.userId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder role(String role) {
|
||||
this.role = role;
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserRoleId build() {
|
||||
UserRoleId id = new UserRoleId();
|
||||
id.userId = this.userId;
|
||||
id.role = this.role;
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.onixbyte.deltaforceguide.client.TokenClient;
|
||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
||||
import com.onixbyte.deltaforceguide.manager.UserManager;
|
||||
import com.onixbyte.deltaforceguide.manager.UserRoleManager;
|
||||
import com.onixbyte.deltaforceguide.security.authentication.UsernamePasswordAuthentication;
|
||||
import com.onixbyte.deltaforceguide.service.CookieService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
@@ -16,6 +17,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
@@ -27,6 +29,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Servlet filter that extracts and validates JWT tokens from httpOnly cookies for each request.
|
||||
@@ -40,17 +43,20 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
private static final Duration ACCESS_TOKEN_RENEW_THRESHOLD = Duration.ofMinutes(5);
|
||||
|
||||
private final UserManager userManager;
|
||||
private final UserRoleManager userRoleManager;
|
||||
private final TokenClient tokenClient;
|
||||
private final CookieService cookieService;
|
||||
private final HandlerExceptionResolver handlerExceptionResolver;
|
||||
|
||||
public TokenAuthenticationFilter(
|
||||
UserManager userManager,
|
||||
UserRoleManager userRoleManager,
|
||||
TokenClient tokenClient,
|
||||
CookieService cookieService,
|
||||
HandlerExceptionResolver handlerExceptionResolver
|
||||
) {
|
||||
this.userManager = userManager;
|
||||
this.userRoleManager = userRoleManager;
|
||||
this.tokenClient = tokenClient;
|
||||
this.cookieService = cookieService;
|
||||
this.handlerExceptionResolver = handlerExceptionResolver;
|
||||
@@ -87,7 +93,11 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
var user = userWrapper.get();
|
||||
var authentication = UsernamePasswordAuthentication.authenticated(user);
|
||||
var roles = userRoleManager.findAllByUserId(user.getId());
|
||||
var authorities = roles.stream()
|
||||
.map(r -> new SimpleGrantedAuthority(r.getRole()))
|
||||
.collect(Collectors.toList());
|
||||
var authentication = UsernamePasswordAuthentication.authenticated(user, authorities);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
if (shouldRenew(decodedToken.getExpiresAt().toInstant())) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.onixbyte.deltaforceguide.manager;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.entity.UserRole;
|
||||
import com.onixbyte.deltaforceguide.repository.UserRoleRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Manager for user role persistence and authorisation data access.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Component
|
||||
public class UserRoleManager {
|
||||
|
||||
private final UserRoleRepository userRoleRepository;
|
||||
|
||||
public UserRoleManager(UserRoleRepository userRoleRepository) {
|
||||
this.userRoleRepository = userRoleRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all role assignments for a specific user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @return list of matching role assignments
|
||||
*/
|
||||
public List<UserRole> findAllByUserId(Long userId) {
|
||||
return userRoleRepository.findAllByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a user has a specific role.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param role the role name
|
||||
* @return true if the role is assigned, false otherwise
|
||||
*/
|
||||
public boolean existsByUserIdAndRole(Long userId, String role) {
|
||||
return userRoleRepository.existsByUserIdAndRole(userId, role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a new role assignment.
|
||||
*
|
||||
* @param userRole the role assignment to save
|
||||
* @return the saved role assignment
|
||||
*/
|
||||
public UserRole save(UserRole userRole) {
|
||||
return userRoleRepository.save(userRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a role assignment for a specific user and role.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param role the role name
|
||||
*/
|
||||
public void deleteByUserIdAndRole(Long userId, String role) {
|
||||
userRoleRepository.deleteByUserIdAndRole(userId, role);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all role assignments belonging to a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
*/
|
||||
public void deleteAllByUserId(Long userId) {
|
||||
userRoleRepository.deleteAllByUserId(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.onixbyte.deltaforceguide.repository;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.entity.UserRole;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.UserRoleId;
|
||||
import org.springframework.data.jpa.repository.EntityGraph;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Spring Data JPA repository for {@link UserRole} entity operations.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Repository
|
||||
public interface UserRoleRepository extends JpaRepository<UserRole, UserRoleId> {
|
||||
|
||||
/**
|
||||
* Find all roles assigned to a given user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @return list of matching role assignments
|
||||
*/
|
||||
@EntityGraph(attributePaths = {"user"})
|
||||
@Query("""
|
||||
select ur
|
||||
from UserRole ur
|
||||
where ur.user.id = :userId
|
||||
""")
|
||||
List<UserRole> findAllByUserId(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* Check whether a user has a specific role.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param role the role name
|
||||
* @return true if the role is assigned, false otherwise
|
||||
*/
|
||||
@Query("""
|
||||
select count(ur) > 0
|
||||
from UserRole ur
|
||||
where ur.user.id = :userId
|
||||
and ur.id.role = :role
|
||||
""")
|
||||
boolean existsByUserIdAndRole(@Param("userId") Long userId, @Param("role") String role);
|
||||
|
||||
/**
|
||||
* Delete a specific role assignment for a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param role the role name
|
||||
*/
|
||||
@Modifying
|
||||
@Query("""
|
||||
delete from UserRole ur
|
||||
where ur.user.id = :userId
|
||||
and ur.id.role = :role
|
||||
""")
|
||||
void deleteByUserIdAndRole(@Param("userId") Long userId, @Param("role") String role);
|
||||
|
||||
/**
|
||||
* Delete all role assignments for a given user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
*/
|
||||
@Modifying
|
||||
@Query("""
|
||||
delete from UserRole ur
|
||||
where ur.user.id = :userId
|
||||
""")
|
||||
void deleteAllByUserId(@Param("userId") Long userId);
|
||||
}
|
||||
+20
-4
@@ -5,6 +5,7 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.CredentialsContainer;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@@ -18,25 +19,40 @@ public class UsernamePasswordAuthentication implements Authentication, Credentia
|
||||
private String password;
|
||||
private boolean authenticated;
|
||||
private User user;
|
||||
private Collection<? extends GrantedAuthority> authorities;
|
||||
|
||||
private UsernamePasswordAuthentication(String username, String password, boolean authenticated, User user) {
|
||||
private UsernamePasswordAuthentication(
|
||||
String username,
|
||||
String password,
|
||||
boolean authenticated,
|
||||
User user,
|
||||
Collection<? extends GrantedAuthority> authorities
|
||||
) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.authenticated = authenticated;
|
||||
this.user = user;
|
||||
this.authorities = authorities == null ? List.of() : new ArrayList<>(authorities);
|
||||
}
|
||||
|
||||
public static UsernamePasswordAuthentication unauthenticated(String username, String password) {
|
||||
return new UsernamePasswordAuthentication(username, password, false, null);
|
||||
return new UsernamePasswordAuthentication(username, password, false, null, List.of());
|
||||
}
|
||||
|
||||
public static UsernamePasswordAuthentication authenticated(User user) {
|
||||
return new UsernamePasswordAuthentication(user.getUsername(), null, true, user);
|
||||
return new UsernamePasswordAuthentication(user.getUsername(), null, true, user, List.of());
|
||||
}
|
||||
|
||||
public static UsernamePasswordAuthentication authenticated(
|
||||
User user,
|
||||
Collection<? extends GrantedAuthority> authorities
|
||||
) {
|
||||
return new UsernamePasswordAuthentication(user.getUsername(), null, true, user, authorities);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of();
|
||||
return authorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.onixbyte.deltaforceguide.security.resolver;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Annotation for injecting the currently authenticated {@code User} into controller method
|
||||
* parameters. The resolver extracts the user from the {@code SecurityContext}.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CurrentUser {}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.onixbyte.deltaforceguide.security.resolver;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||
import com.onixbyte.deltaforceguide.security.authentication.UsernamePasswordAuthentication;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
* Resolves {@link CurrentUser}-annotated controller method parameters by extracting the
|
||||
* {@link User} entity from the current {@link UsernamePasswordAuthentication} in the
|
||||
* security context.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
@Component
|
||||
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.hasParameterAnnotation(CurrentUser.class)
|
||||
&& parameter.getParameterType().equals(User.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User resolveArgument(
|
||||
MethodParameter parameter,
|
||||
ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest,
|
||||
WebDataBinderFactory binderFactory
|
||||
) {
|
||||
var auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth instanceof UsernamePasswordAuthentication upa && upa.isAuthenticated()) {
|
||||
return upa.getDetails();
|
||||
}
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "请先登录");
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,29 @@
|
||||
package com.onixbyte.deltaforceguide.service;
|
||||
|
||||
import com.onixbyte.deltaforceguide.domain.dto.BuildSummaryResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.ChangePasswordRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.PageResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.UpdateProfileRequest;
|
||||
import com.onixbyte.deltaforceguide.domain.dto.UserProfileResponse;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.User;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.UserCredential;
|
||||
import com.onixbyte.deltaforceguide.domain.entity.UserRole;
|
||||
import com.onixbyte.deltaforceguide.exeption.BizException;
|
||||
import com.onixbyte.deltaforceguide.manager.UserCredentialManager;
|
||||
import com.onixbyte.deltaforceguide.manager.UserManager;
|
||||
import com.onixbyte.deltaforceguide.manager.UserRoleManager;
|
||||
import com.onixbyte.deltaforceguide.shared.CredentialProvider;
|
||||
import com.onixbyte.deltaforceguide.shared.Role;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Service for user account management and profile operations.
|
||||
@@ -20,10 +35,19 @@ public class UserService {
|
||||
|
||||
private final UserManager userManager;
|
||||
private final UserCredentialManager userCredentialManager;
|
||||
private final UserRoleManager userRoleManager;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public UserService(UserManager userManager, UserCredentialManager userCredentialManager) {
|
||||
public UserService(
|
||||
UserManager userManager,
|
||||
UserCredentialManager userCredentialManager,
|
||||
UserRoleManager userRoleManager,
|
||||
PasswordEncoder passwordEncoder
|
||||
) {
|
||||
this.userManager = userManager;
|
||||
this.userCredentialManager = userCredentialManager;
|
||||
this.userRoleManager = userRoleManager;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,6 +174,106 @@ public class UserService {
|
||||
return userManager.findById(userId)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "User not found: " + userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full profile response for the given user, including their assigned roles.
|
||||
*
|
||||
* @param user the authenticated user
|
||||
* @return the user's profile response
|
||||
*/
|
||||
public UserProfileResponse getProfile(User user) {
|
||||
var roles = userRoleManager.findAllByUserId(user.getId()).stream()
|
||||
.map(UserRole::getRole)
|
||||
.toList();
|
||||
return UserProfileResponse.from(user, roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current user's profile information (nickname and email).
|
||||
* If the email address changes, the email verification status is reset.
|
||||
*
|
||||
* @param user the authenticated user
|
||||
* @param request the update request containing new nickname and email
|
||||
* @return the updated user profile response
|
||||
*/
|
||||
public UserProfileResponse updateProfile(User user, UpdateProfileRequest request) {
|
||||
if (StringUtils.hasText(request.nickname())) {
|
||||
user.setNickname(request.nickname());
|
||||
}
|
||||
if (StringUtils.hasText(request.email()) && !Objects.equals(user.getEmail(), request.email())) {
|
||||
user.setEmail(request.email());
|
||||
user.setEmailVerified(false);
|
||||
}
|
||||
userManager.save(user);
|
||||
return getProfile(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current user's password after validating the old password.
|
||||
*
|
||||
* @param user the authenticated user
|
||||
* @param request the change password request containing old and new passwords
|
||||
* @throws BizException if the old password is incorrect
|
||||
*/
|
||||
public void changePassword(User user, ChangePasswordRequest request) {
|
||||
var credential = userCredentialManager.findByUserIdAndProvider(user.getId(), CredentialProvider.LOCAL);
|
||||
if (credential.isEmpty() || !passwordEncoder.matches(request.oldPassword(), credential.get().getCredential())) {
|
||||
throw new BizException(HttpStatus.BAD_REQUEST, "旧密码不正确。");
|
||||
}
|
||||
var encoded = passwordEncoder.encode(request.newPassword());
|
||||
upsertCredential(user.getId(), CredentialProvider.LOCAL, encoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a paginated list of the current user's custom modification builds.
|
||||
* This is a skeleton implementation that returns an empty page.
|
||||
* It will be activated when the CustomModification entity is introduced.
|
||||
*
|
||||
* @param user the authenticated user
|
||||
* @param pageable pagination parameters
|
||||
* @return an empty paginated response
|
||||
*/
|
||||
public PageResponse<BuildSummaryResponse> getBuilds(User user, Pageable pageable) {
|
||||
var emptyPage = Page.<BuildSummaryResponse>empty(pageable);
|
||||
return PageResponse.from(emptyPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a role to a user. Only {@code ROLE_ADMIN} may be assigned via this API.
|
||||
*
|
||||
* @param userId the target user ID
|
||||
* @param role the role to assign
|
||||
* @throws BizException if attempting to assign a protected role
|
||||
*/
|
||||
public void assignRole(Long userId, String role) {
|
||||
if (!Role.ROLE_ADMIN.equals(role)) {
|
||||
throw new BizException(HttpStatus.BAD_REQUEST, "只能通过API分配ROLE_ADMIN角色。");
|
||||
}
|
||||
var user = ensureUserExists(userId);
|
||||
if (userRoleManager.existsByUserIdAndRole(userId, role)) {
|
||||
return; // already assigned — idempotent
|
||||
}
|
||||
var userRole = UserRole.builder()
|
||||
.user(user)
|
||||
.role(role)
|
||||
.build();
|
||||
userRoleManager.save(userRole);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role from a user. Only {@code ROLE_ADMIN} may be removed via this API.
|
||||
*
|
||||
* @param userId the target user ID
|
||||
* @param role the role to remove
|
||||
* @throws BizException if attempting to remove a protected role
|
||||
*/
|
||||
public void removeRole(Long userId, String role) {
|
||||
if (!Role.ROLE_ADMIN.equals(role)) {
|
||||
throw new BizException(HttpStatus.BAD_REQUEST, "只能通过API移除ROLE_ADMIN角色。");
|
||||
}
|
||||
ensureUserExists(userId);
|
||||
userRoleManager.deleteByUserIdAndRole(userId, role);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.onixbyte.deltaforceguide.shared;
|
||||
|
||||
/**
|
||||
* Constants for application role names used in authorisation decisions.
|
||||
*
|
||||
* @author zihluwang
|
||||
*/
|
||||
public class Role {
|
||||
|
||||
public static final String ROLE_USER = "ROLE_USER";
|
||||
public static final String ROLE_ADMIN = "ROLE_ADMIN";
|
||||
public static final String ROLE_SUPER_USER = "ROLE_SUPER_USER";
|
||||
|
||||
private Role() {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
ALTER TABLE app_user
|
||||
ADD COLUMN nickname VARCHAR(64),
|
||||
ADD COLUMN avatar_url VARCHAR(512),
|
||||
ADD COLUMN email_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
ADD COLUMN created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
ADD COLUMN updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE user_role
|
||||
(
|
||||
user_id BIGINT NOT NULL REFERENCES app_user (id),
|
||||
role VARCHAR(32) NOT NULL,
|
||||
CONSTRAINT user_role_pkey PRIMARY KEY (user_id, role)
|
||||
);
|
||||
Reference in new issue
Block a user