Преглед на файлове

add get not by userId endpoint

Daniel Bohry преди 2 седмици
родител
ревизия
d8f78908bc

+ 7 - 0
src/main/java/com/lhamacorp/knotes/api/NoteController.java

@@ -9,6 +9,8 @@ import com.lhamacorp.knotes.service.NoteService;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
+import java.util.List;
+
 @RestController
 @RequestMapping("api/notes")
 @CrossOrigin(origins = "*")
@@ -20,6 +22,11 @@ public class NoteController {
         this.service = service;
     }
 
+    @GetMapping
+    public ResponseEntity<List<String>> findByUserId() {
+        return ResponseEntity.ok(service.findAll());
+    }
+
     @GetMapping("{id}")
     public ResponseEntity<NoteResponse> find(@PathVariable String id) {
         Note note = service.findById(id);

+ 50 - 0
src/main/java/com/lhamacorp/knotes/client/AuthClient.java

@@ -0,0 +1,50 @@
+package com.lhamacorp.knotes.client;
+
+import com.lhamacorp.knotes.exception.UnauthorizedException;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class AuthClient {
+
+    private final RestTemplate rest;
+    private final String baseUrl;
+
+    public AuthClient(RestTemplate rest, @Value("${auth.api}") String baseUrl) {
+        this.rest = rest;
+        this.baseUrl = baseUrl;
+    }
+
+    @Cacheable(value = "current", key = "#token")
+    public CurrentUser current(String token) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.add("Authorization", token);
+
+        HttpEntity<Map<String, String>> entity = new HttpEntity<>(null, headers);
+
+        try {
+            ResponseEntity<CurrentUser> response = rest.exchange(baseUrl + "/users/current", HttpMethod.GET, entity, CurrentUser.class);
+
+            if (response.getStatusCode() == HttpStatus.OK) {
+                return response.getBody();
+            } else {
+                throw new UnauthorizedException("Unexpected response status: " + response.getStatusCode());
+            }
+        } catch (HttpClientErrorException e) {
+            throw new UnauthorizedException("Error: " + e.getStatusCode() + " - " + e.getResponseBodyAsString());
+        } catch (Exception e) {
+            throw new UnauthorizedException("An error occurred: " + e.getMessage());
+        }
+    }
+
+    public record CurrentUser(String id, String username, List<String> roles) {
+    }
+
+}

+ 2 - 1
src/main/java/com/lhamacorp/knotes/config/CacheConfig.java

@@ -21,9 +21,10 @@ public class CacheConfig {
     public CacheManager cacheManager() {
         CaffeineCache content = build("content", ofSeconds(60), 1000);
         CaffeineCache metadata = build("metadata", ofSeconds(10), 500);
+        CaffeineCache current = build("current", ofSeconds(60), 1000);
 
         SimpleCacheManager manager = new SimpleCacheManager();
-        manager.setCaches(List.of(content, metadata));
+        manager.setCaches(List.of(content, metadata, current));
         return manager;
     }
 

+ 15 - 0
src/main/java/com/lhamacorp/knotes/config/RestConfig.java

@@ -0,0 +1,15 @@
+package com.lhamacorp.knotes.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestConfig {
+
+    @Bean
+    RestTemplate rest() {
+        return new RestTemplate();
+    }
+
+}

+ 69 - 0
src/main/java/com/lhamacorp/knotes/context/ServiceContextFilter.java

@@ -0,0 +1,69 @@
+package com.lhamacorp.knotes.context;
+
+import com.lhamacorp.knotes.client.AuthClient;
+import com.lhamacorp.knotes.client.AuthClient.CurrentUser;
+import com.lhamacorp.knotes.exception.UnauthorizedException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+import static java.util.Collections.emptyList;
+
+@Component
+public class ServiceContextFilter extends OncePerRequestFilter {
+
+    private final AuthClient authClient;
+
+    public ServiceContextFilter(AuthClient authClient) {
+        this.authClient = authClient;
+    }
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, jakarta.servlet.FilterChain filterChain) throws jakarta.servlet.ServletException, IOException {
+        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
+            response.setStatus(HttpServletResponse.SC_OK);
+            response.setHeader("Access-Control-Allow-Origin", "*");
+            response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
+            response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
+            response.setHeader("Access-Control-Max-Age", "3600");
+            return;
+        }
+
+        String path = request.getRequestURI();
+        if (path.contains("/actuator")) {
+            filterChain.doFilter(request, response);
+            return;
+        }
+
+        try {
+            CurrentUser user = extractCurrentUser(request);
+
+            if (user != null) {
+                UserContextHolder.set(new UserContext(user.id(), user.username(), user.roles()));
+
+                try {
+                    filterChain.doFilter(request, response);
+                } finally {
+                    UserContextHolder.clear();
+                }
+            } else {
+                response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized: User not found");
+            }
+        } catch (UnauthorizedException e) {
+            response.sendError(HttpStatus.UNAUTHORIZED.value(), "Unauthorized: Invalid token");
+        }
+    }
+
+    private CurrentUser extractCurrentUser(HttpServletRequest request) {
+        String token = request.getHeader("Authorization");
+
+        return token == null
+                ? new CurrentUser("1", "anon", emptyList())
+                : authClient.current(token);
+    }
+
+}

