diff --git a/src/main/java/com/harmlessprince/ecommerceApi/brand/BrandRepository.java b/src/main/java/com/harmlessprince/ecommerceApi/brand/BrandRepository.java index 89e1216..70e655d 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/brand/BrandRepository.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/brand/BrandRepository.java @@ -6,7 +6,7 @@ import java.util.Optional; public interface BrandRepository extends MongoRepository { - Optional findByName(String name); + Optional findFirstByName(String name); List findAllByName(String name); List findByNameIn(List names); List findBySlugIn(List slugs); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/cart/Cart.java b/src/main/java/com/harmlessprince/ecommerceApi/cart/Cart.java index acf3f9a..7cdaeb5 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/cart/Cart.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/cart/Cart.java @@ -56,4 +56,6 @@ public class Cart extends BaseEntity { @DBRef Product product; + @Indexed + private String tenantId; } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/color/ColorDTO.java b/src/main/java/com/harmlessprince/ecommerceApi/color/ColorDTO.java new file mode 100644 index 0000000..c75da78 --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/color/ColorDTO.java @@ -0,0 +1,14 @@ +package com.harmlessprince.ecommerceApi.color; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@AllArgsConstructor +public class ColorDTO { + private String id; + private String name; + private String code; +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/color/ColorRepository.java b/src/main/java/com/harmlessprince/ecommerceApi/color/ColorRepository.java index 4df5639..45b8a31 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/color/ColorRepository.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/color/ColorRepository.java @@ -6,8 +6,7 @@ import java.util.Optional; public interface ColorRepository extends MongoRepository { - Optional findByName(String name); - Optional findByCode(String code); - + Optional findFirstByName(String name); + Optional findFirstByCode(String code); Optional findFirstById(String colorId); } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/configs/JwtAuthenticationFilter.java b/src/main/java/com/harmlessprince/ecommerceApi/configs/JwtAuthenticationFilter.java index 418bafe..2fd6896 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/configs/JwtAuthenticationFilter.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/configs/JwtAuthenticationFilter.java @@ -3,6 +3,7 @@ import com.harmlessprince.ecommerceApi.jwt.JwtService; import com.harmlessprince.ecommerceApi.user.User; import com.harmlessprince.ecommerceApi.user.UserService; +import com.harmlessprince.ecommerceApi.custom.AppConstants; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -13,7 +14,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; @@ -23,6 +23,7 @@ import java.io.IOException; import java.util.Collection; + //https://github.com/jwtk/jjwt?tab=readme-ov-file#creating-a-jwt @Component @Slf4j @@ -32,6 +33,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final UserService userService; + JwtAuthenticationFilter(HandlerExceptionResolver handlerExceptionResolver, JwtService jwtService, UserDetailsService userDetailsService, UserService userService) { this.handlerExceptionResolver = handlerExceptionResolver; this.jwtService = jwtService; @@ -58,6 +60,15 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull H Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null && email != null) { User userDetails = (User) userDetailsService.loadUserByUsername(email); + log.info("User found with tenant {}", userDetails.getTenant()); + if (userDetails.getTenant() != null) { + request.setAttribute(AppConstants.CURRENT_USER_TENANT_ID, userDetails.getTenant().getId()); + } + if (userDetails.getRole() != null) { + request.setAttribute(AppConstants.CURRENT_USER_ROLE_NAME, userDetails.getRole().getName()); + request.setAttribute(AppConstants.CURRENT_USER_ROLE_ID, userDetails.getRole().getId()); + } + Collection userAuthorities = userService.getAuthorities(userDetails.getRole()); if (jwtService.isTokenValid(token, userDetails)) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userAuthorities); @@ -71,4 +82,4 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull H handlerExceptionResolver.resolveException(request, response, null, e); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/configs/WebConfig.java b/src/main/java/com/harmlessprince/ecommerceApi/configs/WebConfig.java new file mode 100644 index 0000000..ed889a6 --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/configs/WebConfig.java @@ -0,0 +1,14 @@ +package com.harmlessprince.ecommerceApi.configs; + +import com.harmlessprince.ecommerceApi.interceptors.TenantInterceptor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new TenantInterceptor()); + } +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/contexts/TenantContext.java b/src/main/java/com/harmlessprince/ecommerceApi/contexts/TenantContext.java new file mode 100644 index 0000000..e8682dc --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/contexts/TenantContext.java @@ -0,0 +1,22 @@ +package com.harmlessprince.ecommerceApi.contexts; + +import com.harmlessprince.ecommerceApi.tenant.TenantService; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Component; + +@AllArgsConstructor +@Component +public class TenantContext { + private static final ThreadLocal currentTenant = new ThreadLocal<>(); + public static void setCurrentTenant(String tenantId) { + currentTenant.set(tenantId); + } + + public static String getCurrentTenant() { + return currentTenant.get(); + } + + public static void clear() { + currentTenant.remove(); + } +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/custom/AppConstants.java b/src/main/java/com/harmlessprince/ecommerceApi/custom/AppConstants.java new file mode 100644 index 0000000..b31698e --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/custom/AppConstants.java @@ -0,0 +1,18 @@ +package com.harmlessprince.ecommerceApi.custom; + +public class AppConstants { + public static final String CURRENT_USER_TENANT_ID = "CURRENT_USER_TENANT_ID"; + public static final String CURRENT_TENANT_ID_FROM_HEADER = "X-Tenant-Id"; + public static final String CURRENT_USER_ROLE_ID = "CURRENT_USER_ROLE_ID"; + public static final String CURRENT_USER_ROLE_NAME = "CURRENT_USER_ROLE_NAME"; + + // ROLES + public static final String SUPER_ADMIN = "super_admin"; + public static final String ADMIN = "admin"; + public static final String CUSTOMER = "customer"; + public static final String STORE_OWNER = "store_owner"; + + + //PREFIX + public static final String TENANT_PREFIX = "tao_commerce_tenant_:"; +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/interceptors/TenantInterceptor.java b/src/main/java/com/harmlessprince/ecommerceApi/interceptors/TenantInterceptor.java new file mode 100644 index 0000000..4368be6 --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/interceptors/TenantInterceptor.java @@ -0,0 +1,74 @@ +package com.harmlessprince.ecommerceApi.interceptors; + +import com.harmlessprince.ecommerceApi.contexts.TenantContext; +import com.harmlessprince.ecommerceApi.exceptions.CustomBadRequestException; +import com.harmlessprince.ecommerceApi.custom.AppConstants; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +@Slf4j +public class TenantInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String requestURI = request.getRequestURI(); + if (isPathAllowed(requestURI)) { + return true; + } + String currentTenantFromAttribute = (String) request.getAttribute(AppConstants.CURRENT_USER_TENANT_ID); + String currentUserRole = (String) request.getAttribute(AppConstants.CURRENT_USER_ROLE_NAME); + String currentTenantFromHeader = request.getHeader(AppConstants.CURRENT_TENANT_ID_FROM_HEADER); + log.info("Current tenant id from currentTenantFromAttribute: {}", currentTenantFromAttribute); + log.info("Current tenant id from currentTenantFromHeader: {}", currentTenantFromHeader); + String finalTenantId = StringUtils.hasText(currentTenantFromAttribute) + ? currentTenantFromAttribute + : currentTenantFromHeader; + + if (!StringUtils.hasText(finalTenantId) && !Objects.equals(currentUserRole, AppConstants.SUPER_ADMIN)) { + throw new CustomBadRequestException("Tenant ID is missing from headers"); + } + TenantContext.setCurrentTenant(finalTenantId); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + TenantContext.clear(); + } + + private List allowedPaths(){ + List allowedPaths = new ArrayList<>(); + allowedPaths.add("auth/login"); + allowedPaths.add("auth/logout"); + allowedPaths.add("auth/register"); + allowedPaths.add("countries"); + allowedPaths.add("countries"); + allowedPaths.add("local-governments"); + allowedPaths.add("states"); + allowedPaths.add("payment-methods"); + return allowedPaths; + } + + private boolean isPathAllowed(String url) { + // Remove the version part (e.g., "/v1/") from the URL + String path = url.replaceFirst("^/v\\d+/",""); + + // Get the list of allowed paths + List allowedPaths = allowedPaths(); + + // Check if the remaining path is in the allowed paths + for (String allowedPath : allowedPaths) { + if (path.contains(allowedPath)) { + return true; // Return early when a match is found + } + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/order/Order.java b/src/main/java/com/harmlessprince/ecommerceApi/order/Order.java index a9d1921..b128aed 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/order/Order.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/order/Order.java @@ -5,6 +5,7 @@ import com.harmlessprince.ecommerceApi.user.User; import lombok.*; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; @@ -39,4 +40,6 @@ public class Order extends BaseEntity { private PromotionResource promotion; private AnonymousUser anonymousUser; private AddressResource address; + @Indexed + private String tenantId; } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/order/OrderLine.java b/src/main/java/com/harmlessprince/ecommerceApi/order/OrderLine.java index f63249b..d5843b4 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/order/OrderLine.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/order/OrderLine.java @@ -5,6 +5,7 @@ import com.harmlessprince.ecommerceApi.user.User; import lombok.*; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; @@ -28,4 +29,6 @@ public class OrderLine extends BaseEntity { private User user; @DBRef private Order order; + @Indexed + private String tenantId; } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/product/Product.java b/src/main/java/com/harmlessprince/ecommerceApi/product/Product.java index 7e23c52..7642f30 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/product/Product.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/product/Product.java @@ -8,6 +8,7 @@ import org.springframework.context.annotation.Description; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; import java.util.List; @@ -69,8 +70,13 @@ public class Product extends BaseEntity { private Integer rating; private List features; private Set specifications; + + @DBRef private Set variations; + @Indexed + private String tenantId; + public void setNewPrice(Double price) { if (this.discount != null && this.discount > 0) { this.newPrice = Utils.roundTo(price - Utils.percentageOf(this.getDiscount(), price), 2); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/product/ProductMapper.java b/src/main/java/com/harmlessprince/ecommerceApi/product/ProductMapper.java index 8cad618..80d9f25 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/product/ProductMapper.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/product/ProductMapper.java @@ -83,7 +83,7 @@ public Product toEntity(ProductRequest request, Optional brand, Optional< List variations = request.getVariations(); List productColors = new ArrayList<>(); for (ProductColorDTO variation : variations) { - Optional color1 = colorRepository.findByName(variation.getColor()); + Optional color1 = colorRepository.findFirstByName(variation.getColor()); ProductColor productColor = new ProductColor(); productColor.setImage(variation.getImage()); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/product/ProductService.java b/src/main/java/com/harmlessprince/ecommerceApi/product/ProductService.java index d74edd6..561d0fe 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/product/ProductService.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/product/ProductService.java @@ -118,7 +118,7 @@ public Product updateVariation(Product product, ProductColor variation, @Valid U product.getVariations().forEach(v -> { if (v.getId().equals(variation.getId())) { - Optional color1 = colorRepository.findByName(request.getColor()); + Optional color1 = colorRepository.findFirstByName(request.getColor()); v.setImage(Optional.ofNullable(request.getImage()).orElse(variation.getImage())); color1.ifPresent(v::setColor); v.setCustomColor(Optional.ofNullable(request.getCustomColor()).orElse(variation.getCustomColor())); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/productCategory/ProductCategory.java b/src/main/java/com/harmlessprince/ecommerceApi/productCategory/ProductCategory.java index b2e2db4..a0426b1 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/productCategory/ProductCategory.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/productCategory/ProductCategory.java @@ -32,4 +32,7 @@ public class ProductCategory extends BaseEntity { @Indexed(unique = true) private String slug; + @Indexed + private String tenantId; + } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/seeders/BrandSeeder.java b/src/main/java/com/harmlessprince/ecommerceApi/seeders/BrandSeeder.java index 4d74cd5..56a6511 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/seeders/BrandSeeder.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/seeders/BrandSeeder.java @@ -1,5 +1,6 @@ package com.harmlessprince.ecommerceApi.seeders; +import com.harmlessprince.ecommerceApi.Utils; import com.harmlessprince.ecommerceApi.contracts.ISeeder; import com.harmlessprince.ecommerceApi.brand.Brand; import com.harmlessprince.ecommerceApi.brand.BrandRepository; @@ -30,10 +31,11 @@ public void run() { return; } Arrays.stream(brands).forEach(brand -> { - Optional brandOptional = brandRepository.findByName(brand); + Optional brandOptional = brandRepository.findFirstByName(brand); if(brandOptional.isEmpty()) { Brand brandToAdd = new Brand(); brandToAdd.setName(brand); + brandToAdd.setSlug(Utils.slugify(brand)); brandRepository.save(brandToAdd); } }); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/seeders/ColorSeeder.java b/src/main/java/com/harmlessprince/ecommerceApi/seeders/ColorSeeder.java index b21b133..64ae28e 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/seeders/ColorSeeder.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/seeders/ColorSeeder.java @@ -42,7 +42,7 @@ public void run() { for (String[] color : colors) { String name = color[0]; String code = color[1]; - if (colorRepository.findByName(name).isEmpty()) { + if (colorRepository.findFirstByName(name).isEmpty()) { Color col = new Color(); col.setName(name); col.setCode(code); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/seeders/DatabaseSeeder.java b/src/main/java/com/harmlessprince/ecommerceApi/seeders/DatabaseSeeder.java index 5da15ea..2b75b2e 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/seeders/DatabaseSeeder.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/seeders/DatabaseSeeder.java @@ -22,21 +22,21 @@ public class DatabaseSeeder { private final ColorSeeder colorSeeder; private final RoleSeeder roleSeeder; - @PostConstruct +// @PostConstruct private void seed() { roleSeeder.run(); userSeeder.run(); -// brandSeeder.run(); -// countrySeeder.run(); -// stateSeeder.run(); -// colorSeeder.run(); -// specificationSeeder.run(); -// localGovernmentSeeder.run(); -// paymentMethodSeeder.run(); -// productCategorySeeder.run(); -// variationSeeder.run(); -// variationOptionSeeder.run(); -// fileSeeder.run(); -// productSeeder.run(); + brandSeeder.run(); + countrySeeder.run(); + stateSeeder.run(); + colorSeeder.run(); + specificationSeeder.run(); + localGovernmentSeeder.run(); + paymentMethodSeeder.run(); + productCategorySeeder.run(); + variationSeeder.run(); + variationOptionSeeder.run(); + fileSeeder.run(); + productSeeder.run(); } } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/seeders/ProductCategorySeeder.java b/src/main/java/com/harmlessprince/ecommerceApi/seeders/ProductCategorySeeder.java index e5195e8..386af73 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/seeders/ProductCategorySeeder.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/seeders/ProductCategorySeeder.java @@ -91,6 +91,7 @@ private void seedChildrenCategories(String[] categories) { ProductCategory newChildItem = new ProductCategory(); newChildItem.setName(itemCategory); newChildItem.setParentId(parentItem.get().getId()); + newChildItem.setSlug(Utils.slugify(itemCategory)); productCategoryRepository.save(newChildItem); } }); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/seeders/RoleSeeder.java b/src/main/java/com/harmlessprince/ecommerceApi/seeders/RoleSeeder.java index 2b6dce9..90ce7c5 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/seeders/RoleSeeder.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/seeders/RoleSeeder.java @@ -17,7 +17,7 @@ public class RoleSeeder implements ISeeder { @Override public void run() { - String[] roles = new String[]{"Customer", "Admin", "Super Admin",}; + String[] roles = new String[]{"Customer", "Admin", "Super Admin", "Store Owner",}; for (String role : roles) { Role newRole = new Role(); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/seeders/UserSeeder.java b/src/main/java/com/harmlessprince/ecommerceApi/seeders/UserSeeder.java index 0c1490a..6434f6c 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/seeders/UserSeeder.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/seeders/UserSeeder.java @@ -3,8 +3,13 @@ import com.harmlessprince.ecommerceApi.auth.AuthenticationService; import com.harmlessprince.ecommerceApi.auth.RegisterUserRequest; import com.harmlessprince.ecommerceApi.contracts.ISeeder; +import com.harmlessprince.ecommerceApi.custom.AppConstants; import com.harmlessprince.ecommerceApi.role.Role; import com.harmlessprince.ecommerceApi.role.RoleRepository; +import com.harmlessprince.ecommerceApi.tenant.CreateTenantRequest; +import com.harmlessprince.ecommerceApi.tenant.Tenant; +import com.harmlessprince.ecommerceApi.tenant.TenantRepository; +import com.harmlessprince.ecommerceApi.tenant.TenantService; import com.harmlessprince.ecommerceApi.user.User; import com.harmlessprince.ecommerceApi.user.UserRepository; import lombok.AllArgsConstructor; @@ -19,12 +24,15 @@ @Service public class UserSeeder implements ISeeder { private final AuthenticationService authenticationService; + private final TenantService tenantService; + private final TenantRepository tenantRepository; private final UserRepository userRepository; private final RoleRepository roleRepository; private final PasswordEncoder passwordEncoder; + public void run() { - createAdmin(); + createStoreOwners(); createSuperAdmin(); if (userRepository.count() >= 5) { @@ -44,21 +52,54 @@ public void run() { } - private void createAdmin(){ - User user = new User(); - user.setEmail("admin@yopmail.com"); - user.setPassword("password"); - user.setPassword(passwordEncoder.encode(user.getPassword())); - if (authenticationService.userExists(user.getEmail())) { - return; + private void createStoreOwners() { + List> users = new ArrayList<>(); + users.add(Map.of("email", "system@yopmail.com", "name", "System")); + users.add(Map.of("email", "shopthanau@yopmail.com", "name", "Shop Thanau")); + users.add(Map.of("email", "taoforge@yopmail.com", "name", "Taoforge")); + users.add(Map.of("email", "thescentcastle@yopmail.com", "name", "The Scent Castle")); + + + for (Map user : users) { + User systemAdmin = new User(); + Optional retrievedUser = userRepository.findFirstByEmail(user.get("email")); + if(retrievedUser.isPresent()) { + Optional retrievedTenant = tenantService.findByUser(retrievedUser.get()); + if (retrievedTenant.isEmpty()) { + retrievedTenant = tenantRepository.findFirstByName(user.get("name")); + if (retrievedTenant.isEmpty()) { + CreateTenantRequest createTenantRequest = new CreateTenantRequest(); + createTenantRequest.setName(user.get("name")); + createTenantRequest.setUserId(retrievedUser.get().getId()); + Tenant createdTenant = tenantService.create(createTenantRequest); + retrievedTenant = Optional.of(createdTenant); + } + retrievedUser.get().setTenant(retrievedTenant.get()); + userRepository.save(retrievedUser.get()); + }else { + retrievedUser.get().setTenant(retrievedTenant.get()); + userRepository.save(retrievedUser.get()); + } + continue; + } + systemAdmin.setEmail(user.get("email")); + systemAdmin.setPassword("password"); + systemAdmin.setPassword(passwordEncoder.encode(systemAdmin.getPassword())); + Optional adminRole = roleRepository.findFirstBySlug(AppConstants.STORE_OWNER); + adminRole.ifPresent(systemAdmin::setRole); + User createdUser = userRepository.save(systemAdmin); + CreateTenantRequest createTenantRequest = new CreateTenantRequest(); + createTenantRequest.setName(user.get("name")); + createTenantRequest.setUserId(createdUser.getId()); + Tenant createdTenant = tenantService.create(createTenantRequest); + createdUser.setTenant(createdTenant); + userRepository.save(createdUser); + } - Optional adminRole = roleRepository.findFirstBySlug("admin"); - adminRole.ifPresent(user::setRole); - userRepository.save(user); } - private void createSuperAdmin(){ + private void createSuperAdmin() { User user = new User(); user.setEmail("superadmin@yopmail.com"); user.setPassword("password"); @@ -66,9 +107,9 @@ private void createSuperAdmin(){ if (authenticationService.userExists(user.getEmail())) { return; } - Optional role = roleRepository.findFirstBySlug("super_admin"); + Optional role = roleRepository.findFirstBySlug(AppConstants.SUPER_ADMIN); role.ifPresent(user::setRole); userRepository.save(user); } -} +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/seeders/VariationOptionSeeder.java b/src/main/java/com/harmlessprince/ecommerceApi/seeders/VariationOptionSeeder.java index 45ead1f..151a2a4 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/seeders/VariationOptionSeeder.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/seeders/VariationOptionSeeder.java @@ -27,7 +27,7 @@ public void run() { "Size" }; Arrays.stream(variations).forEach(variation -> { - Optional currentVariation = variationRepository.findByName(variation); + Optional currentVariation = variationRepository.findFirstByName(variation); if (currentVariation.isEmpty()) { return; } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/seeders/VariationSeeder.java b/src/main/java/com/harmlessprince/ecommerceApi/seeders/VariationSeeder.java index 2c0fca9..b5bfad6 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/seeders/VariationSeeder.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/seeders/VariationSeeder.java @@ -1,6 +1,7 @@ package com.harmlessprince.ecommerceApi.seeders; +import com.harmlessprince.ecommerceApi.Utils; import com.harmlessprince.ecommerceApi.contracts.ISeeder; import com.harmlessprince.ecommerceApi.productCategory.ProductCategory; import com.harmlessprince.ecommerceApi.variation.Variation; @@ -27,9 +28,10 @@ public void run() { "Size" }; Arrays.stream(variations).forEach(item -> { - if (variationRepository.findByName(item).isEmpty()) { + if (variationRepository.findFirstByName(item).isEmpty()) { Variation newItem = new Variation(); newItem.setName(item); + newItem.setSlug(Utils.slugify(item)); newItem.setProductCategory(productCategory.get()); variationRepository.save(newItem); } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/tenant/CreateTenantRequest.java b/src/main/java/com/harmlessprince/ecommerceApi/tenant/CreateTenantRequest.java new file mode 100644 index 0000000..8968191 --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/tenant/CreateTenantRequest.java @@ -0,0 +1,14 @@ +package com.harmlessprince.ecommerceApi.tenant; + + +import lombok.*; + +@Setter +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class CreateTenantRequest { + private String name; + private String userId; + +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/tenant/Tenant.java b/src/main/java/com/harmlessprince/ecommerceApi/tenant/Tenant.java new file mode 100644 index 0000000..19dc1df --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/tenant/Tenant.java @@ -0,0 +1,36 @@ +package com.harmlessprince.ecommerceApi.tenant; + + +import com.harmlessprince.ecommerceApi.bases.BaseEntity; +import com.harmlessprince.ecommerceApi.user.User; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +@Builder +@Setter +@Getter +@Document("tenants") +@NoArgsConstructor +@AllArgsConstructor +public class Tenant extends BaseEntity { + + @Id + private String id; + + @Field + @Indexed(unique = true) + private String name; + + @Field + @Indexed(unique = true) + private String tenantId; + + private Boolean status = true; + + @DBRef + private User user; +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantController.java b/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantController.java new file mode 100644 index 0000000..583da57 --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantController.java @@ -0,0 +1,4 @@ +package com.harmlessprince.ecommerceApi.tenant; + +public class TenantController { +} diff --git a/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantRepository.java b/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantRepository.java new file mode 100644 index 0000000..7f0a008 --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantRepository.java @@ -0,0 +1,19 @@ +package com.harmlessprince.ecommerceApi.tenant; + +import com.harmlessprince.ecommerceApi.user.User; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface TenantRepository extends MongoRepository { + Optional findFirstByTenantId(String tenantId); + Optional findFirstByName(String name); + Boolean existsByTenantId(String tenantId); + + @Query("{ 'user': ?0 }") + Optional findByUser(User user); +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantService.java b/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantService.java new file mode 100644 index 0000000..efdf5cc --- /dev/null +++ b/src/main/java/com/harmlessprince/ecommerceApi/tenant/TenantService.java @@ -0,0 +1,91 @@ +package com.harmlessprince.ecommerceApi.tenant; + +import com.harmlessprince.ecommerceApi.exceptions.CustomResourceNotFoundException; +import com.harmlessprince.ecommerceApi.user.User; +import com.harmlessprince.ecommerceApi.user.UserService; +import com.harmlessprince.ecommerceApi.custom.AppConstants; +import lombok.AllArgsConstructor; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.text.Normalizer; +import java.time.LocalDate; +import java.util.Date; +import java.util.Locale; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Service +@AllArgsConstructor +public class TenantService { + private TenantRepository tenantRepository; + private UserService userService; + private ValueOperations valueOperations; + + + public Tenant create(CreateTenantRequest createTenantRequest) { + User user = this.userService.findById(createTenantRequest.getUserId()); + Tenant tenant = Tenant.builder() + .user(user) + .name(createTenantRequest.getName().trim()) + .tenantId(generateTenantId(createTenantRequest.getName())) + .build(); + return this.tenantRepository.save(tenant); + } + + public Tenant findByName(String tenantName) { + return this.tenantRepository.findFirstByName(tenantName).orElseThrow(() -> new CustomResourceNotFoundException("Tenant not found")); + } + + + public Optional findByUser(User user) { + return this.tenantRepository.findByUser(user); + } + + public String validateTenantId(String value) { + String key = AppConstants.TENANT_PREFIX + value; + String tenantId = valueOperations.get(key); + if (!StringUtils.hasText(tenantId)) { + this.tenantRepository.findFirstByTenantId(value).orElseThrow(() -> new CustomResourceNotFoundException("Tenant not found")); + valueOperations.set(key, value, 12, TimeUnit.HOURS); + } + return tenantId; + } + + + public String generateTenantId(String name) { + // Normalize name to remove special characters and convert to lowercase + String baseId = normalizeName(name); + + String tenantId = baseId; + Random random = new Random(); + // Ensure uniqueness by checking against the database + do { + LocalDate currentDate = LocalDate.now(); + var year = currentDate.getYear(); + var month = currentDate.getMonthValue(); + var day = currentDate.getDayOfMonth(); + int randomNumber = random.nextInt(1_000_000); + String paddedNumber = String.format("%07d", randomNumber); + tenantId = baseId.toUpperCase() + "-" + paddedNumber.trim() + "-" + month + "-" + day + "-" + year; + } while (this.tenantRepository.existsByTenantId(tenantId)); + return tenantId; + } + + + private String normalizeName(String name) { + if (!StringUtils.hasText(name)) { + throw new IllegalArgumentException("Tenant name cannot be null or empty"); + } + + String normalized = Normalizer.normalize(name, Normalizer.Form.NFD) + .replaceAll("\\p{M}", ""); // Remove accents + normalized = normalized.toLowerCase(Locale.ENGLISH) + .replaceAll("[^a-z0-9]", "-") // Replace non-alphanumeric characters with hyphens + .replaceAll("-+", "-") // Replace multiple hyphens with a single one + .replaceAll("^-|-$", ""); // Trim leading and trailing hyphens + return normalized; + } +} \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/user/User.java b/src/main/java/com/harmlessprince/ecommerceApi/user/User.java index d11310c..2f87963 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/user/User.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/user/User.java @@ -3,6 +3,7 @@ import com.harmlessprince.ecommerceApi.address.Address; import com.harmlessprince.ecommerceApi.bases.BaseEntity; import com.harmlessprince.ecommerceApi.role.Role; +import com.harmlessprince.ecommerceApi.tenant.Tenant; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.Id; @@ -52,6 +53,9 @@ public class User extends BaseEntity implements UserDetails { @DBRef private Role role; + @DBRef + private Tenant tenant; + @Override public Collection getAuthorities() { return List.of(); diff --git a/src/main/java/com/harmlessprince/ecommerceApi/user/UserRepository.java b/src/main/java/com/harmlessprince/ecommerceApi/user/UserRepository.java index 9b3fbbc..0a11cbd 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/user/UserRepository.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/user/UserRepository.java @@ -6,4 +6,6 @@ public interface UserRepository extends MongoRepository { Optional findByEmail(String email); + + Optional findFirstByEmail(String email); } diff --git a/src/main/java/com/harmlessprince/ecommerceApi/variation/Variation.java b/src/main/java/com/harmlessprince/ecommerceApi/variation/Variation.java index 9d226b4..f2fcfc4 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/variation/Variation.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/variation/Variation.java @@ -6,6 +6,7 @@ import lombok.*; import org.springframework.context.annotation.Description; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; import java.time.LocalDateTime; @@ -40,6 +41,7 @@ public class Variation { // a category of mobile phone may have variation of color, storage capacity and screen size (1, 3, colour, size) // @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.REMOVE) // @JoinColumn(name = "category_id") + @DBRef private ProductCategory productCategory; diff --git a/src/main/java/com/harmlessprince/ecommerceApi/variation/VariationRepository.java b/src/main/java/com/harmlessprince/ecommerceApi/variation/VariationRepository.java index 15f792d..708ec11 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/variation/VariationRepository.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/variation/VariationRepository.java @@ -5,6 +5,6 @@ import java.util.Optional; public interface VariationRepository extends MongoRepository { - Optional findByName(String name); + Optional findFirstByName(String name); } \ No newline at end of file diff --git a/src/main/java/com/harmlessprince/ecommerceApi/variationOption/VariationOption.java b/src/main/java/com/harmlessprince/ecommerceApi/variationOption/VariationOption.java index 9884056..96883fa 100644 --- a/src/main/java/com/harmlessprince/ecommerceApi/variationOption/VariationOption.java +++ b/src/main/java/com/harmlessprince/ecommerceApi/variationOption/VariationOption.java @@ -5,6 +5,7 @@ import com.harmlessprince.ecommerceApi.variation.Variation; import lombok.*; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; import java.time.LocalDateTime; @@ -29,22 +30,6 @@ public class VariationOption extends BaseEntity { // a size variation may have XS, S, M, L, XL // a color variation may have grey, black, dark and blue -// @ManyToOne(fetch = FetchType.EAGER) -// @JoinColumn(name = "variation_id") + @DBRef private Variation variation; - -// @ManyToMany( -// fetch = FetchType.LAZY, -// mappedBy = "variationOptions" -// ) -// private Set productItem = new HashSet<>(); - -// @PrePersist -// public void onPrePersist() { -// this.setSlug(value.toLowerCase().replace(" ", "_").replace("-", "_")); -// } -// @PreUpdate -// public void onPreUpdate() { -// this.setSlug(value.toLowerCase().replace(" ", "_").replace("-", "_")); -// } }