diff --git a/pom.xml b/pom.xml
index 0a14c9b..30a51d3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,11 @@
2.6.0
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
org.springframework.boot
spring-boot-starter-test
diff --git a/src/main/java/com/hacktober/blog/config/FirebaseConfig.java b/src/main/java/com/hacktober/blog/config/FirebaseConfig.java
new file mode 100644
index 0000000..fb0aad6
--- /dev/null
+++ b/src/main/java/com/hacktober/blog/config/FirebaseConfig.java
@@ -0,0 +1,56 @@
+package com.hacktober.blog.config;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.cloud.firestore.Firestore;
+import com.google.cloud.firestore.FirestoreOptions;
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+
+@Configuration
+public class FirebaseConfig {
+ private static final Logger log = LoggerFactory.getLogger(FirebaseConfig.class);
+
+ @Value("${firebase.credentials.path:${FIREBASE_ADMIN_SA_PATH:}}")
+ private String credentialsPath;
+
+ @Value("${firebase.project-id:${FIREBASE_PROJECT_ID:}}")
+ private String projectId;
+
+ @Bean
+ public FirebaseApp firebaseApp() throws IOException {
+ if (!FirebaseApp.getApps().isEmpty()) return FirebaseApp.getInstance();
+
+ FirebaseOptions.Builder builder = FirebaseOptions.builder();
+
+ if (credentialsPath != null && !credentialsPath.isBlank()) {
+ try (FileInputStream in = new FileInputStream(credentialsPath)) {
+ builder.setCredentials(GoogleCredentials.fromStream(in));
+ log.info("Firebase: using service account from {}", credentialsPath);
+ }
+ } else {
+ builder.setCredentials(GoogleCredentials.getApplicationDefault());
+ log.info("Firebase: using application default credentials");
+ }
+
+ if (projectId != null && !projectId.isBlank()) builder.setProjectId(projectId);
+
+ FirebaseApp app = FirebaseApp.initializeApp(builder.build());
+ log.info("Firebase initialized (projectId={})", projectId);
+ return app;
+ }
+
+ @Bean
+ public Firestore firestore(FirebaseApp app) {
+ FirestoreOptions.Builder fo = FirestoreOptions.newBuilder();
+ if (projectId != null && !projectId.isBlank()) fo.setProjectId(projectId);
+ return fo.build().getService();
+ }
+}
diff --git a/src/main/java/com/hacktober/blog/config/FirestoreService.java b/src/main/java/com/hacktober/blog/config/FirestoreService.java
deleted file mode 100644
index 59a3774..0000000
--- a/src/main/java/com/hacktober/blog/config/FirestoreService.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.hacktober.blog.config;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
-import com.google.auth.oauth2.GoogleCredentials;
-import com.google.firebase.FirebaseApp;
-import com.google.firebase.FirebaseOptions;
-
-@SuppressWarnings("deprecation")
-@Configuration
-public class FirestoreService {
-
- @Bean
- public FirebaseApp initFirebaseApp() {
-
- try {
-
- FileInputStream serviceAccount = new FileInputStream("/etc/secrets/firebaseServiceAccountKey.json");
- FirebaseOptions options = new FirebaseOptions.Builder().setCredentials(GoogleCredentials.fromStream(serviceAccount))
- .build();
- return FirebaseApp.initializeApp(options);
-
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- return null;
-
- }
-
-}
diff --git a/src/main/java/com/hacktober/blog/config/RedisConfig.java b/src/main/java/com/hacktober/blog/config/RedisConfig.java
index 9dd5357..a9998cb 100644
--- a/src/main/java/com/hacktober/blog/config/RedisConfig.java
+++ b/src/main/java/com/hacktober/blog/config/RedisConfig.java
@@ -3,27 +3,40 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-
import redis.clients.jedis.DefaultJedisClientConfig;
-import redis.clients.jedis.JedisClientConfig;
-import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.UnifiedJedis;
@Configuration
public class RedisConfig {
-
- @Value("${redis.password}")
- private String password;
- @Bean
+ @Value("${redis.host:127.0.0.1}")
+ private String host;
+
+ @Value("${redis.port:6379}")
+ private int port;
+
+ @Value("${redis.password:}")
+ private String password;
+
+ @Value("${redis.ssl:false}")
+ private boolean ssl;
+
+ @Value("${redis.timeout-ms:2000}")
+ private int timeoutMs;
+
+ @Value("${redis.client-name:hacktoberblog-backend}")
+ private String clientName;
+
+ @Bean(destroyMethod = "close")
public UnifiedJedis unifiedJedis() {
- JedisClientConfig config = DefaultJedisClientConfig.builder()
- .user("default")
- .password(password)
+ DefaultJedisClientConfig cfg = DefaultJedisClientConfig.builder()
+ .password((password != null && !password.isBlank()) ? password : null)
+ .ssl(ssl)
+ .connectionTimeoutMillis(timeoutMs)
+ .socketTimeoutMillis(timeoutMs)
+ .clientName(clientName)
.build();
- return new UnifiedJedis(
- new HostAndPort("redis-14860.crce182.ap-south-1-1.ec2.redns.redis-cloud.com", 14860),
- config
- );
+ return new UnifiedJedis(new HostAndPort(host, port), cfg);
}
}
diff --git a/src/main/java/com/hacktober/blog/email/EmailController.java b/src/main/java/com/hacktober/blog/email/EmailController.java
index f8261bb..afafaaa 100644
--- a/src/main/java/com/hacktober/blog/email/EmailController.java
+++ b/src/main/java/com/hacktober/blog/email/EmailController.java
@@ -1,6 +1,7 @@
package com.hacktober.blog.email;
import com.hacktober.blog.utils.ApiResponse;
+import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -21,7 +22,7 @@ public EmailController(EmailService emailService) {
@PostMapping("/send")
@Operation(summary = "Send email", description = "Dispatch an email message using the configured SMTP provider.")
- public ResponseEntity> sendMail(@RequestBody EmailRequest request) {
+ public ResponseEntity> sendMail(@Valid @RequestBody EmailRequest request) {
emailService.sendEmail(request.getTo(), request.getSubject(), request.getBody());
String message = "Email sent successfully to " + request.getTo();
return ResponseEntity.ok(ApiResponse.success(message, "Email sent successfully"));
diff --git a/src/main/java/com/hacktober/blog/email/EmailRequest.java b/src/main/java/com/hacktober/blog/email/EmailRequest.java
index 5dae6ad..886212e 100644
--- a/src/main/java/com/hacktober/blog/email/EmailRequest.java
+++ b/src/main/java/com/hacktober/blog/email/EmailRequest.java
@@ -1,17 +1,22 @@
package com.hacktober.blog.email;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+@AllArgsConstructor
public class EmailRequest {
+
+ @NotBlank
+ @Email
String to;
+ @NotBlank
String subject;
+ @NotBlank
String body;
-
- public EmailRequest(String to, String subject, String body) {
- super();
- this.to = to;
- this.subject = subject;
- this.body = body;
- }
-
+
public String getTo() {
return to;
}
diff --git a/src/main/java/com/hacktober/blog/email/EmailService.java b/src/main/java/com/hacktober/blog/email/EmailService.java
index ea2689b..9a94b6a 100644
--- a/src/main/java/com/hacktober/blog/email/EmailService.java
+++ b/src/main/java/com/hacktober/blog/email/EmailService.java
@@ -23,4 +23,10 @@ public void sendEmail(String to, String subject, String text) {
message.setText(text);
mailSender.send(message);
}
+ public void sendPasswordResetOtp(String to, String otp) {
+ String subject = "Your HacktoberBlog password reset code";
+ String body = "Use this code to reset your password: " + otp + "\nIt expires in 10 minutes.";
+ sendEmail(to, subject, body);
+ }
+
}
diff --git a/src/main/java/com/hacktober/blog/user/UserService.java b/src/main/java/com/hacktober/blog/user/UserService.java
index 3b3443c..14170a5 100644
--- a/src/main/java/com/hacktober/blog/user/UserService.java
+++ b/src/main/java/com/hacktober/blog/user/UserService.java
@@ -1,9 +1,11 @@
package com.hacktober.blog.user;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
+import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.google.api.core.ApiFuture;
@@ -19,15 +21,18 @@ public class UserService {
private static final String USERNAMES_DOC = "usernames";
private final EmailService emailService;
+ private final PasswordEncoder passwordEncoder;
+
// Inject EmailService through constructor
- public UserService(EmailService emailService) {
+ public UserService(EmailService emailService, PasswordEncoder passwordEncoder) {
this.emailService = emailService;
+ this.passwordEncoder = passwordEncoder;
}
/** Create User + Send Email */
public String create(User user) throws InterruptedException, ExecutionException {
Firestore db = FirestoreClient.getFirestore();
- user.setPassword(Utils.encode(user.getPassword())); // Encrypt password
+ user.setPassword(passwordEncoder.encode(user.getPassword()));
// Save user to Firestore
ApiFuture result = db.collection(COLLECTION_NAME).document(user.getUsername()).set(user);
@@ -68,7 +73,7 @@ public List getAll() throws InterruptedException, ExecutionException {
public String update(User user) throws InterruptedException, ExecutionException {
Firestore db = FirestoreClient.getFirestore();
if (user.getPassword() != null) {
- user.setPassword(Utils.encode(user.getPassword()));
+ user.setPassword(passwordEncoder.encode(user.getPassword()));
}
ApiFuture result = db.collection(COLLECTION_NAME).document(user.getUsername()).set(user);
return result.get().getUpdateTime().toString();
@@ -82,45 +87,66 @@ public String delete(String username) throws InterruptedException, ExecutionExce
removeUsername(username);
return result.get().getUpdateTime().toString();
}
-
+
public List getAllUsernames() throws InterruptedException, ExecutionException {
- Firestore db = FirestoreClient.getFirestore();
- DocumentReference docRef = db.collection(COLLECTION_NAME).document(USERNAMES_DOC);
- DocumentSnapshot snapshot = docRef.get().get();
-
- if (snapshot.exists() && snapshot.contains("usernames")) {
- System.out.println(snapshot.get("usernames"));
- return (List) snapshot.get("usernames");
- }
- return new ArrayList<>();
- }
-
- /** Add or remove username from usernames array */
- public String updateUsernames(String username, boolean add) throws InterruptedException, ExecutionException {
- Firestore db = FirestoreClient.getFirestore();
- DocumentReference docRef = db.collection("usernames").document("usernames");
-
- List currentUsernames = getAllUsernames();
- if (add) {
- if (!currentUsernames.contains(username)) {
- currentUsernames.add(username);
- }
- } else {
- currentUsernames.remove(username);
- }
-
- ApiFuture result = docRef.update("usernames", currentUsernames);
- return result.get().getUpdateTime().toString();
- }
-
- /** Helper: add a username */
- private void addUsername(String username) throws InterruptedException, ExecutionException {
- updateUsernames(username, true);
- }
-
- /** Helper: remove a username */
- public void removeUsername(String username) throws InterruptedException, ExecutionException {
- updateUsernames(username, false);
- }
+ Firestore db = FirestoreClient.getFirestore();
+ DocumentReference docRef = db.collection(COLLECTION_NAME).document(USERNAMES_DOC); // users/usernames
+ DocumentSnapshot snapshot = docRef.get().get();
+
+ if (snapshot.exists() && snapshot.contains("usernames")) {
+ return (List) snapshot.get("usernames");
+ }
+ return new ArrayList<>();
+ }
+
+
+ /** Add or remove username using atomic array ops; creates doc if missing */
+ public String updateUsernames(String username, boolean add) throws InterruptedException, ExecutionException {
+ Firestore db = FirestoreClient.getFirestore();
+ DocumentReference docRef = db.collection(COLLECTION_NAME).document(USERNAMES_DOC); // users/usernames
+
+ // Ensure the doc exists without clobbering existing fields
+ docRef.set(Collections.singletonMap("usernames", Collections.emptyList()), SetOptions.merge()).get();
+
+ ApiFuture write = add
+ ? docRef.update("usernames", FieldValue.arrayUnion(username))
+ : docRef.update("usernames", FieldValue.arrayRemove(username));
+
+ return write.get().getUpdateTime().toString();
+ }
+
+ /** Helper: add a username */
+ private void addUsername(String username) throws InterruptedException, ExecutionException {
+ updateUsernames(username, true);
+ }
+
+ /** Helper: remove a username */
+ public void removeUsername(String username) throws InterruptedException, ExecutionException {
+ updateUsernames(username, false);
+ }
+
+ /** Reset password by email (returns true if a user was updated) */
+ public boolean resetPasswordByEmail(String email, String rawNewPassword) {
+ try {
+ Firestore db = FirestoreClient.getFirestore();
+ // Find the user doc by email (username is the doc id, but we don't know it here)
+ ApiFuture future = db.collection(COLLECTION_NAME)
+ .whereEqualTo("email", email)
+ .limit(1)
+ .get();
+
+ List docs = future.get().getDocuments();
+ if (docs.isEmpty()) {
+ return false;
+ }
+
+ DocumentReference docRef = docs.get(0).getReference();
+ String hash = passwordEncoder.encode(rawNewPassword);
+ docRef.update("password", hash).get();
+ return true;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to reset password", e);
+ }
+ }
}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index a8714d5..f5cad74 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,10 +1,24 @@
+# --- Firebase (local dev) ---
+firebase.credentials.path=/ABSOLUTE/PATH/TO/firebaseServiceAccountKey.json
+firebase.project-id=YOUR_FIREBASE_PROJECT_ID
+
+# --- App ---
spring.application.name=blog
+
+# --- Mail (use env var for password) ---
spring.mail.host=smtp.gmail.com
spring.mail.port=587
-spring.mail.username=${GMAIL_ACCOUNT}
-spring.mail.password=${GMAIL_APP_KEY}
+spring.mail.username=your_email@gmail.com
+spring.mail.password=${MAIL_PASSWORD:}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
-redis.password = ${REDIS_PASSWORD}
-notifications.enabled=${NOTIFICATIONS_ENABLED:true}
\ No newline at end of file
+
+# --- Redis (local dev) ---
+redis.host=127.0.0.1
+redis.port=6379
+redis.password=${REDIS_PASSWORD:}
+redis.ssl=false
+
+# --- Feature flags ---
+notifications.enabled=${NOTIFICATIONS_ENABLED:true}