61 Commits

Author SHA1 Message Date
heyethereum
61e8282e00 includes apk file executable 2024-08-22 21:00:56 +08:00
heyethereum
22bf62473f added source for frontend 2024-08-19 22:52:30 +08:00
heyethereum
04e1ca12c8 sort in scanned gmail in descending order 2024-08-19 21:18:37 +08:00
ltiongku
2876be0d52 added sp to whitelist 2024-08-19 16:52:32 +08:00
heyethereum
deab9666da refine refresh token flow 2024-08-19 08:30:15 +08:00
heyethereum
878bfce095 refine refresh token flow 2024-08-18 22:35:42 +08:00
heyethereum
9f4771f16d intro historyId in gmail 2024-08-17 22:38:00 +08:00
heyethereum
b8ee29ce9a solved logic error 2024-08-17 15:20:03 +08:00
heyethereum
0716214a31 whitelist some url classified wrongly 2024-08-17 13:13:24 +08:00
heyethereum
e8836f1b5e implemented phone checks and sms checks 2024-08-17 12:49:19 +08:00
heyethereum
d24ece60fd implemented keyword search in sms 2024-08-17 10:43:59 +08:00
heyethereum
0301b0e1fb corrupted readme 2024-08-17 08:38:51 +08:00
heyethereum
180f53dcaf Added readme file 2024-08-17 08:36:23 +08:00
heyethereum
3e4f18849e Added readme file 2024-08-17 08:34:33 +08:00
heyethereum
69c8bbb450 Merge remote-tracking branch 'origin/feature-ml-integration' into dev 2024-08-17 08:32:38 +08:00
heyethereum
213a6dfc70 Added readme file 2024-08-17 08:32:11 +08:00
heyethereum
474d785c5c Merge branch 'feature-ml-integration' into dev 2024-08-14 23:30:08 +08:00
heyethereum
624bfdc2f9 added phone, email 2024-08-14 23:28:45 +08:00
heyethereum
3dda5fa770 Merge remote-tracking branch 'origin/feature-ml-integration' into dev 2024-08-14 21:16:45 +08:00
heyethereum
b176e5c54f fix gmail scan null results 2024-08-14 21:15:29 +08:00
ltiongku
9adb53e7ab write sms type to db 2024-08-14 19:44:18 +08:00
ltiongku
b86e680673 Merge remote-tracking branch 'refs/remotes/origin/feature-ml-integration' into dev 2024-08-14 11:59:17 +08:00
ltiongku
334b3867ec fixed email active NULL 2024-08-14 11:57:09 +08:00
ltiongku
e6899eafdb Merge remote-tracking branch 'refs/remotes/origin/feature-ml-integration' into dev 2024-08-14 11:36:31 +08:00
ltiongku
f5d6396b06 fixed email active NULL 2024-08-14 11:36:10 +08:00
ltiongku
393737e0f7 Merge remote-tracking branch 'refs/remotes/origin/feature-ml-integration' into dev 2024-08-14 11:12:23 +08:00
ltiongku
25711506b4 fixed email active NULL 2024-08-14 11:11:50 +08:00
heyethereum
0592f6bfd1 Merge remote-tracking branch 'origin/feature-ml-integration' into dev 2024-08-14 08:47:45 +08:00
heyethereum
28a16ead4f test 2024-08-14 08:47:02 +08:00
heyethereum
bbeb85bb0c Merge remote-tracking branch 'origin/feature-ml-integration' into dev 2024-08-14 00:56:44 +08:00
heyethereum
5f55b073f3 fixed incorrect mapping 2024-08-14 00:56:12 +08:00
heyethereum
08a9b3b630 Merge remote-tracking branch 'origin/feature-ml-integration' into dev 2024-08-13 21:03:45 +08:00
heyethereum
8665693642 complete url verifications 2024-08-13 21:03:15 +08:00
ltiongku
63e2d299fd implemented QR code tips controller, delete email message, delete all email messages 2024-08-13 19:19:56 +08:00
heyethereum
32b604b172 Merge remote-tracking branch 'origin/feature-ml-integration' into dev 2024-08-13 02:38:11 +08:00
heyethereum
c8cfe610a6 add new prediction service 2024-08-13 02:34:47 +08:00
ltiongku
53f9acd922 ml init 2024-08-12 23:55:08 +08:00
heyethereum
e2ca15f556 Merge remote-tracking branch 'origin/feature-gmail-scan' into dev 2024-08-12 23:04:00 +08:00
heyethereum
ef1fb9f6e0 define custom setter for URLFeatures 2024-08-12 08:47:19 +08:00
heyethereum
e51f99bc92 define spark folder 2024-08-12 08:43:11 +08:00
heyethereum
f2e92bd1ca initial built model 2024-08-12 08:28:23 +08:00
heyethereum
cd047b33af Merge remote-tracking branch 'origin/feature-gmail-scan' into dev 2024-08-12 00:52:06 +08:00
heyethereum
a71cbd3093 enhance logging and fix Null pointer exception at wifi Null 2024-08-12 00:51:35 +08:00
heyethereum
d2c2767578 Merge remote-tracking branch 'origin/feature-gmail-scan' into dev 2024-08-11 09:22:23 +08:00
heyethereum
3b2533bb62 install apache spark dependency for testing 2024-08-11 09:19:41 +08:00
heyethereum
0901e7f07f Merge remote-tracking branch 'origin/feature-gmail-scan' into dev 2024-08-10 16:25:36 +08:00
heyethereum
080e695e5d fixed domain null error when query is invalid and implemented new function to check for executable for url 2024-08-10 16:11:13 +08:00
heyethereum
4856417cf0 fixed minor error when query and path is null 2024-08-07 23:20:32 +08:00
ltiongku
3fb5ad039a Merge remote-tracking branch 'origin/dev' into dev 2024-08-07 19:02:13 +08:00
ltiongku
b55c615ed5 added wifi classification and initial class 2024-08-07 18:58:38 +08:00
heyethereum
31476e4d75 Merge remote-tracking branch 'origin/feature-gmail-scan' into dev 2024-08-06 20:23:33 +08:00
ltiongku
529d27d07c hide empty values in dto and edited check endcoded url for false positive 2024-08-06 20:21:47 +08:00
heyethereum
3eb53c6ccd added url encoding check and fix javascript check for false positive 2024-08-06 08:00:03 +08:00
heyethereum
02085b50b9 added tracking function, check has ip in url, url shortener check 2024-08-06 00:41:17 +08:00
ltiongku
2746891645 added javascript check in url and domain embedding checks 2024-08-05 20:45:03 +08:00
heyethereum
1d1ffcf5dc added error checking for connection error 2024-08-05 08:16:53 +08:00
heyethereum
9d5b39e89c Merge remote-tracking branch 'origin/feature-gmail-scan' into dev 2024-08-04 19:37:47 +08:00
heyethereum
76036a2d91 Added new end point, edited getEmails to be Async all and return 202 status code 2024-08-04 19:37:16 +08:00
heyethereum
dcd5058f70 gmail scan to push to main 2024-08-03 13:35:57 +08:00
heyethereum
0fde70a4b6 added debug in downloadImageFromUrl and date of email to scanned Gmail results 2024-07-28 10:59:45 +08:00
heyethereum
3567457026 Initial completion of scanning of qr code from gmail 2024-07-28 03:07:23 +08:00
58 changed files with 2173 additions and 257 deletions

62
README.md Normal file
View File

@@ -0,0 +1,62 @@
# SafeQR Spring Boot Project
This is a Spring Boot project built with Java 17. This guide will help you set up the project and install the necessary dependencies using Maven.
## Prerequisites
Before you begin, ensure you have the following software installed on your system:
- **Java 17**: This project requires Java 17. You can download it from the [official Oracle website](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) or install it via a package manager (e.g., `sdkman` or `brew` on macOS).
- **Maven 3.6+**: Apache Maven is used to manage the project's dependencies. You can download Maven from the [official Apache website](https://maven.apache.org/download.cgi) or install it via a package manager.
## Installation
Follow these steps to set up and run the project locally:
### 1. Clone the Repository
Clone the project repository to your local machine using the following command:
```bash
git clone https://github.com/safeqr/backend-springboot.git
cd backend-springboot
```
### 2. Verify Java and Maven Installation
Ensure that Java 17 and Maven are installed and available on your system by running the following commands:
```bash
java -version
mvn -version
```
You should see output indicating that Java 17 and Maven 3.6+ are installed.
### 3. Install Project Dependencies
Navigate to the root directory of the project (if you haven't already) and run the following command to clean the project and install all necessary dependencies:
```bash
mvn clean install
```
This command will:
- **Clean**: Remove any previously compiled files.
- **Install**: Download all required dependencies as defined in the `pom.xml` file and compile the project.
### 4. Run the Application
Once the dependencies are installed, you can run the application with the following command:
```bash
mvn spring-boot:run
```
This will start the Spring Boot application, and you should see the application logs in the terminal. By default, the application will be available at `http://localhost:8080`.
## Additional Information
- **Configuration**: Any necessary configurations can be adjusted in the `application.properties` or `application.yml` files located in the `src/main/resources` directory.
- **Building the Project**: To build a standalone JAR file, you can use `mvn package`, which will generate a JAR file in the `target` directory.

12
pom.xml
View File

@@ -109,6 +109,18 @@
<artifactId>jsoup</artifactId>
<version>1.18.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>

View File

@@ -13,6 +13,7 @@ public class APIConstants {
public static final String API_URL_QRCODE_VIRUS_TOTAL_CHECK = "/qrcodetypes/virusTotalCheck";
public static final String API_URL_QRCODE_REDIRECT_COUNT = "/qrcodetypes/checkRedirects";
public static final String API_URL_QRCODE_GET_QR_DETAILS = "/qrcodetypes/getQRDetails";
public static final String PREDICTION_API_URL = "http://localhost:8000/predict";
public static final String API_URL_USER_GET = "/user/getUser";
@@ -24,4 +25,11 @@ public class APIConstants {
public static final String API_URL_USER_DELETE_BOOKMARK = "/user/deleteBookmark";
public static final String API_URL_USER_DELETE_ALL_BOOKMARK = "/user/deleteAllBookmark";
public static final String API_URL_GMAIL_GET_EMAILS = "/gmail/getEmails";
public static final String API_URL_GMAIL_GET_SCANNED_EMAILS = "/gmail/getScannedEmails";
public static final String API_URL_GMAIL_DELETE_MESSAGE = "/gmail/deleteMessage";
public static final String API_URL_GMAIL_DELETE_ALL_MESSAGES = "/gmail/deleteAllMessages";
public static final String API_URL_TIPS_GET = "/tips/getTips";
}

View File

@@ -17,4 +17,16 @@ public class CommonConstants {
public static final String INFO_NO_HSTS_HEADER = "No HSTS Header detected";
public static final String INFO_HSTS_HEADER_PREFIX = "HSTS Header: ";
public static final String INFO_HSTS_NOT_APPLICABLE = "N/A";
public static final String CLASSIFY_SAFE = "SAFE";
public static final String CLASSIFY_WARNING = "WARNING";
public static final String CLASSIFY_UNSAFE = "UNSAFE";
public static final String CLASSIFY_UNKNOWN = "UNKNOWN";
public static final String CAT_BENIGN = "Benign";
public static final String CAT_DEFACEMENT = "Defacement";
public static final String CAT_MALWARE = "Malware";
public static final String CAT_PHISHING = "Phishing";
public static final Integer GMAIL_ACTIVE = 1;
}

View File

@@ -16,4 +16,8 @@ public class GlobalExceptionHandler {
public ResponseEntity<ErrorResponse> handleResourceAlreadyExistsException(ResourceAlreadyExists e) {
return new ResponseEntity<>(new ErrorResponse(e.getMessage(), HttpStatus.BAD_REQUEST.value()), HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(InvalidFormatExceptions.class)
public ResponseEntity<ErrorResponse> handleInvalidFormatException(InvalidFormatExceptions e) {
return new ResponseEntity<>(new ErrorResponse(e.getMessage(), HttpStatus.BAD_REQUEST.value()), HttpStatus.BAD_REQUEST);
}
}

View File

@@ -0,0 +1,7 @@
package com.safeqr.app.exceptions;
public class InvalidFormatExceptions extends RuntimeException {
public InvalidFormatExceptions(String message){
super(message);
}
}

View File

@@ -1,7 +1,8 @@
package com.safeqr.app.gmail.controller;
import com.google.api.services.gmail.model.*;
import org.apache.commons.codec.binary.Base64;
import com.safeqr.app.gmail.dto.MessageRequestDto;
import com.safeqr.app.gmail.dto.ScannedGmailResponseDto;
import com.safeqr.app.gmail.dto.BaseResponse;
import org.json.JSONObject;
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
import com.google.api.client.auth.oauth2.Credential;
@@ -25,10 +26,12 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
import static com.safeqr.app.constants.APIConstants.*;
import java.io.IOException;
import java.util.*;
import static com.safeqr.app.constants.CommonConstants.HEADER_USER_ID;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping(API_VERSION)
@@ -99,14 +102,44 @@ public class GmailController {
return new ResponseEntity<>(json.toString(), HttpStatus.OK);
}
@GetMapping(value = "/gmail/getEmails", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getUserEmails(@RequestHeader(name = "accessToken") String accessToken) throws IOException, InterruptedException {
logger.info("Invoking GET Scan User Emails endpoints");
@GetMapping(value = API_URL_GMAIL_GET_SCANNED_EMAILS, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ScannedGmailResponseDto> getUserScannedEmails(@RequestHeader(name = "X-USER-ID") String userId) {
logger.info("User Id Invoking GET User scanned Emails endpoint: {}", userId);
return ResponseEntity.ok(gmailService.fetchScannedGmail(userId));
}
@GetMapping(value = API_URL_GMAIL_GET_EMAILS, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getUserEmails(@RequestHeader(name = "accessToken") String accessToken,
@RequestHeader(name = "refreshToken") String refreshToken,
@RequestHeader(name = "X-USER-ID") String userId
) {
logger.info("User Id Invoking GET Scan User Emails endpoints: {}", userId);
if (accessToken == null || accessToken.isEmpty()) {
return new ResponseEntity<>("Access token is missing", HttpStatus.BAD_REQUEST);
}
logger.info("accessToken -> {}", accessToken);
logger.info("refreshToken -> {}", refreshToken);
logger.info("userId -> {}", userId);
return new ResponseEntity<>(gmailService.getEmail(accessToken).toString(), HttpStatus.OK);
CompletableFuture.runAsync(() -> {
gmailService.getEmailAsync(userId, accessToken, refreshToken);
}).exceptionally(throwable -> {
logger.error("Unexpected error occurred while processing emails", throwable);
return null;
});
return new ResponseEntity<>("Scan Gmail Request is being processed", HttpStatus.ACCEPTED);
}
@PutMapping(value = API_URL_GMAIL_DELETE_MESSAGE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseResponse> deleteMessage(@RequestHeader(name = HEADER_USER_ID) String userId, @RequestBody MessageRequestDto messageRequestDto) {
logger.info("User Id Invoking PUT Delete Single Email endpoint: {}", userId);
return ResponseEntity.ok(gmailService.deleteMessage(userId, messageRequestDto.getMessageId()));
}
@PutMapping(value = API_URL_GMAIL_DELETE_ALL_MESSAGES, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseResponse> deleteAllMessages(@RequestHeader(name = HEADER_USER_ID) String userId) {
logger.info("User Id Invoking PUT Delete All Emails endpoint: {}", userId);
return ResponseEntity.ok(gmailService.deleteAllMessages(userId));
}
}

View File

@@ -0,0 +1,12 @@
package com.safeqr.app.gmail.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@AllArgsConstructor
public class BaseResponse {
private String message;
}

View File

@@ -0,0 +1,10 @@
package com.safeqr.app.gmail.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class MessageRequestDto {
private String messageId;
}

View File

@@ -0,0 +1,16 @@
package com.safeqr.app.gmail.dto;
import com.safeqr.app.gmail.model.EmailMessage;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.Comparator;
import java.util.List;
@Builder
@Data
@AllArgsConstructor
public class ScannedGmailResponseDto {
List<EmailMessage> messages;
}

View File

@@ -0,0 +1,40 @@
package com.safeqr.app.gmail.entity;
import com.safeqr.app.qrcode.entity.QRCodeEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator;
import java.util.UUID;
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "gmail_cid", schema = "safeqr")
public class GmailCidEntity {
@Id
@GeneratedValue(generator = "UUID")
@UuidGenerator
@Column(updatable = false, nullable = false)
private UUID id;
@Column(name = "gmail_id")
private UUID gmailId;
@Column(name = "cid")
private String cid;
@Column(name = "attachment_id")
private String attachmentId;
@Column(name = "decoded_content")
private String decodedContent;
@Column(name = "qr_code_id")
private UUID qrCodeId;
}

View File

@@ -0,0 +1,56 @@
package com.safeqr.app.gmail.entity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator;
import java.math.BigInteger;
import java.time.OffsetDateTime;
import java.util.UUID;
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "gmail_emails", schema = "safeqr")
public class GmailEmailEntity {
@Id
@GeneratedValue(generator = "UUID")
@UuidGenerator
@Column(updatable = false, nullable = false)
private UUID id;
@Column(name = "user_id")
private String userId;
@Column(name = "message_id")
private String messageId;
@Column(name = "thread_id")
private String threadId;
@Column(name = "history_id")
private BigInteger historyId;
@Column(name= "subject")
private String subject;
@Column(name = "date_received")
private OffsetDateTime dateReceived;
@Column(name = "date_created")
private OffsetDateTime dateCreated;
@Column(name = "active")
private int active = 1;
@PrePersist
public void prePersist() {
dateCreated = OffsetDateTime.now();
}
}

View File

@@ -0,0 +1,38 @@
package com.safeqr.app.gmail.entity;
import com.safeqr.app.qrcode.entity.QRCodeEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.UuidGenerator;
import java.util.UUID;
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "gmail_urls", schema = "safeqr")
public class GmailUrlEntity {
@Id
@GeneratedValue(generator = "UUID")
@UuidGenerator
@Column(updatable = false, nullable = false)
private UUID id;
@Column(name = "gmail_id")
private UUID gmailId;
@Column(name = "image_url")
private String imageUrl;
@Column(name = "decoded_content")
private String decodedContent;
@Column(name = "qr_code_id")
private UUID qrCodeId;
}

View File

@@ -0,0 +1,53 @@
package com.safeqr.app.gmail.model;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.safeqr.app.qrcode.model.QRCodeModel;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class EmailMessage {
private String messageId;
private String threadId;
private String subject;
private String historyId;
private String date;
private int active;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
List<QRCodeByContentId> qrCodeByContentId;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
List<QRCodeByURL> qrCodeByURL;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
List<QRCodeModel<?>> decodedContentsDetails;
public EmailMessage(String messageId, String threadId ,String subject, String historyId, String date) {
this.messageId = messageId;
this.threadId = threadId;
this.subject = subject;
this.historyId = historyId;
this.date = date;
this.active = 1;
this.qrCodeByContentId = new ArrayList<>();
this.qrCodeByURL = new ArrayList<>();
this.decodedContentsDetails = new ArrayList<>();
}
public void addQRCodeByContentId(QRCodeByContentId qrCode) {
this.qrCodeByContentId.add(qrCode);
}
public void addQRCodeByURL(QRCodeByURL qrCode) {
this.qrCodeByURL.add(qrCode);
}
public void addQRCodeModel(QRCodeModel<?> qrCode) {
this.decodedContentsDetails.add(qrCode);
}
public boolean hasQRCodes() {
return !qrCodeByContentId.isEmpty() || !qrCodeByURL.isEmpty();
}
}

View File

@@ -0,0 +1,22 @@
package com.safeqr.app.gmail.model;
import com.safeqr.app.qrcode.model.QRCodeModel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
public class QRCodeByContentId {
private String cid;
private String attachmentId;
private List<String> decodedContent;
private int totalQRCodeFound;
public List<String> getDecodedContent() {
return decodedContent;
}
}

View File

@@ -0,0 +1,17 @@
package com.safeqr.app.gmail.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
public class QRCodeByURL {
private String url;
private List<String> decodedContent;
private int totalQRCodeFound;
}

View File

@@ -0,0 +1,11 @@
package com.safeqr.app.gmail.repository;
import com.safeqr.app.gmail.entity.GmailCidEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface GmailCidRespository extends JpaRepository<GmailCidEntity, UUID> {
List<GmailCidEntity> findByGmailId(UUID gmailId);
}

View File

@@ -0,0 +1,33 @@
package com.safeqr.app.gmail.repository;
import com.safeqr.app.gmail.entity.GmailEmailEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
public interface GmailEmailRespository extends JpaRepository<GmailEmailEntity, UUID> {
// Method to find by userId and active status
List<GmailEmailEntity> findByUserIdAndActive(String userId, Integer active);
// Method to update active status to 0 for a specific userId
@Modifying
@Transactional
@Query("UPDATE GmailEmailEntity e SET e.active = (SELECT MIN(e2.active) FROM GmailEmailEntity e2 WHERE e2.userId = :userId) - 1 " +
"WHERE e.userId = :userId " +
"AND e.active = 1 ")
int deactivateEmailsByUserId(String userId);
// Method to update active status to 0 for a specific userId and messageId
@Modifying
@Transactional
@Query("UPDATE GmailEmailEntity e SET e.active = (SELECT MIN(e2.active) FROM GmailEmailEntity e2 WHERE e2.userId = :userId AND e2.messageId = :messageId) - 1 " +
"WHERE e.userId = :userId AND e.messageId = :messageId " +
"AND e.active = 1")
int deactivateEmailByUserIdAndMessageId(String userId, String messageId);
}

View File

@@ -0,0 +1,11 @@
package com.safeqr.app.gmail.repository;
import com.safeqr.app.gmail.entity.GmailUrlEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.UUID;
public interface GmailUrlsRespository extends JpaRepository<GmailUrlEntity, UUID> {
List<GmailUrlEntity> findByGmailId(UUID gmailId);
}

View File

@@ -1,119 +1,502 @@
package com.safeqr.app.gmail.service;
import com.google.api.client.auth.oauth2.BearerToken;
import com.google.api.client.auth.oauth2.*;
import com.google.api.client.googleapis.auth.oauth2.GoogleRefreshTokenRequest;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.gmail.Gmail;
import com.google.api.services.gmail.model.*;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.multi.qrcode.QRCodeMultiReader;
import com.safeqr.app.exceptions.ResourceNotFoundExceptions;
import com.safeqr.app.gmail.dto.BaseResponse;
import com.safeqr.app.gmail.dto.ScannedGmailResponseDto;
import com.safeqr.app.gmail.entity.GmailCidEntity;
import com.safeqr.app.gmail.entity.GmailEmailEntity;
import com.safeqr.app.gmail.entity.GmailUrlEntity;
import com.safeqr.app.gmail.model.EmailMessage;
import com.safeqr.app.gmail.model.QRCodeByContentId;
import com.safeqr.app.gmail.model.QRCodeByURL;
import com.safeqr.app.gmail.repository.GmailCidRespository;
import com.safeqr.app.gmail.repository.GmailEmailRespository;
import com.safeqr.app.gmail.repository.GmailUrlsRespository;
import com.safeqr.app.qrcode.model.QRCodeModel;
import com.safeqr.app.qrcode.service.QRCodeTypeService;
import com.safeqr.app.user.entity.UserEntity;
import com.safeqr.app.user.service.UserService;
import com.safeqr.app.utils.DateParsingUtils;
import io.hypersistence.utils.common.StringUtils;
import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.gmail.Gmail;
import org.springframework.transaction.annotation.Transactional;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.Thread;
import java.math.BigInteger;
import java.net.ConnectException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.http.auth.AuthenticationException;
import static com.google.api.client.googleapis.auth.oauth2.GoogleOAuthConstants.TOKEN_SERVER_URL;
import static com.safeqr.app.constants.APIConstants.APPLICATION_NAME;
import static com.safeqr.app.constants.CommonConstants.GMAIL_ACTIVE;
@Service
public class GmailService {
@Value("${gmail.client.clientId}")
private String clientId;
@Value("${gmail.client.clientSecret}")
private String clientSecret;
private final GmailEmailRespository gmailEmailRespository;
private final GmailCidRespository gmailCidRespository;
private final GmailUrlsRespository gmailUrlsRespository;
private final QRCodeTypeService qrCodeTypeService;
private final UserService userService;
private static final Logger logger = LoggerFactory.getLogger(GmailService.class);
private static final HttpTransport httpTransport = new NetHttpTransport();
private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance();
private static final long MAX_RESULTS = 100L;
public GmailService(GmailEmailRespository gmailEmailRespository,
GmailCidRespository gmailCidRespository,
GmailUrlsRespository gmailUrlsRespository,
QRCodeTypeService qrCodeTypeService,
UserService userService) {
this.gmailEmailRespository = gmailEmailRespository;
this.gmailCidRespository = gmailCidRespository;
this.gmailUrlsRespository = gmailUrlsRespository;
this.qrCodeTypeService = qrCodeTypeService;
this.userService = userService;
}
private Gmail getGmailService(String accessToken, String refreshToken) throws IOException {
Credential userCredentials = new Credential.Builder(BearerToken.authorizationHeaderAccessMethod())
.setTransport(httpTransport)
.setJsonFactory(JSON_FACTORY)
.setTokenServerUrl(new GenericUrl(TOKEN_SERVER_URL))
.setClientAuthentication(new ClientParametersAuthentication(clientId, clientSecret))
.build()
.setAccessToken(accessToken)
.setRefreshToken(refreshToken);
private Gmail getGmailService(String accessToken) {
Credential userCredentials = new Credential(BearerToken.authorizationHeaderAccessMethod()).setAccessToken(accessToken);
return new Gmail.Builder(httpTransport, JSON_FACTORY, userCredentials)
.setApplicationName(APPLICATION_NAME)
.build();
}
// Renew the access token if it has expired using the refresh token.
private String refreshAccessToken(String refreshToken) throws IOException, AuthenticationException {
logger.info("Refresh token in refreshAccessToken: {}", refreshToken);
if (StringUtils.isBlank(refreshToken)) {
logger.error("Refresh token is null or empty");
throw new AuthenticationException("Invalid refresh token");
}
public JSONObject getEmail(String accessToken) throws IOException, InterruptedException {
JSONObject json = new JSONObject();
JSONArray emailArray = new JSONArray();
int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
logger.info("Attempting to refresh access token (attempt {})", retryCount + 1);
TokenResponse response = new GoogleRefreshTokenRequest(
httpTransport, JSON_FACTORY, refreshToken, clientId, clientSecret)
.execute();
String newAccessToken = response.getAccessToken();
logger.info("Access token refreshed successfully");
return newAccessToken;
} catch (TokenResponseException e) {
logger.error("Failed to refresh access token. Status code: {}", e.getStatusCode());
logger.error("Error message: {}", e.getDetails().getError());
logger.error("Error description: {}", e.getDetails().getErrorDescription());
// Build the Gmail service
Gmail service = getGmailService(accessToken);
logger.info("service-> {}", service);
if (e.getStatusCode() == 401) {
logger.warn("Unauthorized error. The refresh token may be invalid or revoked.");
throw new AuthenticationException("Refresh token is invalid. User needs to re-authenticate.");
}
// Get the list of messages
ListMessagesResponse listResponse = service.users().messages().list("me").execute();
List<Message> messages = listResponse.getMessages();
if (++retryCount >= maxRetries) {
logger.error("Max retries reached. Unable to refresh access token.");
throw e;
}
// Implement exponential backoff
try {
Thread.sleep((long) (Math.pow(2, retryCount) * 1000));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("Token refresh interrupted", ie);
}
}
}
throw new IOException("Failed to refresh access token after " + maxRetries + " attempts");
}
private Gmail refreshAndGetGmailService(String accessToken, String refreshToken) throws IOException, AuthenticationException {
try {
Gmail service = getGmailService(accessToken, refreshToken);
service.users().getProfile("me").execute();
logger.info("Gmail service authenticated with provided access token.");
return service;
} catch (GoogleJsonResponseException e) {
if (e.getStatusCode() == 401) {
logger.info("Access token expired. Refreshing...");
String newAccessToken = refreshAccessToken(refreshToken);
Gmail service = getGmailService(newAccessToken, refreshToken);
service.users().getProfile("me").execute();
logger.info("Gmail service authenticated with refreshed token.");
return service;
}
logger.error("Failed to authenticate with Gmail API", e);
throw e;
}
}
// Async method to scan all emails in the user's inbox to prevent timeout.
@Async
public void getEmailAsync(String userId, String accessToken, String refreshToken) {
try {
ScannedGmailResponseDto result = getEmail(userId, accessToken, refreshToken);
CompletableFuture.completedFuture(result);
} catch (IOException e) {
logger.error("Error processing Gmail", e);
} catch (AuthenticationException e) {
logger.error("Error Authenticating", e);
}
}
// Scan all emails in the user's inbox.
public ScannedGmailResponseDto getEmail(String userId, String accessToken, String refreshToken) throws IOException, AuthenticationException {
Gmail service = refreshAndGetGmailService(accessToken, refreshToken);
logger.info("Gmail service initialized: {}", service);
List<EmailMessage> emailMessagesList = new ArrayList<>();
String meUserId = "me";
String nextPageToken = null;
UserEntity userEntity = userService.getUserByIdForGmail(userId);
BigInteger historyId = userEntity.getGmailHistoryId();
// Fetch history if historyId is not 0 (Default db value)
if (historyId.compareTo(BigInteger.ZERO) != 0) {
logger.info("HistoryId from db: {}", historyId);
ListHistoryResponse historyResponse = service.users().history().list(meUserId)
.setStartHistoryId(historyId)
.execute();
List<History> historyList = historyResponse.getHistory();
if (historyList != null) {
for (History history : historyList) {
logger.info("History Response - History Id: {}, Message Id: {}", history.getId(), history.getMessages().get(0).getId());
List<Message> messages = history.getMessages();
for (Message message : messages) {
message = service.users().messages().get("me", message.getId()).setFormat("full").execute();
EmailMessage emailMessage = processMessage(service, meUserId, message);
if (emailMessage != null) {
emailMessagesList.add(emailMessage);
saveEmailMessageAndScanQRCode(userId, emailMessage);
}
}
}
}
} else {
// Fetching email messages with page token and setting max results, Default value is 100.
do {
ListMessagesResponse listResponse = fetchMessages(service, meUserId, nextPageToken);
List<Message> messages = listResponse.getMessages();
nextPageToken = listResponse.getNextPageToken();
// Iterate all the messages and save to gmail db only if it has a valid QR code.
for (Message message : messages) {
EmailMessage emailMessage = processMessage(service, meUserId, message);
if (emailMessage != null) {
emailMessagesList.add(emailMessage);
// Save email message to database.
saveEmailMessageAndScanQRCode(userId, emailMessage);
}
}
} while (nextPageToken != null);
}
// Update user's history id.
BigInteger latestHistoryId = getLatestHistoryId(service, meUserId);
if (latestHistoryId != null) {
userEntity.setGmailHistoryId(latestHistoryId);
userService.updateUserEntity(userEntity);
}
return new ScannedGmailResponseDto(emailMessagesList);
}
private BigInteger getLatestHistoryId(Gmail service, String userId) throws IOException {
Profile profile = service.users().getProfile(userId).execute();
return profile.getHistoryId();
}
// Save email message to database and scan QR code.
private void saveEmailMessageAndScanQRCode(String userId, EmailMessage emailMessage) {
GmailEmailEntity gmailEmailEntity = saveEmailMessage(userId, emailMessage);
if (gmailEmailEntity != null) {
// Save QR codes by content ID
saveQRCodeByContentId(gmailEmailEntity, emailMessage.getQrCodeByContentId());
// Save QR codes by URL
saveQRCodeByURL(gmailEmailEntity, emailMessage.getQrCodeByURL());
} else {
logger.warn("Skipping QR code processing due to failure in saving email message.");
}
}
// Save to gmail_email table
private GmailEmailEntity saveEmailMessage(String userId, EmailMessage emailMessage) {
logger.info("userId: {}", userId);
OffsetDateTime dateReceived = DateParsingUtils.parseDate(emailMessage.getDate());
try {
GmailEmailEntity gmailEmailEntity = GmailEmailEntity.builder()
.userId(userId)
.messageId(emailMessage.getMessageId())
.threadId(emailMessage.getThreadId())
.historyId(new BigInteger(emailMessage.getHistoryId()))
.subject(emailMessage.getSubject())
.dateReceived(dateReceived)
.active(emailMessage.getActive())
.build();
return gmailEmailRespository.save(gmailEmailEntity);
} catch (DataIntegrityViolationException e) {
if (e.getCause() instanceof org.hibernate.exception.ConstraintViolationException) {
logger.warn("Duplicate entry for userId: {}, messageId: {}", userId, emailMessage.getMessageId());
} else {
logger.error("Error saving to gmail_email table: {}", e.getMessage(), e);
}
} catch (Exception e) {
logger.error("Error saving gmail_email table: {}", e.getMessage(), e);
}
return null;
}
// Iterate through decoded contents found in attachment as content id and save to gmail_cid table
private void saveQRCodeByContentId(GmailEmailEntity gmailEmailEntity, List<QRCodeByContentId> qrCodeByContentIdList) {
qrCodeByContentIdList.forEach(cid -> {
cid.getDecodedContent().forEach(decodedContent -> {
try {
QRCodeModel<?> qrCodeModel = qrCodeTypeService.scanGmailDecodedContents(gmailEmailEntity.getUserId(), decodedContent);
GmailCidEntity gmailCidEntity = GmailCidEntity.builder()
.gmailId(gmailEmailEntity.getId())
.qrCodeId(qrCodeModel.getData().getId())
.cid(cid.getCid())
.attachmentId(cid.getAttachmentId())
.decodedContent(decodedContent)
.build();
gmailCidRespository.save(gmailCidEntity);
} catch (Exception e) {
logger.error("Error saving QR code by content ID to gmail_cid table: {}", e.getMessage(), e);
}
});
});
}
// Iterate through decoded content found in url and save to gmail_url table
private void saveQRCodeByURL(GmailEmailEntity gmailEmailEntity, List<QRCodeByURL> qrCodeByURLList) {
qrCodeByURLList.forEach(imageUrl -> {
imageUrl.getDecodedContent().forEach(decodedContent -> {
try {
QRCodeModel<?> qrCodeModel = qrCodeTypeService.scanGmailDecodedContents(gmailEmailEntity.getUserId(), decodedContent);
GmailUrlEntity gmailUrlEntity = GmailUrlEntity.builder()
.gmailId(gmailEmailEntity.getId())
.qrCodeId(qrCodeModel.getData().getId())
.imageUrl(imageUrl.getUrl())
.decodedContent(decodedContent)
.build();
gmailUrlsRespository.save(gmailUrlEntity);
} catch (Exception e) {
logger.error("Error saving QR code by URL to gmail_urls table: {}", e.getMessage(), e);
}
});
});
}
// Fetching Scanned Gmail from database
public ScannedGmailResponseDto fetchScannedGmail(String userId){
// Fetching all emails from gmail_email table
List<GmailEmailEntity> userEmailsList = gmailEmailRespository.findByUserIdAndActive(userId, GMAIL_ACTIVE);
List<EmailMessage> emailMessageList = new ArrayList<>();
if (userEmailsList != null && !userEmailsList.isEmpty()) {
userEmailsList.forEach(email -> {
EmailMessage emailMessage = new EmailMessage(
email.getMessageId(),
email.getThreadId(),
email.getSubject(),
email.getHistoryId().toString(),
email.getDateReceived().toString()
);
// Fetching all CIDs from gmail_cid table
List<GmailCidEntity> cidList = gmailCidRespository.findByGmailId(email.getId());
Map<String, QRCodeByContentId> qrCodeByContentIdMap = new HashMap<>();
for (GmailCidEntity cid : cidList) {
String key = cid.getCid() + "-" + cid.getAttachmentId();
QRCodeByContentId qrCodeByContentId = qrCodeByContentIdMap.get(key);
if (qrCodeByContentId == null) {
qrCodeByContentId = QRCodeByContentId.builder()
.cid(cid.getCid())
.attachmentId(cid.getAttachmentId())
.decodedContent(new ArrayList<>())
.totalQRCodeFound(0)
.build();
qrCodeByContentIdMap.put(key, qrCodeByContentId);
}
// Append decoded content to the existing list
qrCodeByContentId.getDecodedContent().add(cid.getDecodedContent());
// Fetch scanned QR code from database and add to message object
emailMessage.addQRCodeModel(qrCodeTypeService.getScannedQRCodeDetailsInModel(cid.getQrCodeId()));
qrCodeByContentId.setTotalQRCodeFound(qrCodeByContentId.getTotalQRCodeFound() + 1);
}
emailMessage.setQrCodeByContentId(new ArrayList<>(qrCodeByContentIdMap.values()));
// Fetching all URLs from gmail_urls table
List<GmailUrlEntity> urlList = gmailUrlsRespository.findByGmailId(email.getId());
Map<String, QRCodeByURL> qrCodeByURLMap = new HashMap<>();
for (GmailUrlEntity url : urlList) {
String key = url.getImageUrl();
QRCodeByURL qrCodeByURL = qrCodeByURLMap.get(key);
if (qrCodeByURL == null) {
qrCodeByURL = QRCodeByURL.builder()
.url(url.getImageUrl())
.decodedContent(new ArrayList<>())
.totalQRCodeFound(0)
.build();
qrCodeByURLMap.put(key, qrCodeByURL);
}
// Append decoded content to the existing list
qrCodeByURL.getDecodedContent().add(url.getDecodedContent());
// Fetch scanned QR code from database and add to message object
emailMessage.addQRCodeModel(qrCodeTypeService.getScannedQRCodeDetailsInModel(url.getQrCodeId()));
qrCodeByURL.setTotalQRCodeFound(qrCodeByURL.getTotalQRCodeFound() + 1);
}
emailMessage.setQrCodeByURL(new ArrayList<>(qrCodeByURLMap.values()));
emailMessageList.add(emailMessage);
});
emailMessageList.sort(Comparator.comparing(EmailMessage::getDate, Comparator.nullsLast(Comparator.reverseOrder())));
}
return ScannedGmailResponseDto.builder().messages(emailMessageList).build();
}
// Fetching email messages with page token and setting max results
private ListMessagesResponse fetchMessages(Gmail service, String userId, String pageToken) throws IOException {
return service.users().messages().list(userId)
.setPageToken(pageToken)
.setMaxResults(MAX_RESULTS)
.execute();
}
// Processing email message and returning EmailMessage object if it has a valid QR code.
private EmailMessage processMessage(Gmail service, String userId, Message message) {
try {
message = service.users().messages().get(userId, message.getId()).setFormat("full").execute();
List<MessagePart> parts = message.getPayload().getParts();
Set<String> attachmentIds = new HashSet<>();
Set<String> imageUrls = new HashSet<>();
processPartsRecursively(parts, attachmentIds, imageUrls);
// Extract and log the email subject
String subject = getSubject(message);
logger.info("Email Subject-> {}", subject);
if (attachmentIds.isEmpty() && imageUrls.isEmpty()) {
return null;
}
if (attachmentIds.isEmpty() && imageUrls.isEmpty())
continue;
String subject = getHeader(message, "Subject");
String emailDate = getHeader(message, "Date");
logger.info("Email Subject: {}", subject);
logger.info("Message ID: {}", message.getId());
logger.info("History ID: {}", message.getHistoryId());
String messageId = message.getId();
logger.info("messageId-> {}", messageId);
String historyId = String.valueOf(message.getHistoryId());
logger.info("historyId-> {}", historyId);
EmailMessage emailMessage = new EmailMessage(message.getId(), message.getThreadId(), subject, String.valueOf(message.getHistoryId()), emailDate);
processAttachments(service, message.getId(), parts, attachmentIds, emailMessage);
processImageUrls(imageUrls, emailMessage);
return emailMessage.hasQRCodes() ? emailMessage : null;
} catch (GoogleJsonResponseException e) {
if (e.getStatusCode() == 404) {
logger.warn("Message with ID {} not found. It may have been deleted.", message.getId());
return null;
} else {
logger.error("Error processing message with ID {}: {}", message.getId(), e.getMessage());
throw new RuntimeException("Error processing Gmail message", e);
}
} catch (IOException e) {
logger.error("IO error processing message with ID {}: {}", message.getId(), e.getMessage());
throw new RuntimeException("IO error processing Gmail message", e);
}
}
// Process all the attachments.
private void processAttachments(Gmail service, String messageId, List<MessagePart> parts, Set<String> attachmentIds, EmailMessage emailMessage) throws IOException {
for (String attachmentId : attachmentIds) {
Optional<String> attachment = findAttachmentIdByCid(parts, attachmentId);
logger.info("attachment-> {}", attachment);
if (attachment.isPresent()) {
List<String> qrCodeValue = processAttachment(service, messageId, attachment.get());
emailArray.put(qrCodeValue);
if (!qrCodeValue.isEmpty()) {
emailMessage.addQRCodeByContentId(new QRCodeByContentId(attachmentId, attachment.get(), qrCodeValue, qrCodeValue.size()));
}
}
}
}
// Process all the image URLs.
private void processImageUrls(Set<String> imageUrls, EmailMessage emailMessage) {
for (String imageUrl : imageUrls) {
List<String> qrCodeValue = scanQRCodeFromUrl(imageUrl);
if (qrCodeValue != null) {
emailArray.put(qrCodeValue);
if (!qrCodeValue.isEmpty()) {
emailMessage.addQRCodeByURL(new QRCodeByURL(imageUrl, qrCodeValue, qrCodeValue.size()));
}
}
}
logger.info("Total Emails-> {}", messages.size());
json.put("qr_codes", emailArray);
return json;
}
private String getSubject(Message message) {
// Find the header with the given name.
private String getHeader(Message message, String name) {
return message.getPayload().getHeaders().stream()
.filter(header -> "Subject".equals(header.getName()))
.filter(header -> name.equalsIgnoreCase(header.getName()))
.findFirst()
.map(MessagePartHeader::getValue)
.orElse("No Subject");
.orElse("No " + name);
}
// Find the attachment ID in the given message part.
private Optional<String> findAttachmentIdByCid(List<MessagePart> parts, String cid) {
return parts.stream()
.flatMap(part -> Stream.concat(findAttachmentIdInCurrentPart(part, cid).stream(), Optional.ofNullable(part.getParts())
.flatMap(subParts -> findAttachmentIdByCid(subParts, cid)).stream()))
.findFirst();
}
// Find the attachment ID in the message subpart.
private Optional<String> findAttachmentIdInCurrentPart(MessagePart part, String cid) {
return Optional.ofNullable(part.getHeaders())
.flatMap(headers -> headers.stream()
@@ -121,7 +504,7 @@ public class GmailService {
.findFirst()
.map(header -> part.getBody().getAttachmentId()));
}
// Check if the header is a Content-ID header with the given CID.
private boolean isContentIdHeader(MessagePartHeader header, String cid) {
return "Content-ID".equalsIgnoreCase(header.getName()) && header.getValue().contains(cid);
}
@@ -140,35 +523,55 @@ public class GmailService {
}
}
}
private List<String> scanQRCodeFromUrl(String imageUrl) throws IOException, InterruptedException {
private List<String> scanQRCodeFromUrl(String imageUrl) {
try {
BufferedImage image = downloadImageFromUrl(imageUrl);
if (image != null) {
return decodeQRCodes(image);
}
} catch (IllegalArgumentException e) {
logger.error("Invalid URI scheme for URL: {} -> {}", imageUrl, e.getMessage());
} catch(URISyntaxException e) {
logger.error("Error while scanning QR code from URL", e);
}
return null;
return Collections.emptyList();
}
// Download the image from the given URL
private BufferedImage downloadImageFromUrl(String imageUrl) throws IOException, InterruptedException, URISyntaxException {
private BufferedImage downloadImageFromUrl(String imageUrl) throws URISyntaxException {
try {
imageUrl = imageUrl.replace(" ", "%20");
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
logger.info("imageUrl-> {}", imageUrl);
// Encode the URL
logger.info("Downloading image from URL: {}", imageUrl);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(imageUrl.replace(" ", "%20")))
.uri(new URI(imageUrl))
.header("User-Agent", "Mozilla/5.0")
.GET()
.build();
HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() == 200) {
byte[] imageBytes = response.body();
return ImageIO.read(new ByteArrayInputStream(imageBytes));
} else {
logger.error("Failed to download image. HTTP response code: {}", response.statusCode());
logger.warn("Failed to download image. HTTP response code: {}", response.statusCode());
}
} catch (URISyntaxException e) {
logger.error("Invalid URL: {} -> {}", imageUrl, e.getMessage());
} catch (HttpTimeoutException e) {
logger.error("Request timed out for URL: {} -> {}", imageUrl, e.getMessage());
} catch (ConnectException e) {
logger.warn("Failed to connect to URL: {} -> {}", imageUrl, e.getMessage());
} catch (IOException e) {
logger.warn("Error downloading image from URL: {} -> {}", imageUrl, e.getMessage());
if (Thread.currentThread().isInterrupted()) {
logger.warn("Thread was interrupted during IO operation for URL: {}", imageUrl);
}
} catch (InterruptedException e) {
logger.warn("Thread was interrupted during HTTP request for URL: {} -> {}", imageUrl, e.getMessage());
Thread.currentThread().interrupt();
}
return null;
}
@@ -199,15 +602,13 @@ public class GmailService {
qrCodeValues.add(result.getText());
logger.info("Detected QR code: {}", result.getText());
}
} else {
logger.info("No QR codes found in the image");
}
} catch (NotFoundException e) {
logger.info("No QR codes found in the image");
// No QR codes found
} catch (Exception e) {
logger.error("Error decoding QR codes", e);
}
if (!qrCodeValues.isEmpty())
logger.info("Total QR codes found: {}", qrCodeValues.size());
return qrCodeValues;
}
@@ -238,5 +639,21 @@ public class GmailService {
String lowerUrl = url.toLowerCase();
return lowerUrl.endsWith(".jpg") || lowerUrl.endsWith(".jpeg") || lowerUrl.endsWith(".png") || lowerUrl.endsWith(".gif") || lowerUrl.endsWith(".bmp");
}
@Transactional
public BaseResponse deleteMessage(String userId, String messageId) {
int updatedCount = gmailEmailRespository.deactivateEmailByUserIdAndMessageId(userId, messageId);
// throw exception if email message not found
if (updatedCount < 1)
throw new ResourceNotFoundExceptions("Email message not found");
return BaseResponse.builder().message("Email deleted successfully").build();
}
@Transactional
public BaseResponse deleteAllMessages(String userId) {
int updatedCount = gmailEmailRespository.deactivateEmailsByUserId(userId);
return (updatedCount < 1) ?
BaseResponse.builder().message("No Email found").build() :
BaseResponse.builder().message("All Emails deleted successfully").build();
}
}

View File

@@ -0,0 +1,213 @@
package com.safeqr.app.prediction.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.safeqr.app.qrcode.entity.QRCodeTypeEntity;
import com.safeqr.app.qrcode.entity.URLEntity;
import com.safeqr.app.qrcode.model.URLModel;
import lombok.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class URLFeaturesMapper {
private static final Logger logger = LoggerFactory.getLogger(URLFeaturesMapper.class);
@JsonProperty("domain")
private Integer domain;
@JsonProperty("subdomain")
private Integer subdomain;
@JsonProperty("top_level_domain")
private Integer topLevelDomain;
@JsonProperty("query")
private Integer query;
@JsonProperty("fragment")
private Integer fragment;
@JsonProperty("redirect")
private Integer redirect;
@JsonProperty("path")
private Integer path;
@JsonProperty("redirect_chain")
private Integer redirectChain;
@JsonProperty("hsts_header")
private Integer hstsHeader;
@JsonProperty("ssl_stripping")
private Integer sslStripping;
@JsonProperty("hostname_embedding")
private Integer hostnameEmbedding;
@JsonProperty("javascript_check")
private Integer javascriptCheck;
@JsonProperty("shortening_service")
private Integer shorteningService;
@JsonProperty("has_ip_address")
private Integer hasIpAddress;
@JsonProperty("tracking_descriptions")
private Integer trackingDescriptions;
@JsonProperty("url_encoding")
private Integer urlEncoding;
@JsonProperty("has_executable")
private Integer hasExecutable;
@JsonProperty("tls")
private Integer tls;
@JsonProperty("contents")
private Integer contents;
public static URLFeaturesMapper fromEntity(URLModel urlModel) {
URLEntity details = urlModel.getDetails();
QRCodeTypeEntity qrCodeTypeEntity = urlModel.getData().getInfo();
URLFeaturesMapper features = URLFeaturesMapper.builder()
.build();
features.setDomain(details.getDomain());
features.setSubdomain(details.getSubdomain());
features.setTopLevelDomain(details.getTopLevelDomain());
features.setQuery(details.getQuery());
features.setFragment(details.getFragment());
features.setPath(details.getPath());
features.setRedirect(details.getRedirect());
features.setRedirectChain(details.getRedirectChain());
features.setHstsHeader(details.getHstsHeader());
features.setSslStripping(details.getSslStripping());
features.setHostnameEmbedding(details.getHostnameEmbedding());
features.setJavascriptCheck(details.getJavascriptCheck());
features.setShorteningService(details.getShorteningService());
features.setHasIpAddress(details.getHasIpAddress());
features.setTrackingDescriptions(details.getTrackingDescriptions());
features.setUrlEncoding(details.getUrlEncoding());
features.setHasExecutable(details.getHasExecutable());
features.setTls(Math.toIntExact(qrCodeTypeEntity.getId()));
features.setContents(urlModel.getData().getContents());
return features;
}
private void setRedirect(int redirect) {
this.redirect = redirect;
}
// Custom setter for tls (qr_code_type_id)
public void setTls(Integer tls) {
if (tls != null) {
this.tls = tls == 1 ? 0 : tls == 9 ? 1 : tls.intValue();
} else {
this.tls = 0;
}
}
// Custom setter for hostnameEmbedding and other similar columns
public void setHostnameEmbedding(Integer hostnameEmbedding) {
this.hostnameEmbedding = (hostnameEmbedding != null && hostnameEmbedding != 0) ? 1 : 0;
}
public void setJavascriptCheck(String javascriptCheck) {
this.javascriptCheck = (javascriptCheck != null && !javascriptCheck.isEmpty()) ? 1 : 0;
}
public void setShorteningService(String shorteningService) {
this.shorteningService = (shorteningService != null && !shorteningService.isEmpty()) ? 1 : 0;
}
public void setHasIpAddress(String hasIpAddress) {
this.hasIpAddress = (hasIpAddress != null && !hasIpAddress.isEmpty()) ? 1 : 0;
}
public void setUrlEncoding(String urlEncoding) {
this.urlEncoding = (urlEncoding != null && !urlEncoding.isEmpty()) ? 1 : 0;
}
public void setHasExecutable(String hasExecutable) {
this.hasExecutable = (hasExecutable != null && !hasExecutable.isEmpty()) ? 1 : 0;
}
public void setTrackingDescriptions(List<String> trackingDescriptions) {
this.trackingDescriptions = (trackingDescriptions != null && !trackingDescriptions.isEmpty()) ? 1 : 0;
}
// Custom setter for sslStripping
public void setSslStripping(List<Boolean> sslStripping) {
if (sslStripping != null && !sslStripping.isEmpty() && sslStripping.get(0) != null) {
this.sslStripping = sslStripping.get(0) ? 1 : 0;
} else {
this.sslStripping = 0;
}
}
// Custom setter for hstsHeader
public void setHstsHeader(List<String> hstsHeader) {
logger.info("HSTS header value: {}", hstsHeader);
if (hstsHeader == null || hstsHeader.isEmpty()) {
this.hstsHeader = 0;
} else {
logger.info("first hsts header value: {}", hstsHeader.get(0));
if (hstsHeader.get(0).toLowerCase().contains("no")) {
this.hstsHeader = 0;
} else {
this.hstsHeader = 1;
}
}
}
// Custom setters for calculating string lengths
public void setDomain(String domain) {
this.domain = (domain != null) ? domain.length() : 0;
}
public void setSubdomain(String subdomain) {
this.subdomain = (subdomain != null) ? subdomain.length() : 0;
}
public void setTopLevelDomain(String topLevelDomain) {
this.topLevelDomain = (topLevelDomain != null) ? topLevelDomain.length() : 0;
}
public void setQuery(String query) {
this.query = (query != null) ? query.length() : 0;
}
public void setFragment(String fragment) {
this.fragment = (fragment != null) ? fragment.length() : 0;
}
public void setPath(String path) {
this.path = (path != null) ? path.length() : 0;
}
public void setRedirectChain(List<String> redirectChain) {
logger.info("Redirect chain: {}", redirectChain);
if (redirectChain != null) {
// Calculate the total number of characters in the list of strings
int totalChars;
totalChars = redirectChain.stream()
.mapToInt(String::length)
.sum();
this.redirectChain = totalChars;
} else {
this.redirectChain = 0;
}
}
public void setContents(String contents) {
this.contents = (contents != null) ? contents.length() : 0;
}
}

View File

@@ -0,0 +1,80 @@
package com.safeqr.app.prediction.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.safeqr.app.prediction.model.URLFeaturesMapper;
import com.safeqr.app.qrcode.model.URLModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import static com.safeqr.app.constants.APIConstants.PREDICTION_API_URL;
@Service
public class PredictionService {
private static final Logger logger = LoggerFactory.getLogger(PredictionService.class);
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
public PredictionService(RestTemplate restTemplate, ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
public String predict(URLModel urlModel) {
// Convert URLModel to URLFeatures
URLFeaturesMapper features = URLFeaturesMapper.fromEntity(urlModel);
logger.info("Prediction request: {}", features);
logger.info("feature contents : {}", features.getContents());
logger.info("feature domain : {}", features.getDomain());
logger.info("feature sub-domain : {}", features.getSubdomain());
logger.info("feature tld : {}", features.getTopLevelDomain());
logger.info("feature path : {}", features.getPath());
logger.info("feature query : {}", features.getQuery());
logger.info("feature fragment : {}", features.getFragment());
logger.info("feature redirect : {}", features.getRedirect());
logger.info("feature redirect chain: {}", features.getRedirectChain());
logger.info("feature shortening service: {}", features.getShorteningService());
logger.info("feature hasExecutable: {}", features.getHasExecutable());
logger.info("feature hasIP: {}", features.getHasIpAddress());
logger.info("feature hostname embedding: {}", features.getHostnameEmbedding());
logger.info("feature hsts header: {}", features.getHstsHeader());
logger.info("feature javascript check: {}", features.getJavascriptCheck());
logger.info("feature tracking: {}", features.getTrackingDescriptions());
logger.info("feature urlencoding: {}", features.getUrlEncoding());
// Prepare the HTTP headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// Create the HTTP entity containing the features and headers
HttpEntity<URLFeaturesMapper> requestEntity = new HttpEntity<>(features, headers);
// Make the HTTP POST request to the FastAPI prediction endpoint
ResponseEntity<String> response = restTemplate.exchange(
PREDICTION_API_URL,
HttpMethod.POST,
requestEntity,
String.class
);
// Use ObjectMapper to deserialize the response and automatically remove quotes
String prediction = response.getBody();
try {
prediction = objectMapper.readValue(prediction, String.class);
} catch (Exception e) {
logger.error("Failed to parse prediction response", e);
prediction = "Unknown";
}
logger.info("Prediction response: {}", prediction);
// Return the prediction
return prediction;
}
}

View File

@@ -3,12 +3,9 @@ package com.safeqr.app.qrcode.controller;
import static com.safeqr.app.constants.APIConstants.*;
import static com.safeqr.app.constants.CommonConstants.*;
import com.safeqr.app.qrcode.dto.request.QRCodePayload;
import com.safeqr.app.qrcode.dto.RedirectCountResponse;
import com.safeqr.app.qrcode.dto.URLVerificationResponse;
import com.safeqr.app.qrcode.dto.response.BaseScanResponse;
import com.safeqr.app.qrcode.entity.QRCodeTypeEntity;
import com.safeqr.app.qrcode.service.QRCodeTypeService;
import com.safeqr.app.qrcode.service.RedirectCountService;
import com.safeqr.app.qrcode.service.URLVerificationService;
import com.safeqr.app.qrcode.service.VirusTotalService;
import org.slf4j.Logger;
@@ -36,9 +33,6 @@ public class QRCodeTypeController {
@Autowired
private VirusTotalService virusTotalService;
@Autowired
private RedirectCountService redirectCountService;
@GetMapping(value = API_URL_QRCODE_GET_ALL)
public ResponseEntity<List<QRCodeTypeEntity>> getAllTypes() {
return ResponseEntity.ok(qrCodeTypeService.getAllTypes());
@@ -52,19 +46,16 @@ public class QRCodeTypeController {
@PostMapping(value = API_URL_QRCODE_SCAN, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseScanResponse> scanQRCode(@RequestBody QRCodePayload payload,
@RequestHeader(required = false, name = HEADER_USER_ID) String userId) {
logger.info("Invoking scan endpoint");
logger.info("User Id Invoking scan endpoint: {}", userId);
return ResponseEntity.ok(qrCodeTypeService.scanQRCode(userId, payload));
}
@PostMapping(API_URL_QRCODE_DETECT)
public ResponseEntity<String> detectType(@RequestBody QRCodePayload payload) {
return ResponseEntity.ok(qrCodeTypeService.detectType(payload).block());
}
@PostMapping(value = API_URL_QRCODE_VERIFY_URL, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseScanResponse> verifyURL(@RequestBody QRCodePayload payload,
@RequestHeader(required = false, name = HEADER_USER_ID) String userId) {
logger.info("User Id Invoking verify url endpoint: {}", userId);
return ResponseEntity.ok(qrCodeTypeService.scanQRCode(userId, payload));
@PostMapping(API_URL_QRCODE_VERIFY_URL)
public ResponseEntity<URLVerificationResponse> verifyURL(@RequestBody QRCodePayload payload) {
URLVerificationResponse response = urlVerificationService.verifyURL(payload);
return ResponseEntity.ok(response);
}
@PostMapping(API_URL_QRCODE_VIRUS_TOTAL_CHECK)
@@ -78,9 +69,5 @@ public class QRCodeTypeController {
}
}
@PostMapping(API_URL_QRCODE_REDIRECT_COUNT)
public ResponseEntity<RedirectCountResponse> checkRedirects(@RequestBody QRCodePayload payload) {
return ResponseEntity.ok(redirectCountService.countRedirects(payload).block());
}
}

View File

@@ -1,9 +0,0 @@
package com.safeqr.app.qrcode.dto;
import lombok.Data;
@Data
public class RedirectCountResponse {
private int redirectCount;
private String message;
}

View File

@@ -2,6 +2,7 @@ package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -30,4 +31,8 @@ public class PhoneEntity {
private UUID qrCodeId;
private String phone;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name = "remarks")
private String remarks;
}

View File

@@ -2,6 +2,7 @@
package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -12,6 +13,8 @@ import org.hibernate.annotations.UuidGenerator;
import java.time.LocalDateTime;
import java.util.UUID;
import static com.safeqr.app.constants.CommonConstants.CLASSIFY_UNKNOWN;
@Entity
@Table(name = "qr_code", schema = "safeqr")
@Data
@@ -36,4 +39,8 @@ public class QRCodeEntity {
@Column(name = "created_at", insertable = false, updatable = false)
private LocalDateTime createdAt;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name = "result_category")
private String result = CLASSIFY_UNKNOWN;
}

View File

@@ -2,6 +2,7 @@ package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -31,4 +32,8 @@ public class SMSEntity {
private String phone;
private String message;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Column(name = "keyword_detected")
private String keywordDetected;
}

View File

@@ -52,4 +52,9 @@ public class ScanHistoryEntity {
dateCreated = now;
dateUpdated = now;
}
@PreUpdate
public void preUpdate() {
dateUpdated = OffsetDateTime.now();
}
}

View File

@@ -1,6 +1,7 @@
package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.hypersistence.utils.hibernate.type.array.ListArrayType;
import jakarta.persistence.*;
@@ -11,6 +12,7 @@ import lombok.Builder;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.UuidGenerator;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -21,6 +23,10 @@ import java.util.UUID;
@NoArgsConstructor
@AllArgsConstructor
public class URLEntity {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name="classifications")
private String classifications;
@Id
@JsonIgnore
@GeneratedValue(generator = "UUID")
@@ -34,28 +40,86 @@ public class URLEntity {
private String domain;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private String subdomain;
private String topLevelDomain;
private String path;
@JsonProperty
private String query;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private String fragment;
private int redirect = 0;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Type(ListArrayType.class)
@Column(name = "hsts_header", columnDefinition = "text[]")
private List<String> hstsHeader;
private List<String> hstsHeader = new ArrayList<>();
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Type(ListArrayType.class)
@Column(name = "ssl_stripping", columnDefinition = "boolean[]")
private List<Boolean> sslStripping;
private List<Boolean> sslStripping = new ArrayList<>();
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Type(ListArrayType.class)
@Column(name = "redirect_chain", columnDefinition = "text[]")
private List<String> redirectChain;
private List<String> redirectChain = new ArrayList<>();
@Column(name = "hostname_embedding")
private Integer hostnameEmbedding = 0;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name = "javascript_check")
private String javascriptCheck = "";
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name = "shortening_service")
private String shorteningService = "";
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name = "has_ip_address")
private String hasIpAddress = "";
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Type(ListArrayType.class)
@Column(name = "tracking_descriptions", columnDefinition = "text[]")
private List<String> trackingDescriptions = new ArrayList<>();
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name = "url_encoding")
private String urlEncoding = "";
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name="has_executable")
private String hasExecutable = "";
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name = "dns_error")
private String dnsError = "";
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Column(name="ssl_error")
private String sslError = "";
// Custom getter for hostnameEmbedding
@JsonInclude(JsonInclude.Include.NON_NULL)
public Integer getHostnameEmbedding() {
return hostnameEmbedding == 0 ? null : hostnameEmbedding;
}
// Custom getter for path
@JsonInclude(JsonInclude.Include.NON_NULL)
public String getPath() {
return path == null || path.isEmpty() ? null : path;
}
// Custom getter for query
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonProperty
public String getQuery() {
return query == null || query.equals("{}") ? null : query;
}
}

