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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@
<version>2.6.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
56 changes: 56 additions & 0 deletions src/main/java/com/hacktober/blog/config/FirebaseConfig.java
Original file line number Diff line number Diff line change
@@ -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:}}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please explain why the FirestoreService.java was delete?

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();
}
}
35 changes: 0 additions & 35 deletions src/main/java/com/hacktober/blog/config/FirestoreService.java

This file was deleted.

41 changes: 27 additions & 14 deletions src/main/java/com/hacktober/blog/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/hacktober/blog/email/EmailController.java
Original file line number Diff line number Diff line change
@@ -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.*;

Expand All @@ -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<ApiResponse<String>> sendMail(@RequestBody EmailRequest request) {
public ResponseEntity<ApiResponse<String>> 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"));
Expand Down
21 changes: 13 additions & 8 deletions src/main/java/com/hacktober/blog/email/EmailRequest.java
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/hacktober/blog/email/EmailService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
110 changes: 68 additions & 42 deletions src/main/java/com/hacktober/blog/user/UserService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<WriteResult> result = db.collection(COLLECTION_NAME).document(user.getUsername()).set(user);
Expand Down Expand Up @@ -68,7 +73,7 @@ public List<User> 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<WriteResult> result = db.collection(COLLECTION_NAME).document(user.getUsername()).set(user);
return result.get().getUpdateTime().toString();
Expand All @@ -82,45 +87,66 @@ public String delete(String username) throws InterruptedException, ExecutionExce
removeUsername(username);
return result.get().getUpdateTime().toString();
}


public List<String> 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<String>) 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<String> currentUsernames = getAllUsernames();
if (add) {
if (!currentUsernames.contains(username)) {
currentUsernames.add(username);
}
} else {
currentUsernames.remove(username);
}

ApiFuture<WriteResult> 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<String>) 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<WriteResult> 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<QuerySnapshot> future = db.collection(COLLECTION_NAME)
.whereEqualTo("email", email)
.limit(1)
.get();

List<QueryDocumentSnapshot> 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);
}
}
}
22 changes: 18 additions & 4 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -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}

# --- 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}