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