Added more auth stuff.
This commit is contained in:
		
							parent
							
								
									c2dd6e8a4a
								
							
						
					
					
						commit
						597c79eb7c
					
				| 
						 | 
					@ -2,12 +2,13 @@ package com.andrewlalis.onyx;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import org.springframework.boot.SpringApplication;
 | 
					import org.springframework.boot.SpringApplication;
 | 
				
			||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
 | 
					import org.springframework.boot.autoconfigure.SpringBootApplication;
 | 
				
			||||||
 | 
					import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@SpringBootApplication
 | 
					@SpringBootApplication(
 | 
				
			||||||
 | 
							exclude = {UserDetailsServiceAutoConfiguration.class}
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
public class OnyxApplication {
 | 
					public class OnyxApplication {
 | 
				
			||||||
 | 
					 | 
				
			||||||
	public static void main(String[] args) {
 | 
						public static void main(String[] args) {
 | 
				
			||||||
		SpringApplication.run(OnyxApplication.class, args);
 | 
							SpringApplication.run(OnyxApplication.class, args);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,71 @@
 | 
				
			||||||
 | 
					package com.andrewlalis.onyx.auth.components;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.auth.model.User;
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.dao.ContentNodeRepository;
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.model.ContentNode;
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * A filter that's run on any request to "/content/nodes/:id/**", which checks
 | 
				
			||||||
 | 
					 * that the user accessing the specific node is actually allowed to access it,
 | 
				
			||||||
 | 
					 * based on the node's access rules.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Component
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					@Slf4j
 | 
				
			||||||
 | 
					public class ContentAccessFilter extends OncePerRequestFilter {
 | 
				
			||||||
 | 
					    private final ContentNodeRepository contentNodeRepository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected void doFilterInternal(
 | 
				
			||||||
 | 
					            HttpServletRequest request,
 | 
				
			||||||
 | 
					            HttpServletResponse response,
 | 
				
			||||||
 | 
					            FilterChain filterChain) throws ServletException, IOException {
 | 
				
			||||||
 | 
					        log.info("Calling ContentAccessFilter for path {}", request.getServletPath());
 | 
				
			||||||
 | 
					        String nodeId = extractNodeId(request);
 | 
				
			||||||
 | 
					        if (nodeId == null) {
 | 
				
			||||||
 | 
					            log.warn("Couldn't extract node id!");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        log.info("Extracted node id: {}", nodeId);
 | 
				
			||||||
 | 
					        ContentNode node;
 | 
				
			||||||
 | 
					        if (nodeId.equalsIgnoreCase("root")) {
 | 
				
			||||||
 | 
					            node = contentNodeRepository.findRoot();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            node = contentNodeRepository.findById(Long.parseLong(nodeId)).orElse(null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (node == null) {
 | 
				
			||||||
 | 
					            log.warn("Node doesn't exist!");
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        TokenAuthentication auth = (TokenAuthentication) SecurityContextHolder.getContext().getAuthentication();
 | 
				
			||||||
 | 
					        User user = auth.getPrincipal();
 | 
				
			||||||
 | 
					        // TODO: Actually check access rules.
 | 
				
			||||||
 | 
					        filterChain.doFilter(request, response);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    protected boolean shouldNotFilter(HttpServletRequest request) {
 | 
				
			||||||
 | 
					        return !request.getServletPath().startsWith("/content/nodes/");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private String extractNodeId(HttpServletRequest request) {
 | 
				
			||||||
 | 
					        String path = request.getServletPath();
 | 
				
			||||||
 | 
					        final String PREFIX = "/content/nodes/";
 | 
				
			||||||
 | 
					        if (!path.startsWith(PREFIX)) return null;
 | 
				
			||||||
 | 
					        String suffix = path.substring(PREFIX.length());
 | 
				
			||||||
 | 
					        int suffixEnd = suffix.indexOf('/');
 | 
				
			||||||
 | 
					        if (suffixEnd != -1) return suffix.substring(0, suffixEnd);
 | 
				
			||||||
 | 
					        return suffix;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,11 +1,13 @@
 | 
				
			||||||
package com.andrewlalis.onyx.config;
 | 
					package com.andrewlalis.onyx.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.auth.components.ContentAccessFilter;
 | 
				
			||||||
import com.andrewlalis.onyx.auth.components.JwtFilter;
 | 
					import com.andrewlalis.onyx.auth.components.JwtFilter;
 | 
				
			||||||
import lombok.RequiredArgsConstructor;
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
import org.springframework.context.annotation.Bean;
 | 
					import org.springframework.context.annotation.Bean;
 | 
				
			||||||
import org.springframework.context.annotation.Configuration;
 | 
					import org.springframework.context.annotation.Configuration;
 | 
				
			||||||
import org.springframework.http.HttpMethod;
 | 
					import org.springframework.http.HttpMethod;
 | 
				
			||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 | 
					import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 | 
				
			||||||
 | 
					import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 | 
				
			||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 | 
					import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 | 
				
			||||||
import org.springframework.security.config.http.SessionCreationPolicy;
 | 
					import org.springframework.security.config.http.SessionCreationPolicy;
 | 
				
			||||||
import org.springframework.security.web.SecurityFilterChain;
 | 
					import org.springframework.security.web.SecurityFilterChain;
 | 
				
			||||||
| 
						 | 
					@ -14,9 +16,11 @@ import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
 | 
				
			||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 | 
					import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Configuration
 | 
					@Configuration
 | 
				
			||||||
 | 
					@EnableWebSecurity
 | 
				
			||||||
@RequiredArgsConstructor
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
public class SecurityConfig {
 | 
					public class SecurityConfig {
 | 
				
			||||||
    private final JwtFilter jwtFilter;
 | 
					    private final JwtFilter jwtFilter;
 | 
				
			||||||
 | 
					    private final ContentAccessFilter contentAccessFilter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Bean
 | 
					    @Bean
 | 
				
			||||||
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 | 
					    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 | 
				
			||||||
| 
						 | 
					@ -28,6 +32,7 @@ public class SecurityConfig {
 | 
				
			||||||
        http.csrf(AbstractHttpConfigurer::disable);
 | 
					        http.csrf(AbstractHttpConfigurer::disable);
 | 
				
			||||||
        http.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.NEVER));
 | 
					        http.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.NEVER));
 | 
				
			||||||
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
 | 
					        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
 | 
				
			||||||
 | 
					        http.addFilterAfter(contentAccessFilter, JwtFilter.class);
 | 
				
			||||||
        http.cors(configurer -> configurer.configure(http));
 | 
					        http.cors(configurer -> configurer.configure(http));
 | 
				
			||||||
        return http.build();
 | 
					        return http.build();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					package com.andrewlalis.onyx.content.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.service.ContentNodeService;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import org.springframework.web.bind.annotation.*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@RestController
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class ContentNodeController {
 | 
				
			||||||
 | 
					    private final ContentNodeService contentNodeService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @GetMapping("/content/nodes/{nodeId}")
 | 
				
			||||||
 | 
					    public ContentNodeResponse getContentNode(@PathVariable long nodeId) {
 | 
				
			||||||
 | 
					        return contentNodeService.getNodeById(nodeId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @GetMapping("/content/nodes/root")
 | 
				
			||||||
 | 
					    public ContentNodeResponse getRootNode() {
 | 
				
			||||||
 | 
					        return contentNodeService.getRootNode();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @PostMapping("/content/nodes/{nodeId}/children")
 | 
				
			||||||
 | 
					    public ContentNodeResponse createContentNode(@PathVariable long nodeId, @RequestBody ObjectNode body) {
 | 
				
			||||||
 | 
					        return contentNodeService.createNode(nodeId, body);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @DeleteMapping("/content/nodes/{nodeId}")
 | 
				
			||||||
 | 
					    public void deleteContentNode(@PathVariable long nodeId) {
 | 
				
			||||||
 | 
					        contentNodeService.deleteNode(nodeId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,58 @@
 | 
				
			||||||
 | 
					package com.andrewlalis.onyx.content.api;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.model.ContentContainerNode;
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.model.ContentDocumentNode;
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.model.ContentNode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public abstract class ContentNodeResponse {
 | 
				
			||||||
 | 
					    public final long id;
 | 
				
			||||||
 | 
					    public final String name;
 | 
				
			||||||
 | 
					    public final String nodeType;
 | 
				
			||||||
 | 
					    public final boolean archived;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ContentNodeResponse(ContentNode node) {
 | 
				
			||||||
 | 
					        this.id = node.getId();
 | 
				
			||||||
 | 
					        this.name = node.getName();
 | 
				
			||||||
 | 
					        this.nodeType = node.getNodeType().name();
 | 
				
			||||||
 | 
					        this.archived = node.isArchived();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static ContentNodeResponse forNode(ContentNode node) {
 | 
				
			||||||
 | 
					        return switch (node) {
 | 
				
			||||||
 | 
					            case ContentContainerNode c -> new ContentNodeResponse.Container(c);
 | 
				
			||||||
 | 
					            case ContentDocumentNode d -> new ContentNodeResponse.Document(d);
 | 
				
			||||||
 | 
					            default -> throw new IllegalStateException("Unexpected value: " + node);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static class Container extends ContentNodeResponse {
 | 
				
			||||||
 | 
					        public record ChildInfo (long id, String name, String nodeType) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public final ChildInfo[] children;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public Container(ContentContainerNode node) {
 | 
				
			||||||
 | 
					            super(node);
 | 
				
			||||||
 | 
					            Set<ContentNode> childrenSet = node.getChildren();
 | 
				
			||||||
 | 
					            this.children = new ChildInfo[childrenSet.size()];
 | 
				
			||||||
 | 
					            int i = 0;
 | 
				
			||||||
 | 
					            for (ContentNode child : childrenSet) {
 | 
				
			||||||
 | 
					                children[i++] = new ChildInfo(
 | 
				
			||||||
 | 
					                        child.getId(),
 | 
				
			||||||
 | 
					                        child.getName(),
 | 
				
			||||||
 | 
					                        child.getNodeType().name()
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static class Document extends ContentNodeResponse {
 | 
				
			||||||
 | 
					        public final String contentType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public Document(ContentDocumentNode node) {
 | 
				
			||||||
 | 
					            super(node);
 | 
				
			||||||
 | 
					            this.contentType = node.getContentType();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -2,9 +2,15 @@ package com.andrewlalis.onyx.content.dao;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.andrewlalis.onyx.content.model.ContentNode;
 | 
					import com.andrewlalis.onyx.content.model.ContentNode;
 | 
				
			||||||
import org.springframework.data.jpa.repository.JpaRepository;
 | 
					import org.springframework.data.jpa.repository.JpaRepository;
 | 
				
			||||||
 | 
					import org.springframework.data.jpa.repository.Query;
 | 
				
			||||||
import org.springframework.stereotype.Repository;
 | 
					import org.springframework.stereotype.Repository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Repository
 | 
					@Repository
 | 
				
			||||||
public interface ContentNodeRepository extends JpaRepository<ContentNode, Long> {
 | 
					public interface ContentNodeRepository extends JpaRepository<ContentNode, Long> {
 | 
				
			||||||
    boolean existsByName(String name);
 | 
					    boolean existsByName(String name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Query("SELECT cn FROM ContentNode cn WHERE cn.name = '" + ContentNode.ROOT_NODE_NAME + "'")
 | 
				
			||||||
 | 
					    ContentNode findRoot();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,8 +2,10 @@ package com.andrewlalis.onyx.content.model;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import jakarta.persistence.*;
 | 
					import jakarta.persistence.*;
 | 
				
			||||||
import lombok.AccessLevel;
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
 | 
					import lombok.Getter;
 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.HashSet;
 | 
				
			||||||
import java.util.Set;
 | 
					import java.util.Set;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -13,6 +15,7 @@ import java.util.Set;
 | 
				
			||||||
@Entity
 | 
					@Entity
 | 
				
			||||||
@Table(name = "onyx_content_container_node")
 | 
					@Table(name = "onyx_content_container_node")
 | 
				
			||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
 | 
					@Getter
 | 
				
			||||||
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.
 | 
				
			||||||
| 
						 | 
					@ -22,5 +25,6 @@ public final class ContentContainerNode extends ContentNode {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public ContentContainerNode(String name, ContentContainerNode parentContainer) {
 | 
					    public ContentContainerNode(String name, ContentContainerNode parentContainer) {
 | 
				
			||||||
        super(name, Type.CONTAINER, parentContainer);
 | 
					        super(name, Type.CONTAINER, parentContainer);
 | 
				
			||||||
 | 
					        this.children = new HashSet<>();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,7 @@ import jakarta.persistence.Entity;
 | 
				
			||||||
import jakarta.persistence.Lob;
 | 
					import jakarta.persistence.Lob;
 | 
				
			||||||
import jakarta.persistence.Table;
 | 
					import jakarta.persistence.Table;
 | 
				
			||||||
import lombok.AccessLevel;
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
 | 
					import lombok.Getter;
 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
import lombok.Setter;
 | 
					import lombok.Setter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +15,7 @@ import lombok.Setter;
 | 
				
			||||||
@Entity
 | 
					@Entity
 | 
				
			||||||
@Table(name = "onyx_content_document_node")
 | 
					@Table(name = "onyx_content_document_node")
 | 
				
			||||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
					@NoArgsConstructor(access = AccessLevel.PROTECTED)
 | 
				
			||||||
 | 
					@Getter
 | 
				
			||||||
public class ContentDocumentNode extends ContentNode {
 | 
					public class ContentDocumentNode extends ContentNode {
 | 
				
			||||||
    @Column(nullable = false, updatable = false, length = 127)
 | 
					    @Column(nullable = false, updatable = false, length = 127)
 | 
				
			||||||
    private String contentType;
 | 
					    private String contentType;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ import jakarta.persistence.*;
 | 
				
			||||||
import lombok.AccessLevel;
 | 
					import lombok.AccessLevel;
 | 
				
			||||||
import lombok.Getter;
 | 
					import lombok.Getter;
 | 
				
			||||||
import lombok.NoArgsConstructor;
 | 
					import lombok.NoArgsConstructor;
 | 
				
			||||||
 | 
					import lombok.Setter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * The abstract model that represents all nodes in the system's hierarchical
 | 
					 * The abstract model that represents all nodes in the system's hierarchical
 | 
				
			||||||
| 
						 | 
					@ -43,7 +44,7 @@ public abstract class ContentNode {
 | 
				
			||||||
    @OneToOne(fetch = FetchType.LAZY, optional = false, orphanRemoval = true, cascade = CascadeType.ALL)
 | 
					    @OneToOne(fetch = FetchType.LAZY, optional = false, orphanRemoval = true, cascade = CascadeType.ALL)
 | 
				
			||||||
    private ContentNodeHistory history;
 | 
					    private ContentNodeHistory history;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @Column(nullable = false)
 | 
					    @Column(nullable = false) @Setter
 | 
				
			||||||
    private boolean archived;
 | 
					    private boolean archived;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public enum Type {
 | 
					    public enum Type {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,82 @@
 | 
				
			||||||
 | 
					package com.andrewlalis.onyx.content.service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.api.ContentNodeResponse;
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.dao.ContentNodeRepository;
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.model.ContentContainerNode;
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.model.ContentDocumentNode;
 | 
				
			||||||
 | 
					import com.andrewlalis.onyx.content.model.ContentNode;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.node.ObjectNode;
 | 
				
			||||||
 | 
					import lombok.RequiredArgsConstructor;
 | 
				
			||||||
 | 
					import org.springframework.http.HttpStatus;
 | 
				
			||||||
 | 
					import org.springframework.stereotype.Service;
 | 
				
			||||||
 | 
					import org.springframework.transaction.annotation.Transactional;
 | 
				
			||||||
 | 
					import org.springframework.web.server.ResponseStatusException;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Service
 | 
				
			||||||
 | 
					@RequiredArgsConstructor
 | 
				
			||||||
 | 
					public class ContentNodeService {
 | 
				
			||||||
 | 
					    private final ContentNodeRepository contentNodeRepository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
 | 
					    public ContentNodeResponse getNodeById(long nodeId) {
 | 
				
			||||||
 | 
					        ContentNode node = contentNodeRepository.findById(nodeId)
 | 
				
			||||||
 | 
					                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
					        return ContentNodeResponse.forNode(node);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Transactional(readOnly = true)
 | 
				
			||||||
 | 
					    public ContentNodeResponse getRootNode() {
 | 
				
			||||||
 | 
					        ContentNode rootNode = contentNodeRepository.findRoot();
 | 
				
			||||||
 | 
					        return ContentNodeResponse.forNode(rootNode);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Transactional
 | 
				
			||||||
 | 
					    public ContentNodeResponse createNode(long parentNodeId, ObjectNode body) {
 | 
				
			||||||
 | 
					        ContentNode parentNode = contentNodeRepository.findById(parentNodeId)
 | 
				
			||||||
 | 
					                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
					        if (!(parentNode instanceof ContentContainerNode container)) {
 | 
				
			||||||
 | 
					            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Can only create new content nodes inside containers.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            ContentNode node;
 | 
				
			||||||
 | 
					            // First validate that the name is legitimate, and doesn't conflict with any siblings.
 | 
				
			||||||
 | 
					            String name = body.get("name").asText().trim();
 | 
				
			||||||
 | 
					            if (name.isBlank() || name.length() > ContentNode.MAX_NAME_LENGTH || name.equalsIgnoreCase(ContentNode.ROOT_NODE_NAME)) {
 | 
				
			||||||
 | 
					                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid name.");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            for (ContentNode child : container.getChildren()) {
 | 
				
			||||||
 | 
					                if (child.getName().equals(name)) {
 | 
				
			||||||
 | 
					                    throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Duplicate name.");
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            String nodeTypeStr = body.get("nodeType").asText();
 | 
				
			||||||
 | 
					            if (nodeTypeStr.equalsIgnoreCase(ContentNode.Type.CONTAINER.name())) {
 | 
				
			||||||
 | 
					                node = new ContentContainerNode(name, container);
 | 
				
			||||||
 | 
					            } else if (nodeTypeStr.equalsIgnoreCase(ContentNode.Type.DOCUMENT.name())) {
 | 
				
			||||||
 | 
					                String contentType = body.get("contentType").asText();
 | 
				
			||||||
 | 
					                String content = body.get("content").asText();
 | 
				
			||||||
 | 
					                node = new ContentDocumentNode(contentType, content.getBytes(StandardCharsets.UTF_8));
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid nodeType.");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            node = contentNodeRepository.save(node);
 | 
				
			||||||
 | 
					            return ContentNodeResponse.forNode(node);
 | 
				
			||||||
 | 
					        } catch (NullPointerException e) {
 | 
				
			||||||
 | 
					            e.printStackTrace();
 | 
				
			||||||
 | 
					            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid request body.", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Transactional
 | 
				
			||||||
 | 
					    public void deleteNode(long nodeId) {
 | 
				
			||||||
 | 
					        ContentNode node = contentNodeRepository.findById(nodeId)
 | 
				
			||||||
 | 
					                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
 | 
				
			||||||
 | 
					        if (node.getName().equals(ContentNode.ROOT_NODE_NAME)) {
 | 
				
			||||||
 | 
					            throw new ResponseStatusException(HttpStatus.FORBIDDEN, "You cannot delete the root node.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        contentNodeRepository.delete(node);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue