diff --git a/client/src/Profile.scss b/client/src/Profile.scss index 1aa22a1..d57e48f 100644 --- a/client/src/Profile.scss +++ b/client/src/Profile.scss @@ -25,6 +25,28 @@ } + form { + display: flex; + flex-direction: column; + max-width: 10rem; + + input { + margin-bottom: 0.2rem; + } + } + + button { + text-align: left; + background: none; + border: none; + cursor: pointer; + margin: 0; + padding: 0; + padding-top: 0.5rem; + color: black; + font-family: inherit; + } + } .newPost { @@ -42,4 +64,4 @@ .posts { padding: 0 2rem 2rem; } -} \ No newline at end of file +} diff --git a/client/src/Profile.tsx b/client/src/Profile.tsx index d356955..3c4da1e 100644 --- a/client/src/Profile.tsx +++ b/client/src/Profile.tsx @@ -1,4 +1,9 @@ -import { Form, useLoaderData, useNavigation } from "react-router-dom"; +import { + Form, + useLoaderData, + useNavigation, + useSubmit, +} from "react-router-dom"; import { LoaderToType, profileLoader } from "./loaders"; import { isError } from "./api/dto"; import { useHomeContext } from "./HomeContext"; @@ -6,6 +11,7 @@ import { PostList } from "./PostList"; import "./PostForm.scss"; import "./Profile.scss"; +import { useState } from "react"; export interface IProfileProps { self: boolean; @@ -29,12 +35,60 @@ export function Profile({ self }: IProfileProps) { } const navigation = useNavigation(); const busy = navigation.state === "submitting"; + const [editing, setEditing] = useState(false); + const submit = useSubmit(); return (
- {user.fullName} - {user.username} + {editing ? ( + <> +
+ + + + + + + +
+ + ) : ( + <> + {user.fullName} + {user.username} + {} +
+ +
+ + )}
{self && (
diff --git a/client/src/actions.ts b/client/src/actions.ts index b30c683..e7ff8a4 100644 --- a/client/src/actions.ts +++ b/client/src/actions.ts @@ -1,4 +1,10 @@ -import { addFollower, removeFollower, signup } from "./api/Person"; +import { + addFollower, + deleteSelf, + removeFollower, + signup, + updateSelf, +} from "./api/Person"; import { ActionFunctionArgs, redirect } from "react-router-dom"; import { login } from "./api/Token"; import { isError } from "./api/dto"; @@ -66,6 +72,16 @@ export async function profileSelfAction({ request }: ActionFunctionArgs) { formData.get("text")!.toString(), parseInt(formData.get("postId")!.toString()), ); + } else if (intent == "user") { + return await updateSelf( + formData.get("username")!.toString(), + formData.get("fullName")!.toString(), + formData.get("password")!.toString(), + ); + } else if (intent == "deleteSelf") { + await deleteSelf(); + deleteToken(); + return redirect("/"); } } diff --git a/client/src/api/Person.ts b/client/src/api/Person.ts index 6140f08..9ac5354 100644 --- a/client/src/api/Person.ts +++ b/client/src/api/Person.ts @@ -20,6 +20,22 @@ export async function signup( }); } +export async function updateSelf( + username: string, + fullName: string, + password: string, +): Promise { + return fetchJSONAuth("/person/self", "PATCH", PersonToResp, { + username, + fullName, + password, + }); +} + +export async function deleteSelf(): Promise { + return fetchJSONAuth("/person/self", "DELETE", NoContentToResp); +} + export async function getPersonByUuid(uuid: string): Promise { return fetchJSONAuth("/person/by-uuid/" + uuid, "GET", PersonToResp); } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java index fac971b..6fb0863 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java @@ -3,13 +3,17 @@ package com.usatiuk.tjv.y.server.controller; import com.usatiuk.tjv.y.server.dto.PersonSignupTo; import com.usatiuk.tjv.y.server.dto.PersonTo; import com.usatiuk.tjv.y.server.dto.converters.PersonMapper; +import com.usatiuk.tjv.y.server.entity.Chat; import com.usatiuk.tjv.y.server.entity.Person; +import com.usatiuk.tjv.y.server.service.ChatService; import com.usatiuk.tjv.y.server.service.PersonService; import com.usatiuk.tjv.y.server.service.exceptions.UserAlreadyExistsException; import com.usatiuk.tjv.y.server.service.exceptions.UserNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; import java.security.Principal; import java.util.Optional; @@ -20,11 +24,15 @@ import java.util.stream.StreamSupport; @RequestMapping(value = "/person", produces = MediaType.APPLICATION_JSON_VALUE) public class PersonController { private final PersonService personService; + private final ChatService chatService; private final PersonMapper personMapper; + private final PasswordEncoder passwordEncoder; - public PersonController(PersonService personService, PersonMapper personMapper) { + public PersonController(PersonService personService, ChatService chatService, PersonMapper personMapper, PasswordEncoder passwordEncoder) { this.personService = personService; + this.chatService = chatService; this.personMapper = personMapper; + this.passwordEncoder = passwordEncoder; } @PostMapping @@ -67,6 +75,27 @@ public class PersonController { return personMapper.makeDto(found.get()); } + @PatchMapping(path = "/self") + public PersonTo update(Principal principal, @RequestBody PersonSignupTo personSignupTo) { + var person = personService.readById(principal.getName()).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + person.setUsername(personSignupTo.username()) + .setFullName(personSignupTo.fullName()); + if (!personSignupTo.password().isEmpty()) person.setPassword(passwordEncoder.encode(personSignupTo.password())); + personService.update(person); + return personMapper.makeDto(person); + } + + @DeleteMapping(path = "/self") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void delete(Principal principal) { + var person = personService.readById(principal.getName()).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); + for (Chat c : person.getChats()) { + c.getMembers().remove(person); + chatService.update(c); + } + personService.deleteById(principal.getName()); + } + @GetMapping public Stream getAll() throws UserNotFoundException { return StreamSupport.stream(personService.readAll().spliterator(), false).map(personMapper::makeDto); diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/entity/Chat.java b/server/src/main/java/com/usatiuk/tjv/y/server/entity/Chat.java index d6b91f8..193cf6c 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/entity/Chat.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/entity/Chat.java @@ -25,7 +25,7 @@ public class Chat implements EntityWithId { @NotBlank private String name; - @OneToMany(mappedBy = "chat") + @OneToMany(mappedBy = "chat", orphanRemoval = true) private Collection messages = new ArrayList<>(); @ManyToMany diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/entity/Person.java b/server/src/main/java/com/usatiuk/tjv/y/server/entity/Person.java index bffe62a..81fcedd 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/entity/Person.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/entity/Person.java @@ -34,13 +34,13 @@ public class Person implements EntityWithId { @NotBlank(message = "Password can't be empty") private String password; - @OneToMany(mappedBy = "author") + @OneToMany(mappedBy = "author", orphanRemoval = true) private Collection posts = new ArrayList<>(); - @OneToMany(mappedBy = "creator") + @OneToMany(mappedBy = "creator", orphanRemoval = true) private Collection createdChats = new ArrayList<>(); - @OneToMany(mappedBy = "author") + @OneToMany(mappedBy = "author", orphanRemoval = true) private Collection messages = new ArrayList<>(); @ManyToMany