diff --git a/.gitignore b/.gitignore index 8e0e8c6..459a556 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,9 @@ backend/.db/ backend/init/* backend/**/*temp.java +backend/google-services.json +backend/etf-oglasi-firebase.json + # Java / Eclipse / STS .classpath .project diff --git a/backend/build.gradle b/backend/build.gradle index ed0ad38..4570538 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -39,6 +39,8 @@ dependencies { runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.13.0") implementation("io.jsonwebtoken:jjwt-api:0.13.0") runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0") + + implementation 'com.google.firebase:firebase-admin:9.9.0' } generateJava { diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/config/FirebaseConfig.java b/backend/src/main/java/dev/ksan/etfoglasiserver/config/FirebaseConfig.java new file mode 100644 index 0000000..dbe5741 --- /dev/null +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/config/FirebaseConfig.java @@ -0,0 +1,31 @@ +package dev.ksan.etfoglasiserver.config; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +import java.io.IOException; +import java.util.List; + +@Configuration +public class FirebaseConfig { + + @Bean + public FirebaseApp firebaseApp() throws IOException { + GoogleCredentials credentials = GoogleCredentials + .fromStream(new ClassPathResource("etf-oglasi-firebase.json").getInputStream()) + .createScoped(List.of("https://www.googleapis.com/auth/firebase.messaging")); + + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(credentials) + .build(); + + if (FirebaseApp.getApps().isEmpty()) { + return FirebaseApp.initializeApp(options); + } + return FirebaseApp.getInstance(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/SubjectAltController.java b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/SubjectAltController.java index 9e40018..25d71d2 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/SubjectAltController.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/SubjectAltController.java @@ -23,13 +23,13 @@ public class SubjectAltController { return service.getSubjectAlt(subjectId); } - @PostMapping("/subjectalt") - public void addSubject(@RequestBody SubjectAlt subject) { - service.addSubjectAlt(subject); - } - - @DeleteMapping("/subjectalt/{subjectId}") - public void deleteSubject(@PathVariable UUID subjectId) { - service.deleteSubjectAlt(subjectId); - } +// @PostMapping("/subjectalt") +// public void addSubject(@RequestBody SubjectAlt subject) { +// service.addSubjectAlt(subject); +// } +// +// @DeleteMapping("/subjectalt/{subjectId}") +// public void deleteSubject(@PathVariable UUID subjectId) { +// service.deleteSubjectAlt(subjectId); +// } } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/SubjectController.java b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/SubjectController.java index 7ddf09f..cd53585 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/SubjectController.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/SubjectController.java @@ -22,18 +22,21 @@ public class SubjectController { return service.getSubject(subjectId); } + @PostMapping("/subjects") public void addSubject(@RequestBody Subject subject) { service.addSubject(subject); } - @PutMapping("/subjects") - public void updateSubject(@RequestBody Subject subject) { - service.updateSubject(subject); - } +//TODO uncomment this if ever roles are added so only admin should modify this (didnt really feel like doing it rn) +// +// @PutMapping("/subjects") +// public void updateSubject(@RequestBody Subject subject) { +// service.updateSubject(subject); +// } - @DeleteMapping("/subjects/{subjectId}") - public void deleteSubject(@PathVariable UUID subjectId) { - service.deleteSubject(subjectId); - } +// @DeleteMapping("/subjects/{subjectId}") +// public void deleteSubject(@PathVariable UUID subjectId) { +// service.deleteSubject(subjectId); +// } } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/UserController.java b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/UserController.java index 9e8c8d2..d828168 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/controller/UserController.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/controller/UserController.java @@ -4,13 +4,13 @@ import dev.ksan.etfoglasiserver.dto.JwtResponseDTO; import dev.ksan.etfoglasiserver.dto.UserCreationDTO; import dev.ksan.etfoglasiserver.dto.UserDTO; import dev.ksan.etfoglasiserver.dto.UserLoginDTO; -import dev.ksan.etfoglasiserver.model.NotificationMethod; +import dev.ksan.etfoglasiserver.model.User; import dev.ksan.etfoglasiserver.service.UserService; -import java.util.List; import java.util.UUID; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @RestController @@ -20,48 +20,52 @@ public class UserController { @Autowired UserService service; - @GetMapping("users/{userId}") - public ResponseEntity getUserById(@PathVariable UUID userId) { + @GetMapping("users/me") + public ResponseEntity getUserById(Authentication authentication) { - UserDTO user = service.getUserById(userId); + String email = authentication.getName(); + + + User user = service.getAccountByEmail(email).orElseThrow(() -> new RuntimeException("User not found")); - if (user != null) { - return new ResponseEntity<>(user, HttpStatus.OK); - } return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - @PutMapping("/users") - public ResponseEntity updateUser(@RequestBody UserCreationDTO user) { + @PutMapping("/users/me") + public ResponseEntity updateUser( + @RequestBody UserCreationDTO user, + Authentication authentication) { - UserDTO userUpdated = service.updateUser(user); - if (userUpdated != null) { - return new ResponseEntity<>(userUpdated, HttpStatus.OK); - } - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + String email = authentication.getName(); + + UserDTO userUpdated = service.updateUser(email, user); + + return ResponseEntity.ok(userUpdated); } - @PutMapping("/users/{userId}/subjects/{subjectId}") - public ResponseEntity addSubject( - @PathVariable UUID userId, - @PathVariable UUID subjectId - ) { + @PutMapping("/users/me/subjects/{subjectId}") + public ResponseEntity addSubject( + @PathVariable UUID subjectId, + Authentication authentication) { - UserDTO updatedUser = service.addSubject(userId, subjectId); + String email = authentication.getName(); - return ResponseEntity.ok(updatedUser); - } + UserDTO updatedUser = service.addSubject(email, subjectId); - @DeleteMapping("/users/{userId}/subjects/{subjectId}") - public ResponseEntity removeSubject( - @PathVariable UUID userId, - @PathVariable UUID subjectId - ) { + return ResponseEntity.ok(updatedUser); + } - UserDTO updatedUser = service.removeSubject(userId, subjectId); + @DeleteMapping("/users/me/subjects/{subjectId}") + public ResponseEntity removeSubject( + @PathVariable UUID subjectId, + Authentication authentication) { - return ResponseEntity.ok(updatedUser); - } + String email = authentication.getName(); + + UserDTO updatedUser = service.removeSubject(email, subjectId); + + return ResponseEntity.ok(updatedUser); + } @PostMapping("/login") public ResponseEntity login(@RequestBody UserLoginDTO user) { @@ -78,18 +82,14 @@ public class UserController { if (newUser != null) return new ResponseEntity<>(newUser, HttpStatus.CREATED); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } -// -// @DeleteMapping("/users/{userId}") -// public void deleteUser(@PathVariable UUID userId) { -// service.deleteUser(userId); -// } - /* - @PostMapping("/users") - public void addUser(@RequestBody UserCreationDTO user) { - NotificationMethod method = NotificationMethod.valueOf(user.getNotification()); - user.setNotificationMethod(method); - service.addUser(user); + @DeleteMapping("/users/me") + public ResponseEntity deleteUser(Authentication authentication) { + + String email = authentication.getName(); + + service.deleteUser(email); + + return ResponseEntity.noContent().build(); } - */ } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserDTO.java b/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserDTO.java index 3bdb5c8..b1d26dc 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserDTO.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/dto/UserDTO.java @@ -2,6 +2,8 @@ package dev.ksan.etfoglasiserver.dto; import dev.ksan.etfoglasiserver.model.NotificationMethod; import dev.ksan.etfoglasiserver.model.Subject; +import dev.ksan.etfoglasiserver.model.User; + import java.util.Set; import java.util.UUID; @@ -19,7 +21,14 @@ public class UserDTO { this.subjectSet = subjectSet; } - public UUID getId() { + public UserDTO(User user) { + this.id = user.getId(); + this.email = user.getEmail(); + this.subjectSet = user.getSubjectSet(); + + } + + public UUID getId() { return id; } diff --git a/backend/src/main/java/dev/ksan/etfoglasiserver/service/UserService.java b/backend/src/main/java/dev/ksan/etfoglasiserver/service/UserService.java index 9f34ff2..9faeaa7 100644 --- a/backend/src/main/java/dev/ksan/etfoglasiserver/service/UserService.java +++ b/backend/src/main/java/dev/ksan/etfoglasiserver/service/UserService.java @@ -61,8 +61,8 @@ public class UserService { } } - public UserDTO updateUser(UserCreationDTO user) { - Optional existingUserOpt = userRepo.findByEmail(user.getEmail()); + public UserDTO updateUser(String email, UserCreationDTO user) { + Optional existingUserOpt = userRepo.findByEmail(email); if (userRepo.findByEmail(user.getNewEmail()).isPresent()) { throw new RuntimeException("Email taken"); @@ -96,8 +96,12 @@ public class UserService { } else throw new RuntimeException("User not found"); } - public void deleteUser(UUID userId) { - userRepo.deleteById(userId); + public void deleteUser(String email) { + User user = userRepo.findByEmail(email).orElseThrow(() -> new RuntimeException("User not found")); + + user.getSubjectSet().clear(); + userRepo.save(user); + userRepo.delete(user); } public UserDTO register(UserCreationDTO user) { @@ -170,9 +174,9 @@ public class UserService { } - public UserDTO addSubject(UUID userId, UUID subjectId) { + public UserDTO addSubject(String email, UUID subjectId) { - User user = userRepo.findById(userId) + User user = userRepo.findByEmail(email) .orElseThrow(() -> new RuntimeException("User not found")); Subject subject = subjectRepo.findById(subjectId) @@ -185,9 +189,9 @@ public class UserService { return getUserDTO(user); } - public UserDTO removeSubject(UUID userId, UUID subjectId) { + public UserDTO removeSubject(String email, UUID subjectId) { - User user = userRepo.findById(userId) + User user = userRepo.findByEmail(email) .orElseThrow(() -> new RuntimeException("User not found")); Subject subject = subjectRepo.findById(subjectId) diff --git a/frontend/context/AuthContext.tsx b/frontend/context/AuthContext.tsx index 7420cb5..1889f4d 100644 --- a/frontend/context/AuthContext.tsx +++ b/frontend/context/AuthContext.tsx @@ -50,7 +50,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { let subscribedSubjectIds: string[] = []; if (res.userId) { try { - const dto = await authApi.getUser(res.userId, res.token); + const dto = await authApi.getUser( res.token); subscribedSubjectIds = dto.subjectSet?.map((s) => s.id) ?? []; } catch { // aaaa @@ -67,7 +67,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const register = async (payload: RegisterPayload) => { await authApi.register(payload); - await login({ email: payload.email, password: payload.password }); + await login({ + email: payload.email.trim(), + password: payload.password.trim(), + }); }; const updateUser = async (newEmail?: string, newPassword?: string) => { @@ -91,7 +94,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const subscribe = async (subjectId: string) => { if (!user) return; - const dto = await subscriptionsApi.subscribe(user.id, subjectId, user.token); + const dto = await subscriptionsApi.subscribe( subjectId, user.token); const updated = { ...user, subscribedSubjectIds: dto.subjectSet?.map((s) => s.id) ?? [...user.subscribedSubjectIds, subjectId], @@ -101,7 +104,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const unsubscribe = async (subjectId: string) => { if (!user) return; - const dto = await subscriptionsApi.unsubscribe(user.id, subjectId, user.token); + const dto = await subscriptionsApi.unsubscribe( subjectId, user.token); const updated = { ...user, subscribedSubjectIds: dto.subjectSet?.map((s) => s.id) ?? user.subscribedSubjectIds.filter((id) => id !== subjectId), diff --git a/frontend/screens/AuthGate.tsx b/frontend/screens/AuthGate.tsx index 83c6337..61b6791 100644 --- a/frontend/screens/AuthGate.tsx +++ b/frontend/screens/AuthGate.tsx @@ -25,13 +25,19 @@ export default function AuthGate({ onSuccess }: { onSuccess?: () => void }) { const accent = dark ? "#E07B45" : "#C4622D"; const submit = async () => { + + const trimmedEmail = email.trim(); + const trimmedPassword = password.trim(); + + + setError(""); - if (!email || !password) { setError("Please fill in all fields."); return; } + if (!trimmedEmail || !trimmedPassword) { setError("Please fill in all fields."); return; } setLoading(true); try { - if (mode === "login") await login({ email, password }); - else await register({ email, password, name }); + if (mode === "login") await login({ email:trimmedEmail, password:trimmedPassword}); + else await register({ email: trimmedEmail,password:trimmedPassword , name }); onSuccess?.(); } catch (e: any) { setError(e.message ?? "Something went wrong."); diff --git a/frontend/services/api.tsx b/frontend/services/api.tsx index 6c9ca8c..24197b7 100644 --- a/frontend/services/api.tsx +++ b/frontend/services/api.tsx @@ -103,8 +103,15 @@ export type RegisterPayload = { email: string; password: string; name?: string } export const authApi = { login: (p: LoginPayload) => post("/login", p), register: (p: RegisterPayload) => post("/register", p), - getUser: (userId: string, token: string) => get(`/users/${userId}`, token), - updateUser: (body: UserUpdateDTO, token: string) => request("PUT", "/users", token, body), + + getUser: (token: string) => + get("/users/me", token), + + updateUser: (body: UserUpdateDTO, token: string) => + request("PUT", "/users/me", token, body), + + deleteUser: (token: string) => + del("/users/me", token), }; @@ -114,19 +121,20 @@ export const subjectsApi = { }; export const subscriptionsApi = { - subscribe: (userId: string, subjectId: string, token: string) => + subscribe: (subjectId: string, token: string) => put( - `/users/${userId}/subjects/${subjectId}`, - {}, // body placeholder + `/users/me/subjects/${subjectId}`, + {}, token ), - unsubscribe: (userId: string, subjectId: string, token: string) => + unsubscribe: (subjectId: string, token: string) => del( - `/users/${userId}/subjects/${subjectId}`, + `/users/me/subjects/${subjectId}`, token ), }; + export const entriesApi = { getEntries: (params: { subjectId?: string; groupName?: string; page?: number }) => get>("/entries", undefined, params),