Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package hr.algebra.socialnetwork.payload;

import hr.algebra.socialnetwork.model.Gender;
import hr.algebra.socialnetwork.validation.AlgebraEmail;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record UserUpdateRequest(
@NotNull(message = "User ID is required") Long userId,
@NotBlank(message = "Email is required") @AlgebraEmail String email,
@Size(min = 6, message = "Password must be at least 6 characters") String password,
@NotBlank(message = "First name is required") String firstName,
@NotBlank(message = "Last name is required") String lastName,
@NotNull(message = "Gender is required") Gender gender) {}
@NotBlank(message = "Last name is required") String lastName) {}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import hr.algebra.socialnetwork.exception.ResourceNotFoundException;
import hr.algebra.socialnetwork.mapper.UserDTOMapper;
import hr.algebra.socialnetwork.mapper.UserSummaryDTOMapper;
import hr.algebra.socialnetwork.model.Gender;
import hr.algebra.socialnetwork.model.User;
import hr.algebra.socialnetwork.payload.UserUpdateRequest;
import hr.algebra.socialnetwork.repository.UserRepository;
Expand All @@ -16,7 +15,6 @@
import lombok.AllArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

Expand All @@ -27,7 +25,6 @@ public class UserService {
private final UserRepository userRepository;
private final UserDTOMapper userDTOMapper;
private final UserSummaryDTOMapper userSummaryDTOMapper;
private final PasswordEncoder passwordEncoder;
private final S3Service s3Service;

public Page<UserSummaryDTO> getAllUsers(Pageable pageable) {
Expand All @@ -54,54 +51,12 @@ public UserDTO updateUserByEmail(String email, UserUpdateRequest request) {
return userDTOMapper.apply(user);
}

private User findUserByEmail(String email) {
return userRepository
.findByEmail(email)
.orElseThrow(
() -> new ResourceNotFoundException("User with email [%s] not found".formatted(email)));
}

public void uploadProfileImage(Long userId, MultipartFile file) {
User user =
userRepository
.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId));

String imageId = UUID.randomUUID().toString();
String key = "profile-images/%s/%s.jpg".formatted(userId, imageId);

try {
s3Service.putObject(key, file.getBytes());
} catch (IOException e) {
throw new RuntimeException("Failed to upload profile image", e);
}

user.setProfileImageId(imageId);
userRepository.save(user);
}

public byte[] getProfileImage(Long userId) {
User user =
userRepository
.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId));

if (user.getProfileImageId() == null || user.getProfileImageId().isBlank()) {
throw new ResourceNotFoundException("Profile image not set for user " + userId);
}

String key = "profile-images/%s/%s.jpg".formatted(userId, user.getProfileImageId());
return s3Service.getObject(key);
}

private boolean applyUserUpdates(User user, UserUpdateRequest request) {
boolean updated = false;

updated |= updateFirstName(user, request.firstName());
updated |= updateLastName(user, request.lastName());
updated |= updateEmail(user, request.email());
updated |= updateGender(user, request.gender());
updated |= updatePassword(user, request.password());

return updated;
}
Expand All @@ -111,7 +66,6 @@ private boolean updateFirstName(User user, String newFirstName) {
user.setFirstName(newFirstName);
return true;
}

return false;
}

Expand All @@ -120,7 +74,6 @@ private boolean updateLastName(User user, String newLastName) {
user.setLastName(newLastName);
return true;
}

return false;
}

Expand All @@ -129,29 +82,50 @@ private boolean updateEmail(User user, String newEmail) {
user.setEmail(newEmail);
return true;
}

return false;
}

private boolean updateGender(User user, Gender newGender) {
if (newGender != null && !newGender.equals(user.getGender())) {
user.setGender(newGender);
return true;
private boolean isValidUpdate(String newValue, String oldValue) {
return newValue != null && !newValue.isBlank() && !newValue.equals(oldValue);
}

public void uploadProfileImage(Long userId, MultipartFile file) {
User user =
userRepository
.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId));

String imageId = UUID.randomUUID().toString();
String key = "profile-images/%s/%s.jpg".formatted(userId, imageId);

try {
s3Service.putObject(key, file.getBytes());
} catch (IOException e) {
throw new RuntimeException("Failed to upload profile image", e);
}

return false;
user.setProfileImageId(imageId);
userRepository.save(user);
}

