diff --git a/.env.example b/.env.example
index 1da8948..a2f1e07 100644
--- a/.env.example
+++ b/.env.example
@@ -1,5 +1,27 @@
-APP_PORT_BACKEND=8888
-MYSQL_DB_HOST=db
-MYSQL_DB_PORT=3311
-MYSQL_DB_UNAME=root
-MYSQL_DB_PASSWD=root
\ No newline at end of file
+# ==============================================================================
+# Moo: Twitter Clone - Developer Environment Configuration
+# ==============================================================================
+
+# --- DATABASE CONFIGURATION ---
+# Internal host used by Docker; use 'localhost' if running app natively
+DB_HOST=db
+DB_PORT=3306
+DB_PORT_HOST=3306
+DB_NAME=twitter_clone
+DB_USER=moouser
+DB_PASS=moopass
+DB_ROOT_PASS=rootpass
+
+# --- SECURITY CONFIGURATION ---
+# Minimum 64 characters for high-performance hashing
+JWT_SECRET=9a4f4342453527245a462d4a614e645267556b58703273357638792f423f4528
+JWT_EXPIRATION=3600000
+
+# --- APP PORT CONFIGURATION ---
+# Port exposed to the host machine (avoids 8080 collision)
+HOST_PORT=8082
+ADMINER_PORT=8083
+
+# --- SPRING BOOT CONFIGURATION ---
+SPRING_PROFILES_ACTIVE=prod
+LOGGING_LEVEL_XYZ_SUBHO=INFO
diff --git a/.gitignore b/.gitignore
index 6a1d604..0bbfac1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,3 +35,4 @@ build/
.env
GEMINI.md
UPGRADE_PROPOSAL.md
+ROBUST_EVOLUTION_PLAN.md
diff --git a/.run/Moo_API.run.xml b/.run/Moo_API.run.xml
new file mode 100644
index 0000000..9024d94
--- /dev/null
+++ b/.run/Moo_API.run.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/Moo_Dev_Dependencies.run.xml b/.run/Moo_Dev_Dependencies.run.xml
new file mode 100644
index 0000000..4f2abf7
--- /dev/null
+++ b/.run/Moo_Dev_Dependencies.run.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/Moo_Full_Stack.run.xml b/.run/Moo_Full_Stack.run.xml
new file mode 100644
index 0000000..4073e7d
--- /dev/null
+++ b/.run/Moo_Full_Stack.run.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 1f38929..f0b0433 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
[](https://github.com/scaleracademy/twitter-backend-java/actions/workflows/pr-checker.yml)
[](https://www.gnu.org/licenses/agpl-3.0)
-An robust, high-performance Twitter clone backend built with the latest
+A robust, high-performance Twitter clone backend built with the latest
industry standards.
## Modern Tech Stack
@@ -28,38 +28,30 @@ industry standards.
### Quick Start
-1. **Clone the Repository:**
+Choose the setup that fits your workflow:
- ```bash
- git clone https://github.com/scaleracademy/twitter-backend-java
- cd twitter-backend-java
- ```
+#### Option A: Zero-Installation (Full Stack)
-2. **Environment Setup:**
+Ideal for testing or a quick look. Runs everything in Docker.
- ```bash
- cp .env.example .env
- # Edit .env with your local MySQL credentials if needed
- ```
+1. `cp .env.example .env`
+2. `docker compose up -d`
+3. API: `http://localhost:8082` | DB Admin: `http://localhost:8083`
-3. **Run with Maven:**
+#### Option B: Native Development (Dependencies Only)
- ```bash
- ./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
- ```
+Ideal for coding. Runs DB in Docker, App in your IDE/CLI.
-4. **Run with Docker:**
-
- ```bash
- docker-compose up -d
- ```
-
-The API will be available at `http://localhost:8080`.
+1. `cp .env.example .env`
+2. `docker compose -f docker-compose.dev.yml up -d`
+3. Run the **"Moo API"** configuration in IntelliJ IDEA, or use:
+ `./mvnw spring-boot:run -Dspring-boot.run.profiles=dev`
+4. API: `http://localhost:8080` (Standard) | DB Admin: `http://localhost:8083`
## API Documentation
Access the Interactive Swagger UI at:
-👉 `http://localhost:8080/swagger-ui/index.html`
+👉 `http://localhost:8082/swagger-ui/index.html`
### Key Endpoints
diff --git a/docker-compose.base.yml b/docker-compose.base.yml
new file mode 100644
index 0000000..dd75d29
--- /dev/null
+++ b/docker-compose.base.yml
@@ -0,0 +1,62 @@
+#
+# Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+# Copyright © 2021-2023 Subhrodip Mohanta (hello@subho.xyz)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+#
+# Twitter Backend - Moo: Base Dependencies (Shared)
+#
+
+services:
+ db:
+ image: mysql:8.4
+ container_name: moo-db
+ command: --default-authentication-plugin=mysql_native_password
+ environment:
+ MYSQL_DATABASE: ${DB_NAME:-twitter_clone}
+ MYSQL_USER: ${DB_USER:-moouser}
+ MYSQL_PASSWORD: ${DB_PASS:-moopass}
+ MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS:-rootpass}
+ ports:
+ - "${DB_PORT_HOST:-3306}:3306"
+ volumes:
+ - moo-data:/var/lib/mysql
+ networks:
+ - moo-network
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u$$MYSQL_USER", "-p$$MYSQL_PASSWORD"]
+ timeout: 5s
+ retries: 10
+ restart: always
+
+ adminer:
+ image: adminer:latest
+ container_name: moo-adminer
+ ports:
+ - "${ADMINER_PORT:-8083}:8080"
+ depends_on:
+ - db
+ networks:
+ - moo-network
+ restart: always
+
+networks:
+ moo-network:
+ driver: bridge
+
+volumes:
+ moo-data:
+ driver: local
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..a231346
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,24 @@
+#
+# Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+# Copyright © 2021-2023 Subhrodip Mohanta (hello@subho.xyz)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+
+#
+# Twitter Backend - Moo: Developer Dependencies Stack (DB + Adminer)
+#
+
+include:
+ - docker-compose.base.yml
diff --git a/docker-compose.yml b/docker-compose.yml
index d0edf59..be8c073 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,40 +16,35 @@
# along with this program. If not, see .
#
-version: "3"
-
+#
+# Twitter Backend - Moo: Full Stack (API + Dependencies)
+#
+
+include:
+ - docker-compose.base.yml
+
services:
-
- db:
- image: mysql:8
- container_name: db
- volumes:
- - db_data:/var/lib/mysql
- restart: always
- hostname: db
- ports:
- - "${MYSQL_DB_PORT}:3306"
- environment:
- MYSQL_ROOT_PASSWORD: root
- MYSQL_DATABASE: twitter
- MYSQL_USER: twitter
- MYSQL_PASSWORD: twitter
-
- twitter-backend:
- depends_on:
- - db
- container_name: twitter-backend
+ app:
build:
context: .
- dockerfile: Dockerfile
+ args:
+ JAR_FILE: target/*.jar
+ container_name: moo-api
ports:
- - "${APP_PORT_BACKEND}:8080"
- restart: always
+ - "${HOST_PORT:-8082}:8080"
environment:
- MYSQL_DB_HOST: ${MYSQL_DB_HOST}
- MYSQL_DB_PORT: ${MYSQL_DB_PORT}
- MYSQL_DB_UNAME: ${MYSQL_DB_UNAME}
- MYSQL_DB_PASSWD: ${MYSQL_DB_PASSWD}
-
-volumes:
- db_data: {}
+ - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-prod}
+ - MYSQL_DB_HOST=${DB_HOST:-db}
+ - MYSQL_DB_PORT=${DB_PORT:-3306}
+ - MYSQL_DB_UNAME=${DB_USER:-moouser}
+ - MYSQL_DB_PASSWD=${DB_PASS:-moopass}
+ - JWT_SECRET=${JWT_SECRET}
+ - JWT_EXPIRATION=${JWT_EXPIRATION}
+ depends_on:
+ db:
+ condition: service_healthy
+ restart: always
+ deploy:
+ resources:
+ limits:
+ memory: 512M
diff --git a/pom.xml b/pom.xml
index bc53f3f..bc4ba34 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
org.springframework.boot
spring-boot-starter-parent
- 4.0.0
+ 4.0.5
@@ -81,6 +81,17 @@
spring-boot-starter-actuator
+
+ org.springframework.boot
+ spring-boot-starter-aop
+ 3.4.3
+
+
+
+ io.micrometer
+ micrometer-registry-prometheus
+
+
org.springframework.boot
spring-boot-starter-data-jpa
@@ -139,9 +150,9 @@
- org.projectlombok
- lombok
- true
+ org.mapstruct
+ mapstruct
+ 1.6.0.Beta1
@@ -177,6 +188,20 @@
spring-boot-maven-plugin
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.6.0.Beta1
+
+
+
+
+
com.diffplug.spotless
spotless-maven-plugin
diff --git a/src/main/java/xyz/subho/clone/twitter/config/JpaConfig.java b/src/main/java/xyz/subho/clone/twitter/config/JpaConfig.java
new file mode 100644
index 0000000..9e2bfd1
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/config/JpaConfig.java
@@ -0,0 +1,26 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@Configuration
+@EnableJpaAuditing
+public class JpaConfig {}
diff --git a/src/main/java/xyz/subho/clone/twitter/config/MicrometerConfig.java b/src/main/java/xyz/subho/clone/twitter/config/MicrometerConfig.java
new file mode 100644
index 0000000..82bd2e2
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/config/MicrometerConfig.java
@@ -0,0 +1,41 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.config;
+
+import io.micrometer.core.aop.CountedAspect;
+import io.micrometer.core.aop.TimedAspect;
+import io.micrometer.core.instrument.MeterRegistry;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+@Configuration
+@EnableAspectJAutoProxy
+public class MicrometerConfig {
+
+ @Bean
+ public TimedAspect timedAspect(MeterRegistry registry) {
+ return new TimedAspect(registry);
+ }
+
+ @Bean
+ public CountedAspect countedAspect(MeterRegistry registry) {
+ return new CountedAspect(registry);
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/constant/ApiVersion.java b/src/main/java/xyz/subho/clone/twitter/constant/ApiVersion.java
new file mode 100644
index 0000000..236ddd2
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/constant/ApiVersion.java
@@ -0,0 +1,29 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.constant;
+
+/** ApiVersion - Holds the global API versioning constants. */
+public class ApiVersion {
+
+ public static final String V1 = "/v1";
+
+ private ApiVersion() {
+ // Prevent instantiation
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/constant/AuthV1Constants.java b/src/main/java/xyz/subho/clone/twitter/constant/AuthV1Constants.java
new file mode 100644
index 0000000..7749887
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/constant/AuthV1Constants.java
@@ -0,0 +1,32 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.constant;
+
+/** AuthV1Constants - API endpoint constants for Authentication V1 Controller. */
+public class AuthV1Constants {
+
+ public static final String BASE_PATH = ApiVersion.V1;
+ public static final String AUTHENTICATE = "/authenticate";
+ public static final String REFRESH = "/refresh";
+ public static final String LOGOUT = "/logout";
+
+ private AuthV1Constants() {
+ // Prevent instantiation
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/constant/HashtagV1Constants.java b/src/main/java/xyz/subho/clone/twitter/constant/HashtagV1Constants.java
new file mode 100644
index 0000000..2af98ae
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/constant/HashtagV1Constants.java
@@ -0,0 +1,30 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.constant;
+
+/** HashtagV1Constants - API endpoint constants for Hashtag V1 Controller. */
+public class HashtagV1Constants {
+
+ public static final String BASE_PATH = ApiVersion.V1 + "/hashtags";
+ public static final String TAG_POSTS = "/{tag}/posts";
+
+ private HashtagV1Constants() {
+ // Prevent instantiation
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/constant/PostV1Constants.java b/src/main/java/xyz/subho/clone/twitter/constant/PostV1Constants.java
new file mode 100644
index 0000000..7f88ada
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/constant/PostV1Constants.java
@@ -0,0 +1,32 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.constant;
+
+/** PostV1Constants - API endpoint constants for Post V1 Controller. */
+public class PostV1Constants {
+
+ public static final String BASE_PATH = ApiVersion.V1 + "/posts";
+ public static final String POST_ID = "/{postId}";
+ public static final String LIKE = "/{postId}/like";
+ public static final String REPLIES = "/{postId}/replies";
+
+ private PostV1Constants() {
+ // Prevent instantiation
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/constant/UserV1Constants.java b/src/main/java/xyz/subho/clone/twitter/constant/UserV1Constants.java
new file mode 100644
index 0000000..5fa1694
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/constant/UserV1Constants.java
@@ -0,0 +1,33 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.constant;
+
+/** UserV1Constants - API endpoint constants for User V1 Controller. */
+public class UserV1Constants {
+
+ public static final String BASE_PATH = ApiVersion.V1 + "/users";
+ public static final String USER_ID_OR_NAME = "/{userNameOrUserId}";
+ public static final String FOLLOW = "/{userId}/follow";
+ public static final String FOLLOWERS = "/{userId}/followers";
+ public static final String FOLLOWINGS = "/{userId}/followings";
+
+ private UserV1Constants() {
+ // Prevent instantiation
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/controller/AuthenticationController.java b/src/main/java/xyz/subho/clone/twitter/controller/AuthenticationController.java
index b97dcb9..2d01790 100644
--- a/src/main/java/xyz/subho/clone/twitter/controller/AuthenticationController.java
+++ b/src/main/java/xyz/subho/clone/twitter/controller/AuthenticationController.java
@@ -18,46 +18,95 @@
package xyz.subho.clone.twitter.controller;
-import org.springframework.beans.factory.annotation.Autowired;
+import io.micrometer.core.annotation.Counted;
+import io.micrometer.core.annotation.Timed;
+import jakarta.validation.Valid;
+import java.security.Principal;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import xyz.subho.clone.twitter.constant.AuthV1Constants;
import xyz.subho.clone.twitter.exception.BadRequestException;
import xyz.subho.clone.twitter.model.AuthenticationRequest;
import xyz.subho.clone.twitter.model.AuthenticationResponse;
+import xyz.subho.clone.twitter.model.TokenRefreshRequest;
import xyz.subho.clone.twitter.security.JwtUtil;
import xyz.subho.clone.twitter.security.UserDetailsServiceImpl;
+import xyz.subho.clone.twitter.service.RefreshTokenService;
+import xyz.subho.clone.twitter.service.UserService;
@RestController
+@RequestMapping(AuthV1Constants.BASE_PATH)
+@Timed(value = "moo.auth.timer", description = "Time taken to process auth requests")
public class AuthenticationController {
- @Autowired private AuthenticationManager authenticationManager;
+ private final AuthenticationManager authenticationManager;
+ private final JwtUtil jwtTokenUtil;
+ private final UserDetailsServiceImpl userDetailsService;
+ private final RefreshTokenService refreshTokenService;
+ private final UserService userService;
- @Autowired private JwtUtil jwtTokenUtil;
-
- @Autowired private UserDetailsServiceImpl userDetailsService;
+ public AuthenticationController(
+ AuthenticationManager authenticationManager,
+ JwtUtil jwtTokenUtil,
+ UserDetailsServiceImpl userDetailsService,
+ RefreshTokenService refreshTokenService,
+ UserService userService) {
+ this.authenticationManager = authenticationManager;
+ this.jwtTokenUtil = jwtTokenUtil;
+ this.userDetailsService = userDetailsService;
+ this.refreshTokenService = refreshTokenService;
+ this.userService = userService;
+ }
- @PostMapping("/authenticate")
- public ResponseEntity> createAuthenticationToken(
+ @PostMapping(AuthV1Constants.AUTHENTICATE)
+ @Counted(value = "moo.auth.login", description = "Number of user logins")
+ public ResponseEntity createAuthenticationToken(
@RequestBody AuthenticationRequest authenticationRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
- authenticationRequest.getUsername(), authenticationRequest.getPassword()));
+ authenticationRequest.username(), authenticationRequest.password()));
} catch (BadCredentialsException e) {
throw new BadRequestException("Incorrect username or password", e);
}
- final var userDetails =
- userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
+ final var userDetails = userDetailsService.loadUserByUsername(authenticationRequest.username());
final String jwt = jwtTokenUtil.generateToken(userDetails);
+ var user = userService.getUserByUserName(authenticationRequest.username());
+ var refreshToken = refreshTokenService.createRefreshToken(user.id());
+
+ return ResponseEntity.ok(new AuthenticationResponse(jwt, refreshToken.getToken()));
+ }
+
+ @PostMapping(AuthV1Constants.REFRESH)
+ public ResponseEntity refreshToken(
+ @Valid @RequestBody TokenRefreshRequest request) {
+ return refreshTokenService
+ .findByToken(request.refreshToken())
+ .map(refreshTokenService::verifyExpiration)
+ .map(
+ token -> {
+ var user = token.getUsers();
+ String jwt =
+ jwtTokenUtil.generateToken(
+ userDetailsService.loadUserByUsername(user.getUsername()));
+ return ResponseEntity.ok(new AuthenticationResponse(jwt, token.getToken()));
+ })
+ .orElseThrow(() -> new BadRequestException("Refresh token is not in database!"));
+ }
- return ResponseEntity.ok(new AuthenticationResponse(jwt));
+ @PostMapping(AuthV1Constants.LOGOUT)
+ public ResponseEntity logoutUser(Principal principal) {
+ var user = userService.getUserByUserName(principal.getName());
+ refreshTokenService.deleteByUserId(user.id());
+ return ResponseEntity.ok("Log out successful!");
}
}
diff --git a/src/main/java/xyz/subho/clone/twitter/controller/HashtagController.java b/src/main/java/xyz/subho/clone/twitter/controller/HashtagController.java
index 432b4b1..0593e7f 100644
--- a/src/main/java/xyz/subho/clone/twitter/controller/HashtagController.java
+++ b/src/main/java/xyz/subho/clone/twitter/controller/HashtagController.java
@@ -18,27 +18,38 @@
package xyz.subho.clone.twitter.controller;
-import java.util.List;
-import org.springframework.beans.factory.annotation.Autowired;
+import io.micrometer.core.annotation.Counted;
+import io.micrometer.core.annotation.Timed;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import xyz.subho.clone.twitter.constant.HashtagV1Constants;
import xyz.subho.clone.twitter.model.HashtagModel;
import xyz.subho.clone.twitter.model.PostModel;
import xyz.subho.clone.twitter.service.HashtagService;
@RestController
+@RequestMapping(HashtagV1Constants.BASE_PATH)
+@Timed(value = "moo.hashtags.timer", description = "Time taken to process hashtag requests")
public class HashtagController {
- @Autowired private HashtagService hashtagService;
+ private final HashtagService hashtagService;
- @GetMapping("/hashtags")
- public List getAllHashtags() {
- return hashtagService.getHashtags();
+ public HashtagController(HashtagService hashtagService) {
+ this.hashtagService = hashtagService;
}
- @GetMapping("/hashtag/{tag}/posts")
- public List getPosts(@PathVariable("tag") String tag) {
- return hashtagService.getPosts(tag);
+ @GetMapping
+ @Counted(value = "moo.hashtags.viewed", description = "Number of times hashtags were viewed")
+ public Page getAllHashtags(Pageable pageable) {
+ return hashtagService.getHashtags(pageable);
+ }
+
+ @GetMapping(HashtagV1Constants.TAG_POSTS)
+ public Page getPosts(@PathVariable("tag") String tag, Pageable pageable) {
+ return hashtagService.getPosts(tag, pageable);
}
}
diff --git a/src/main/java/xyz/subho/clone/twitter/controller/PostController.java b/src/main/java/xyz/subho/clone/twitter/controller/PostController.java
index 14fc01c..96a1fdf 100644
--- a/src/main/java/xyz/subho/clone/twitter/controller/PostController.java
+++ b/src/main/java/xyz/subho/clone/twitter/controller/PostController.java
@@ -18,10 +18,13 @@
package xyz.subho.clone.twitter.controller;
+import io.micrometer.core.annotation.Counted;
+import io.micrometer.core.annotation.Timed;
+import jakarta.validation.Valid;
import java.security.Principal;
import java.util.UUID;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
@@ -34,18 +37,25 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import xyz.subho.clone.twitter.constant.PostV1Constants;
import xyz.subho.clone.twitter.model.PostModel;
import xyz.subho.clone.twitter.service.PostService;
import xyz.subho.clone.twitter.service.UserService;
@RestController
-@RequestMapping("/posts")
-@Slf4j
+@RequestMapping(PostV1Constants.BASE_PATH)
+@Timed(value = "moo.posts.timer", description = "Time taken to process post requests")
public class PostController {
- @Autowired private PostService postService;
+ private static final Logger log = LoggerFactory.getLogger(PostController.class);
- @Autowired private UserService userService;
+ private final PostService postService;
+ private final UserService userService;
+
+ public PostController(PostService postService, UserService userService) {
+ this.postService = postService;
+ this.userService = userService;
+ }
@GetMapping
public ResponseEntity> getAllPosts(Pageable pageable) {
@@ -53,41 +63,76 @@ public ResponseEntity> getAllPosts(Pageable pageable) {
return new ResponseEntity<>(posts, HttpStatus.OK);
}
- @GetMapping("/{postId}")
+ @GetMapping(PostV1Constants.POST_ID)
public ResponseEntity getPost(@PathVariable("postId") UUID postId) {
PostModel post = postService.getPost(postId);
return new ResponseEntity<>(post, HttpStatus.OK);
}
@PostMapping
- public ResponseEntity addPost(@RequestBody PostModel postModel, Principal principal) {
+ @Counted(value = "moo.posts.created", description = "Number of posts created")
+ public ResponseEntity addPost(
+ @Valid @RequestBody PostModel postModel, Principal principal) {
var user = userService.getUserByUserName(principal.getName());
- postModel.setUserId(user.getId());
- PostModel post = postService.addPost(postModel);
+ if (user == null) {
+ return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
+ }
+
+ // Create new record instance with the authenticated userId
+ PostModel enrichedPost =
+ new PostModel(
+ postModel.id(),
+ postModel.text(),
+ user.id(),
+ postModel.images(),
+ postModel.likeCount(),
+ postModel.repostCount(),
+ postModel.originalPostId(),
+ postModel.replyToId(),
+ postModel.timestamp(),
+ postModel.hashtags(),
+ postModel.mentions());
+
+ PostModel post = postService.addPost(enrichedPost);
return new ResponseEntity<>(post, HttpStatus.OK);
}
- @DeleteMapping("/{postId}")
+ @DeleteMapping(PostV1Constants.POST_ID)
public ResponseEntity deletePost(
@PathVariable("postId") UUID postId, Principal principal) {
var user = userService.getUserByUserName(principal.getName());
- postService.deletePost(postId, user.getId());
+ if (user != null) {
+ postService.deletePost(postId, user.id());
+ }
return new ResponseEntity<>(HttpStatus.OK);
}
- @PutMapping("/{postId}/like")
+ @PutMapping(PostV1Constants.LIKE)
+ @Counted(value = "moo.posts.liked", description = "Number of posts liked")
public ResponseEntity likePost(@PathVariable("postId") UUID postId, Principal principal) {
var user = userService.getUserByUserName(principal.getName());
- return new ResponseEntity<>(postService.addLike(postId, user.getId()), HttpStatus.CREATED);
+ if (user == null) {
+ return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
+ }
+ return new ResponseEntity<>(postService.addLike(postId, user.id()), HttpStatus.CREATED);
}
- @DeleteMapping("/{postId}/like")
+ @DeleteMapping(PostV1Constants.LIKE)
public ResponseEntity removeLikePost(
@PathVariable("postId") UUID postId, Principal principal) {
var user = userService.getUserByUserName(principal.getName());
- return new ResponseEntity<>(postService.removeLike(postId, user.getId()), HttpStatus.OK);
+ if (user == null) {
+ return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
+ }
+ return new ResponseEntity<>(postService.removeLike(postId, user.id()), HttpStatus.OK);
+ }
+
+ @GetMapping(PostV1Constants.REPLIES)
+ public ResponseEntity> getReplies(
+ @PathVariable("postId") UUID postId, Pageable pageable) {
+ return new ResponseEntity<>(postService.getReplies(postId, pageable), HttpStatus.OK);
}
}
diff --git a/src/main/java/xyz/subho/clone/twitter/controller/UserController.java b/src/main/java/xyz/subho/clone/twitter/controller/UserController.java
index 142df81..6e1b966 100644
--- a/src/main/java/xyz/subho/clone/twitter/controller/UserController.java
+++ b/src/main/java/xyz/subho/clone/twitter/controller/UserController.java
@@ -18,11 +18,13 @@
package xyz.subho.clone.twitter.controller;
+import io.micrometer.core.annotation.Counted;
+import io.micrometer.core.annotation.Timed;
import jakarta.validation.Valid;
import java.security.Principal;
import java.util.UUID;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
@@ -36,20 +38,27 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import xyz.subho.clone.twitter.constant.UserV1Constants;
import xyz.subho.clone.twitter.model.UserModel;
import xyz.subho.clone.twitter.service.UserService;
import xyz.subho.clone.twitter.utility.Utility;
@RestController
-@RequestMapping("/users")
-@Slf4j
+@RequestMapping(UserV1Constants.BASE_PATH)
+@Timed(value = "moo.users.timer", description = "Time taken to process user requests")
public class UserController {
- @Autowired private UserService userService;
+ private static final Logger log = LoggerFactory.getLogger(UserController.class);
- @Autowired private Utility utility;
+ private final UserService userService;
+ private final Utility utility;
- @GetMapping("/{userNameOrUserId}")
+ public UserController(UserService userService, Utility utility) {
+ this.userService = userService;
+ this.utility = utility;
+ }
+
+ @GetMapping(UserV1Constants.USER_ID_OR_NAME)
public ResponseEntity getUserByUserIdOrUserName(
@PathVariable("userNameOrUserId") String userNameOrUserId) {
@@ -69,37 +78,44 @@ public ResponseEntity getUserByUserIdOrUserName(
}
@PostMapping
+ @Counted(value = "moo.users.signup", description = "Number of user signups")
public ResponseEntity createUser(@Valid @RequestBody UserModel userResponse) {
var user = userService.addUser(userResponse);
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
@PatchMapping
+ @Timed(value = "moo.users.update", description = "Time taken to update user profile")
public UserModel updateUser(@Valid @RequestBody UserModel userResponse, Principal principal) {
return userService.editUser(userResponse);
}
- @PutMapping("/{userId}/follow")
+ @PutMapping(UserV1Constants.FOLLOW)
+ @Counted(value = "moo.users.follow", description = "Number of follow actions")
public ResponseEntity addFollower(@PathVariable UUID userId, Principal principal) {
var follower = userService.getUserByUserName(principal.getName());
- userService.addFollower(follower.getId(), userId);
+ if (follower != null) {
+ userService.addFollower(follower.id(), userId);
+ }
return new ResponseEntity<>(HttpStatus.CREATED);
}
- @DeleteMapping("/{userId}/follow")
+ @DeleteMapping(UserV1Constants.FOLLOW)
public ResponseEntity removeFollower(
@PathVariable("userId") UUID userId, Principal principal) {
var follower = userService.getUserByUserName(principal.getName());
- userService.removeFollower(follower.getId(), userId);
+ if (follower != null) {
+ userService.removeFollower(follower.id(), userId);
+ }
return new ResponseEntity<>(HttpStatus.CREATED);
}
- @GetMapping("/{userId}/followers")
+ @GetMapping(UserV1Constants.FOLLOWERS)
public Page getFollowers(@PathVariable("userId") UUID userId, Pageable pageable) {
return userService.getFollowers(userId, pageable);
}
- @GetMapping("/{userId}/followings")
+ @GetMapping(UserV1Constants.FOLLOWINGS)
public Page getFollowings(@PathVariable("userId") UUID userId, Pageable pageable) {
return userService.getFollowings(userId, pageable);
}
diff --git a/src/main/java/xyz/subho/clone/twitter/entity/Auditable.java b/src/main/java/xyz/subho/clone/twitter/entity/Auditable.java
new file mode 100644
index 0000000..8b4a58e
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/entity/Auditable.java
@@ -0,0 +1,60 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.EntityListeners;
+import jakarta.persistence.MappedSuperclass;
+import jakarta.persistence.Temporal;
+import jakarta.persistence.TemporalType;
+import java.util.Date;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class Auditable {
+
+ @CreatedDate
+ @Temporal(TemporalType.TIMESTAMP)
+ @Column(name = "created_at", nullable = false, updatable = false)
+ private Date createdAt;
+
+ @LastModifiedDate
+ @Temporal(TemporalType.TIMESTAMP)
+ @Column(name = "updated_at", nullable = false)
+ private Date updatedAt;
+
+ public Date getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Date createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public Date getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setUpdatedAt(Date updatedAt) {
+ this.updatedAt = updatedAt;
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/entity/Follow.java b/src/main/java/xyz/subho/clone/twitter/entity/Follow.java
new file mode 100644
index 0000000..cb19d7b
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/entity/Follow.java
@@ -0,0 +1,95 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import java.util.Objects;
+import java.util.UUID;
+
+@Entity
+@Table(
+ name = "follows",
+ uniqueConstraints = {@UniqueConstraint(columnNames = {"follower_id", "following_id"})})
+public class Follow extends Auditable {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(columnDefinition = "BINARY(16)")
+ private UUID id;
+
+ @ManyToOne
+ @JoinColumn(name = "follower_id", nullable = false)
+ private Users follower;
+
+ @ManyToOne
+ @JoinColumn(name = "following_id", nullable = false)
+ private Users following;
+
+ public Follow() {}
+
+ public Follow(Users follower, Users following) {
+ this.follower = follower;
+ this.following = following;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public Users getFollower() {
+ return follower;
+ }
+
+ public void setFollower(Users follower) {
+ this.follower = follower;
+ }
+
+ public Users getFollowing() {
+ return following;
+ }
+
+ public void setFollowing(Users following) {
+ this.following = following;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Follow follow = (Follow) o;
+ return Objects.equals(id, follow.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/entity/HashtagPosts.java b/src/main/java/xyz/subho/clone/twitter/entity/HashtagPosts.java
index 6ffa08d..2944c90 100644
--- a/src/main/java/xyz/subho/clone/twitter/entity/HashtagPosts.java
+++ b/src/main/java/xyz/subho/clone/twitter/entity/HashtagPosts.java
@@ -26,23 +26,19 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
-import java.util.Date;
+import java.util.Objects;
import java.util.UUID;
-import lombok.Data;
-import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
@Entity
@Table(name = "hashtag_posts")
-@Data
-public class HashtagPosts {
+public class HashtagPosts extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(columnDefinition = "BINARY(16)")
private UUID id;
- @ManyToOne(targetEntity = Hashtags.class)
+ @ManyToOne
@JoinColumn(
name = "hashtags_id",
columnDefinition = "BINARY(16)",
@@ -50,7 +46,7 @@ public class HashtagPosts {
nullable = false)
private Hashtags hashtags;
- @ManyToOne(targetEntity = Posts.class)
+ @ManyToOne
@JoinColumn(
name = "posts_id",
columnDefinition = "BINARY(16)",
@@ -58,7 +54,45 @@ public class HashtagPosts {
nullable = false)
private Posts posts;
- @CreationTimestamp private Date createdAt;
+ public UUID getId() {
+ return id;
+ }
- @UpdateTimestamp private Date updatedAt;
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public Hashtags getHashtags() {
+ return hashtags;
+ }
+
+ public void setHashtags(Hashtags hashtags) {
+ this.hashtags = hashtags;
+ }
+
+ public Posts getPosts() {
+ return posts;
+ }
+
+ public void setPosts(Posts posts) {
+ this.posts = posts;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ HashtagPosts that = (HashtagPosts) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+
+ @Override
+ public String toString() {
+ return "HashtagPosts{" + "id=" + id + '}';
+ }
}
diff --git a/src/main/java/xyz/subho/clone/twitter/entity/Hashtags.java b/src/main/java/xyz/subho/clone/twitter/entity/Hashtags.java
index 8922e2a..d5ebd08 100644
--- a/src/main/java/xyz/subho/clone/twitter/entity/Hashtags.java
+++ b/src/main/java/xyz/subho/clone/twitter/entity/Hashtags.java
@@ -30,36 +30,78 @@
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
-import java.util.Date;
import java.util.List;
+import java.util.Objects;
import java.util.UUID;
-import lombok.Data;
-import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
@Entity
@Table(
name = "hashtags",
indexes = {@Index(columnList = "tag")})
-@Data
-public class Hashtags {
+public class Hashtags extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(columnDefinition = "BINARY(16)")
private UUID id;
- @Column(unique = true, nullable = false)
+ @Column(unique = true, nullable = false, length = 50)
private String tag;
- @Column(name = "recent_post_count", columnDefinition = "BIGINT(20) default '1'", nullable = false)
- private Long recentPostCount = 1L;
+ @Column(name = "post_count", columnDefinition = "BIGINT(20) default '0'", nullable = false)
+ private long recentPostCount = 0L;
@OneToMany(mappedBy = "hashtags", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JsonIgnore
private List hashtagPosts = new ArrayList<>();
- @CreationTimestamp private Date createdAt;
+ public UUID getId() {
+ return id;
+ }
- @UpdateTimestamp private Date updatedAt;
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getTag() {
+ return tag;
+ }
+
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
+
+ public long getRecentPostCount() {
+ return recentPostCount;
+ }
+
+ public void setRecentPostCount(long recentPostCount) {
+ this.recentPostCount = recentPostCount;
+ }
+
+ public List getHashtagPosts() {
+ return hashtagPosts;
+ }
+
+ public void setHashtagPosts(List hashtagPosts) {
+ this.hashtagPosts = hashtagPosts;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Hashtags hashtags = (Hashtags) o;
+ return Objects.equals(id, hashtags.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+
+ @Override
+ public String toString() {
+ return "Hashtags{" + "id=" + id + ", tag='" + tag + '\'' + '}';
+ }
}
diff --git a/src/main/java/xyz/subho/clone/twitter/entity/Likes.java b/src/main/java/xyz/subho/clone/twitter/entity/Likes.java
index 415c520..fe9805e 100644
--- a/src/main/java/xyz/subho/clone/twitter/entity/Likes.java
+++ b/src/main/java/xyz/subho/clone/twitter/entity/Likes.java
@@ -26,39 +26,73 @@
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
-import java.util.Date;
+import java.util.Objects;
import java.util.UUID;
-import lombok.Data;
-import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
@Entity
@Table(name = "likes")
-@Data
-public class Likes {
+public class Likes extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(columnDefinition = "BINARY(16)")
private UUID id;
- @ManyToOne(targetEntity = Posts.class)
+ @ManyToOne
@JoinColumn(
- name = "posts_id",
+ name = "users_id",
columnDefinition = "BINARY(16)",
updatable = false,
nullable = false)
- private Posts posts;
+ private Users users;
- @ManyToOne(targetEntity = Users.class)
+ @ManyToOne
@JoinColumn(
- name = "users_id",
+ name = "posts_id",
columnDefinition = "BINARY(16)",
updatable = false,
nullable = false)
- private Users users;
+ private Posts posts;
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public Users getUsers() {
+ return users;
+ }
+
+ public void setUsers(Users users) {
+ this.users = users;
+ }
+
+ public Posts getPosts() {
+ return posts;
+ }
+
+ public void setPosts(Posts posts) {
+ this.posts = posts;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Likes likes = (Likes) o;
+ return Objects.equals(id, likes.id);
+ }
- @CreationTimestamp private Date createdAt;
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
- @UpdateTimestamp private Date updatedAt;
+ @Override
+ public String toString() {
+ return "Likes{" + "id=" + id + '}';
+ }
}
diff --git a/src/main/java/xyz/subho/clone/twitter/entity/Posts.java b/src/main/java/xyz/subho/clone/twitter/entity/Posts.java
index 228b92d..027291a 100644
--- a/src/main/java/xyz/subho/clone/twitter/entity/Posts.java
+++ b/src/main/java/xyz/subho/clone/twitter/entity/Posts.java
@@ -36,16 +36,13 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.UUID;
-import lombok.Data;
-import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedBy;
@Entity
@Table(name = "posts")
-@Data
-public class Posts {
+public class Posts extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@@ -78,10 +75,6 @@ public class Posts {
@Column(name = "reply_to_id")
private UUID replyToId;
- @CreationTimestamp private Date timestamp;
-
- @UpdateTimestamp private Date updatedAt;
-
@ElementCollection private Map hashtags = new HashMap<>();
@ElementCollection private Map mentions = new HashMap<>();
@@ -109,4 +102,118 @@ public long incrementRepostCount() {
public long decrementRepostCount() {
return (repostCount < 1) ? 0 : --repostCount;
}
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public Users getUsers() {
+ return users;
+ }
+
+ public void setUsers(Users users) {
+ this.users = users;
+ }
+
+ public Map getImages() {
+ return images;
+ }
+
+ public void setImages(Map images) {
+ this.images = images;
+ }
+
+ public Long getLikeCount() {
+ return likeCount;
+ }
+
+ public void setLikeCount(Long likeCount) {
+ this.likeCount = likeCount;
+ }
+
+ public Long getRepostCount() {
+ return repostCount;
+ }
+
+ public void setRepostCount(Long repostCount) {
+ this.repostCount = repostCount;
+ }
+
+ public UUID getOriginalPostId() {
+ return originalPostId;
+ }
+
+ public void setOriginalPostId(UUID originalPostId) {
+ this.originalPostId = originalPostId;
+ }
+
+ public UUID getReplyToId() {
+ return replyToId;
+ }
+
+ public void setReplyToId(UUID replyToId) {
+ this.replyToId = replyToId;
+ }
+
+ public Map getHashtags() {
+ return hashtags;
+ }
+
+ public void setHashtags(Map hashtags) {
+ this.hashtags = hashtags;
+ }
+
+ public Map getMentions() {
+ return mentions;
+ }
+
+ public void setMentions(Map mentions) {
+ this.mentions = mentions;
+ }
+
+ public List getPostHashtags() {
+ return postHashtags;
+ }
+
+ public void setPostHashtags(List postHashtags) {
+ this.postHashtags = postHashtags;
+ }
+
+ public List getPostLikes() {
+ return postLikes;
+ }
+
+ public void setPostLikes(List postLikes) {
+ this.postLikes = postLikes;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Posts posts = (Posts) o;
+ return Objects.equals(id, posts.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+
+ @Override
+ public String toString() {
+ return "Posts{" + "id=" + id + ", text='" + text + '\'' + '}';
+ }
}
diff --git a/src/main/java/xyz/subho/clone/twitter/entity/RefreshToken.java b/src/main/java/xyz/subho/clone/twitter/entity/RefreshToken.java
new file mode 100644
index 0000000..5f03bfb
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/entity/RefreshToken.java
@@ -0,0 +1,96 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.entity;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Table;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.UUID;
+
+@Entity
+@Table(name = "refresh_tokens")
+public class RefreshToken extends Auditable {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ @Column(columnDefinition = "BINARY(16)")
+ private UUID id;
+
+ @OneToOne
+ @JoinColumn(name = "users_id", referencedColumnName = "id")
+ private Users users;
+
+ @Column(nullable = false, unique = true)
+ private String token;
+
+ @Column(nullable = false)
+ private Instant expiryDate;
+
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public Users getUsers() {
+ return users;
+ }
+
+ public void setUsers(Users users) {
+ this.users = users;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public void setToken(String token) {
+ this.token = token;
+ }
+
+ public Instant getExpiryDate() {
+ return expiryDate;
+ }
+
+ public void setExpiryDate(Instant expiryDate) {
+ this.expiryDate = expiryDate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ RefreshToken that = (RefreshToken) o;
+ return Objects.equals(id, that.id);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/entity/Users.java b/src/main/java/xyz/subho/clone/twitter/entity/Users.java
index 3dfdff2..94c123f 100644
--- a/src/main/java/xyz/subho/clone/twitter/entity/Users.java
+++ b/src/main/java/xyz/subho/clone/twitter/entity/Users.java
@@ -21,7 +21,6 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
-import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
@@ -31,21 +30,15 @@
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
+import java.util.Objects;
import java.util.UUID;
-import lombok.Data;
-import org.hibernate.annotations.CreationTimestamp;
-import org.hibernate.annotations.UpdateTimestamp;
@Entity
@Table(
name = "users",
indexes = {@Index(columnList = "username")})
-@Data
-public class Users {
+public class Users extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@@ -63,6 +56,9 @@ public class Users {
@Column(unique = true)
private String email;
+ @Column(nullable = false)
+ private String password;
+
@Column(length = 240)
private String bio;
@@ -75,35 +71,158 @@ public class Users {
@Column(columnDefinition = "boolean default false", nullable = false)
private Boolean verified = false;
- @CreationTimestamp private Date createdAt;
-
- @UpdateTimestamp private Date updatedAt;
-
@OneToMany(mappedBy = "users", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JsonIgnore
private List userLikes = new ArrayList<>();
- @ElementCollection private Map follower = new HashMap<>();
+ @OneToMany(mappedBy = "following", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JsonIgnore
+ private List followers = new ArrayList<>();
- @ElementCollection private Map following = new HashMap<>();
+ @OneToMany(mappedBy = "follower", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+ @JsonIgnore
+ private List following = new ArrayList<>();
@OneToMany(mappedBy = "users", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JsonIgnore
private List userPosts = new ArrayList<>();
- public void setFollower(final UUID userId) {
- follower.put(userId, new Date());
+ public UUID getId() {
+ return id;
+ }
+
+ public void setId(UUID id) {
+ this.id = id;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAvatar() {
+ return avatar;
+ }
+
+ public void setAvatar(String avatar) {
+ this.avatar = avatar;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getBio() {
+ return bio;
+ }
+
+ public void setBio(String bio) {
+ this.bio = bio;
+ }
+
+ public long getFollowerCount() {
+ return followerCount;
+ }
+
+ public void setFollowerCount(long followerCount) {
+ this.followerCount = followerCount;
+ }
+
+ public Long getFollowingCount() {
+ return followingCount;
+ }
+
+ public void setFollowingCount(Long followingCount) {
+ this.followingCount = followingCount;
+ }
+
+ public Boolean getVerified() {
+ return verified;
+ }
+
+ public void setVerified(Boolean verified) {
+ this.verified = verified;
+ }
+
+ public List getUserLikes() {
+ return userLikes;
+ }
+
+ public void setUserLikes(List userLikes) {
+ this.userLikes = userLikes;
+ }
+
+ public List getFollowers() {
+ return followers;
+ }
+
+ public void setFollowers(List followers) {
+ this.followers = followers;
+ }
+
+ public List getFollowing() {
+ return following;
+ }
+
+ public void setFollowing(List following) {
+ this.following = following;
+ }
+
+ public List getUserPosts() {
+ return userPosts;
+ }
+
+ public void setUserPosts(List userPosts) {
+ this.userPosts = userPosts;
}
- public void setFollowing(final UUID userId) {
- following.put(userId, new Date());
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Users users = (Users) o;
+ return Objects.equals(id, users.id);
}
- public void removeFollower(final UUID userId) {
- follower.remove(userId);
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(id);
}
- public void removeFollowing(final UUID userId) {
- following.remove(userId);
+ @Override
+ public String toString() {
+ return "Users{"
+ + "id="
+ + id
+ + ", username='"
+ + username
+ + '\''
+ + ", name='"
+ + name
+ + '\''
+ + '}';
}
}
diff --git a/src/main/java/xyz/subho/clone/twitter/exception/ControllerExceptionHandler.java b/src/main/java/xyz/subho/clone/twitter/exception/ControllerExceptionHandler.java
index 2f6dd61..eaf7ecb 100644
--- a/src/main/java/xyz/subho/clone/twitter/exception/ControllerExceptionHandler.java
+++ b/src/main/java/xyz/subho/clone/twitter/exception/ControllerExceptionHandler.java
@@ -1,6 +1,6 @@
/*
* Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
- * Copyright © 2021-2023 Subhrodip Mohanta (hello@subho.xyz)
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -21,6 +21,8 @@
import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@@ -29,17 +31,63 @@
@ControllerAdvice
public class ControllerExceptionHandler {
+ @ExceptionHandler(ResourceNotFoundException.class)
+ public ResponseEntity resourceNotFoundExceptionHandler(
+ ResourceNotFoundException ex, WebRequest request) {
+ var errorResponse =
+ new ErrorResponse(
+ HttpStatus.NOT_FOUND.value(),
+ new Date(),
+ ex.getMessage(),
+ request.getDescription(false));
+ return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
+ }
+
+ @ExceptionHandler(BadRequestException.class)
+ public ResponseEntity badRequestExceptionHandler(
+ BadRequestException ex, WebRequest request) {
+ var errorResponse =
+ new ErrorResponse(
+ HttpStatus.BAD_REQUEST.value(),
+ new Date(),
+ ex.getMessage(),
+ request.getDescription(false));
+ return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity validationExceptionHandler(
+ MethodArgumentNotValidException ex, WebRequest request) {
+ var errorResponse =
+ new ErrorResponse(
+ HttpStatus.BAD_REQUEST.value(),
+ new Date(),
+ "Validation Failed: " + ex.getBindingResult().toString(),
+ request.getDescription(false));
+ return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(AccessDeniedException.class)
+ public ResponseEntity accessDeniedExceptionHandler(
+ AccessDeniedException ex, WebRequest request) {
+ var errorResponse =
+ new ErrorResponse(
+ HttpStatus.FORBIDDEN.value(),
+ new Date(),
+ "Access Denied: " + ex.getMessage(),
+ request.getDescription(false));
+ return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN);
+ }
+
@ExceptionHandler(Exception.class)
public ResponseEntity globalExceptionHandler(
Exception exception, WebRequest request) {
-
var errorResponse =
new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
- new Date(System.currentTimeMillis()),
+ new Date(),
exception.getMessage(),
request.getDescription(false));
-
- return new ResponseEntity(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
+ return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
diff --git a/src/main/java/xyz/subho/clone/twitter/model/AuthenticationRequest.java b/src/main/java/xyz/subho/clone/twitter/model/AuthenticationRequest.java
index 0925b01..6d0c833 100644
--- a/src/main/java/xyz/subho/clone/twitter/model/AuthenticationRequest.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/AuthenticationRequest.java
@@ -18,11 +18,4 @@
package xyz.subho.clone.twitter.model;
-import lombok.Value;
-
-@Value
-public class AuthenticationRequest {
-
- private String username;
- private String password;
-}
+public record AuthenticationRequest(String username, String password) {}
diff --git a/src/main/java/xyz/subho/clone/twitter/model/AuthenticationResponse.java b/src/main/java/xyz/subho/clone/twitter/model/AuthenticationResponse.java
index 2cd06b2..a4916bb 100644
--- a/src/main/java/xyz/subho/clone/twitter/model/AuthenticationResponse.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/AuthenticationResponse.java
@@ -18,10 +18,4 @@
package xyz.subho.clone.twitter.model;
-import lombok.Value;
-
-@Value
-public class AuthenticationResponse {
-
- private final String jwt;
-}
+public record AuthenticationResponse(String jwt, String refreshToken) {}
diff --git a/src/main/java/xyz/subho/clone/twitter/model/ErrorResponse.java b/src/main/java/xyz/subho/clone/twitter/model/ErrorResponse.java
index e76341c..bb7d804 100644
--- a/src/main/java/xyz/subho/clone/twitter/model/ErrorResponse.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/ErrorResponse.java
@@ -20,61 +20,5 @@
import java.util.Date;
-public class ErrorResponse {
-
- private Integer statusCode;
- private Date timestamp;
- private String message;
- private String description;
-
- /**
- * @param statusCode
- * @param timestamp
- * @param message
- * @param description
- */
- public ErrorResponse(Integer statusCode, Date timestamp, String message, String description) {
- this.statusCode = statusCode;
- this.timestamp = timestamp;
- this.message = message;
- this.description = description;
- }
-
- /**
- * @return the statusCode
- */
- public Integer getStatusCode() {
- return statusCode;
- }
-
- /**
- * @return the timestamp
- */
- public Date getTimestamp() {
- return timestamp;
- }
-
- /**
- * @return the message
- */
- public String getMessage() {
- return message;
- }
-
- /**
- * @return the description
- */
- public String getDescription() {
- return description;
- }
-
- @Override
- public String toString() {
- return "ErrorResponse ["
- + (statusCode != null ? "statusCode=" + statusCode + ", " : "")
- + (timestamp != null ? "timestamp=" + timestamp + ", " : "")
- + (message != null ? "message=" + message + ", " : "")
- + (description != null ? "description=" + description : "")
- + "]";
- }
-}
+public record ErrorResponse(
+ Integer statusCode, Date timestamp, String message, String description) {}
diff --git a/src/main/java/xyz/subho/clone/twitter/model/HashtagModel.java b/src/main/java/xyz/subho/clone/twitter/model/HashtagModel.java
index 6f9fd87..8e4f1bc 100644
--- a/src/main/java/xyz/subho/clone/twitter/model/HashtagModel.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/HashtagModel.java
@@ -19,12 +19,5 @@
package xyz.subho.clone.twitter.model;
import java.util.UUID;
-import lombok.Data;
-@Data
-public class HashtagModel {
-
- private UUID id;
- private String tag;
- private Long recentPostCount;
-}
+public record HashtagModel(UUID id, String tag, Long recentPostCount) {}
diff --git a/src/main/java/xyz/subho/clone/twitter/model/HashtagPostModel.java b/src/main/java/xyz/subho/clone/twitter/model/HashtagPostModel.java
index c0bd403..2d6140f 100644
--- a/src/main/java/xyz/subho/clone/twitter/model/HashtagPostModel.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/HashtagPostModel.java
@@ -19,12 +19,5 @@
package xyz.subho.clone.twitter.model;
import java.util.UUID;
-import lombok.Data;
-@Data
-public class HashtagPostModel {
-
- private UUID id;
- private HashtagModel hashtag;
- private PostModel post;
-}
+public record HashtagPostModel(UUID id, HashtagModel hashtag, PostModel post) {}
diff --git a/src/main/java/xyz/subho/clone/twitter/model/LikeModel.java b/src/main/java/xyz/subho/clone/twitter/model/LikeModel.java
index c8e8e5c..0b5c67b 100644
--- a/src/main/java/xyz/subho/clone/twitter/model/LikeModel.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/LikeModel.java
@@ -19,12 +19,5 @@
package xyz.subho.clone.twitter.model;
import java.util.UUID;
-import lombok.Data;
-@Data
-public class LikeModel {
-
- private UUID id;
- private PostModel post;
- private UserModel user;
-}
+public record LikeModel(UUID id, PostModel post, UserModel user) {}
diff --git a/src/main/java/xyz/subho/clone/twitter/model/PostModel.java b/src/main/java/xyz/subho/clone/twitter/model/PostModel.java
index 9a452ea..f159b22 100644
--- a/src/main/java/xyz/subho/clone/twitter/model/PostModel.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/PostModel.java
@@ -18,24 +18,32 @@
package xyz.subho.clone.twitter.model;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
-import lombok.Data;
+import org.jspecify.annotations.Nullable;
-@Data
-public class PostModel {
+public record PostModel(
+ @Nullable UUID id,
+ @NotBlank(message = "Post text cannot be empty")
+ @Size(max = 240, message = "Post text cannot exceed 240 characters")
+ String text,
+ @Nullable UUID userId,
+ List images,
+ @Nullable Long likeCount,
+ @Nullable Long repostCount,
+ @Nullable UUID originalPostId,
+ @Nullable UUID replyToId,
+ @Nullable Date timestamp,
+ List hashtags,
+ List mentions) {
- private UUID id;
- private String text;
- private UUID userId;
- private List images = new ArrayList<>(4);
- private Long likeCount;
- private Long repostCount;
- private UUID originalPostId;
- private UUID replyToId;
- private Date timestamp;
- private List hashtags = new ArrayList<>();
- private List mentions = new ArrayList<>();
+ public PostModel {
+ if (images == null) images = new ArrayList<>(4);
+ if (hashtags == null) hashtags = new ArrayList<>();
+ if (mentions == null) mentions = new ArrayList<>();
+ }
}
diff --git a/src/main/java/xyz/subho/clone/twitter/utility/Mapper.java b/src/main/java/xyz/subho/clone/twitter/model/TokenRefreshRequest.java
similarity index 77%
rename from src/main/java/xyz/subho/clone/twitter/utility/Mapper.java
rename to src/main/java/xyz/subho/clone/twitter/model/TokenRefreshRequest.java
index 5a8efad..46517bb 100644
--- a/src/main/java/xyz/subho/clone/twitter/utility/Mapper.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/TokenRefreshRequest.java
@@ -1,6 +1,6 @@
/*
* Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
- * Copyright © 2021-2023 Subhrodip Mohanta (hello@subho.xyz)
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -16,11 +16,8 @@
* along with this program. If not, see .
*/
-package xyz.subho.clone.twitter.utility;
+package xyz.subho.clone.twitter.model;
-public interface Mapper {
+import jakarta.validation.constraints.NotBlank;
- T transform(S source);
-
- S transformBack(T source);
-}
+public record TokenRefreshRequest(@NotBlank String refreshToken) {}
diff --git a/src/main/java/xyz/subho/clone/twitter/model/UserModel.java b/src/main/java/xyz/subho/clone/twitter/model/UserModel.java
index 691e0c9..b4bd82e 100644
--- a/src/main/java/xyz/subho/clone/twitter/model/UserModel.java
+++ b/src/main/java/xyz/subho/clone/twitter/model/UserModel.java
@@ -18,28 +18,25 @@
package xyz.subho.clone.twitter.model;
+import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import java.util.UUID;
-import lombok.Data;
import org.jspecify.annotations.Nullable;
-@Data
-public class UserModel {
-
- private @Nullable UUID id;
-
- @NotBlank(message = "Username is mandatory")
- @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
- private String username;
-
- @NotBlank(message = "Name is mandatory")
- @Size(min = 1, max = 100, message = "Name must be between 1 and 100 characters")
- private String name;
-
- private @Nullable String avatar;
- private @Nullable String bio;
- private @Nullable Long followerCount;
- private @Nullable Long followingCount;
- private @Nullable Boolean verified;
-}
+public record UserModel(
+ @Nullable UUID id,
+ @NotBlank(message = "Username is mandatory")
+ @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
+ String username,
+ @NotBlank(message = "Name is mandatory")
+ @Size(min = 1, max = 100, message = "Name must be between 1 and 100 characters")
+ String name,
+ @NotBlank(message = "Password is mandatory") String password,
+ @NotBlank(message = "Email is mandatory") @Email(message = "Email should be valid")
+ String email,
+ @Nullable String avatar,
+ @Nullable String bio,
+ @Nullable Long followerCount,
+ @Nullable Long followingCount,
+ @Nullable Boolean verified) {}
diff --git a/src/main/java/xyz/subho/clone/twitter/repository/FollowRepository.java b/src/main/java/xyz/subho/clone/twitter/repository/FollowRepository.java
new file mode 100644
index 0000000..3c9759d
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/repository/FollowRepository.java
@@ -0,0 +1,37 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.repository;
+
+import java.util.UUID;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import xyz.subho.clone.twitter.entity.Follow;
+import xyz.subho.clone.twitter.entity.Users;
+
+public interface FollowRepository extends JpaRepository {
+
+ Page findByFollowing(Users following, Pageable pageable);
+
+ Page findByFollower(Users follower, Pageable pageable);
+
+ void deleteByFollowerAndFollowing(Users follower, Users following);
+
+ boolean existsByFollowerAndFollowing(Users follower, Users following);
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/repository/HashtagPostsRepository.java b/src/main/java/xyz/subho/clone/twitter/repository/HashtagPostsRepository.java
index 2ce9434..7bf51d1 100644
--- a/src/main/java/xyz/subho/clone/twitter/repository/HashtagPostsRepository.java
+++ b/src/main/java/xyz/subho/clone/twitter/repository/HashtagPostsRepository.java
@@ -20,6 +20,8 @@
import java.util.List;
import java.util.UUID;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import xyz.subho.clone.twitter.entity.HashtagPosts;
import xyz.subho.clone.twitter.entity.Hashtags;
@@ -27,7 +29,7 @@
public interface HashtagPostsRepository extends JpaRepository {
- public List findByHashtags(Hashtags hashtag);
+ public Page findByHashtags(Hashtags hashtag, Pageable pageable);
public List findByPosts(Posts post);
}
diff --git a/src/main/java/xyz/subho/clone/twitter/repository/PostsRepository.java b/src/main/java/xyz/subho/clone/twitter/repository/PostsRepository.java
index 9be01df..2f48151 100644
--- a/src/main/java/xyz/subho/clone/twitter/repository/PostsRepository.java
+++ b/src/main/java/xyz/subho/clone/twitter/repository/PostsRepository.java
@@ -19,7 +19,12 @@
package xyz.subho.clone.twitter.repository;
import java.util.UUID;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import xyz.subho.clone.twitter.entity.Posts;
-public interface PostsRepository extends JpaRepository {}
+public interface PostsRepository extends JpaRepository {
+
+ public Page findByReplyToId(UUID replyToId, Pageable pageable);
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/repository/RefreshTokenRepository.java b/src/main/java/xyz/subho/clone/twitter/repository/RefreshTokenRepository.java
new file mode 100644
index 0000000..7816a54
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/repository/RefreshTokenRepository.java
@@ -0,0 +1,34 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.repository;
+
+import java.util.Optional;
+import java.util.UUID;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import xyz.subho.clone.twitter.entity.RefreshToken;
+import xyz.subho.clone.twitter.entity.Users;
+
+public interface RefreshTokenRepository extends JpaRepository {
+
+ Optional findByToken(String token);
+
+ @Modifying
+ int deleteByUsers(Users user);
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/security/JwtRequestFilter.java b/src/main/java/xyz/subho/clone/twitter/security/JwtRequestFilter.java
index dbd2e8c..3d2a02b 100644
--- a/src/main/java/xyz/subho/clone/twitter/security/JwtRequestFilter.java
+++ b/src/main/java/xyz/subho/clone/twitter/security/JwtRequestFilter.java
@@ -23,7 +23,6 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
@@ -33,9 +32,13 @@
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
- @Autowired private UserDetailsServiceImpl userDetailsService;
+ private final UserDetailsServiceImpl userDetailsService;
+ private final JwtUtil jwtUtil;
- @Autowired private JwtUtil jwtUtil;
+ public JwtRequestFilter(UserDetailsServiceImpl userDetailsService, JwtUtil jwtUtil) {
+ this.userDetailsService = userDetailsService;
+ this.jwtUtil = jwtUtil;
+ }
@Override
protected void doFilterInternal(
diff --git a/src/main/java/xyz/subho/clone/twitter/security/SecurityConfig.java b/src/main/java/xyz/subho/clone/twitter/security/SecurityConfig.java
index 3e33a4a..a4233aa 100644
--- a/src/main/java/xyz/subho/clone/twitter/security/SecurityConfig.java
+++ b/src/main/java/xyz/subho/clone/twitter/security/SecurityConfig.java
@@ -18,7 +18,6 @@
package xyz.subho.clone.twitter.security;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
@@ -26,23 +25,33 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.crypto.password.NoOpPasswordEncoder;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import xyz.subho.clone.twitter.constant.AuthV1Constants;
+import xyz.subho.clone.twitter.constant.UserV1Constants;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
- @Autowired private JwtRequestFilter jwtRequestFilter;
+ private final JwtRequestFilter jwtRequestFilter;
+
+ public SecurityConfig(JwtRequestFilter jwtRequestFilter) {
+ this.jwtRequestFilter = jwtRequestFilter;
+ }
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(
auth ->
- auth.requestMatchers("/authenticate", "/users", "/swagger-ui/**", "/v3/api-docs/**")
+ auth.requestMatchers(
+ AuthV1Constants.BASE_PATH + AuthV1Constants.AUTHENTICATE,
+ UserV1Constants.BASE_PATH,
+ "/swagger-ui/**",
+ "/v3/api-docs/**")
.permitAll()
.anyRequest()
.authenticated())
@@ -56,7 +65,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
@Bean
public PasswordEncoder passwordEncoder() {
- return NoOpPasswordEncoder.getInstance();
+ return new BCryptPasswordEncoder();
}
@Bean
diff --git a/src/main/java/xyz/subho/clone/twitter/security/UserDetailsServiceImpl.java b/src/main/java/xyz/subho/clone/twitter/security/UserDetailsServiceImpl.java
index 488a310..52859e3 100644
--- a/src/main/java/xyz/subho/clone/twitter/security/UserDetailsServiceImpl.java
+++ b/src/main/java/xyz/subho/clone/twitter/security/UserDetailsServiceImpl.java
@@ -19,7 +19,6 @@
package xyz.subho.clone.twitter.security;
import java.util.ArrayList;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@@ -30,7 +29,11 @@
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
- @Autowired private UsersRepository userRepository;
+ private final UsersRepository userRepository;
+
+ public UserDetailsServiceImpl(UsersRepository userRepository) {
+ this.userRepository = userRepository;
+ }
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
@@ -38,6 +41,6 @@ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundEx
if (user == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
- return new User(user.getUsername(), "", new ArrayList<>());
+ return new User(user.getUsername(), user.getPassword(), new ArrayList<>());
}
}
diff --git a/src/main/java/xyz/subho/clone/twitter/service/HashtagService.java b/src/main/java/xyz/subho/clone/twitter/service/HashtagService.java
index 5f9ad0c..53cb90f 100644
--- a/src/main/java/xyz/subho/clone/twitter/service/HashtagService.java
+++ b/src/main/java/xyz/subho/clone/twitter/service/HashtagService.java
@@ -19,15 +19,19 @@
package xyz.subho.clone.twitter.service;
import java.util.List;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import xyz.subho.clone.twitter.entity.Hashtags;
import xyz.subho.clone.twitter.model.HashtagModel;
import xyz.subho.clone.twitter.model.PostModel;
public interface HashtagService {
- public List getHashtags();
+ public @NonNull Page getHashtags(@NonNull Pageable pageable);
- public List getPosts(String tag);
+ public @NonNull Page getPosts(@NonNull String tag, @NonNull Pageable pageable);
- public List getHashtagsByTags(List hashtag);
+ public @Nullable List getHashtagsByTags(@NonNull List hashtag);
}
diff --git a/src/main/java/xyz/subho/clone/twitter/service/PostService.java b/src/main/java/xyz/subho/clone/twitter/service/PostService.java
index f85042f..2853b76 100644
--- a/src/main/java/xyz/subho/clone/twitter/service/PostService.java
+++ b/src/main/java/xyz/subho/clone/twitter/service/PostService.java
@@ -19,21 +19,25 @@
package xyz.subho.clone.twitter.service;
import java.util.UUID;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import xyz.subho.clone.twitter.model.PostModel;
public interface PostService {
- public Page getAllPosts(Pageable pageable);
+ public @NonNull Page getAllPosts(@NonNull Pageable pageable);
- public PostModel getPost(UUID postId);
+ public @Nullable PostModel getPost(@NonNull UUID postId);
- public PostModel addPost(PostModel postModel);
+ public @NonNull PostModel addPost(@NonNull PostModel postModel);
- public boolean deletePost(UUID postId, UUID userId);
+ public boolean deletePost(@NonNull UUID postId, @NonNull UUID userId);
- public long addLike(UUID postId, UUID userId);
+ public long addLike(@NonNull UUID postId, @NonNull UUID userId);
- public long removeLike(UUID postId, UUID userId);
+ public long removeLike(@NonNull UUID postId, @NonNull UUID userId);
+
+ public @NonNull Page getReplies(@NonNull UUID postId, @NonNull Pageable pageable);
}
diff --git a/src/main/java/xyz/subho/clone/twitter/service/RefreshTokenService.java b/src/main/java/xyz/subho/clone/twitter/service/RefreshTokenService.java
new file mode 100644
index 0000000..f7f51ec
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/service/RefreshTokenService.java
@@ -0,0 +1,35 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.service;
+
+import java.util.Optional;
+import java.util.UUID;
+import org.jspecify.annotations.NonNull;
+import xyz.subho.clone.twitter.entity.RefreshToken;
+
+public interface RefreshTokenService {
+
+ public RefreshToken createRefreshToken(@NonNull UUID userId);
+
+ public Optional findByToken(@NonNull String token);
+
+ public RefreshToken verifyExpiration(@NonNull RefreshToken token);
+
+ public int deleteByUserId(@NonNull UUID userId);
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/service/impl/HashtagServiceImpl.java b/src/main/java/xyz/subho/clone/twitter/service/impl/HashtagServiceImpl.java
index f6409f9..9f856f0 100644
--- a/src/main/java/xyz/subho/clone/twitter/service/impl/HashtagServiceImpl.java
+++ b/src/main/java/xyz/subho/clone/twitter/service/impl/HashtagServiceImpl.java
@@ -24,63 +24,60 @@
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import xyz.subho.clone.twitter.entity.Hashtags;
-import xyz.subho.clone.twitter.entity.Posts;
import xyz.subho.clone.twitter.model.HashtagModel;
import xyz.subho.clone.twitter.model.PostModel;
import xyz.subho.clone.twitter.repository.HashtagPostsRepository;
import xyz.subho.clone.twitter.repository.HashtagsRepository;
import xyz.subho.clone.twitter.service.HashtagService;
-import xyz.subho.clone.twitter.utility.Mapper;
+import xyz.subho.clone.twitter.utility.HashtagMapper;
+import xyz.subho.clone.twitter.utility.PostMapper;
@Service
public class HashtagServiceImpl implements HashtagService {
- @Autowired private HashtagsRepository hashtagsRepository;
-
- @Autowired private HashtagPostsRepository hashtagPostsRepository;
-
- @Autowired
- @Qualifier("HashtagMapper")
- private Mapper hashtagMapper;
-
- @Autowired
- @Qualifier("PostMapper")
- private Mapper postMapper;
+ private final HashtagsRepository hashtagsRepository;
+ private final HashtagPostsRepository hashtagPostsRepository;
+ private final HashtagMapper hashtagMapper;
+ private final PostMapper postMapper;
+
+ public HashtagServiceImpl(
+ HashtagsRepository hashtagsRepository,
+ HashtagPostsRepository hashtagPostsRepository,
+ HashtagMapper hashtagMapper,
+ PostMapper postMapper) {
+ this.hashtagsRepository = hashtagsRepository;
+ this.hashtagPostsRepository = hashtagPostsRepository;
+ this.hashtagMapper = hashtagMapper;
+ this.postMapper = postMapper;
+ }
@Override
- public List getHashtags() {
-
- var hashtags = hashtagsRepository.findAll();
- List hashtagModels = new ArrayList<>();
- Optional.ofNullable(hashtags)
- .ifPresent(
- hashtag -> hashtag.forEach(hTag -> hashtagModels.add(hashtagMapper.transform(hTag))));
- return hashtagModels;
- } // TODO: Create a stored procedure in DB.
+ public @NonNull Page getHashtags(@NonNull Pageable pageable) {
+ var hashtagsPage = hashtagsRepository.findAll(pageable);
+ return hashtagsPage.map(hashtagMapper::toModel);
+ }
@Override
- public List getPosts(String tag) {
-
+ public @NonNull Page getPosts(@NonNull String tag, @NonNull Pageable pageable) {
var hashtag = hashtagsRepository.findByTag(tag);
- List posts = new ArrayList<>();
- if (null != hashtag) {
- posts = hashtagPostsRepository.findByHashtags(hashtag);
+ if (null == hashtag) {
+ return Page.empty();
}
- List postModels = new ArrayList<>();
- Optional.ofNullable(posts)
- .ifPresent(post -> post.forEach(pst -> postModels.add(postMapper.transform(pst))));
- return postModels;
+ var hashtagPostsPage = hashtagPostsRepository.findByHashtags(hashtag, pageable);
+ return hashtagPostsPage.map(hp -> postMapper.toModel(hp.getPosts()));
}
@Override
@Transactional
- public List getHashtagsByTags(List tags) {
+ public @Nullable List getHashtagsByTags(@NonNull List tags) {
List outputListOfHashtags = new ArrayList<>();
List hashTags = hashtagsRepository.findByTagIn(tags);
@@ -115,7 +112,7 @@ private void setHashTagCount(List hashTags) {
private Set fetchExistingTags(List hashTags) {
if (!CollectionUtils.isEmpty(hashTags)) {
- return hashTags.stream().map(hTag -> hTag.getTag()).collect(Collectors.toSet());
+ return hashTags.stream().map(Hashtags::getTag).collect(Collectors.toSet());
}
return new HashSet<>();
}
diff --git a/src/main/java/xyz/subho/clone/twitter/service/impl/PostServiceImpl.java b/src/main/java/xyz/subho/clone/twitter/service/impl/PostServiceImpl.java
index 18e8f3e..e5ac63a 100644
--- a/src/main/java/xyz/subho/clone/twitter/service/impl/PostServiceImpl.java
+++ b/src/main/java/xyz/subho/clone/twitter/service/impl/PostServiceImpl.java
@@ -22,21 +22,19 @@
import java.util.List;
import java.util.Optional;
import java.util.UUID;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import xyz.subho.clone.twitter.entity.HashtagPosts;
import xyz.subho.clone.twitter.entity.Likes;
-import xyz.subho.clone.twitter.entity.Posts;
-import xyz.subho.clone.twitter.entity.Users;
import xyz.subho.clone.twitter.exception.ErrorSavingEntityToDatabaseException;
import xyz.subho.clone.twitter.exception.ResourceNotFoundException;
import xyz.subho.clone.twitter.model.PostModel;
-import xyz.subho.clone.twitter.model.UserModel;
import xyz.subho.clone.twitter.repository.HashtagPostsRepository;
import xyz.subho.clone.twitter.repository.LikesRepository;
import xyz.subho.clone.twitter.repository.PostsRepository;
@@ -44,54 +42,64 @@
import xyz.subho.clone.twitter.service.HashtagService;
import xyz.subho.clone.twitter.service.PostService;
import xyz.subho.clone.twitter.service.UserService;
-import xyz.subho.clone.twitter.utility.Mapper;
+import xyz.subho.clone.twitter.utility.PostMapper;
+import xyz.subho.clone.twitter.utility.UserMapper;
@Service
-@Slf4j
public class PostServiceImpl implements PostService {
- @Autowired private PostsRepository postsRepository;
-
- @Autowired private HashtagPostsRepository hashtagPostRepository;
-
- @Autowired private UserService userService;
-
- @Autowired private HashtagService hashtagService;
-
- @Autowired private LikesRepository likeRepository;
-
- @Autowired private UsersRepository usersRepository;
-
- @Autowired
- @Qualifier("PostMapper")
- private Mapper postMapper;
-
- @Autowired
- @Qualifier("UserMapper")
- private Mapper userMapper;
+ private static final Logger log = LoggerFactory.getLogger(PostServiceImpl.class);
+
+ private final PostsRepository postsRepository;
+ private final HashtagPostsRepository hashtagPostRepository;
+ private final UserService userService;
+ private final HashtagService hashtagService;
+ private final LikesRepository likeRepository;
+ private final UsersRepository usersRepository;
+ private final PostMapper postMapper;
+ private final UserMapper userMapper;
+
+ public PostServiceImpl(
+ PostsRepository postsRepository,
+ HashtagPostsRepository hashtagPostRepository,
+ UserService userService,
+ HashtagService hashtagService,
+ LikesRepository likeRepository,
+ UsersRepository usersRepository,
+ PostMapper postMapper,
+ UserMapper userMapper) {
+ this.postsRepository = postsRepository;
+ this.hashtagPostRepository = hashtagPostRepository;
+ this.userService = userService;
+ this.hashtagService = hashtagService;
+ this.likeRepository = likeRepository;
+ this.usersRepository = usersRepository;
+ this.postMapper = postMapper;
+ this.userMapper = userMapper;
+ }
@Override
- public Page getAllPosts(Pageable pageable) {
+ public @NonNull Page getAllPosts(@NonNull Pageable pageable) {
var postsPage = postsRepository.findAll(pageable);
- return postsPage.map(postMapper::transform);
+ return postsPage.map(postMapper::toModel);
}
@Override
- public PostModel getPost(UUID postId) {
+ public @Nullable PostModel getPost(@NonNull UUID postId) {
var post = postsRepository.findById(postId);
- if (post.isPresent()) return postMapper.transform(post.get());
+ if (post.isPresent()) return postMapper.toModel(post.get());
throw new ResourceNotFoundException("Post ID is Invalid");
}
@Override
@Transactional
- public PostModel addPost(PostModel postModel) {
+ public @NonNull PostModel addPost(@NonNull PostModel postModel) {
List hashtagPosts = new ArrayList<>();
- var post = postMapper.transformBack(postModel);
- post.setUsers(usersRepository.getById(postModel.getUserId()));
- Optional.ofNullable(hashtagService.getHashtagsByTags(postModel.getHashtags()))
+ var post = postMapper.toEntity(postModel);
+ post.setUsers(usersRepository.getById(postModel.userId()));
+ Optional.ofNullable(hashtagService.getHashtagsByTags(postModel.hashtags()))
.ifPresent(
hashtags -> {
hashtags.forEach(
@@ -104,12 +112,12 @@ public PostModel addPost(PostModel postModel) {
});
post.setPostHashtags(hashtagPosts);
hashtagPostRepository.saveAll(hashtagPosts);
- return postMapper.transform(postsRepository.save(post));
+ return postMapper.toModel(postsRepository.save(post));
}
@Override
@Transactional
- public boolean deletePost(UUID postId, UUID userId) {
+ public boolean deletePost(@NonNull UUID postId, @NonNull UUID userId) {
if (Optional.ofNullable((getPost(postId))).isPresent()) {
postsRepository.deleteById(postId);
@@ -120,11 +128,15 @@ public boolean deletePost(UUID postId, UUID userId) {
@Override
@Transactional
- public long addLike(UUID postId, UUID userId) {
+ public long addLike(@NonNull UUID postId, @NonNull UUID userId) {
- var post = postMapper.transformBack(getPost(postId));
+ var postModel = getPost(postId);
+ if (postModel == null) throw new ResourceNotFoundException("Post not found");
+ var post = postMapper.toEntity(postModel);
post.incrementLikeCount();
- var user = userMapper.transformBack(userService.getUserByUserId(userId));
+ var userModel = userService.getUserByUserId(userId);
+ if (userModel == null) throw new ResourceNotFoundException("User not found");
+ var user = userMapper.toEntity(userModel);
var likeMapping = new Likes();
likeMapping.setPosts(post);
@@ -141,11 +153,15 @@ public long addLike(UUID postId, UUID userId) {
@Override
@Transactional
- public long removeLike(UUID postId, UUID userId) {
+ public long removeLike(@NonNull UUID postId, @NonNull UUID userId) {
- var post = postMapper.transformBack(getPost(postId));
+ var postModel = getPost(postId);
+ if (postModel == null) throw new ResourceNotFoundException("Post not found");
+ var post = postMapper.toEntity(postModel);
post.decrementLikeCount();
- var user = userMapper.transformBack(userService.getUserByUserId(userId));
+ var userModel = userService.getUserByUserId(userId);
+ if (userModel == null) throw new ResourceNotFoundException("User not found");
+ var user = userMapper.toEntity(userModel);
try {
likeRepository.deleteByPostsAndUsers(post, user);
@@ -156,4 +172,10 @@ public long removeLike(UUID postId, UUID userId) {
throw new ErrorSavingEntityToDatabaseException("Cannot Save to Database");
}
}
+
+ @Override
+ public @NonNull Page getReplies(@NonNull UUID postId, @NonNull Pageable pageable) {
+ var repliesPage = postsRepository.findByReplyToId(postId, pageable);
+ return repliesPage.map(postMapper::toModel);
+ }
}
diff --git a/src/main/java/xyz/subho/clone/twitter/service/impl/RefreshTokenServiceImpl.java b/src/main/java/xyz/subho/clone/twitter/service/impl/RefreshTokenServiceImpl.java
new file mode 100644
index 0000000..6969a1f
--- /dev/null
+++ b/src/main/java/xyz/subho/clone/twitter/service/impl/RefreshTokenServiceImpl.java
@@ -0,0 +1,84 @@
+/*
+ * Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.subho.clone.twitter.service.impl;
+
+import java.time.Instant;
+import java.util.Optional;
+import java.util.UUID;
+import org.jspecify.annotations.NonNull;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import xyz.subho.clone.twitter.entity.RefreshToken;
+import xyz.subho.clone.twitter.exception.BadRequestException;
+import xyz.subho.clone.twitter.repository.RefreshTokenRepository;
+import xyz.subho.clone.twitter.repository.UsersRepository;
+import xyz.subho.clone.twitter.service.RefreshTokenService;
+
+@Service
+public class RefreshTokenServiceImpl implements RefreshTokenService {
+
+ @Value("${jwt.refreshExpiration}")
+ private Long refreshTokenDurationMs;
+
+ private final RefreshTokenRepository refreshTokenRepository;
+ private final UsersRepository usersRepository;
+
+ public RefreshTokenServiceImpl(
+ RefreshTokenRepository refreshTokenRepository, UsersRepository usersRepository) {
+ this.refreshTokenRepository = refreshTokenRepository;
+ this.usersRepository = usersRepository;
+ }
+
+ @Override
+ @Transactional
+ public RefreshToken createRefreshToken(@NonNull UUID userId) {
+ var user = usersRepository.getById(userId);
+
+ // Clean up existing tokens for the user
+ refreshTokenRepository.deleteByUsers(user);
+
+ RefreshToken refreshToken = new RefreshToken();
+ refreshToken.setUsers(user);
+ refreshToken.setExpiryDate(Instant.now().plusMillis(refreshTokenDurationMs));
+ refreshToken.setToken(UUID.randomUUID().toString());
+
+ return refreshTokenRepository.save(refreshToken);
+ }
+
+ @Override
+ public Optional findByToken(@NonNull String token) {
+ return refreshTokenRepository.findByToken(token);
+ }
+
+ @Override
+ public RefreshToken verifyExpiration(@NonNull RefreshToken token) {
+ if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
+ refreshTokenRepository.delete(token);
+ throw new BadRequestException("Refresh token was expired. Please make a new signin request");
+ }
+ return token;
+ }
+
+ @Override
+ @Transactional
+ public int deleteByUserId(@NonNull UUID userId) {
+ return refreshTokenRepository.deleteByUsers(usersRepository.getById(userId));
+ }
+}
diff --git a/src/main/java/xyz/subho/clone/twitter/service/impl/UserServiceImpl.java b/src/main/java/xyz/subho/clone/twitter/service/impl/UserServiceImpl.java
index 106b16a..ef9c61f 100644
--- a/src/main/java/xyz/subho/clone/twitter/service/impl/UserServiceImpl.java
+++ b/src/main/java/xyz/subho/clone/twitter/service/impl/UserServiceImpl.java
@@ -19,85 +19,120 @@
package xyz.subho.clone.twitter.service.impl;
import java.util.UUID;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
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.transaction.annotation.Transactional;
+import xyz.subho.clone.twitter.entity.Follow;
import xyz.subho.clone.twitter.entity.Users;
import xyz.subho.clone.twitter.model.UserModel;
+import xyz.subho.clone.twitter.repository.FollowRepository;
import xyz.subho.clone.twitter.repository.UsersRepository;
import xyz.subho.clone.twitter.service.UserService;
-import xyz.subho.clone.twitter.utility.Mapper;
+import xyz.subho.clone.twitter.utility.UserMapper;
@Service
public class UserServiceImpl implements UserService {
- @Autowired private UsersRepository usersRepository;
+ private final UsersRepository usersRepository;
+ private final FollowRepository followRepository;
+ private final PasswordEncoder passwordEncoder;
+ private final UserMapper userMapper;
- @Autowired
- @Qualifier("UserMapper")
- private Mapper userMapper;
+ public UserServiceImpl(
+ UsersRepository usersRepository,
+ FollowRepository followRepository,
+ PasswordEncoder passwordEncoder,
+ UserMapper userMapper) {
+ this.usersRepository = usersRepository;
+ this.followRepository = followRepository;
+ this.passwordEncoder = passwordEncoder;
+ this.userMapper = userMapper;
+ }
@Override
- public UserModel getUserByUserName(String username) {
- return userMapper.transform(usersRepository.findByUsername(username));
+ public @Nullable UserModel getUserByUserName(@NonNull String username) {
+ return userMapper.toModel(usersRepository.findByUsername(username));
}
@Override
- public UserModel getUserByUserId(UUID userId) {
+ public @Nullable UserModel getUserByUserId(@NonNull UUID userId) {
var user = usersRepository.getById(userId);
- return userMapper.transform(user);
+ return userMapper.toModel(user);
}
@Override
- public Users getUserEntityByUserId(UUID userId) {
+ public @Nullable Users getUserEntityByUserId(@NonNull UUID userId) {
return usersRepository.getById(userId);
}
@Override
@Transactional
- public UserModel addUser(UserModel user) {
- Users users = userMapper.transformBack(user);
- return userMapper.transform(usersRepository.save(users));
+ public @NonNull UserModel addUser(@NonNull UserModel userModel) {
+ var user = userMapper.toEntity(userModel);
+ user.setPassword(passwordEncoder.encode(userModel.password()));
+ return userMapper.toModel(usersRepository.save(user));
}
@Override
@Transactional
- public UserModel editUser(UserModel user) {
- Users users = userMapper.transformBack(user);
- return userMapper.transform(usersRepository.save(users));
+ public @NonNull UserModel editUser(@NonNull UserModel userModel) {
+ Users users = userMapper.toEntity(userModel);
+ return userMapper.toModel(usersRepository.save(users));
}
@Override
@Transactional
- public boolean addFollower(UUID followerId, UUID userId) {
+ public boolean addFollower(@NonNull UUID followerId, @NonNull UUID userId) {
+ if (followRepository.existsByFollowerAndFollowing(
+ usersRepository.getById(followerId), usersRepository.getById(userId))) {
+ return false;
+ }
+
Users user = usersRepository.getById(userId);
- user.setFollower(followerId);
+ Users follower = usersRepository.getById(followerId);
+
+ Follow follow = new Follow(follower, user);
+ followRepository.save(follow);
+
+ user.setFollowerCount(user.getFollowerCount() + 1);
usersRepository.save(user);
+
+ follower.setFollowingCount(follower.getFollowingCount() + 1);
+ usersRepository.save(follower);
return true;
}
@Override
- public boolean removeFollower(UUID followerId, UUID userId) {
+ @Transactional
+ public boolean removeFollower(@NonNull UUID followerId, @NonNull UUID userId) {
Users user = usersRepository.getById(userId);
- user.removeFollower(followerId);
+ Users follower = usersRepository.getById(followerId);
+
+ followRepository.deleteByFollowerAndFollowing(follower, user);
+
+ user.setFollowerCount(Math.max(0, user.getFollowerCount() - 1));
usersRepository.save(user);
+
+ follower.setFollowingCount(Math.max(0, follower.getFollowingCount() - 1));
+ usersRepository.save(follower);
return true;
}
@Override
- public Page getFollowers(UUID userId, Pageable pageable) {
+ public @NonNull Page getFollowers(@NonNull UUID userId, @NonNull Pageable pageable) {
Users user = usersRepository.getById(userId);
- var usersPage = usersRepository.findByIdIn(user.getFollower().keySet(), pageable);
- return usersPage.map(userMapper::transform);
+ var followsPage = followRepository.findByFollowing(user, pageable);
+ return followsPage.map(f -> userMapper.toModel(f.getFollower()));
}
@Override
- public Page getFollowings(UUID userId, Pageable pageable) {
+ public @NonNull Page getFollowings(@NonNull UUID userId, @NonNull Pageable pageable) {
Users user = usersRepository.getById(userId);
- var usersPage = usersRepository.findByIdIn(user.getFollowing().keySet(), pageable);
- return usersPage.map(userMapper::transform);
+ var followsPage = followRepository.findByFollower(user, pageable);
+ return followsPage.map(f -> userMapper.toModel(f.getFollowing()));
}
}
diff --git a/src/main/java/xyz/subho/clone/twitter/utility/HashtagMapper.java b/src/main/java/xyz/subho/clone/twitter/utility/HashtagMapper.java
index e86c4d2..531c0a4 100644
--- a/src/main/java/xyz/subho/clone/twitter/utility/HashtagMapper.java
+++ b/src/main/java/xyz/subho/clone/twitter/utility/HashtagMapper.java
@@ -1,6 +1,6 @@
/*
* Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
- * Copyright © 2021-2023 Subhrodip Mohanta (hello@subho.xyz)
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -18,25 +18,18 @@
package xyz.subho.clone.twitter.utility;
-import org.springframework.beans.BeanUtils;
-import org.springframework.stereotype.Component;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
import xyz.subho.clone.twitter.entity.Hashtags;
import xyz.subho.clone.twitter.model.HashtagModel;
-@Component("HashtagMapper")
-public class HashtagMapper implements Mapper {
+@Mapper(componentModel = "spring")
+public interface HashtagMapper {
- @Override
- public HashtagModel transform(Hashtags hashtag) {
- var hashtagModel = new HashtagModel();
- BeanUtils.copyProperties(hashtag, hashtagModel);
- return hashtagModel;
- }
+ HashtagModel toModel(Hashtags hashtag);
- @Override
- public Hashtags transformBack(HashtagModel hashtagModel) {
- var hashtag = new Hashtags();
- BeanUtils.copyProperties(hashtagModel, hashtag);
- return hashtag;
- }
+ @Mapping(target = "hashtagPosts", ignore = true)
+ @Mapping(target = "createdAt", ignore = true)
+ @Mapping(target = "updatedAt", ignore = true)
+ Hashtags toEntity(HashtagModel hashtagModel);
}
diff --git a/src/main/java/xyz/subho/clone/twitter/utility/PostMapper.java b/src/main/java/xyz/subho/clone/twitter/utility/PostMapper.java
index d12bae7..9916b48 100644
--- a/src/main/java/xyz/subho/clone/twitter/utility/PostMapper.java
+++ b/src/main/java/xyz/subho/clone/twitter/utility/PostMapper.java
@@ -1,6 +1,6 @@
/*
* Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
- * Copyright © 2021-2023 Subhrodip Mohanta (hello@subho.xyz)
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -21,37 +21,46 @@
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
-import org.springframework.beans.BeanUtils;
-import org.springframework.stereotype.Component;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
import xyz.subho.clone.twitter.entity.Posts;
import xyz.subho.clone.twitter.model.PostModel;
-@Component("PostMapper")
-public class PostMapper implements Mapper {
+@Mapper(componentModel = "spring")
+public interface PostMapper {
- @Override
- public PostModel transform(Posts post) {
- PostModel postModel = new PostModel();
- BeanUtils.copyProperties(post, postModel, "hashtags", "mentions");
- postModel.setHashtags(new ArrayList<>(post.getHashtags().keySet()));
- postModel.setMentions(new ArrayList<>(post.getMentions().keySet()));
- postModel.setUserId(post.getUsers().getId());
- return postModel;
+ @Mapping(target = "userId", source = "users.id")
+ @Mapping(target = "hashtags", source = "hashtags", qualifiedByName = "mapToKeysList")
+ @Mapping(target = "mentions", source = "mentions", qualifiedByName = "mapToKeysList")
+ @Mapping(target = "images", source = "images", qualifiedByName = "mapToKeysList")
+ @Mapping(target = "timestamp", source = "createdAt")
+ PostModel toModel(Posts post);
+
+ @Mapping(target = "users", ignore = true)
+ @Mapping(target = "postHashtags", ignore = true)
+ @Mapping(target = "postLikes", ignore = true)
+ @Mapping(target = "createdAt", ignore = true)
+ @Mapping(target = "updatedAt", ignore = true)
+ @Mapping(target = "hashtags", source = "hashtags", qualifiedByName = "listToDateMap")
+ @Mapping(target = "mentions", source = "mentions", qualifiedByName = "listToDateMap")
+ @Mapping(target = "images", source = "images", qualifiedByName = "listToDateMap")
+ Posts toEntity(PostModel postModel);
+
+ @Named("mapToKeysList")
+ default List mapToKeysList(Map map) {
+ if (map == null) return new ArrayList<>();
+ return new ArrayList<>(map.keySet());
}
- @Override
- public Posts transformBack(PostModel postModel) {
- Posts post = new Posts();
- BeanUtils.copyProperties(postModel, post, "hashtags", "mentions", "likeCount", "repostCount");
- Map hashtags = new HashMap<>();
- Map mentions = new HashMap<>();
- postModel.getHashtags().forEach(tag -> hashtags.put(tag, new Date()));
- postModel.getMentions().forEach(mention -> mentions.put(mention, new Date()));
- post.setHashtags(hashtags);
- post.setMentions(mentions);
- post.setLikeCount(null != postModel.getLikeCount() ? postModel.getLikeCount() : 0L);
- post.setRepostCount(null != postModel.getRepostCount() ? postModel.getRepostCount() : 0L);
- return post;
+ @Named("listToDateMap")
+ default Map listToDateMap(List list) {
+ Map map = new HashMap<>();
+ if (list != null) {
+ list.forEach(item -> map.put(item, new Date()));
+ }
+ return map;
}
}
diff --git a/src/main/java/xyz/subho/clone/twitter/utility/UserMapper.java b/src/main/java/xyz/subho/clone/twitter/utility/UserMapper.java
index 25dc0f9..6fd9792 100644
--- a/src/main/java/xyz/subho/clone/twitter/utility/UserMapper.java
+++ b/src/main/java/xyz/subho/clone/twitter/utility/UserMapper.java
@@ -1,6 +1,6 @@
/*
* Twitter Backend - Moo: Twitter Clone Application Backend by Scaler
- * Copyright © 2021-2023 Subhrodip Mohanta (hello@subho.xyz)
+ * Copyright © 2021-2026 Subhrodip Mohanta (hello@subho.xyz)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -18,29 +18,22 @@
package xyz.subho.clone.twitter.utility;
-import org.springframework.beans.BeanUtils;
-import org.springframework.stereotype.Component;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
import xyz.subho.clone.twitter.entity.Users;
import xyz.subho.clone.twitter.model.UserModel;
-@Component("UserMapper")
-public class UserMapper implements Mapper {
+@Mapper(componentModel = "spring")
+public interface UserMapper {
- @Override
- public UserModel transform(Users user) {
- var userModel = new UserModel();
- BeanUtils.copyProperties(user, userModel);
- return userModel;
- }
+ @Mapping(target = "password", ignore = true)
+ UserModel toModel(Users user);
- @Override
- public Users transformBack(UserModel userModel) {
- var user = new Users();
- BeanUtils.copyProperties(userModel, user, "followerCount", "followingCount", "verified");
- user.setFollowerCount(userModel.getFollowerCount() != null ? userModel.getFollowerCount() : 0L);
- user.setFollowingCount(
- userModel.getFollowingCount() != null ? userModel.getFollowingCount() : 0L);
- user.setVerified(userModel.getVerified() != null ? userModel.getVerified() : false);
- return user;
- }
+ @Mapping(target = "userLikes", ignore = true)
+ @Mapping(target = "userPosts", ignore = true)
+ @Mapping(target = "followers", ignore = true)
+ @Mapping(target = "following", ignore = true)
+ @Mapping(target = "createdAt", ignore = true)
+ @Mapping(target = "updatedAt", ignore = true)
+ Users toEntity(UserModel userModel);
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 94204bc..5353a2e 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -32,3 +32,4 @@ jwt.secret=9a4f4342453527245a462d4a614e645267556b58703273357638792f423f4528
jwt.expiration=3600000
spring.mvc.apiversion.enabled=true
spring.mvc.apiversion.default-version=1
+jwt.refreshExpiration=604800000