Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FE: Edit title , Delete title functionality #47

Merged
merged 11 commits into from
May 12, 2024
2 changes: 1 addition & 1 deletion .github/workflows/backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ jobs:

- name: build with Maven
run: |
./mvnw -B -V -ntp verify
./mvnw -B -V -ntp verify
2 changes: 1 addition & 1 deletion .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ jobs:
- name: build the application
run: |
cd ./frontend
pnpm build
pnpm build
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.llm_service.llm_service.persistance.entities.Role;
import com.llm_service.llm_service.service.jwt.filter.JwtAuthenticationFilter;
import com.llm_service.llm_service.service.user.UserDetailsServiceImp;
import java.util.Arrays;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -19,6 +21,7 @@
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;

@Configuration
@EnableWebSecurity
Expand All @@ -35,10 +38,21 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti

// TODO fix the swagger security config
return http.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("*"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(Arrays.asList("Authorization", "Cache-Control", "Content-Type"));
return config;
}))
.authorizeHttpRequests(req -> req.requestMatchers(
"/login/**", "/register/**", "/forget-password/**", "/swagger-ui/**", "/v3/api-docs/**")
"/api/v1/login/**",
"/api/v1/register/**",
"/api/v1/forget-password/**",
"/swagger-ui/**",
"/v3/api-docs/**")
.permitAll()
.requestMatchers("/paid/**")
.requestMatchers("/api/v1/paid/**")
.hasAuthority(Role.PAID.name())
.anyRequest()
.authenticated())
Expand All @@ -48,7 +62,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.exceptionHandling(e -> e.accessDeniedHandler((request, response, accessDeniedException) ->
response.setStatus(HttpStatus.FORBIDDEN.value()))
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)))
.logout(l -> l.logoutUrl("/logout")
.logout(l -> l.logoutUrl("/api/v1/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler(
(request, response, authentication) -> SecurityContextHolder.clearContext()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@CrossOrigin("http://localhost:4040")
@RestController
@RequiredArgsConstructor
@RequestMapping("/conversation")
public class ConversationController {

private final ConversationService conversationService;
Expand All @@ -34,7 +32,7 @@ public class ConversationController {
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "Get all conversations")
@GetMapping
@GetMapping(value = "/api/v1/paid/conversation")
public ResponseEntity<List<ConversationResponseCompact>> getAllConversations() throws UnAuthorizedException {
return ResponseEntity.ok(conversationService.getAll().stream()
.map(conversationApiMapper::mapCompact)
Expand All @@ -49,7 +47,7 @@ public ResponseEntity<List<ConversationResponseCompact>> getAllConversations() t
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "Get conversation by ID")
@GetMapping("/{id}")
@GetMapping("/api/v1/conversation/{id}")
public ResponseEntity<ConversationResponse> getConversationById(@PathVariable UUID id)
throws ConversationNotFoundException, UnAuthorizedException {
Conversation conversation =
Expand All @@ -66,7 +64,7 @@ public ResponseEntity<ConversationResponse> getConversationById(@PathVariable UU
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "Create new conversation")
@PostMapping
@PostMapping("/api/v1/conversation")
public ResponseEntity<ConversationResponse> createConversation() throws Exception {
Conversation conversation = conversationService.start();

Expand All @@ -81,7 +79,7 @@ public ResponseEntity<ConversationResponse> createConversation() throws Exceptio
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "Continue conversation using conversation ID")
@PutMapping("/{id}/continue")
@PutMapping("/api/v1/conversation/{id}/continue")
public ResponseEntity<List<DiscussionResponse>> continueConversation(
@PathVariable UUID id, @RequestBody ConversationRequest conversationRequest)
throws ConversationNotFoundException, UnAuthorizedException {
Expand All @@ -100,14 +98,19 @@ public ResponseEntity<List<DiscussionResponse>> continueConversation(
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "update conversation title")
@PutMapping("/{id}")
@PutMapping("/api/v1/conversation/{id}")
public ResponseEntity<ConversationResponseCompact> editConversation(
@PathVariable UUID id, @RequestBody ConversationTitleRequest conversationTitleRequest) throws Exception {
Conversation conversation =
Conversation conversationOld =
conversationService.getByID(id).orElseThrow(() -> new ConversationNotFoundException(id));
conversationService.editTitle(conversation, conversationTitleRequest.getTitle());
Conversation conversationNew =
conversationService.editTitle(conversationOld, conversationTitleRequest.getTitle());

// TODO find why save does not return discussions
// TEMP Solution
conversationNew.setDiscussions(conversationOld.getDiscussions());

return ResponseEntity.status(HttpStatus.OK).body(conversationApiMapper.mapCompact(conversation));
return ResponseEntity.status(HttpStatus.OK).body(conversationApiMapper.mapCompact(conversationNew));
}

@ApiResponses(
Expand All @@ -118,7 +121,7 @@ public ResponseEntity<ConversationResponseCompact> editConversation(
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "deletes a conversation")
@DeleteMapping("/{id}")
@DeleteMapping("/api/v1/conversation/{id}")
public ResponseEntity<Void> deleteConversation(@PathVariable UUID id)
throws ConversationNotFoundException, UnAuthorizedException {
conversationService.getByID(id).orElseThrow(() -> new ConversationNotFoundException(id));
Expand All @@ -134,7 +137,7 @@ public ResponseEntity<Void> deleteConversation(@PathVariable UUID id)
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "deletes all conversations")
@DeleteMapping
@DeleteMapping("/api/v1/conversation")
public ResponseEntity<Void> deleteConversation() {
conversationService.deleteAll();
return ResponseEntity.status(HttpStatus.OK).body(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;

@CrossOrigin("http://localhost:4040")
@RestController
public class UserController {
private final AuthenticationService authenticationService;
Expand All @@ -35,7 +34,7 @@ public UserController(AuthenticationService authenticationService, UserApiMapper
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "get the current user")
@GetMapping("/me")
@GetMapping("/api/v1/me")
public ResponseEntity<UserResponse> register() throws UnAuthorizedException {
User user = authenticationService.getUser().orElseThrow(UnAuthorizedException::new);
return ResponseEntity.status(HttpStatus.OK).body(userApiMapper.map(user));
Expand All @@ -49,7 +48,7 @@ public ResponseEntity<UserResponse> register() throws UnAuthorizedException {
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "register the user")
@PostMapping("/register")
@PostMapping("/api/v1/register")
public ResponseEntity<UserResponse> register(@RequestBody UserRequest userRequest)
throws UsernameAlreadyExistsException {
User user = authenticationService.register(userRequest);
Expand All @@ -64,7 +63,7 @@ public ResponseEntity<UserResponse> register(@RequestBody UserRequest userReques
content = {@Content(mediaType = "application/json")})
})
@Operation(summary = "login phase of the user")
@PostMapping("/login")
@PostMapping("/api/v1/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginRequest)
throws UserNotFoundException, AuthenticationException {
AuthenticationResponse authenticationResponse = authenticationService.authenticate(loginRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import jakarta.persistence.*;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.*;

@EqualsAndHashCode(
callSuper = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@Entity
@Table(name = "discussion")
@Data
@Builder
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class DiscussionEntity extends BaseEntity {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ public class ConversationService {
private final DiscussionPersistenceManager discussionPersistenceManager;
private final UserContext userContext;

public Conversation start() throws Exception {
public Conversation start() throws UnAuthorizedException {
Optional<User> user = userContext.getUserFromContext();

if (user.isEmpty()) {
// TODO fix it later
throw new Exception("");
throw new UnAuthorizedException();
}

Conversation conversation = Conversation.builder().discussions(null).build();
Expand Down Expand Up @@ -106,15 +105,14 @@ public void deleteAll() {
conversationPersistenceManager.deleteAll();
}

public void editTitle(Conversation conversation, String title) throws Exception {
public Conversation editTitle(Conversation conversation, String title) throws UnAuthorizedException {
Optional<User> user = userContext.getUserFromContext();

if (user.isEmpty()) {
// TODO fix it later
throw new Exception("");
throw new UnAuthorizedException();
}

conversationPersistenceManager.save(
return conversationPersistenceManager.save(
conversation.toBuilder().title(title).build(), user.get());
}

Expand Down
6 changes: 4 additions & 2 deletions frontend/src/hooks/api/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ export const Queries = {
User: "User",
} as const;

const basePath = import.meta.env.VITE_APP_BASE_URL;
const basePath = `${import.meta.env.VITE_APP_BASE_URL}/api/v1`;

// Conversation
export const ConversationsPath = `${basePath}/conversation`;
export const ConversationStartPath = `${basePath}/conversation`;
export const ConversationsPath = `${basePath}/paid/conversation`;
export const getConversationPath = (id: ConversationId) => `${basePath}/conversation/${id}`;
export const getContinueConversationPath = (id: ConversationId) => `${getConversationPath(id)}/continue`;

// User
export const UserPath = `${basePath}/me`;
export const LoginPath = `${basePath}/login`;
export const LogoutPath = `${basePath}/logout`;
export const RegisterPath = `${basePath}/register`;
24 changes: 24 additions & 0 deletions frontend/src/hooks/api/useDeleteConversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ConversationCompact } from "models/Conversation.ts";
import useApi from "hooks/useApi.ts";
import { ConversationId } from "models/Id.ts";
import { getConversationPath, Queries } from "./constants.ts";

export default function useDeleteConversation() {
const callApi = useApi();
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (id: ConversationId) => {
await callApi<void>({
url: getConversationPath(id),
method: "DELETE",
responseAs: "text",
});
},
onSuccess: (_, id) => {
const queryKey = [Queries.Conversation];
queryClient.setQueryData(queryKey, (old: ConversationCompact[]) => old?.filter((item) => item.id !== id));
},
});
}
36 changes: 36 additions & 0 deletions frontend/src/hooks/api/useEditConversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ConversationCompact } from "models/Conversation.ts";
import useApi from "hooks/useApi.ts";
import { ConversationId } from "models/Id.ts";
import { getConversationPath, Queries } from "./constants.ts";

export default function useEditConversation() {
const callApi = useApi();
const queryClient = useQueryClient();

return useMutation<ConversationCompact, unknown, { title: string; id: ConversationId }>({
mutationFn: async ({ title, id }) => {
const conversation = await callApi<ConversationCompact>({
url: getConversationPath(id),
method: "PUT",
body: { title },
});
// TODO add zod validation

return conversation;
},
onSuccess: (conversation) => {
const queryKey = [Queries.Conversation];
queryClient.setQueryData(queryKey, (old: ConversationCompact[]) => {
const conversations = old?.map((item) => {
if (item.id === conversation.id) {
return conversation;
}
return item;
});

return conversations;
});
},
});
}
1 change: 1 addition & 0 deletions frontend/src/hooks/api/useGetConversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export default function useGetConversation(id: ConversationId) {
// TODO add zod validation
return conversation;
},
staleTime: 5 * 60 * 60,
});
}
1 change: 1 addition & 0 deletions frontend/src/hooks/api/useGetConversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export default function useGetConversations() {
// TODO add zod validation
return conversation;
},
staleTime: 5 * 60 * 60,
});
}
9 changes: 0 additions & 9 deletions frontend/src/hooks/api/useLogout.ts

This file was deleted.

16 changes: 16 additions & 0 deletions frontend/src/hooks/api/useLogoutCall.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useMutation } from "@tanstack/react-query";
import useApi from "hooks/useApi.ts";
import { LogoutPath } from "./constants.ts";

export default function useLogoutCall() {
const callApi = useApi();

return useMutation({
mutationFn: () =>
callApi({
method: "POST",
url: LogoutPath,
responseAs: "text",
}),
});
}
4 changes: 2 additions & 2 deletions frontend/src/hooks/api/useStartConversation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import useApi from "hooks/useApi.ts";
import { Conversation } from "models/Conversation.ts";
import { ConversationsPath } from "./constants.ts";
import { ConversationStartPath } from "./constants.ts";

type OnSuccessCallback = (conversation: Conversation) => void;

Expand All @@ -11,7 +11,7 @@ export default function useStartConversation(onSuccessCallback: OnSuccessCallbac
return useMutation<Conversation>({
mutationFn: async () => {
const conversation = await callApi<Conversation>({
url: ConversationsPath,
url: ConversationStartPath,
method: "POST",
});
// TODO add zod validation
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/hooks/useLogout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import useAuth from "context/useAuth.ts";
import { useLogoutCall } from "./api/useLogoutCall.ts";

export default function useLogout() {
const { setToken } = useAuth();
const { mutateAsync } = useLogoutCall();

return async () => {
await mutateAsync();
setToken(null);
};
}
Loading