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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ JWT_ISSUER=your-jwt-issuer

# GOOGLE
GOOGLE_CLIENT_ID=your-google-android-client-id
GOOGLE_API_URL=https://oauth2.googleapis.com
GOOGLE_API_URL=your-google-client-id.apps.googleusercontent.com

# CLOUDINARY
CLOUDINARY_CLOUD_NAME=your-cloudinary-cloud-name
Expand All @@ -35,3 +35,9 @@ APP_SHA256_CERT_FINGERPRINT=dummy-sha256-fingerprint-for-app

# API
API_CATALOG_URL=your-api-catalog-url

# GRAFANA
OTEL_SERVICE_NAME=your-otel-serivce-name
OTEL_EXPORTER_OTLP_PROTOCOL=your-exporter-otlp-protocol
OTEL_EXPORTER_OTLP_ENDPOINT=your-otlp-endpoint
OTEL_EXPORTER_OTLP_HEADERS=your-otel-headers
6 changes: 6 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@ FROM eclipse-temurin:21-jdk-alpine

RUN apk add --no-cache curl

RUN mkdir -p /otel && \
curl -L -o /otel/opentelemetry-javaagent.jar \
https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

WORKDIR /app

COPY --from=build /app/target/*.jar app.jar

EXPOSE 8080

ENV JAVA_TOOL_OPTIONS="-javaagent:/otel/opentelemetry-javaagent.jar"

ENTRYPOINT ["java", "-jar", "app.jar"]
72 changes: 37 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,61 @@

## Overview

This module handles the core security flow of the **Bazaar** platform, managing user identity, session persistence, and secure access via JWT (JSON Web Tokens).
This module handles the core users flow of the **Bazaar** platform, managing user identity, session persistence, and secure access via JWT (JSON Web Tokens).

---

## Environment Variables

Create a `.env` file based on [.env.example](/auth/.env.example).
Create a `.env` file based on the [.env.example](/api-user/.env.example):

```bash
cp .env.example .env
```

* `PORT` - Backend running port.
* `HOST` - Backend running host address.
* `ENVIORMENT` - Running environment mode.
* `DATABASE_HOST` - Database running host address.
* `DATABASE_NAME` - Database name.
* `DATABASE_PORT` - Database running port.
* `DATABASE_USER` - Database user.
* `DATABASE_PASSWORD` - Database very hard to guess password.
* `EXPOSE_PORT` - Docker expose port for the database.
* `JWT_SECRET` - Your JWT secret key which should be kept in secret.
* `JWT_ISSUER` - Your JWT issuer.
* `CLOUDINARY_CLOUD_NAME` - Your Cloudinary cloud name.
* `CLOUDINARY_API_KEY` - Your Cloudinary API key.
* `CLOUDINARY_API_SECRET` - Your Cloudinary API secret.
* `API_CATALOG_URL` - Your API catalog domain name.
* `GMAIL_NAME` - Your Gmail name.
* `GMAIL_KEY` - Your Gmail API key.
* `GMAIL_ADDRESS` - Your Gmail address.
* `APP_BASE_URL` - Your service domain name.
* `APP_SHA256_CERT_FINGERPRINT` - Your SHA256 cert fingerprint.
* `SENDERGRID_KEY` - Your very secure sendergrid key.

---

## Running the Project

1. **Clone the repository**
```bash
git clone https://github.com/Baza-ar/api-auth.git api-user
cd api-user
```
2. **Configure the environment variables** Create a `.env` file based on [.env.example](/auth/.env.example):
* `PORT` - Backend running port.
* `HOST` - Backend running host address.
* `ENVIORMENT` - Running environment mode.
* `DATABASE_HOST` - Database running host address.
* `DATABASE_NAME` - Database name.
* `DATABASE_PORT` - Database running port.
* `DATABASE_USER` - Database user.
* `DATABASE_PASSWORD` - Database very hard to guess password.
* `EXPOSE_PORT` - Docker expose port for the database.
* `JWT_SECRET` - Your JWT secret key which should be kept in secret.
* `JWT_ISSUER` - Your JWT issuer.
* `CLOUDINARY_CLOUD_NAME` - Your Cloudinary cloud name.
* `CLOUDINARY_API_KEY` - Your Cloudinary API key.
* `CLOUDINARY_API_SECRET` - Your Cloudinary API secret.
* `API_CATALOG_URL` - Your API catalog domain name.
* `GMAIL_NAME` - Your Gmail name.
* `GMAIL_KEY` - Your Gmail API key.
* `GMAIL_ADDRESS` - Your Gmail address.
* `APP_BASE_URL` - Your service domain name.
* `APP_SHA256_CERT_FINGERPRINT` - Your SHA256 cert fingerprint.
* `SENDERGRID_KEY` - Your very secure sendergrid key.
3. **Running the whole project**
```bash
git clone https://github.com/Baza-ar/api-user.git api-user
cd api-user
```
2. **Run the whole project**
```bash
docker create network -d bridge bazaar-network

docker-compose up --build
```
> [!NOTE]
> You should only create the **Docker Network** once, check with.
> ```bash
> docker network ls
> ```
```

> [!NOTE]
> You should only create the **Docker Network** once, check with.
> ```bash
> docker network ls
> ```
---

## Stopping
Expand Down
1 change: 0 additions & 1 deletion src/main/java/com/bazaar/user/client/CatalogClient.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.bazaar.user.client;

import com.bazaar.user.dto.catalog.ProductUpdateStatusRequestDTO;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.service.annotation.HttpExchange;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/bazaar/user/controller/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.bazaar.user.common.Role;
import com.bazaar.user.dto.admin.UserDetailsPageResponseDTO;
import com.bazaar.user.dto.admin.UserStatsResponseDTO;
import com.bazaar.user.service.AdminService;

import io.swagger.v3.oas.annotations.Operation;
Expand Down Expand Up @@ -96,4 +97,25 @@ public void unblockUser(@RequestHeader("X-User-Role") Role role, @PathVariable L
adminService.unblockUser(role, unblockUserId);
}

/**
* Handles HTTP GET requests to fetch historical user registration rates.
* @param role Role of the user making the request, needs to be administrator.
* @return A {@link UserStatsResponseDTO} containing the computed registration values.
*/
@Operation(summary = "Obtains the users stats",
description = "Obtains the users registration rates as a system administrator")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Users stats obtained successfully",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = UserDetailsPageResponseDTO.class))),
@ApiResponse(responseCode = "403", description = "User Forbidden",
content = @Content(mediaType = MediaType.APPLICATION_PROBLEM_JSON_VALUE,
schema = @Schema(implementation = ErrorResponse.class))) })
@GetMapping(value = "/users/stats", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
public UserStatsResponseDTO getUsersStats(@RequestHeader("X-User-Role") Role role,
@RequestParam(defaultValue = "30") Integer period) {
return adminService.getUsersStats(role, period);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.bazaar.user.apiResponse.ErrorResponse;
import com.bazaar.user.apiResponse.swaggerResponses.AuthResponse;
import com.bazaar.user.common.Role;
import com.bazaar.user.common.Source;
import com.bazaar.user.dto.auth.AuthResponseDTO;
import com.bazaar.user.dto.auth.ForgotPasswordRequestDTO;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/bazaar/user/dto/admin/UserStatsResponseDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.bazaar.user.dto.admin;

/**
* Data Transfer Object (DTO) representing user registration metrics.
*
* @param totalUsers The total number of registered users in the system.
* @param activeUsers The total number of active users in the system.
* @param blockedUsers The total number of blocked users in the system.
* @param growthRate The rate growth or decline compared to the previous identical period.
*/
public record UserStatsResponseDTO(Long totalUsers, Long activeUsers, Long blockedUsers, double growthRate) {
}
20 changes: 20 additions & 0 deletions src/main/java/com/bazaar/user/repository/UserRepository.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.bazaar.user.repository;

import com.bazaar.user.common.Status;
import com.bazaar.user.model.User;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.Optional;

/**
Expand Down Expand Up @@ -45,4 +47,22 @@ public interface UserRepository extends JpaRepository<User, Long> {
""")
Page<User> findByNameAndEmail(@Param("name") String name, @Param("email") String email, Pageable pageable);

/**
* Counts users registered within a specific time range.
* @param start The start timestamp (exclusive).
* @param end The end timestamp (inclusive).
* @return The total count of matching users.
*/
long countByCreatedAtAfterAndCreatedAtBefore(LocalDateTime start, LocalDateTime end);

/**
* Counts users registered within a specific time range filtered by their current
* status.
* @param start The start timestamp (exclusive).
* @param status The user status filter.
* @param end The end timestamp (inclusive).
* @return The count of matching users by status.
*/
long countByCreatedAtAfterAndCreatedAtBeforeAndStatus(LocalDateTime start, LocalDateTime end, Status status);

}
38 changes: 37 additions & 1 deletion src/main/java/com/bazaar/user/service/AdminService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import com.bazaar.user.common.Role;
import com.bazaar.user.common.Status;
import com.bazaar.user.dto.admin.UserDetailsPageResponseDTO;
import com.bazaar.user.dto.admin.UserStatsResponseDTO;
import com.bazaar.user.dto.catalog.ProductUpdateStatusRequestDTO;
import com.bazaar.user.exception.UserBlockedException;
import com.bazaar.user.exception.UserForbiddenException;
import com.bazaar.user.exception.UserNotFoundException;
import com.bazaar.user.exception.UserUnblockedException;
Expand All @@ -21,6 +21,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Objects;

Expand Down Expand Up @@ -119,4 +120,39 @@ public void unblockUser(Role role, Long unblockUserId) {
catalogClient.modifyProductStatus(unblockUserId, new ProductUpdateStatusRequestDTO(true));
}

/**
* Retrieves user metrics filtered by status for a given period, including
* period-over-period growth.
* @param role The role context of the request issuer.
* @param days The number of days defining the current analytical period.
* @return A {@link UserStatsResponseDTO} containing absolute stats and percentage
* trends.
*/
public UserStatsResponseDTO getUsersStats(Role role, Integer days) {
checkPrivileges(role);

LocalDateTime now = LocalDateTime.now();
LocalDateTime startDate = now.minusDays(days);
LocalDateTime previousStartDate = startDate.minusDays(days);

long totalUsers = userRepository.countByCreatedAtAfterAndCreatedAtBefore(startDate, now);
long activeUsers = userRepository.countByCreatedAtAfterAndCreatedAtBeforeAndStatus(startDate, now,
Status.ACTIVE);
long blockedUsers = userRepository.countByCreatedAtAfterAndCreatedAtBeforeAndStatus(startDate, now,
Status.BLOCKED);

long previousTotalUsers = userRepository.countByCreatedAtAfterAndCreatedAtBefore(previousStartDate, startDate);

double growthRate = 0.0;
if (previousTotalUsers > 0) {
growthRate = ((double) (totalUsers - previousTotalUsers) / previousTotalUsers) * 100.0;
growthRate = Math.round(growthRate * 100.0) / 100.0;
}
else if (totalUsers > 0) {
growthRate = 100.0;
}

return new UserStatsResponseDTO(totalUsers, activeUsers, blockedUsers, growthRate);
}

}
4 changes: 2 additions & 2 deletions src/main/java/com/bazaar/user/service/CloudinaryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ public String upload(MultipartFile file) {
messageSource.getMessage("exception.cloudinary.NOT_ALLOWED_TYPE", null, Locale.getDefault()));

try {
Map result = cloudinary.uploader().upload(file.getBytes(), ObjectUtils.emptyMap());
return (String) result.get("secure_url");
Map<?, ?> result = cloudinary.uploader().upload(file.getBytes(), ObjectUtils.emptyMap());
return result.get("secure_url").toString();
}
catch (Exception e) {
throw new ExternalServiceException(
Expand Down
Loading
Loading