View File

@@ -2,6 +2,7 @@ package com.safeqr.app.qrcode.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -31,6 +32,7 @@ public class WifiEntity {
private String ssid;
private String password;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private String encryption;
private boolean hidden;
}

View File

@@ -27,6 +27,8 @@ public final class EmailModel extends QRCodeModel<EmailEntity> {
@Override
public void setDetails() {
details = EmailEntity.builder().qrCodeId(data.getId()).build();
emailVerificationService.parseEmailString(details, data.getContents());
// Insert into email table
emailVerificationService.insertDB(details);
}
@@ -35,4 +37,8 @@ public final class EmailModel extends QRCodeModel<EmailEntity> {
public EmailEntity getDetails () {
return emailVerificationService.getEmailEntityByQRCodeId(data.getId());
}
@Override
public String retrieveClassification() {
return "";
}
}

View File

@@ -27,6 +27,8 @@ public final class PhoneModel extends QRCodeModel<PhoneEntity> {
@Override
public void setDetails() {
details = PhoneEntity.builder().qrCodeId(data.getId()).build();
phoneVerificationService.parsePhoneString(details, data.getContents());
// Insert into phone table
phoneVerificationService.insertDB(details);
}
@@ -34,4 +36,8 @@ public final class PhoneModel extends QRCodeModel<PhoneEntity> {
public PhoneEntity getDetails () {
return phoneVerificationService.getPhoneEntityByQRCodeId(data.getId());
}
@Override
public String retrieveClassification() {
return phoneVerificationService.checkPhoneNumber(details);
}
}

View File

@@ -10,4 +10,5 @@ public abstract class QRCodeModel<T>{
public abstract void setDetails();
public abstract T getDetails();
public abstract String retrieveClassification();
}

View File

@@ -27,6 +27,8 @@ public final class SMSModel extends QRCodeModel<SMSEntity> {
@Override
public void setDetails() {
details = SMSEntity.builder().qrCodeId(data.getId()).build();
smsVerificationService.parseSMSString(details, data.getContents());
// Insert into sms table
smsVerificationService.insertDB(details);
}
@@ -34,4 +36,8 @@ public final class SMSModel extends QRCodeModel<SMSEntity> {
public SMSEntity getDetails () {
return smsVerificationService.getSMSEntityByQRCodeId(data.getId());
}
@Override
public String retrieveClassification() {
return smsVerificationService.getClassification(details);
}
}

View File

@@ -34,4 +34,9 @@ public final class TextModel extends QRCodeModel<TextEntity> {
public TextEntity getDetails () {
return textVerificationService.getTextEntityByQRCodeId(data.getId());
}
@Override
public String retrieveClassification() {
return "";
}
}

View File

@@ -1,7 +1,6 @@
package com.safeqr.app.qrcode.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.safeqr.app.qrcode.entity.EmailEntity;
import com.safeqr.app.qrcode.entity.QRCodeEntity;
import com.safeqr.app.qrcode.entity.URLEntity;
import com.safeqr.app.qrcode.service.URLVerificationService;
@@ -12,8 +11,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.net.URISyntaxException;
@EqualsAndHashCode(callSuper = true)
@Data
public final class URLModel extends QRCodeModel<URLEntity> {
@@ -47,4 +44,9 @@ public final class URLModel extends QRCodeModel<URLEntity> {
public URLEntity getDetails () {
return urlVerificationService.getURLEntityByQRCodeId(data.getId());
}
@Override
public String retrieveClassification() {
return urlVerificationService.getClassification(this);
}
}

View File

@@ -26,6 +26,10 @@ public final class WifiModel extends QRCodeModel<WifiEntity> {
@Override
public void setDetails() {
details = WifiEntity.builder().qrCodeId(data.getId()).build();
// Parse wifi string
wifiVerificationService.parseWifiString(details, data.getContents());
// Insert into wifi table
wifiVerificationService.insertDB(details);
}
@@ -33,4 +37,9 @@ public final class WifiModel extends QRCodeModel<WifiEntity> {
public WifiEntity getDetails () {
return wifiVerificationService.getWifiEntityByQRCodeId(data.getId());
}
@Override
public String retrieveClassification() {
return wifiVerificationService.getClassification(details.getEncryption());
}
}

View File

@@ -25,7 +25,8 @@ public class QRCodeFactoryProvider {
case QR_CODE_TYPE_EMAIL -> applicationContext.getBean(EmailFactory.class).create(scannedQRCodeEntity);
case QR_CODE_TYPE_WIFI -> applicationContext.getBean(WifiFactory.class).create(scannedQRCodeEntity);
case DEFAULT_QR_CODE_TYPE -> applicationContext.getBean(TextFactory.class).create(scannedQRCodeEntity);
default -> throw new IllegalArgumentException("Unsupported QR code type: " + scannedQRCodeEntity.getInfo().getType());
//default -> throw new IllegalArgumentException("Unsupported QR code type: " + scannedQRCodeEntity.getInfo().getType());
default -> applicationContext.getBean(TextFactory.class).create(scannedQRCodeEntity);
};
}
}

View File

@@ -1,10 +1,12 @@
package com.safeqr.app.qrcode.repository;
import com.safeqr.app.qrcode.entity.URLEntity;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import java.util.UUID;
public interface URLRepository extends GenericRepository<URLEntity> {
@Transactional
Optional<URLEntity> findByQrCodeId(UUID qrCodeId);
}

View File

@@ -1,5 +1,6 @@
package com.safeqr.app.qrcode.service;
import com.safeqr.app.exceptions.InvalidFormatExceptions;
import com.safeqr.app.exceptions.ResourceNotFoundExceptions;
import com.safeqr.app.qrcode.entity.EmailEntity;
import com.safeqr.app.qrcode.repository.EmailRepository;
@@ -8,7 +9,11 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class EmailVerificationService {
@@ -28,4 +33,35 @@ public class EmailVerificationService {
emailRepository.save(emailEntity);
}
public void parseEmailString(EmailEntity emailEntity, String emailString) {
Optional.ofNullable(emailString)
.filter(s -> !s.isEmpty())
.filter(s -> s.startsWith("MAILTO:"))
.map(s -> s.substring(7))
.map(s -> s.split("\\?", 2))
.filter(parts -> parts.length > 0)
.ifPresentOrElse(
parts -> {
String email = parts[0];
Map<String, String> params = (parts.length == 2)
? Arrays.stream(parts[1].split("&"))
.map(param -> param.split("=", 2))
.filter(keyValue -> keyValue.length == 2)
.collect(Collectors.toMap(
keyValue -> keyValue[0],
keyValue -> keyValue[1],
(v1, v2) -> v1
))
: Map.of();
emailEntity.setEmail(email);
emailEntity.setTitle(params.getOrDefault("subject", ""));
emailEntity.setMessage(params.getOrDefault("body", ""));
},
() -> {
throw new InvalidFormatExceptions("Invalid email format. Expected format: MAILTO:<email>?subject=<title>&body=<message>");
}
);
}
}

View File

@@ -1,5 +1,6 @@
package com.safeqr.app.qrcode.service;
import com.safeqr.app.exceptions.InvalidFormatExceptions;
import com.safeqr.app.exceptions.ResourceNotFoundExceptions;
import com.safeqr.app.qrcode.entity.PhoneEntity;
import com.safeqr.app.qrcode.repository.PhoneRepository;
@@ -10,6 +11,8 @@ import org.springframework.stereotype.Service;
import java.util.UUID;
import static com.safeqr.app.constants.CommonConstants.*;
@Service
public class PhoneVerificationService {
private final PhoneRepository phoneRepository;
@@ -28,4 +31,49 @@ public class PhoneVerificationService {
phoneRepository.save(phoneEntity);
}
public void parsePhoneString(PhoneEntity phoneEntity, String phoneString) {
// Validate the string format
if (phoneString == null || phoneString.isEmpty()) {
throw new InvalidFormatExceptions("Phone string cannot be null or empty.");
}
// Remove the "TEL:" prefix
String phoneNumber = phoneString.substring(4);
// Further validation for phone number can be done here (optional)
if (phoneNumber.matches("\\+?[0-9]*")) {
// Populate the PhoneEntity object
phoneEntity.setPhone(phoneNumber);
} else {
throw new InvalidFormatExceptions("Invalid phone number format.");
}
}
public String checkPhoneNumber(PhoneEntity phoneEntity) {
// Remove any spaces, dashes, parentheses, and trim the ends
String phoneNumber = phoneEntity.getPhone().replaceAll("[\\s\\-()]", "").trim();
// Check if the number starts with +65 or just 65
if (phoneNumber.startsWith("+65")) {
phoneNumber = phoneNumber.substring(3); // Remove the "+65"
} else if (phoneNumber.startsWith("65")) {
phoneNumber = phoneNumber.substring(2); // Remove the "65"
}
// Check if it's a valid Singapore mobile or landline number
if (phoneNumber.matches("^[689]\\d{7}$")) {
if (phoneNumber.startsWith("8") || phoneNumber.startsWith("9")) {
phoneEntity.setRemarks("Singapore mobile number - This number has not been scanned for scam. Please do not divulge your personal information.");
} else if (phoneNumber.startsWith("6")) {
phoneEntity.setRemarks("Singapore landline number - This phone number has not been scanned for scam. Please do not divulge your personal information.");
}
return CLASSIFY_UNKNOWN;
}
// If it doesn't match mobile or landline pattern
phoneEntity.setRemarks("Warning: This is either an overseas number or an invalid Singapore number. Please exercise caution.");
return CLASSIFY_WARNING;
}
}

View File

@@ -68,13 +68,18 @@ public class QRCodeTypeService {
}
// Get scanned qrcode details
public BaseScanResponse getScannedQRCodeDetails(UUID qrCodeId){
return BaseScanResponse.builder().qrcode(getScannedQRCodeDetailsInModel(qrCodeId)).build();
}
public QRCodeModel<?> getScannedQRCodeDetailsInModel(UUID qrCodeId){
// Find scanned qr code in qr code table
QRCodeEntity qrCodeEntity = qrCodeRepository.findById(qrCodeId)
.orElseThrow(() -> new ResourceNotFoundExceptions("QR Code not found with id: " + qrCodeId));
logger.info("qrCodeEntity: {}", qrCodeEntity);
QRCodeModel<?> qrCodeModel = qrCodeFactoryProvider.createQRCodeInstance(qrCodeEntity);
logger.info("Retrieved details: {}", qrCodeModel.getDetails());
return BaseScanResponse.builder().qrcode(qrCodeModel).build();
return qrCodeModel;
}
// Process Scanned QR Code
@@ -83,6 +88,33 @@ public class QRCodeTypeService {
String data = payload.getData();
logger.info("scanQRCode: userId={}, data={}", userId, data);
QRCodeModel<?> qrCodeModel = scanAndClassify(userId, data);
UUID qrId = qrCodeModel.getData().getId();
// Insert into Scan History table if userId is not null
logger.info("scanQRCode: scannedQR new ID={}", qrId);
if (userId != null) {
scanHistoryRepository.save(ScanHistoryEntity.builder()
.qrCodeId(qrId)
.userId(userId)
.scanStatus(ScanHistoryEntity.ScanStatus.ACTIVE)
.build());
}
return BaseScanResponse.builder().qrcode(qrCodeModel).build();
}
// Scan decoded contents from email message
@Transactional
public QRCodeModel<?> scanGmailDecodedContents(String userId, String data) {
logger.info("Scan Gmail content: userId={}, data={}", userId, data);
return scanAndClassify(userId, data);
}
// ScanAndClassify
private QRCodeModel<?> scanAndClassify(String userId, String data) {
// Get the QR Code Type
QRCodeTypeEntity qrType = getQRCodeType(data);
@@ -94,21 +126,16 @@ public class QRCodeTypeService {
.createdAt(LocalDateTime.now())
.build());
// Insert into Scan History table if userId is not null
logger.info("scanQRCode: scannedQR new ID={}", scannedQR.getId());
if (userId != null) {
scanHistoryRepository.save(ScanHistoryEntity.builder()
.qrCodeId(scannedQR.getId())
.userId(userId)
.scanStatus(ScanHistoryEntity.ScanStatus.ACTIVE)
.build());
}
// Create the QR Code Instance based on the QR Code Type & insert into the respective table
QRCodeModel<?> qrCodeModel = qrCodeFactoryProvider.createQRCodeInstance(scannedQR);
qrCodeModel.setDetails();
return BaseScanResponse.builder().qrcode(qrCodeModel).build();
// Get classifications based on verifications
scannedQR.setResult(qrCodeModel.retrieveClassification());
return qrCodeModel;
}
// Returns Default type as text if it does not fit into any of the category
private QRCodeTypeEntity getQRCodeType(String data) {
return configs.stream()
@@ -116,27 +143,4 @@ public class QRCodeTypeService {
.findFirst()
.orElse(defaultQRCodeTypeEntity);
}
public Mono<String> detectType(QRCodePayload payload) {
String data = payload.getData();
for (QRCodeTypeEntity config : configs) {
if (data.startsWith(config.getPrefix())) {
if ("URL".equals(config.getType())) {
try
{
return safeBrowsingService.isSafeUrl(data)
.map(isSafe -> isSafe ? "Safe URL" : "Unsafe URL");
} catch (NoSuchAlgorithmException e)
{
// TODO Auto-generated catch block
return Mono.just("Error checking URL safety: " + e.getMessage());
}
}
return Mono.just(config.getType());
}
}
return Mono.just("Unknown");
}
}

View File

@@ -1,30 +0,0 @@
package com.safeqr.app.qrcode.service;
import com.safeqr.app.qrcode.dto.request.QRCodePayload;
import com.safeqr.app.qrcode.dto.RedirectCountResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Service
public class RedirectCountService {
private static final Logger logger = LoggerFactory.getLogger(RedirectCountService.class);
public Mono<RedirectCountResponse> countRedirects(QRCodePayload payload) {
String url = payload.getData();
logger.info("RedirectCountService: countRedirects: url={}", url);
return WebClient.create()
.get()
.uri("https://google.com")// replace with url when logic is complete
.exchangeToMono(response -> {
RedirectCountResponse redirectCountResponse = new RedirectCountResponse();
redirectCountResponse.setRedirectCount(response.cookies().size());
redirectCountResponse.setMessage("Redirect count calculated.");
return Mono.just(redirectCountResponse);
});
}
}

View File

@@ -1,20 +1,51 @@
package com.safeqr.app.qrcode.service;
import com.safeqr.app.exceptions.InvalidFormatExceptions;
import com.safeqr.app.exceptions.ResourceNotFoundExceptions;
import com.safeqr.app.qrcode.entity.PhoneEntity;
import com.safeqr.app.qrcode.entity.SMSEntity;
import com.safeqr.app.qrcode.repository.SMSRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import java.util.*;
import static com.safeqr.app.constants.CommonConstants.*;
@Service
public class SMSVerificationService {
private final SMSRepository smsRepository;
private static final Logger logger = LoggerFactory.getLogger(SMSVerificationService.class);
// Define phishing keywords categories
private static final Map<String, List<String>> PHISHING_KEYWORDS_MAP = new HashMap<>();
static {
PHISHING_KEYWORDS_MAP.put("Generic", Arrays.asList("password", "verify", "urgent", "prize", "account update"));
PHISHING_KEYWORDS_MAP.put("Tax Refund", Arrays.asList("tax refund", "claim your refund", "tax return"));
PHISHING_KEYWORDS_MAP.put("Suspicious Activity", Arrays.asList("suspicious activity detected", "action required", "account compromised"));
PHISHING_KEYWORDS_MAP.put("Social Media", Arrays.asList("social media account", "unauthorized login attempt", "verify your account"));
PHISHING_KEYWORDS_MAP.put("Bogus Payment", Arrays.asList("payment confirmation", "transaction details", "payment receipt"));
PHISHING_KEYWORDS_MAP.put("Incorrect Billing", Arrays.asList("incorrect billing information", "update billing details", "billing account"));
PHISHING_KEYWORDS_MAP.put("iCloud", Arrays.asList("icloud account", "update your icloud", "icloud security alert"));
PHISHING_KEYWORDS_MAP.put("HR Survey", Arrays.asList("human resources survey", "employee feedback", "survey participation"));
PHISHING_KEYWORDS_MAP.put("Google Docs", Arrays.asList("google docs", "view shared document", "google drive"));
PHISHING_KEYWORDS_MAP.put("USPS", Arrays.asList("usps delivery", "package tracking", "shipping details"));
PHISHING_KEYWORDS_MAP.put("Voicemail", Arrays.asList("voicemail notification", "missed call", "listen to voicemail"));
PHISHING_KEYWORDS_MAP.put("Bogus Invoice", Arrays.asList("invoice details", "view invoice", "payment invoice"));
PHISHING_KEYWORDS_MAP.put("Email Upgrade", Arrays.asList("email account upgrade", "email settings update", "upgrade your email"));
PHISHING_KEYWORDS_MAP.put("Dropbox", Arrays.asList("dropbox", "view shared file", "dropbox account"));
PHISHING_KEYWORDS_MAP.put("CEO Phishing", Arrays.asList("ceo email", "urgent message from ceo", "ceo authorization"));
PHISHING_KEYWORDS_MAP.put("Costco", Arrays.asList("costco", "costco membership", "costco rewards"));
PHISHING_KEYWORDS_MAP.put("Bank", Arrays.asList("bank account", "unusual activity", "account login"));
PHISHING_KEYWORDS_MAP.put("Fake App", Arrays.asList("app purchase", "app subscription", "confirm your purchase"));
PHISHING_KEYWORDS_MAP.put("Advanced Fee", Arrays.asList("advance fee", "processing fee", "fee payment"));
PHISHING_KEYWORDS_MAP.put("Account Suspension", Arrays.asList("account suspension", "suspend your account", "account deactivation"));
}
@Autowired
public SMSVerificationService(SMSRepository smsRepository) {
this.smsRepository = smsRepository;
@@ -29,4 +60,76 @@ public class SMSVerificationService {
smsRepository.save(smsEntity);
}
public void parseSMSString(SMSEntity smsEntity, String smsto) throws IllegalArgumentException{
// Validate the string format
if (smsto == null || smsto.isEmpty()) {
throw new InvalidFormatExceptions("sms cannot be null or empty.");
}
// Remove the "SMSTO:" prefix
String data = smsto.substring(6);
// Split the data into phone number and message
String[] parts = data.split(":", 2);
// If both phone number and message are available
if (parts.length == 2) {
String phone = parts[0];
String message = parts[1];
// Populate the SMSEntity object
smsEntity.setPhone(phone);
smsEntity.setMessage(message);
} else {
// Handle the case where the format is invalid
throw new InvalidFormatExceptions("Invalid SMSTO format. Expected format: SMSTO:<phone>:<message>");
}
}
public String getClassification (SMSEntity smsEntity) {
String lowerCaseSms = smsEntity.getMessage().toLowerCase();
logger.info("Sms: {}", lowerCaseSms);
// Iterate over the map of phishing keywords
for (Map.Entry<String, List<String>> entry : PHISHING_KEYWORDS_MAP.entrySet()) {
String category = entry.getKey();
List<String> keywords = entry.getValue();
// Check if the SMS contains any of the phishing keywords
for (String keyword : keywords) {
if (lowerCaseSms.contains(keyword)) {
logger.info("Phishing keyword detected: {}", keyword);
smsEntity.setKeywordDetected("Potential Phishing - " + category);
return checkPhoneNumber(smsEntity.getPhone()).equals(CLASSIFY_WARNING) ?
CLASSIFY_WARNING :
CLASSIFY_UNSAFE;
}
}
}
// If no phishing keywords are found, sent for local phone number checks
return checkPhoneNumber(smsEntity.getPhone()).equals(CLASSIFY_UNSAFE) ?
CLASSIFY_WARNING :
CLASSIFY_SAFE;
}
private String checkPhoneNumber(String phoneNumber) {
// Remove any spaces, dashes, parentheses, and trim the ends
phoneNumber = phoneNumber.replaceAll("[\\s\\-()]", "").trim();
// Check if the number starts with +65 or just 65
if (phoneNumber.startsWith("+65")) {
phoneNumber = phoneNumber.substring(3); // Remove the "+65"
} else if (phoneNumber.startsWith("65")) {
phoneNumber = phoneNumber.substring(2); // Remove the "65"
}
// Check if it's a valid Singapore mobile or landline number
if (phoneNumber.matches("^[689]\\d{7}$") && (phoneNumber.startsWith("8") || phoneNumber.startsWith("9"))) {
return CLASSIFY_WARNING;
}
// If it doesn't match mobile
return CLASSIFY_UNSAFE;
}
}

View File

@@ -1,92 +1,297 @@
package com.safeqr.app.qrcode.service;
import static com.safeqr.app.constants.CommonConstants.*;
import com.safeqr.app.exceptions.ResourceNotFoundExceptions;
import com.safeqr.app.qrcode.dto.request.QRCodePayload;
import com.safeqr.app.qrcode.dto.URLVerificationResponse;
import com.safeqr.app.qrcode.entity.URLEntity;
import com.safeqr.app.qrcode.model.URLModel;
import com.safeqr.app.qrcode.repository.URLRepository;
import com.safeqr.app.prediction.service.PredictionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
public class URLVerificationService {
private static final int CONNECTION_TIMEOUT_MS = 10000;
private static final int READ_TIMEOUT_MS = 10000;
private static final Logger logger = LoggerFactory.getLogger(URLVerificationService.class);
private final URLRepository urlRepository;
private final PredictionService predictionService;
@Autowired
public URLVerificationService(URLRepository urlRepository) {
public URLVerificationService(URLRepository urlRepository, PredictionService predictionService) {
this.urlRepository = urlRepository;
this.predictionService = predictionService;
}
// Regular expression pattern for shortening services
private static final String SHORTENING_PATTERN =
"bit\\.ly|goo\\.gl|shorte\\.st|go2l\\.ink|x\\.co|ow\\.ly|t\\.co|tinyurl|tr\\.im|is\\.gd|cli\\.gs|" +
"yfrog\\.com|migre\\.me|ff\\.im|tiny\\.cc|url4\\.eu|twit\\.ac|su\\.pr|twurl\\.nl|snipurl\\.com|" +
"short\\.to|BudURL\\.com|ping\\.fm|post\\.ly|Just\\.as|bkite\\.com|snipr\\.com|fic\\.kr|loopt\\.us|" +
"doiop\\.com|short\\.ie|kl\\.am|wp\\.me|rubyurl\\.com|om\\.ly|to\\.ly|bit\\.do|t\\.co|lnkd\\.in|" +
"db\\.tt|qr\\.ae|adf\\.ly|goo\\.gl|bitly\\.com|cur\\.lv|tinyurl\\.com|ow\\.ly|bit\\.ly|ity\\.im|" +
"q\\.gs|is\\.gd|po\\.st|bc\\.vc|twitthis\\.com|u\\.to|j\\.mp|buzurl\\.com|cutt\\.us|u\\.bb|yourls\\.org|" +
"x\\.co|prettylinkpro\\.com|scrnch\\.me|filoops\\.info|vzturl\\.com|qr\\.net|1url\\.com|tweez\\.me|v\\.gd|" +
"tr\\.im|link\\.zip\\.net";
// Regular expression pattern to match various IP address formats
private static final String IP_PATTERN =
"(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\/)|" +
"(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." +
"([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\/)|" +
"((0x[0-9a-fA-F]{1,2})\\.(0x[0-9a-fA-F]{1,2})\\.(0x[0-9a-fA-F]{1,2})\\.(0x[0-9a-fA-F]{1,2})\\/)" +
"(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|" +
"([0-9]+(?:\\.[0-9]+){3}:[0-9]+)|" +
"((?:(?:\\d|[01]?\\d\\d|2[0-4]\\d|25[0-5])\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d|\\d)(?:\\/\\d{1,2})?)";
// Define a Set of suspicious file extensions
private static final Set<String> SUSPICIOUS_EXTENSIONS = Stream.of(
".exe", ".bat", ".sh", ".cmd", ".scr", ".pif", ".application", ".gadget",
".vb", ".vbs", ".js", ".jse", ".ws", ".wsf", ".msc", ".cpl",
".msi", ".ps1", ".py", ".pyc", ".pyo", ".rb", ".bin", ".run", "apk"
).collect(Collectors.toUnmodifiableSet());
// Checks if the URL has executable file
public String hasExecutableFile(String urlPath) {
return Stream.of(urlPath)
.map(String::toLowerCase)
.map(path -> {
int lastDotIndex = path.lastIndexOf('.');
if (lastDotIndex != -1) {
return path.substring(lastDotIndex);
}
return path.contains(".") || path.endsWith("/") ? null : "";
})
.filter(Objects::nonNull)
.map(extension -> SUSPICIOUS_EXTENSIONS.contains(extension) || extension.isEmpty() ? "Yes" : "")
.findFirst()
.orElse("");
}
public URLEntity getURLEntityByQRCodeId(UUID qrCodeId) {
logger.info("qrCodeId retrieving: {}", qrCodeId);
return urlRepository.findByQrCodeId(qrCodeId)
.orElseThrow(() -> new ResourceNotFoundExceptions("URL not found for QR Code id: " + qrCodeId));
// return urlRepository.findByQrCodeId(qrCodeId)
// .orElseThrow(() -> new ResourceNotFoundExceptions("URL not found for QR Code id: " + qrCodeId));
return urlRepository.findByQrCodeId(qrCodeId).orElse(null);
}
public void insertDB(URLEntity urlEntity) {
urlRepository.save(urlEntity);
}
// Function to breakdown URL into subdomain, domain, topLevelDomain, query params, fragment
public URLEntity breakdownURL(String urlString) throws MalformedURLException {
public URLEntity breakdownURL(String urlString) {
URLEntity urlObj = new URLEntity();
try {
// Ensure the URL is properly encoded
String encodedUrl = encodeUrl(urlString);
URI uri = new URI(encodedUrl);
URL url = uri.toURL();
//URL url = new URI(encodeUrl(urlString)).toURL();
URL url = new URI(urlString.replace(" ", "")).toURL();
// Check for URL encoding in path and query
String query = parseQueryParams(url.getQuery());
String pathEncoding = checkURLEncoding(url.getPath());
String queryEncoding = query != null ? checkURLEncoding(query) : "";
// Combine encoding results
urlObj.setUrlEncoding(pathEncoding.equals("Yes") || queryEncoding.equals("Yes") ? "Yes" : "");
// encode url before proceeding the rest of the checks
url = new URI(encodeUrl(urlString)).toURL();
String host = url.getHost();
// split host into subdomain, domain, topLevelDomain
String[] hostParts = host.split("\\.");
String subdomain = "";
populateHostDetails(host, urlObj);
if (hostParts.length >= 2) {
// set topLevelDomain to the last part of the host
urlObj.setTopLevelDomain(hostParts[hostParts.length - 1]);
// set domain to the second last part of the host
urlObj.setDomain(hostParts[hostParts.length - 2]);
// set subdomain to the first part of the host
if (hostParts.length > 2) {
subdomain = String.join(".", java.util.Arrays.copyOfRange(hostParts, 0, hostParts.length - 2));
}
}
// set subdomain to URL host
urlObj.setSubdomain(subdomain);
// Check for deceptive URL
urlObj.setHostnameEmbedding(checkDeceptiveUrl(url));
String path = url.getPath();
//set path to URL path if it's not empty, otherwise set it to root path
urlObj.setPath(path.isEmpty() ? "/" : path);
// Check for Javascript code in url
urlObj.setJavascriptCheck(checkForJavascriptCode(urlString));
String query = url.getQuery();
Map<String, String> queryParams = new HashMap<>();
if (query != null) {
// split query params into key value pairs
for (String param : query.split("&")) {
String[] pair = param.split("=");
queryParams.put(pair[0], pair.length > 1 ? pair[1] : "");
}
logger.info("queryParams: {}", queryParams);
}
// set query params to URL query
urlObj.setQuery(queryParams.toString());
// set fragment to URL ref
// Check for url shortener
urlObj.setShorteningService(hasShorteningService(urlString));
// Check for IP address
urlObj.setHasIpAddress(hasIPAddress(urlString));
// Check for suspicious file extensions
urlObj.setHasExecutable(hasExecutableFile(urlString));
urlObj.setPath(Optional.ofNullable(url.getPath()).filter(p -> !p.isEmpty()).orElse(""));
urlObj.setQuery(parseQueryParams(url.getQuery()));
urlObj.setFragment(Optional.ofNullable(url.getRef()).orElse(""));
} catch (URISyntaxException | MalformedURLException e) {
// Check for tracking parameters
urlObj.setTrackingDescriptions(getTrackingDescriptions(url.getQuery()));
} catch (Exception e) {
logger.error("Error in breaking down URL: {}", e.getMessage());
e.printStackTrace();
}
return urlObj;
}
private void populateHostDetails(String host, URLEntity urlObj) {
logger.info("Host: {}", host);
if (host != null && !host.isEmpty()) {
if (isIpAddress(host)) {
// Handle IP address
urlObj.setDomain(host);
urlObj.setTopLevelDomain(""); // No TLD for IP addresses
urlObj.setSubdomain(""); // No subdomain for IP addresses
} else {
// Handle regular domain name
String[] hostParts = host.split("\\.");
int length = hostParts.length;
if (length >= 2) {
urlObj.setTopLevelDomain(hostParts[length - 1]); // TLD, e.g., "com"
urlObj.setDomain(hostParts[length - 2]); // Domain, e.g., "example"
urlObj.setSubdomain(length > 2 ? String.join(".", Arrays.copyOfRange(hostParts, 0, length - 2)) : "");
} else if (length == 1) {
// Handle cases like 'localhost' where there's no TLD
urlObj.setDomain(hostParts[0]);
urlObj.setTopLevelDomain(""); // No TLD
urlObj.setSubdomain(""); // No subdomain
}
}
}
}
// List of common tracking parameters with their descriptions
private static final Map<String, String> TRACKING_DESCRIPTIONS = Map.ofEntries(
Map.entry("utm_source", "Campaign Source: Identifies which site sent the traffic."),
Map.entry("utm_medium", "Campaign Medium: Identifies what type of link was used."),
Map.entry("utm_campaign", "Campaign Name: Identifies a specific product promotion or campaign."),
Map.entry("utm_term", "Campaign Term: Identifies search terms."),
Map.entry("utm_content", "Campaign Content: Differentiates similar content or links within the same ad."),
Map.entry("gclid", "Google Click Identifier: Used by Google Ads to track clicks."),
Map.entry("fbclid", "Facebook Click Identifier: Used by Facebook to track clicks."),
Map.entry("tracking_id", "Tracking ID: General identifier for tracking purposes."),
Map.entry("affiliate_id", "Affiliate ID: Identifies traffic from affiliates."),
Map.entry("ref", "Referrer: Identifies the referrer site."),
Map.entry("referrer", "Referrer: Identifies the referrer site.")
);
// Regex pattern to capture key-value pairs in the query string
private static final Pattern PARAM_PATTERN = Pattern.compile(
"(?<key>[^=&]+)=(?<value>[^&]+)",
Pattern.CASE_INSENSITIVE
);
// Static method to detect and return tracking parameter descriptions in a URL
private List<String> getTrackingDescriptions(String query) {
if (query == null || query.isEmpty()) {
return Collections.emptyList();
}
Matcher matcher = PARAM_PATTERN.matcher(query);
List<String> foundDescriptions = new ArrayList<>();
while (matcher.find()) {
String key = matcher.group("key").toLowerCase();
String value = URLDecoder.decode(matcher.group("value"), StandardCharsets.UTF_8);
if (TRACKING_DESCRIPTIONS.containsKey(key)) {
foundDescriptions.add(TRACKING_DESCRIPTIONS.get(key) + ": " + value);
}
}
return foundDescriptions;
}
private int checkDeceptiveUrl(URL url) {
String[] parts = url.getHost().split("\\.");
if (parts.length < 3) return 0;
Set<String> commonTlds = new HashSet<>(Arrays.asList("com", "org", "net", "edu", "gov"));
for (int i = parts.length - 2; i >= 1; i--) {
if (commonTlds.contains(parts[i]) && !commonTlds.contains(parts[i - 1]) && i != parts.length - 2) {
logger.warn("Potentially deceptive URL detected: {} (Suspicious domain: {}.{})",
url, parts[i - 1], parts[i]);
return 1;
}
}
return 0;
}
private String checkForJavascriptCode(String url) {
// Decode the URL
String decodedUrl = URLDecoder.decode(url, StandardCharsets.UTF_8);
// Patterns to detect 'javascript:', '<script>', and 'on*=' attributes
List<Pattern> maliciousPatterns = Arrays.asList(
Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
Pattern.compile("<\\s*script", Pattern.CASE_INSENSITIVE),
Pattern.compile("on(click|mouseover|load|error|unload|submit|reset|focus|blur|change|select|keydown|keyup|keypress|mousedown|mousemove|mouseup|mouseenter|mouseleave|contextmenu|dblclick)\\s*=", Pattern.CASE_INSENSITIVE)
);
// Check for any malicious pattern in the URL
for (Pattern pattern : maliciousPatterns) {
Matcher matcher = pattern.matcher(decodedUrl);
if (matcher.find()) {
return "Javascript found in URL.";
}
}
return "";
}
// Function to detect if the URL uses a shortening service
private String hasShorteningService(String url) {
Pattern pattern = Pattern.compile(SHORTENING_PATTERN, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(url);
return matcher.find() ? "Yes" : "";
}
// Function to check text encoding in a URL
private static String checkURLEncoding(String pathTextPart) {
// Decode the text
String decodedText = URLDecoder.decode(pathTextPart, StandardCharsets.UTF_8);
// Check if the decoded text matches the original text
return decodedText.equals(pathTextPart) ? "" : "Yes";
}
// Function to detect if the URL has an IP address
private static String hasIPAddress(String url) {
Pattern pattern = Pattern.compile(IP_PATTERN, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(url);
return matcher.find() ? "URL contains IP address." : "";
}
// Check if the host is an IP address
private boolean isIpAddress(String host) {
// Regex to match IPv4 addresses
String ipv4Pattern = "\\d+\\.\\d+\\.\\d+\\.\\d+";
// Regex to match IPv6 addresses
String ipv6Pattern = "([a-fA-F0-9:]+:+)+[a-fA-F0-9]+";
return host.matches(ipv4Pattern) || host.matches(ipv6Pattern);
}
private String parseQueryParams(String query) {
if (query == null || query.isEmpty()) return "{}";
Map<String, String> queryParams = new HashMap<>();
for (String param : query.split("&")) {
String[] pair = param.split("=", 2);
String key = pair[0];
String value = pair.length > 1 ? pair[1] : "";
if (!key.isEmpty()) {
queryParams.put(key, value);
}
}
return queryParams.toString();
}
private String encodeUrl(String urlString) throws MalformedURLException {
try {
URL url = new URL(urlString);
@@ -119,7 +324,7 @@ public class URLVerificationService {
public void countAndTrackRedirects(String urlString, URLEntity details) throws IOException {
try {
URI uri = new URI(urlString);
URI uri = new URI(encodeUrl(urlString));
URL url = uri.toURL();
List<String> redirectChain = new ArrayList<>();
List<String> hstsHeaderList = new ArrayList<>();
@@ -143,6 +348,8 @@ public class URLVerificationService {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setInstanceFollowRedirects(false);
connection.setConnectTimeout(CONNECTION_TIMEOUT_MS);
connection.setReadTimeout(READ_TIMEOUT_MS);
int responseCode = connection.getResponseCode();
redirected = (responseCode >= 300 && responseCode < 400);
@@ -181,6 +388,23 @@ public class URLVerificationService {
details.setHstsHeader(hstsHeaderList);
} catch (URISyntaxException e){
logger.error("Error in breaking down URL: {}", e.getMessage());
} catch (SSLHandshakeException e) {
logger.error("SSL Handshake Exception: {}", e.getMessage());
details.setSslError("SSL Handshake Exception: " + e.getMessage());
} catch (SocketTimeoutException e) {
logger.error("Connection timed out: {}", e.getMessage());
details.setDnsError("Connection timed out: " + e.getMessage());
} catch (UnknownHostException e) {
logger.error("Unknown Host Exception: {}", e.getMessage());
details.setDnsError("Unknown Host Exception: " + e.getMessage());
} catch (NoRouteToHostException e) {
details.setDnsError("Error: " + e.getMessage());
} catch (ConnectException e) {
details.setDnsError("Connection Error: " + e.getMessage());
} catch (SocketException e) {
details.setDnsError("Socket Error: " + e.getMessage());
} catch (Exception e) {
details.setDnsError("Exception: " + e.getMessage());
}
}
// Function to check if the redirect is from HTTPS to HTTP
@@ -203,22 +427,48 @@ public class URLVerificationService {
return INFO_NON_SECURE_CONNECTION;
}
public URLVerificationResponse verifyURL(QRCodePayload payload) {
URLVerificationResponse response = new URLVerificationResponse();
try {
java.net.URL url = new java.net.URL(payload.getData());
String protocol = url.getProtocol();
if ("https".equalsIgnoreCase(protocol)) {
response.setSecure(true);
response.setMessage("The connection is secure.");
} else {
response.setSecure(false);
response.setMessage("The connection is not secure.");
}
} catch (Exception e) {
response.setSecure(false);
response.setMessage("Invalid URL.");
}
return response;
// Get Classification using ML Model
public String getClassification(URLModel urlModel){
String content = urlModel.getData().getContents();
// if in whitelist, return Benign and Safe
for (String domain : WHITELIST_DOMAINS) {
if (content.contains(domain)) {
// If in whitelist, set category to BENIGN and return SAFE
urlModel.getDetails().setClassifications(CAT_BENIGN);
return CLASSIFY_SAFE;
}
}
// Call ML model
String category = predictionService.predict(urlModel);
//update in category in url table
urlModel.getDetails().setClassifications(category);
// return classification results
if (category.equals(CAT_BENIGN)) {
if (!urlModel.getDetails().getTrackingDescriptions().isEmpty() || // contains tracking
urlModel.getData().getInfo().getPrefix().equalsIgnoreCase("http://") || // uses http
urlModel.getDetails().getSslStripping().contains(true) || // has SSL stripping
urlModel.getDetails().getHasExecutable().equalsIgnoreCase("yes") || // contains executable
!urlModel.getDetails().getJavascriptCheck().isEmpty() || // contains javascript
!urlModel.getDetails().getHasIpAddress().isEmpty() || // contains IP address
urlModel.getDetails().getHostnameEmbedding() != null // contains hostname embedding
) {
return CLASSIFY_WARNING;
}
return CLASSIFY_SAFE;
}
return CLASSIFY_UNSAFE;
}
// Static array for whitelist domains
private static final List<String> WHITELIST_DOMAINS = Arrays.asList(
"safeqr.github.io/marketing",
"uow.edu.au",
"nus.edu.sg",
"sim.edu.sg",
"sp.edu.sg"
);
}

View File

@@ -10,6 +10,8 @@ import org.springframework.stereotype.Service;
import java.util.UUID;
import static com.safeqr.app.constants.CommonConstants.*;
@Service
public class WifiVerificationService {
private final WifiRepository wifiRepository;
@@ -28,4 +30,51 @@ public class WifiVerificationService {
wifiRepository.save(wifiEntity);
}
public void parseWifiString(WifiEntity wifiEntity, String wifiString) {
wifiString = wifiString.substring(5);
// Split the string by semicolons
String[] parts = wifiString.split(";");
for (String part : parts) {
if (part.startsWith("T:")) {
wifiEntity.setEncryption(part.substring(2));
} else if (part.startsWith("S:")) {
wifiEntity.setSsid(part.substring(2));
} else if (part.startsWith("P:")) {
wifiEntity.setPassword(part.substring(2));
} else if (part.startsWith("H:")) {
wifiEntity.setHidden(Boolean.parseBoolean(part.substring(2)));
}
}
// Unescape special characters in SSID and password
wifiEntity.setSsid(unescapeString(wifiEntity.getSsid()));
wifiEntity.setPassword(unescapeString(wifiEntity.getPassword()));
}
private String unescapeString(String input) {
return input.replace("\\:", ":")
.replace("\\;", ";")
.replace("\\,", ",")
.replace("\\\\", "\\");
}
public String getClassification(String encryptionType) {
// Check if encryptionType is null
if (encryptionType == null) {
return CLASSIFY_UNSAFE;
}
if (encryptionType.equalsIgnoreCase("WPA") ||
encryptionType.equalsIgnoreCase("WPA2") ||
encryptionType.equalsIgnoreCase("WPA3")) {
return CLASSIFY_SAFE;
} else if (encryptionType.equalsIgnoreCase("WEP")) {
return CLASSIFY_WARNING;
} else if (encryptionType.equalsIgnoreCase("nopass")) {
return CLASSIFY_UNSAFE;
} else {
return CLASSIFY_UNKNOWN;
}
}
}

View File

@@ -0,0 +1,30 @@
package com.safeqr.app.qrcodetips.controller;
import com.safeqr.app.qrcodetips.entity.QrCodeTipEntity;
import com.safeqr.app.qrcodetips.service.QrCodeTipsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static com.safeqr.app.constants.APIConstants.*;
@RestController
@RequestMapping(API_VERSION)
public class QRCodeTipsController {
private static final Logger logger = LoggerFactory.getLogger(QRCodeTipsController.class);
QrCodeTipsService qrCodeTipsService;
@Autowired
public QRCodeTipsController (QrCodeTipsService qrCodeTipsService) { this.qrCodeTipsService = qrCodeTipsService;}
@GetMapping(value = API_URL_TIPS_GET)
public ResponseEntity<QrCodeTipEntity> getRandomTips() {
logger.info("Invoking GET QR Code tips endpoint");
return ResponseEntity.ok(qrCodeTipsService.getTips());
}
}

View File

@@ -0,0 +1,24 @@
package com.safeqr.app.qrcodetips.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "qr_code_tips", schema = "safeqr")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class QrCodeTipEntity {
@Id
@JsonIgnore
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String tips;
}

View File

@@ -0,0 +1,13 @@
package com.safeqr.app.qrcodetips.repository;
import com.safeqr.app.qrcodetips.entity.QrCodeTipEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface QrCodeTipRepository extends JpaRepository<QrCodeTipEntity, Long> {
@Query(value = "SELECT * FROM safeqr.qr_code_tips ORDER BY RANDOM() LIMIT 1", nativeQuery = true)
QrCodeTipEntity findRandomTip();
}

View File

@@ -0,0 +1,14 @@
package com.safeqr.app.qrcodetips.service;
import com.safeqr.app.qrcodetips.entity.QrCodeTipEntity;
import com.safeqr.app.qrcodetips.repository.QrCodeTipRepository;
import org.springframework.stereotype.Service;
@Service
public class QrCodeTipsService {
QrCodeTipRepository qrCodeTipRepository;
public QrCodeTipsService (QrCodeTipRepository qrCodeTipRepository) { this.qrCodeTipRepository = qrCodeTipRepository; }
public QrCodeTipEntity getTips() {
return qrCodeTipRepository.findRandomTip();
}
}

View File

@@ -37,49 +37,49 @@ public class UserController {
@GetMapping(value = API_URL_USER_GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<UserResponseDto> getUser(@RequestHeader(name = HEADER_USER_ID) String userId) {
logger.info("Invoking GET User endpoint");
logger.info("User Id Invoking GET User endpoint: {}", userId);
return ResponseEntity.ok(userService.getUserById(userId));
}
@GetMapping(value = API_URL_USER_GET_SCANNED_HISTORIES, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<ScannedHistoriesDto>> getUserScannedHistories(@RequestHeader(name = HEADER_USER_ID) String userId) {
logger.info("Invoking GET User Scanned Histories endpoint");
logger.info("User Id Invoking GET User Scanned Histories endpoint: {}", userId);
return ResponseEntity.ok(userService.getUserScannedHistories(userId));
}
@PutMapping(value = API_URL_USER_DELETE_SCANNED_HISTORIES, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseResponse> deleteScannedHistory(@RequestHeader(name = HEADER_USER_ID) String userId, @RequestBody BookmarkRequestDto bookmarkRequestDto) {
logger.info("Invoking PUT Delete Single Scanned History endpoint");
logger.info("User Id Invoking PUT Delete Single Scanned History endpoint: {}", userId);
return ResponseEntity.ok(userService.deleteScannedHistory(userId, bookmarkRequestDto.getQrCodeId()));
}
@PutMapping(value = API_URL_USER_DELETE_ALL_SCANNED_HISTORIES, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseResponse> deleteAllScannedHistories(@RequestHeader(name = HEADER_USER_ID) String userId) {
logger.info("Invoking PUT Delete All Scanned Histories endpoint");
logger.info("User Id Invoking PUT Delete All Scanned Histories endpoint: {}", userId);
return ResponseEntity.ok(userService.deleteAllScannedHistoriesByUserId(userId));
}
@GetMapping(value = API_URL_USER_GET_BOOKMARKS, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<ScannedHistoriesDto>> getUserBookmarks(@RequestHeader(name = HEADER_USER_ID) String userId) {
logger.info("Invoking GET User bookmarks endpoint");
logger.info("User Id Invoking GET User bookmarks endpoint: {}", userId);
return ResponseEntity.ok(userService.getUserBookmarks(userId));
}
@PostMapping(value = API_URL_USER_SET_BOOKMARK, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseResponse> setBookmark(@RequestHeader(name = HEADER_USER_ID) String userId, @RequestBody BookmarkRequestDto bookmarkRequestDto) {
logger.info("Invoking POST User bookmark endpoint");
logger.info("User Id Invoking POST User bookmark endpoint: {}", userId);
return ResponseEntity.ok(userService.setBookmark(userId, bookmarkRequestDto.getQrCodeId()));
}
@PutMapping(value = API_URL_USER_DELETE_BOOKMARK, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseResponse> deleteBookmark(@RequestHeader(name = HEADER_USER_ID) String userId, @RequestBody BookmarkRequestDto bookmarkRequestDto) {
logger.info("Invoking PUT Delete Single Bookmark endpoint");
logger.info("User Id Invoking PUT Delete Single Bookmark endpoint: {}", userId);
return ResponseEntity.ok(userService.deleteBookmark(userId, bookmarkRequestDto.getQrCodeId()));
}
@PutMapping(value = API_URL_USER_DELETE_ALL_BOOKMARK, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<BaseResponse> deleteAllBookmark(@RequestHeader(name = HEADER_USER_ID) String userId) {
logger.info("Invoking PUT Delete All Bookmark endpoint");
logger.info("User Id Invoking PUT Delete All Bookmark endpoint: {}", userId);
return ResponseEntity.ok(userService.deleteAllBookmarkByUserId(userId));
}
}

View File

@@ -12,6 +12,7 @@ public class UserResponseDto {
private String id;
private String name;
private String email;
private String source;
private OffsetDateTime dateJoined;
private OffsetDateTime dateUpdated;
private List<String> roles;

View File

@@ -8,6 +8,7 @@ import jakarta.persistence.Table;
import lombok.*;
import org.hibernate.annotations.Type;
import java.math.BigInteger;
import java.time.OffsetDateTime;
import java.util.List;
@@ -33,4 +34,10 @@ public class UserEntity {
@Column(name = "roles", columnDefinition = "text[]")
private List<String> roles;
private String status;
@Column(name = "source")
private String source;
@Column(name = "gmail_history_id")
private BigInteger gmailHistoryId = BigInteger.ZERO;
}

View File

@@ -2,7 +2,7 @@ package com.safeqr.app.user.service;
import com.safeqr.app.exceptions.ResourceAlreadyExists;
import com.safeqr.app.exceptions.ResourceNotFoundExceptions;
import com.safeqr.app.qrcode.entity.ScanBookmarkEntity;
import com.safeqr.app.qrcode.entity.ScanHistoryEntity;
import com.safeqr.app.qrcode.repository.ScanBookmarkRepository;
import com.safeqr.app.qrcode.repository.ScanHistoryRepository;
@@ -14,7 +14,7 @@ import com.safeqr.app.user.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -48,12 +48,20 @@ public class UserService {
.id(userEntity.getId())
.email(userEntity.getEmail())
.name(userEntity.getName())
.source(userEntity.getSource())
.dateJoined(userEntity.getDateCreated())
.dateUpdated(userEntity.getDateUpdated())
.roles(userEntity.getRoles())
.status(userEntity.getStatus())
.build();
}
public UserEntity getUserByIdForGmail(String userId){
return userRepository.findById(userId)
.orElseThrow(()-> new ResourceNotFoundExceptions("User id not found: " + userId));
}
public UserEntity updateUserEntity(UserEntity userEntity) {
return userRepository.save(userEntity);
}
public List<ScannedHistoriesDto> getUserScannedHistories(String userId) {
return scanHistoryRepository.findAllQRCodesByUserId(userId);
}

View File

@@ -0,0 +1,34 @@
package com.safeqr.app.utils;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// Sets the number of core threads. These threads are always kept alive.
executor.setCorePoolSize(2);
// Sets the maximum number of threads that can be created by the pool.
executor.setMaxPoolSize(2);
// Sets the size of the queue to hold tasks before they are executed.
executor.setQueueCapacity(500);
// Sets the prefix for the names of the threads created by this pool.
executor.setThreadNamePrefix("GmailProcessing-");
// Initializes the executor to apply the configuration and make it ready to use.
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,42 @@
package com.safeqr.app.utils;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.util.Locale;
public class DateParsingUtils {
private DateParsingUtils() {
// private constructor to hide the implicit public one
}
private static final DateTimeFormatter INPUT_FORMATTER = new DateTimeFormatterBuilder()
.appendPattern("EEE, ")
.appendPattern("[ ]") // This makes a single space optional
.appendPattern("[ ]") // This allows for a second optional space
.appendValue(ChronoField.DAY_OF_MONTH, 1, 2, java.time.format.SignStyle.NOT_NEGATIVE)
.appendPattern(" MMM yyyy HH:mm:ss Z")
.toFormatter(Locale.ENGLISH);
private static final DateTimeFormatter OUTPUT_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS Z");
public static String parseAndFormatDate(String inputDate) {
try {
OffsetDateTime dateTime = OffsetDateTime.parse(inputDate, INPUT_FORMATTER);
return dateTime.format(OUTPUT_FORMATTER);
} catch (Exception e) {
throw new RuntimeException("Error parsing date: " + inputDate, e);
}
}
public static OffsetDateTime parseDate(String inputDate) {
try {
return OffsetDateTime.parse(inputDate, INPUT_FORMATTER);
} catch (Exception e) {
throw new RuntimeException("Error parsing date: " + inputDate, e);
}
}
}

View File

@@ -0,0 +1,12 @@
package com.safeqr.app.utils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -32,4 +32,4 @@ gmail.client.clientAuthenticationScheme=query
gmail.client.scope=https://www.googleapis.com/auth/gmail.readonly
gmail.resource.userInfoUri=https://www.googleapis.com/gmail/v1/users/me/profile
gmail.resource.preferTokenInfo=true
gmail.client.redirectUri=https://bk5wiynzsi.execute-api.ap-southeast-1.amazonaws.com/api/gmail/callback
gmail.client.redirectUri=https://localhost/v1/gmail/callback