Added users, authentication API, tokens, and more content access rule models.
This commit is contained in:
parent
be880e312f
commit
19c7f35abc
|
@ -34,3 +34,6 @@ build/
|
||||||
|
|
||||||
*.mv.db
|
*.mv.db
|
||||||
*.db
|
*.db
|
||||||
|
|
||||||
|
*.der
|
||||||
|
*.pem
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>3.1.4</version>
|
<version>3.1.5</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.andrewlalis</groupId>
|
<groupId>com.andrewlalis</groupId>
|
||||||
|
@ -56,6 +56,25 @@
|
||||||
<artifactId>spring-security-test</artifactId>
|
<artifactId>spring-security-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JWT dependencies -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.11.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.11.5</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
|
||||||
|
<version>0.11.5</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.andrewlalis.onyx.auth.api;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
|
import com.andrewlalis.onyx.auth.service.TokenService;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AuthController {
|
||||||
|
private final TokenService tokenService;
|
||||||
|
|
||||||
|
@PostMapping("/auth/login")
|
||||||
|
public TokenPair login(@RequestBody LoginRequest loginRequest) {
|
||||||
|
return tokenService.generateTokenPair(loginRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/auth/access")
|
||||||
|
public Object getAccessToken(HttpServletRequest request) {
|
||||||
|
return Map.of("accessToken", tokenService.generateAccessToken(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/auth/refresh-tokens")
|
||||||
|
public void removeAllRefreshTokens(@AuthenticationPrincipal User user) {
|
||||||
|
tokenService.removeAllRefreshTokens(user);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.andrewlalis.onyx.auth.api;
|
||||||
|
|
||||||
|
public record LoginRequest(String username, String password) {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.andrewlalis.onyx.auth.api;
|
||||||
|
|
||||||
|
public record TokenPair(String refreshToken, String accessToken) {}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package com.andrewlalis.onyx.auth.components;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.service.TokenService;
|
||||||
|
import com.andrewlalis.onyx.auth.service.UserService;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jws;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class JwtFilter extends OncePerRequestFilter {
|
||||||
|
private final UserService userService;
|
||||||
|
private final TokenService tokenService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain
|
||||||
|
) throws ServletException, IOException {
|
||||||
|
try {
|
||||||
|
Jws<Claims> jws = tokenService.getToken(request);
|
||||||
|
if (jws != null) {
|
||||||
|
long userId = Long.parseLong(jws.getBody().getSubject());
|
||||||
|
userService.findById(userId).ifPresent(user -> SecurityContextHolder.getContext()
|
||||||
|
.setAuthentication(new TokenAuthentication(user, jws))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Exception occurred in JwtFilter.", e);
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.andrewlalis.onyx.auth.components;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jws;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The authentication implementation that's used when a user logs in with an
|
||||||
|
* access token.
|
||||||
|
* @param user The user that the token belongs to.
|
||||||
|
* @param jws The raw token.
|
||||||
|
*/
|
||||||
|
public record TokenAuthentication(User user, Jws<Claims> jws) implements Authentication {
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getCredentials() {
|
||||||
|
return this.jws;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getDetails() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getPrincipal() {
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAuthenticated() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
|
||||||
|
throw new RuntimeException("Cannot set the authenticated status of TokenAuthentication.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return user.getUsername();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.andrewlalis.onyx.auth.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "onyx_auth_refresh_token")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class RefreshToken {
|
||||||
|
public static final int SUFFIX_LENGTH = 7;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private String hash;
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false, length = SUFFIX_LENGTH)
|
||||||
|
private String tokenSuffix;
|
||||||
|
|
||||||
|
@ManyToOne(optional = false, fetch = FetchType.LAZY)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false)
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
|
||||||
|
public RefreshToken(String hash, String tokenSuffix, User user, LocalDateTime expiresAt) {
|
||||||
|
this.hash = hash;
|
||||||
|
this.tokenSuffix = tokenSuffix;
|
||||||
|
this.user = user;
|
||||||
|
this.expiresAt = expiresAt;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.andrewlalis.onyx.auth.model;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {
|
||||||
|
Iterable<RefreshToken> findAllByTokenSuffix(String suffix);
|
||||||
|
void deleteAllByUserId(long userId);
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package com.andrewlalis.onyx.auth.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main authentication principal entity, representing a single user that
|
||||||
|
* has an account on this onyx node.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "onyx_auth_user")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class User {
|
||||||
|
public static final int MAX_USERNAME_LENGTH = 32;
|
||||||
|
public static final int MAX_DISPLAY_NAME_LENGTH = 64;
|
||||||
|
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true, length = MAX_USERNAME_LENGTH)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = MAX_DISPLAY_NAME_LENGTH)
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 60)
|
||||||
|
private String passwordHash;
|
||||||
|
|
||||||
|
@CreationTimestamp
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
public User(String username, String displayName, String passwordHash) {
|
||||||
|
this.username = username;
|
||||||
|
this.displayName = displayName;
|
||||||
|
this.passwordHash = passwordHash;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.andrewlalis.onyx.auth.model;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
Optional<User> findByUsername(String username);
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package com.andrewlalis.onyx.auth.service;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.api.LoginRequest;
|
||||||
|
import com.andrewlalis.onyx.auth.api.TokenPair;
|
||||||
|
import com.andrewlalis.onyx.auth.model.RefreshToken;
|
||||||
|
import com.andrewlalis.onyx.auth.model.RefreshTokenRepository;
|
||||||
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
|
import com.andrewlalis.onyx.auth.model.UserRepository;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jws;
|
||||||
|
import io.jsonwebtoken.JwtParserBuilder;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class TokenService {
|
||||||
|
private static final String BEARER_PREFIX = "Bearer ";
|
||||||
|
private static final String ISSUER = "Onyx API";
|
||||||
|
|
||||||
|
private PrivateKey signingKey;
|
||||||
|
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
|
||||||
|
private final RefreshTokenRepository refreshTokenRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public TokenPair generateTokenPair(LoginRequest request) {
|
||||||
|
Optional<User> optionalUser = userRepository.findByUsername(request.username());
|
||||||
|
if (
|
||||||
|
optionalUser.isEmpty() ||
|
||||||
|
!passwordEncoder.matches(request.password(), optionalUser.get().getPasswordHash())
|
||||||
|
) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
|
||||||
|
}
|
||||||
|
User user = optionalUser.get();
|
||||||
|
String refreshToken = generateRefreshToken(user);
|
||||||
|
String accessToken = generateAccessToken(refreshToken);
|
||||||
|
return new TokenPair(refreshToken, accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public String generateRefreshToken(User user) {
|
||||||
|
Instant expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusDays(7).toInstant();
|
||||||
|
try {
|
||||||
|
String token = Jwts.builder()
|
||||||
|
.setSubject(Long.toString(user.getId()))
|
||||||
|
.setIssuer(ISSUER)
|
||||||
|
.setAudience("Onyx App")
|
||||||
|
.setExpiration(Date.from(expiresAt))
|
||||||
|
.claim("username", user.getUsername())
|
||||||
|
.claim("token-type", "refresh")
|
||||||
|
.signWith(getSigningKey())
|
||||||
|
.compact();
|
||||||
|
refreshTokenRepository.saveAndFlush(new RefreshToken(
|
||||||
|
passwordEncoder.encode(token),
|
||||||
|
token.substring(token.length() - RefreshToken.SUFFIX_LENGTH),
|
||||||
|
user,
|
||||||
|
LocalDateTime.ofInstant(expiresAt, ZoneOffset.UTC)
|
||||||
|
));
|
||||||
|
return token;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate refresh token.", e);
|
||||||
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public String generateAccessToken(String refreshTokenString) {
|
||||||
|
String suffix = refreshTokenString.substring(refreshTokenString.length() - RefreshToken.SUFFIX_LENGTH);
|
||||||
|
RefreshToken refreshToken = null;
|
||||||
|
for (RefreshToken possibleToken : refreshTokenRepository.findAllByTokenSuffix(suffix)) {
|
||||||
|
if (passwordEncoder.matches(refreshTokenString, possibleToken.getHash())) {
|
||||||
|
refreshToken = possibleToken;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (refreshToken == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Unknown refresh token.");
|
||||||
|
}
|
||||||
|
if (refreshToken.getExpiresAt().isBefore(LocalDateTime.now(ZoneOffset.UTC))) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Refresh token is expired.");
|
||||||
|
}
|
||||||
|
Instant expiresAt = OffsetDateTime.now(ZoneOffset.UTC).plusHours(1).toInstant();
|
||||||
|
try {
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(Long.toString(refreshToken.getUser().getId()))
|
||||||
|
.setIssuer(ISSUER)
|
||||||
|
.setAudience("Onyx App")
|
||||||
|
.setExpiration(Date.from(expiresAt))
|
||||||
|
.claim("username", refreshToken.getUser().getUsername())
|
||||||
|
.claim("token-type", "access")
|
||||||
|
.signWith(getSigningKey())
|
||||||
|
.compact();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate access token.", e);
|
||||||
|
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to generate token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public String generateAccessToken(HttpServletRequest request) {
|
||||||
|
String refreshTokenString = extractBearerToken(request);
|
||||||
|
if (refreshTokenString == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Missing refresh token.");
|
||||||
|
}
|
||||||
|
return generateAccessToken(refreshTokenString);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void removeAllRefreshTokens(User user) {
|
||||||
|
refreshTokenRepository.deleteAllByUserId(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Jws<Claims> getToken(HttpServletRequest request) throws Exception {
|
||||||
|
String rawToken = extractBearerToken(request);
|
||||||
|
if (rawToken == null) return null;
|
||||||
|
JwtParserBuilder parserBuilder = Jwts.parserBuilder()
|
||||||
|
.setSigningKey(getSigningKey())
|
||||||
|
.requireIssuer(ISSUER);
|
||||||
|
return parserBuilder.build().parseClaimsJws(rawToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractBearerToken(HttpServletRequest request) {
|
||||||
|
String authorizationHeader = request.getHeader("Authorization");
|
||||||
|
if (authorizationHeader == null || !authorizationHeader.startsWith(BEARER_PREFIX)) return null;
|
||||||
|
String rawToken = authorizationHeader.substring(BEARER_PREFIX.length());
|
||||||
|
if (rawToken.isBlank()) return null;
|
||||||
|
return rawToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PrivateKey getSigningKey() throws Exception {
|
||||||
|
if (signingKey == null) {
|
||||||
|
byte[] keyBytes = Files.readAllBytes(Path.of("private_key.der"));
|
||||||
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||||
|
signingKey = kf.generatePrivate(spec);
|
||||||
|
}
|
||||||
|
return signingKey;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.andrewlalis.onyx.auth.service;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
|
import com.andrewlalis.onyx.auth.model.UserRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserService {
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
public Optional<User> findById(long id) {
|
||||||
|
return userRepository.findById(id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.andrewlalis.onyx.config;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.components.JwtFilter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityConfig {
|
||||||
|
private final JwtFilter jwtFilter;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.authorizeHttpRequests(registry -> {
|
||||||
|
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.POST, "/auth/login")).permitAll();
|
||||||
|
registry.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/auth/access")).permitAll();
|
||||||
|
registry.anyRequest().authenticated();
|
||||||
|
});
|
||||||
|
http.csrf(AbstractHttpConfigurer::disable);
|
||||||
|
http.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.NEVER));
|
||||||
|
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
http.cors(configurer -> configurer.configure(http));
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
package com.andrewlalis.onyx.content;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Lob;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A type of content node that contains a single document.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "content_node_document")
|
|
||||||
public class ContentDocumentNode extends ContentNode {
|
|
||||||
@Column(nullable = false, updatable = false, length = 127)
|
|
||||||
private String type;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The raw file content for this document.
|
|
||||||
*/
|
|
||||||
@Lob @Column(nullable = false)
|
|
||||||
private byte[] content;
|
|
||||||
}
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.andrewlalis.onyx.content;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.content.dao.ContentNodeRepository;
|
||||||
|
import com.andrewlalis.onyx.content.model.access.ContentAccessLevel;
|
||||||
|
import com.andrewlalis.onyx.content.model.ContentContainerNode;
|
||||||
|
import com.andrewlalis.onyx.content.model.ContentNode;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class ContentTreeInitializer implements CommandLineRunner {
|
||||||
|
private final ContentNodeRepository contentNodeRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String... args) throws Exception {
|
||||||
|
log.info("Check if content root container node exists.");
|
||||||
|
if (!contentNodeRepository.existsByName(ContentNode.ROOT_NODE_NAME)) {
|
||||||
|
ContentNode rootNode = new ContentContainerNode(ContentNode.ROOT_NODE_NAME, null);
|
||||||
|
rootNode.getAccessInfo().setPublicAccessLevel(ContentAccessLevel.NONE);
|
||||||
|
rootNode.getAccessInfo().setNetworkAccessLevel(ContentAccessLevel.NONE);
|
||||||
|
rootNode.getAccessInfo().setNodeAccessLevel(ContentAccessLevel.NONE);
|
||||||
|
contentNodeRepository.saveAndFlush(rootNode);
|
||||||
|
log.info("Created content root container node.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.andrewlalis.onyx.content.dao;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.content.model.ContentNode;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ContentNodeRepository extends JpaRepository<ContentNode, Long> {
|
||||||
|
boolean existsByName(String name);
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package com.andrewlalis.onyx.content;
|
package com.andrewlalis.onyx.content.model;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -9,11 +11,16 @@ import java.util.Set;
|
||||||
* themselves be any type of content node.
|
* themselves be any type of content node.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_node_container")
|
@Table(name = "onyx_content_container_node")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
public final class ContentContainerNode extends ContentNode {
|
public final class ContentContainerNode extends ContentNode {
|
||||||
/**
|
/**
|
||||||
* The set of children that belong to this container.
|
* The set of children that belong to this container.
|
||||||
*/
|
*/
|
||||||
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "parentContainer")
|
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "parentContainer")
|
||||||
private Set<ContentNode> children;
|
private Set<ContentNode> children;
|
||||||
|
|
||||||
|
public ContentContainerNode(String name, ContentContainerNode parentContainer) {
|
||||||
|
super(name, Type.CONTAINER, parentContainer);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.andrewlalis.onyx.content.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Lob;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type of content node that contains a single document.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "onyx_content_document_node")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
public class ContentDocumentNode extends ContentNode {
|
||||||
|
@Column(nullable = false, updatable = false, length = 127)
|
||||||
|
private String contentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw file content for this document.
|
||||||
|
*/
|
||||||
|
@Lob @Column(nullable = false) @Setter
|
||||||
|
private byte[] content;
|
||||||
|
|
||||||
|
public ContentDocumentNode(String contentType, byte[] content) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
package com.andrewlalis.onyx.content;
|
package com.andrewlalis.onyx.content.model;
|
||||||
|
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistory;
|
import com.andrewlalis.onyx.content.model.access.ContentAccessRules;
|
||||||
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistory;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,11 +12,13 @@ import lombok.NoArgsConstructor;
|
||||||
* content tree, including both <em>containers</em> and <em>documents</em>.
|
* content tree, including both <em>containers</em> and <em>documents</em>.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_node")
|
@Table(name = "onyx_content_node")
|
||||||
@Inheritance(strategy = InheritanceType.JOINED)
|
@Inheritance(strategy = InheritanceType.JOINED)
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
public abstract class ContentNode {
|
public abstract class ContentNode {
|
||||||
public static final int MAX_NAME_LENGTH = 127;
|
public static final int MAX_NAME_LENGTH = 127;
|
||||||
|
public static final String ROOT_NODE_NAME = "___ROOT___";
|
||||||
|
|
||||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private long id;
|
private long id;
|
||||||
|
@ -23,7 +27,7 @@ public abstract class ContentNode {
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Enumerated(EnumType.ORDINAL) @Column(columnDefinition = "TINYINT NOT NULL")
|
@Enumerated(EnumType.ORDINAL) @Column(columnDefinition = "TINYINT NOT NULL")
|
||||||
private Type type;
|
private Type nodeType;
|
||||||
|
|
||||||
@OneToOne(fetch = FetchType.LAZY, optional = false, orphanRemoval = true, cascade = CascadeType.ALL)
|
@OneToOne(fetch = FetchType.LAZY, optional = false, orphanRemoval = true, cascade = CascadeType.ALL)
|
||||||
private ContentAccessRules accessInfo;
|
private ContentAccessRules accessInfo;
|
||||||
|
@ -49,7 +53,8 @@ public abstract class ContentNode {
|
||||||
|
|
||||||
public ContentNode(String name, Type type, ContentContainerNode parentContainer) {
|
public ContentNode(String name, Type type, ContentContainerNode parentContainer) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.type = type;
|
this.nodeType = type;
|
||||||
|
this.accessInfo = new ContentAccessRules();
|
||||||
this.parentContainer = parentContainer;
|
this.parentContainer = parentContainer;
|
||||||
this.history = new ContentNodeHistory(this);
|
this.history = new ContentNodeHistory(this);
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.andrewlalis.onyx.content;
|
package com.andrewlalis.onyx.content.model.access;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The different levels of access that can be set for content in an onyx node.
|
* The different levels of access that can be set for content in an onyx node.
|
|
@ -1,14 +1,18 @@
|
||||||
package com.andrewlalis.onyx.content;
|
package com.andrewlalis.onyx.content.model.access;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.content.model.ContentNode;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An entity meant to be attached to a {@link ContentNode}, which contains the
|
* An entity meant to be attached to a {@link ContentNode}, which contains the
|
||||||
* access levels for the different ways in which a content node can be accessed.
|
* access levels for the different ways in which a content node can be accessed.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_access_rules")
|
@Table(name = "onyx_content_access_rules")
|
||||||
@Getter
|
@Getter
|
||||||
public class ContentAccessRules {
|
public class ContentAccessRules {
|
||||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@ -21,17 +25,20 @@ public class ContentAccessRules {
|
||||||
private ContentNode contentNode;
|
private ContentNode contentNode;
|
||||||
|
|
||||||
/** The access level that applies to users from outside this onyx network. */
|
/** The access level that applies to users from outside this onyx network. */
|
||||||
@Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL")
|
@Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") @Setter
|
||||||
private ContentAccessLevel publicAccessLevel = ContentAccessLevel.INHERIT;
|
private ContentAccessLevel publicAccessLevel = ContentAccessLevel.INHERIT;
|
||||||
|
|
||||||
/** The access level that applies to users from within this onyx network. */
|
/** The access level that applies to users from within this onyx network. */
|
||||||
@Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL")
|
@Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") @Setter
|
||||||
private ContentAccessLevel networkAccessLevel = ContentAccessLevel.INHERIT;
|
private ContentAccessLevel networkAccessLevel = ContentAccessLevel.INHERIT;
|
||||||
|
|
||||||
/** The access level that applies to users within only this onyx node. */
|
/** The access level that applies to users within only this onyx node. */
|
||||||
@Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL")
|
@Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") @Setter
|
||||||
private ContentAccessLevel nodeAccessLevel = ContentAccessLevel.INHERIT;
|
private ContentAccessLevel nodeAccessLevel = ContentAccessLevel.INHERIT;
|
||||||
|
|
||||||
// TODO: Add a user allowlist.
|
/**
|
||||||
|
* User-specific access rules that override other more generic rules, if present.
|
||||||
|
*/
|
||||||
|
@OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL, mappedBy = "contentAccessRules")
|
||||||
|
private Set<UserContentAccessRule> userAccessRules;
|
||||||
}
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.andrewlalis.onyx.content.model.access;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.auth.model.User;
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "onyx_content_access_rules_user")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class UserContentAccessRule {
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private long id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
private ContentAccessRules contentAccessRules;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") @Setter
|
||||||
|
private ContentAccessLevel accessLevel;
|
||||||
|
|
||||||
|
public UserContentAccessRule(User user, ContentAccessRules contentAccessRules, ContentAccessLevel accessLevel) {
|
||||||
|
this.user = user;
|
||||||
|
this.contentAccessRules = contentAccessRules;
|
||||||
|
this.accessLevel = accessLevel;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
package com.andrewlalis.onyx.content.history;
|
package com.andrewlalis.onyx.content.model.history;
|
||||||
|
|
||||||
import com.andrewlalis.onyx.content.ContentNode;
|
import com.andrewlalis.onyx.content.model.ContentNode;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
|
|
||||||
|
@ -15,8 +16,9 @@ import java.util.Set;
|
||||||
* storing an ordered list of entries detailing how that node has changed.
|
* storing an ordered list of entries detailing how that node has changed.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_node_history")
|
@Table(name = "onyx_content_node_history")
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
public class ContentNodeHistory {
|
public class ContentNodeHistory {
|
||||||
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private long id;
|
private long id;
|
|
@ -1,4 +1,4 @@
|
||||||
package com.andrewlalis.onyx.content.history;
|
package com.andrewlalis.onyx.content.model.history;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
|
@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||||
* some basic properties that all entries should have.
|
* some basic properties that all entries should have.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_node_history_entry")
|
@Table(name = "onyx_content_node_history_entry")
|
||||||
@Inheritance(strategy = InheritanceType.JOINED)
|
@Inheritance(strategy = InheritanceType.JOINED)
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
|
@ -1,8 +1,8 @@
|
||||||
package com.andrewlalis.onyx.content.history.entry;
|
package com.andrewlalis.onyx.content.model.history.entry;
|
||||||
|
|
||||||
import com.andrewlalis.onyx.content.ContentAccessLevel;
|
import com.andrewlalis.onyx.content.model.access.ContentAccessLevel;
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistory;
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistory;
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistoryEntry;
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistoryEntry;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -13,7 +13,7 @@ import lombok.NoArgsConstructor;
|
||||||
* by a user or the system.
|
* by a user or the system.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_node_history_entry_access_rules")
|
@Table(name = "onyx_content_node_history_entry__access_rules")
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
public class AccessRulesEntry extends ContentNodeHistoryEntry {
|
public class AccessRulesEntry extends ContentNodeHistoryEntry {
|
|
@ -1,7 +1,7 @@
|
||||||
package com.andrewlalis.onyx.content.history.entry;
|
package com.andrewlalis.onyx.content.model.history.entry;
|
||||||
|
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistory;
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistory;
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistoryEntry;
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistoryEntry;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
@ -13,7 +13,7 @@ import lombok.NoArgsConstructor;
|
||||||
* History entry for tracking the state of a content node's `archived` status.
|
* History entry for tracking the state of a content node's `archived` status.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_node_history_entry_archived")
|
@Table(name = "onyx_content_node_history_entry__archived")
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
public class ArchivedEntry extends ContentNodeHistoryEntry {
|
public class ArchivedEntry extends ContentNodeHistoryEntry {
|
|
@ -1,8 +1,9 @@
|
||||||
package com.andrewlalis.onyx.content.history.entry;
|
package com.andrewlalis.onyx.content.model.history.entry;
|
||||||
|
|
||||||
import com.andrewlalis.onyx.content.ContentNode;
|
import com.andrewlalis.onyx.content.model.ContentContainerNode;
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistory;
|
import com.andrewlalis.onyx.content.model.ContentNode;
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistoryEntry;
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistory;
|
||||||
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistoryEntry;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
@ -11,7 +12,7 @@ import lombok.NoArgsConstructor;
|
||||||
* History entry for tracking updates to the contents of a container node.
|
* History entry for tracking updates to the contents of a container node.
|
||||||
*/
|
*/
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_node_history_entry_container_edit")
|
@Table(name = "onyx_content_node_history_entry__container_edit")
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
public class ContainerEditEntry extends ContentNodeHistoryEntry {
|
public class ContainerEditEntry extends ContentNodeHistoryEntry {
|
||||||
public enum EditType {
|
public enum EditType {
|
||||||
|
@ -29,6 +30,9 @@ public class ContainerEditEntry extends ContentNodeHistoryEntry {
|
||||||
|
|
||||||
public ContainerEditEntry(ContentNodeHistory history, EditType type, String affectedNodeName) {
|
public ContainerEditEntry(ContentNodeHistory history, EditType type, String affectedNodeName) {
|
||||||
super(history);
|
super(history);
|
||||||
|
if (!(history.getContentNode() instanceof ContentContainerNode)) {
|
||||||
|
throw new IllegalArgumentException("Only the history of a ContentContainerNode may be used to create a ContainerEditEntry.");
|
||||||
|
}
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.affectedNodeName = affectedNodeName;
|
this.affectedNodeName = affectedNodeName;
|
||||||
}
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.andrewlalis.onyx.content.model.history.entry;
|
||||||
|
|
||||||
|
import com.andrewlalis.onyx.content.model.ContentDocumentNode;
|
||||||
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistory;
|
||||||
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistoryEntry;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Lob;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A history entry that records an edit that was made to a document.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "onyx_content_node_history_entry__document_edit")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
public class DocumentEditEntry extends ContentNodeHistoryEntry {
|
||||||
|
@Lob @Column(nullable = false, updatable = false)
|
||||||
|
private byte[] diff;
|
||||||
|
|
||||||
|
public DocumentEditEntry(ContentNodeHistory history, byte[] diff) {
|
||||||
|
super(history);
|
||||||
|
if (!(history.getContentNode() instanceof ContentDocumentNode)) {
|
||||||
|
throw new IllegalArgumentException("Only the history of a ContentDocumentNode may be used to create a DocumentEditEntry.");
|
||||||
|
}
|
||||||
|
this.diff = diff;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
package com.andrewlalis.onyx.content.history.entry;
|
package com.andrewlalis.onyx.content.model.history.entry;
|
||||||
|
|
||||||
import com.andrewlalis.onyx.content.ContentNode;
|
import com.andrewlalis.onyx.content.model.ContentNode;
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistory;
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistory;
|
||||||
import com.andrewlalis.onyx.content.history.ContentNodeHistoryEntry;
|
import com.andrewlalis.onyx.content.model.history.ContentNodeHistoryEntry;
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
@ -11,7 +11,7 @@ import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "content_node_history_entry_rename")
|
@Table(name = "onyx_content_node_history_entry__rename")
|
||||||
@Getter
|
@Getter
|
||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
public class RenameEntry extends ContentNodeHistoryEntry {
|
public class RenameEntry extends ContentNodeHistoryEntry {
|
Loading…
Reference in New Issue