private boolean updatePassword(User user, String newPassword) {
if (newPassword != null && !newPassword.isBlank()) {
user.setPassword(passwordEncoder.encode(newPassword));
return true;
public byte[] getProfileImage(Long userId) {
User user =
userRepository
.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId));

if (user.getProfileImageId() == null || user.getProfileImageId().isBlank()) {
throw new ResourceNotFoundException("Profile image not set for user " + userId);
}

return false;
String key = "profile-images/%s/%s.jpg".formatted(userId, user.getProfileImageId());
return s3Service.getObject(key);
}

private boolean isValidUpdate(String newValue, String oldValue) {
return newValue != null && !newValue.isBlank() && !newValue.equals(oldValue);
private User findUserByEmail(String email) {
return userRepository
.findByEmail(email)
.orElseThrow(
() -> new ResourceNotFoundException("User with email [%s] not found".formatted(email)));
}
}
100 changes: 14 additions & 86 deletions frontend/src/pages/EditProfilePage.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useState } from "react";
import {
Flex,
Box,
Expand All @@ -11,89 +11,63 @@ import {
Heading,
Card,
Input,
Select,
Portal,
InputGroup,
} from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import Navbar from "../components/layout/Navbar";
import { PasswordInput } from "../components/common/PasswordInput";
import {
getEmailPrefix,
getFirstName,
getFullName,
getGender,
getLastName,
getUID,
} from "../utils/utils";
import { updateUser, uploadProfileImage } from "../services/usersService";
import { createListCollection } from "@chakra-ui/react";

function EditProfile() {
const navigate = useNavigate();

const [firstName, setFirstName] = useState(getFirstName());
const [lastName, setLastName] = useState(getLastName());
const [gender, setGender] = useState("");
const [email, setEmail] = useState(getEmailPrefix());
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [selectedProfileImage, setSelectedProfileImage] = useState(null);

const fullName = getFullName();
const UID = getUID();

const genderOptions = createListCollection({
items: [
{ label: "FEMALE", value: "FEMALE" },
{ label: "MALE", value: "MALE" },
{ label: "OTHER", value: "OTHER" },
],
});

useEffect(() => {
const tempGenderFetch = getGender();
setGender(tempGenderFetch);
}, []);

async function handleSubmit(e) {
e.preventDefault();

if (password !== confirmPassword) {
alert("Passwords do not match.");
return;
if (selectedProfileImage) {
const formData = new FormData();
formData.append("file", selectedProfileImage);

try {
await uploadProfileImage(UID, formData);
} catch (err) {
console.error("Image upload failed", err);
alert("Failed to upload profile image");
return;
}
}

const userData = {
email,
password,
userId: UID,
email: email + "@algebra.hr",
firstName,
lastName,
gender,
};

try {
const response = await updateUser(UID, userData);

if (response?.status === 200) {
if (selectedProfileImage) {
const formData = new FormData();
formData.append("file", selectedProfileImage);
await uploadProfileImage(UID, formData);
console.log("Profile image uploaded");
}

alert("Profile updated successfully!");
} else {
alert("Failed to update profile.");
}
} catch (error) {
console.error("Update failed:", error);
}

console.log(
`${firstName} | ${lastName} | ${gender} | ${email} | ${password} | ${confirmPassword}`,
);
}

return (
Expand Down Expand Up @@ -164,36 +138,6 @@ function EditProfile() {
/>
</Field.Root>

<Select.Root
collection={genderOptions}
onValueChange={(key) => {
setGender(key.value[0]);
}}
>
<Select.HiddenSelect />
<Select.Label>Gender</Select.Label>
<Select.Control>
<Select.Trigger>
<Select.ValueText placeholder="Select gender" />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
</Select.IndicatorGroup>
</Select.Control>
<Portal>
<Select.Positioner>
<Select.Content>
{genderOptions.items.map((item) => (
<Select.Item item={item} key={item.value}>
{item.label}
<Select.ItemIndicator />
</Select.Item>
))}
</Select.Content>
</Select.Positioner>
</Portal>
</Select.Root>

<Field.Root>
<Field.Label>Email</Field.Label>
<InputGroup endAddon="@algebra.hr">
Expand All @@ -205,22 +149,6 @@ function EditProfile() {
</InputGroup>
</Field.Root>

<Field.Root>
<Field.Label>Password</Field.Label>
<PasswordInput
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Field.Root>

<Field.Root>
<Field.Label>Confirm password</Field.Label>
<PasswordInput
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
</Field.Root>

<Field.Root>
<Field.Label>Profile Image</Field.Label>
<input
Expand Down