+ 6 - 0
src/main/java/com/lhamacorp/knotes/context/UserContext.java

@@ -0,0 +1,6 @@
+package com.lhamacorp.knotes.context;
+
+import java.util.List;
+
+public record UserContext(String id, String username, List<String> roles) {
+}

+ 19 - 0
src/main/java/com/lhamacorp/knotes/context/UserContextHolder.java

@@ -0,0 +1,19 @@
+package com.lhamacorp.knotes.context;
+
+public class UserContextHolder {
+
+    private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();
+
+    public static void set(UserContext serviceContext) {
+        CONTEXT.set(serviceContext);
+    }
+
+    public static UserContext get() {
+        return CONTEXT.get();
+    }
+
+    public static void clear() {
+        CONTEXT.remove();
+    }
+
+}

+ 7 - 6
src/main/java/com/lhamacorp/knotes/domain/Note.java

@@ -10,14 +10,15 @@ import java.time.Instant;
 
 @Document("notes")
 public record Note(
-    @Id String id,
-    @Field("content") Binary compressedData,
-    Instant createdAt,
-    Instant modifiedAt
+        @Id String id,
+        @Field("content") Binary compressedData,
+        String createdBy,
+        Instant createdAt,
+        Instant modifiedAt
 ) {
 
-    public Note(String id, String content, Instant createdAt, Instant modifiedAt) {
-        this(id, content != null ? new Binary(CompressionUtils.compress(content)) : null, createdAt, modifiedAt);
+    public Note(String id, String content, String createdBy, Instant createdAt, Instant modifiedAt) {
+        this(id, content != null ? new Binary(CompressionUtils.compress(content)) : null, createdBy, createdAt, modifiedAt);
     }
 
     public String content() {

+ 12 - 0
src/main/java/com/lhamacorp/knotes/exception/UnauthorizedException.java

@@ -0,0 +1,12 @@
+package com.lhamacorp.knotes.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(value = HttpStatus.UNAUTHORIZED)
+public class UnauthorizedException extends RuntimeException {
+
+    public UnauthorizedException(String message) {
+        super(message);
+    }
+}

+ 3 - 0
src/main/java/com/lhamacorp/knotes/repository/NoteRepository.java

@@ -16,4 +16,7 @@ public interface NoteRepository extends MongoRepository<Note, String> {
 
     @Query(value = "{ 'content': BinData(0, '') }", fields = "{ '_id': 1, 'createdAt': 1 }")
     List<Note> findEmptyNotes();
+
+    List<Note> findAllByCreatedBy(String createdBy);
+
 }

+ 17 - 2
src/main/java/com/lhamacorp/knotes/service/NoteService.java

@@ -3,6 +3,8 @@ package com.lhamacorp.knotes.service;
 import com.github.f4b6a3.ulid.Ulid;
 import com.github.f4b6a3.ulid.UlidCreator;
 import com.lhamacorp.knotes.api.dto.NoteMetadata;
+import com.lhamacorp.knotes.context.UserContext;
+import com.lhamacorp.knotes.context.UserContextHolder;
 import com.lhamacorp.knotes.domain.Note;
 import com.lhamacorp.knotes.exception.NotFoundException;
 import com.lhamacorp.knotes.repository.NoteRepository;
@@ -12,8 +14,11 @@ import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
 import java.time.Instant;
+import java.util.Collections;
+import java.util.List;
 
 import static java.time.Instant.now;
+import static java.util.Collections.emptyList;
 import static org.slf4j.LoggerFactory.getLogger;
 
 @Service
@@ -32,6 +37,16 @@ public class NoteService {
         return repository.existsById(id);
     }
 
+    public List<String> findAll() {
+        UserContext user = UserContextHolder.get();
+
+        //id 1 is anon and this should only return a list for authenticated users
+        return "1".equals(user.id())
+                ? emptyList()
+                : repository.findAllByCreatedBy(user.id()).stream().map(Note::id).toList();
+
+    }
+
     @Cacheable(value = "content", key = "#id")
     public Note findById(String id) {
         return repository.findById(id).orElseThrow(() -> new NotFoundException(NOT_FOUND));
@@ -50,7 +65,7 @@ public class NoteService {
         log.debug("Saving note [{}]", id);
 
         Instant now = now();
-        return repository.save(new Note(id.toString(), content, now, now));
+        return repository.save(new Note(id.toString(), content, UserContextHolder.get().id(), now, now));
     }
 
     @CacheEvict(value = {"content", "metadata"}, key = "#id")
@@ -59,7 +74,7 @@ public class NoteService {
 
         log.debug("Updating note [{}]", id);
 
-        return repository.save(new Note(id, content, note.createdAt(), now()));
+        return repository.save(new Note(id, content, note.createdBy(), note.createdAt(), now()));
     }
 
 }

+ 4 - 1
src/main/resources/application.yml

@@ -4,4 +4,7 @@ server:
 spring:
   mongodb:
     database: ${database:knotes}
-    uri: ${mongo:mongodb://localhost:27017}
+    uri: ${mongo:mongodb://localhost:27017}
+
+auth:
+  api: ${auth_api:}