From a0c9d0375b4225456d975ecc731fc2947e3ba2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Dufour?= Date: Sun, 2 Feb 2025 00:13:31 +0100 Subject: [PATCH] feat: create activity log request --- .../api/config/ActivityInterceptor.java | 34 +++++ .../org/blueline/api/config/WebConfig.java | 14 ++ .../api/controller/ActivityLogController.java | 64 +++++++++ .../api/controller/UserController.java | 15 ++ .../org/blueline/api/model/ActivityLog.java | 29 ++++ .../java/org/blueline/api/model/User.java | 2 +- .../api/model/dto/ActiveUsersDto.java | 18 +++ .../org/blueline/api/model/enums/Period.java | 8 ++ .../blueline/api/model/enums/UserAction.java | 9 ++ .../api/repository/ActivityLogRepository.java | 28 ++++ .../api/service/ActivityLogService.java | 129 ++++++++++++++++++ .../org/blueline/api/service/AuthService.java | 11 ++ .../0010-create-activity-logs-table.sql | 8 ++ .../0011-increase-avatar-field-length.sql | 5 + .../db/migrations/db.changelog-master.yaml | 22 +++ .../org/blueline/api/config/WebConfig$1.class | Bin 1692 -> 1692 bytes .../org/blueline/api/config/WebConfig.class | Bin 904 -> 1958 bytes .../api/controller/UserController.class | Bin 9276 -> 9600 bytes .../classes/org/blueline/api/model/User.class | Bin 5975 -> 5994 bytes .../blueline/api/service/AuthService.class | Bin 3214 -> 3499 bytes .../blueline/api/service/UserService.class | Bin 7161 -> 7145 bytes 21 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/blueline/api/config/ActivityInterceptor.java create mode 100644 src/main/java/org/blueline/api/controller/ActivityLogController.java create mode 100644 src/main/java/org/blueline/api/model/ActivityLog.java create mode 100644 src/main/java/org/blueline/api/model/dto/ActiveUsersDto.java create mode 100644 src/main/java/org/blueline/api/model/enums/Period.java create mode 100644 src/main/java/org/blueline/api/model/enums/UserAction.java create mode 100644 src/main/java/org/blueline/api/repository/ActivityLogRepository.java create mode 100644 src/main/java/org/blueline/api/service/ActivityLogService.java create mode 100644 src/main/resources/db/migrations/0010-create-activity-logs-table.sql create mode 100644 src/main/resources/db/migrations/0011-increase-avatar-field-length.sql diff --git a/src/main/java/org/blueline/api/config/ActivityInterceptor.java b/src/main/java/org/blueline/api/config/ActivityInterceptor.java new file mode 100644 index 0000000..c037a6b --- /dev/null +++ b/src/main/java/org/blueline/api/config/ActivityInterceptor.java @@ -0,0 +1,34 @@ +package org.blueline.api.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.blueline.api.service.ActivityLogService; +import org.blueline.api.service.AuthService; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class ActivityInterceptor implements HandlerInterceptor { + + private final ActivityLogService activityLogService; + private final AuthService authService; + + public ActivityInterceptor(ActivityLogService activityLogService, AuthService authService) { + this.activityLogService = activityLogService; + this.authService = authService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + String action = request.getMethod(); + String endpoint = request.getRequestURI(); + + activityLogService.saveActivity(authentication, action, endpoint); + return true; + } +} + diff --git a/src/main/java/org/blueline/api/config/WebConfig.java b/src/main/java/org/blueline/api/config/WebConfig.java index b3bad9c..d1f7792 100644 --- a/src/main/java/org/blueline/api/config/WebConfig.java +++ b/src/main/java/org/blueline/api/config/WebConfig.java @@ -5,12 +5,26 @@ import org.springframework.lang.NonNull; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { + private final ActivityInterceptor activityInterceptor; + + public WebConfig(ActivityInterceptor activityInterceptor) { + this.activityInterceptor = activityInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(activityInterceptor) + .addPathPatterns("/api/**") + .excludePathPatterns("/api/users/login", "/api/users/register"); + } + @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { diff --git a/src/main/java/org/blueline/api/controller/ActivityLogController.java b/src/main/java/org/blueline/api/controller/ActivityLogController.java new file mode 100644 index 0000000..4d1b25a --- /dev/null +++ b/src/main/java/org/blueline/api/controller/ActivityLogController.java @@ -0,0 +1,64 @@ +package org.blueline.api.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.blueline.api.model.dto.ActiveUsersDto; +import org.blueline.api.model.dto.ExceptionDto; +import org.blueline.api.model.enums.Gender; +import org.blueline.api.model.enums.Period; +import org.blueline.api.model.enums.Status; +import org.blueline.api.model.enums.UserAction; +import org.blueline.api.service.ActivityLogService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/stats") +@RequiredArgsConstructor +@Validated +@Tag(name = "Activity", description = "Manage activity statistics of users") +public class ActivityLogController { + + private final ActivityLogService activityLogService; + + @GetMapping("/activeUsers") + @Operation( + summary = "Get number of active users by period", + description = "Get number of active users by period", + responses = { + @ApiResponse(responseCode = "200", description = "Actives users found"), + @ApiResponse(responseCode = "400", description = "Bad request", content = @Content(schema = @Schema(implementation = ExceptionDto.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(schema = @Schema(implementation = ExceptionDto.class))), + @ApiResponse(responseCode = "403", description = "Forbidden", content = @Content(schema = @Schema(implementation = ExceptionDto.class))), + @ApiResponse(responseCode = "404", description = "Actives users not found", content = @Content(schema = @Schema(implementation = ExceptionDto.class))) + }, + security = @SecurityRequirement(name = "bearerAuth") + ) + public ResponseEntity getActiveUsers(Authentication authentication, @RequestParam String startDate, + @RequestParam String endDate, + @RequestParam Period period, + @RequestParam(required = false) Status userStatus, + @RequestParam(required = false) Gender gender, + @RequestParam(required = false) UserAction userAction) { + return activityLogService.getActiveUsers( + authentication, + startDate, + endDate, + period, + userStatus, + gender, + userAction + ); + } + +} diff --git a/src/main/java/org/blueline/api/controller/UserController.java b/src/main/java/org/blueline/api/controller/UserController.java index 4647b01..f05a1b7 100644 --- a/src/main/java/org/blueline/api/controller/UserController.java +++ b/src/main/java/org/blueline/api/controller/UserController.java @@ -64,6 +64,21 @@ public ResponseEntity login( return new ResponseEntity<>(authService.login(loginDto), HttpStatus.OK); } + @PostMapping("/login/admin") + @Operation( + summary = "Login an admin user", + description = "There is no need to be authenticated to login a user. The token returned is used to authenticate the user in the future.", + responses = { + @ApiResponse(responseCode = "200", description = "User logged in"), + @ApiResponse(responseCode = "400", description = "Bad request", content = @Content(schema = @Schema(implementation = ExceptionDto.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(schema = @Schema(implementation = ExceptionDto.class))) + } + ) + public ResponseEntity loginAdmin( + @Valid @RequestBody LoginDto loginDto) { + return new ResponseEntity<>(authService.loginAdmin(loginDto), HttpStatus.OK); + } + @GetMapping("/me") @Operation( summary = "Get the authenticated user", diff --git a/src/main/java/org/blueline/api/model/ActivityLog.java b/src/main/java/org/blueline/api/model/ActivityLog.java new file mode 100644 index 0000000..a5e871a --- /dev/null +++ b/src/main/java/org/blueline/api/model/ActivityLog.java @@ -0,0 +1,29 @@ +package org.blueline.api.model; +import jakarta.persistence.*; +import lombok.Data; +import org.blueline.api.model.enums.UserAction; + +import java.time.LocalDateTime; + +@Entity +@Data +@Table(name = "activity_logs") +public class ActivityLog { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private UserAction action; + + @Column(nullable = false) + private LocalDateTime timestamp; + +} + diff --git a/src/main/java/org/blueline/api/model/User.java b/src/main/java/org/blueline/api/model/User.java index 80b78ad..020e53e 100644 --- a/src/main/java/org/blueline/api/model/User.java +++ b/src/main/java/org/blueline/api/model/User.java @@ -40,7 +40,7 @@ public class User { @Column(name = "gender") private Gender gender; - @Column(name = "avatar") + @Column(name = "avatar", length = 750) private String avatar; @Enumerated(EnumType.STRING) diff --git a/src/main/java/org/blueline/api/model/dto/ActiveUsersDto.java b/src/main/java/org/blueline/api/model/dto/ActiveUsersDto.java new file mode 100644 index 0000000..ef27e14 --- /dev/null +++ b/src/main/java/org/blueline/api/model/dto/ActiveUsersDto.java @@ -0,0 +1,18 @@ +package org.blueline.api.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ActiveUsersDto { + + private int totalActiveUsers; + + private Map activeUsersPerPeriod; +} diff --git a/src/main/java/org/blueline/api/model/enums/Period.java b/src/main/java/org/blueline/api/model/enums/Period.java new file mode 100644 index 0000000..f9f289d --- /dev/null +++ b/src/main/java/org/blueline/api/model/enums/Period.java @@ -0,0 +1,8 @@ +package org.blueline.api.model.enums; + +public enum Period { + DAY, + WEEK, + MONTH, + YEAR +} diff --git a/src/main/java/org/blueline/api/model/enums/UserAction.java b/src/main/java/org/blueline/api/model/enums/UserAction.java new file mode 100644 index 0000000..6d52148 --- /dev/null +++ b/src/main/java/org/blueline/api/model/enums/UserAction.java @@ -0,0 +1,9 @@ +package org.blueline.api.model.enums; + +public enum UserAction { + CREATE_EVENT, + CREATE_CHALLENGE, + JOIN_EVENT, + JOIN_CHALLENGE, + OTHER +} diff --git a/src/main/java/org/blueline/api/repository/ActivityLogRepository.java b/src/main/java/org/blueline/api/repository/ActivityLogRepository.java new file mode 100644 index 0000000..8271221 --- /dev/null +++ b/src/main/java/org/blueline/api/repository/ActivityLogRepository.java @@ -0,0 +1,28 @@ +package org.blueline.api.repository; + +import org.blueline.api.model.ActivityLog; +import org.blueline.api.model.enums.Gender; +import org.blueline.api.model.enums.Status; +import org.blueline.api.model.enums.UserAction; +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.List; + +public interface ActivityLogRepository extends JpaRepository { + + @Query("SELECT a FROM ActivityLog a " + + "WHERE a.timestamp BETWEEN :start AND :end " + + "AND (:userStatus IS NULL OR a.user.status = :userStatus) " + + "AND (:gender IS NULL OR a.user.gender = :gender)" + + "AND (:userAction IS NULL OR a.action = :userAction)") + List findActiveUsers( + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("userStatus") Status userStatus, + @Param("gender") Gender gender, + @Param("userAction") UserAction userAction + ); +} diff --git a/src/main/java/org/blueline/api/service/ActivityLogService.java b/src/main/java/org/blueline/api/service/ActivityLogService.java new file mode 100644 index 0000000..ad07f25 --- /dev/null +++ b/src/main/java/org/blueline/api/service/ActivityLogService.java @@ -0,0 +1,129 @@ +package org.blueline.api.service; + +import lombok.RequiredArgsConstructor; +import org.blueline.api.exception.UnauthorizedException; +import org.blueline.api.model.ActivityLog; +import org.blueline.api.model.User; +import org.blueline.api.model.dto.ActiveUsersDto; +import org.blueline.api.model.enums.Gender; +import org.blueline.api.model.enums.Period; +import org.blueline.api.model.enums.Status; +import org.blueline.api.model.enums.UserAction; +import org.blueline.api.repository.ActivityLogRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.security.core.Authentication; + + +import java.sql.Timestamp; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class ActivityLogService { + + private final ActivityLogRepository activityLogRepository; + private final AuthService authService; + + public void saveActivity(Authentication authentication, String action, String endpoint) { + ActivityLog activityLog = new ActivityLog(); + + User user = authService.authenticate(authentication); + activityLog.setUser(user); + activityLog.setAction(getUserAction(action, endpoint)); + activityLog.setTimestamp(new Timestamp(System.currentTimeMillis()).toLocalDateTime()); + + activityLogRepository.save(activityLog); + } + + private UserAction getUserAction(String action, String endpoint) { + String joinEventRegex = "^api/events/\\d+/join$"; + Pattern joinEventPattern = Pattern.compile(joinEventRegex); + + String joinChallengeRegex = "^api/challenge-completions/challenge/\\d+$"; + Pattern joinChallengePattern = Pattern.compile(joinChallengeRegex); + + if(Objects.equals(action, "POST") && joinEventPattern.matcher(endpoint).matches()) { + return UserAction.JOIN_EVENT; + } else if(Objects.equals(action, "POST") && joinChallengePattern.matcher(endpoint).matches()) { + return UserAction.JOIN_CHALLENGE; + } else if (Objects.equals(action, "POST") && Objects.equals(endpoint, "api/challenges")) { + return UserAction.CREATE_CHALLENGE; + } else if (Objects.equals(action, "POST") && Objects.equals(endpoint, "api/events")) { + return UserAction.CREATE_EVENT; + } else { + return UserAction.OTHER; + } + + } + + public ResponseEntity getActiveUsers(Authentication authentication, + String startDate, + String endDate, + Period period, + Status userStatus, + Gender gender, + UserAction userAction) { + User user = authService.authenticate(authentication); + if(!user.isAdmin()) { + throw new UnauthorizedException("You do not have permission to get users statistics"); + } + + LocalDateTime start = LocalDate.parse(startDate).atStartOfDay(); + LocalDateTime end = LocalDate.parse(endDate).atTime(23, 59, 59); + + List activityLogs = activityLogRepository.findActiveUsers( + start, + end, + userStatus, + gender, + userAction + ); + + int totalActiveUsersCount = (int) activityLogs.stream() + .map(ActivityLog::getUser) + .distinct() + .count(); + + Map> activeUsersByPeriod = new TreeMap<>(); + + for (ActivityLog log : activityLogs) { + LocalDateTime timestamp = log.getTimestamp(); + Long userId = log.getUser().getId(); + + + LocalDateTime periodStart = getPeriodStart(timestamp, period); + + activeUsersByPeriod + .computeIfAbsent(Timestamp.valueOf(periodStart.toLocalDate().atStartOfDay()), k -> new HashSet<>()) + .add(userId); + } + + Map activeUsersCountByPeriod = activeUsersByPeriod.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue().size() + )); + + ActiveUsersDto activeUsersDto = new ActiveUsersDto(); + activeUsersDto.setTotalActiveUsers(totalActiveUsersCount); + activeUsersDto.setActiveUsersPerPeriod(activeUsersCountByPeriod); + + + return ResponseEntity.ok(activeUsersDto); + } + + private LocalDateTime getPeriodStart(LocalDateTime timestamp, Period period) { + return switch (period) { + case DAY -> timestamp.toLocalDate().atStartOfDay(); + case WEEK -> timestamp.with(DayOfWeek.MONDAY).toLocalDate().atStartOfDay(); + case MONTH -> timestamp.withDayOfMonth(1).toLocalDate().atStartOfDay(); + case YEAR -> timestamp.withDayOfYear(1).toLocalDate().atStartOfDay(); + }; + } +} diff --git a/src/main/java/org/blueline/api/service/AuthService.java b/src/main/java/org/blueline/api/service/AuthService.java index 315ad9b..2295598 100644 --- a/src/main/java/org/blueline/api/service/AuthService.java +++ b/src/main/java/org/blueline/api/service/AuthService.java @@ -25,6 +25,17 @@ public String login(LoginDto loginDto) { throw new UnauthorizedException("Invalid login credentials"); } + public String loginAdmin(LoginDto loginDto) { + User user = userRepository.findByEmail(loginDto.getEmail()); + if(user != null && !user.isAdmin()) { + throw new UnauthorizedException("You do not have permission get all users"); + } + if (user != null && passwordEncoder.matches(loginDto.getPassword(), user.getPassword())) { + return jwtService.generateToken(user); + } + throw new UnauthorizedException("Invalid login credentials"); + } + public User authenticate(Authentication authentication) { return userRepository.findById(jwtService.getUserIdFromToken(authentication.getPrincipal().toString())).orElseThrow(() -> new UnauthorizedException("Invalid token")); } diff --git a/src/main/resources/db/migrations/0010-create-activity-logs-table.sql b/src/main/resources/db/migrations/0010-create-activity-logs-table.sql new file mode 100644 index 0000000..6a6ea25 --- /dev/null +++ b/src/main/resources/db/migrations/0010-create-activity-logs-table.sql @@ -0,0 +1,8 @@ +-- liquibase formatted sql + +-- changeset aurel:1738433636225-1 +CREATE TABLE activity_logs (id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, action VARCHAR(255) NOT NULL, timestamp TIMESTAMP(6) WITHOUT TIME ZONE NOT NULL, user_id BIGINT NOT NULL, CONSTRAINT "activity_logsPK" PRIMARY KEY (id)); + +-- changeset aurel:1738433636225-2 +ALTER TABLE activity_logs ADD CONSTRAINT "FK5bm1lt4f4eevt8lv2517soakd" FOREIGN KEY (user_id) REFERENCES users (id); + diff --git a/src/main/resources/db/migrations/0011-increase-avatar-field-length.sql b/src/main/resources/db/migrations/0011-increase-avatar-field-length.sql new file mode 100644 index 0000000..ac9a6f6 --- /dev/null +++ b/src/main/resources/db/migrations/0011-increase-avatar-field-length.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql + +-- changeset aurel:1738451427408-1 +ALTER TABLE users ALTER COLUMN avatar TYPE VARCHAR(750) USING (avatar::VARCHAR(750)); + diff --git a/src/main/resources/db/migrations/db.changelog-master.yaml b/src/main/resources/db/migrations/db.changelog-master.yaml index 9d32829..176d7c9 100644 --- a/src/main/resources/db/migrations/db.changelog-master.yaml +++ b/src/main/resources/db/migrations/db.changelog-master.yaml @@ -87,4 +87,26 @@ databaseChangeLog: relativeToChangelogFile: true encoding: UTF-8 splitStatements: true + stripComments: true + + - changeSet: + id: 0010-create-activity-logs-table + author: Aurélien + changes: + - sqlFile: + path: 0010-create-activity-logs-table.sql + relativeToChangelogFile: true + encoding: UTF-8 + splitStatements: true + stripComments: true + + - changeSet: + id: 0011-increase-avatar-field-length + author: Aurélien + changes: + - sqlFile: + path: 0011-increase-avatar-field-length.sql + relativeToChangelogFile: true + encoding: UTF-8 + splitStatements: true stripComments: true \ No newline at end of file diff --git a/target/classes/org/blueline/api/config/WebConfig$1.class b/target/classes/org/blueline/api/config/WebConfig$1.class index a3922f4a16dee860cea8baed4fc68839c1f07ec7..4116f59a4be9f681a79fbd6c9c93d481a4148e24 100644 GIT binary patch delta 35 rcmbQkJBN3}Ojbs@$+K7|3Mw-2Gbl0WGbl4SFsLxNFsM$pVM_o2nUDuM delta 35 rcmbQkJBN3}OjbsL$+K7|3JNpuGl($gGl()cFo-d@Fo;jKVM_o2l!ylA diff --git a/target/classes/org/blueline/api/config/WebConfig.class b/target/classes/org/blueline/api/config/WebConfig.class index d9d195084c1f92ed0300cb8d24d815d391e60cad..0d3e895d82bda02d7c8a829869d4b9adc4eef636 100644 GIT binary patch literal 1958 zcmb_d>rN9v6#j-5wk@@2Q4udFg7kv8B8Zowa)~Gg)QBk|{&Cw5WptS(yHk*#zL6%1 ziAEp5hcceoEoH@0FQLhFX3m^*zH{I2KR$m0Fpa4M+7K}iO`;tg3`bvCo0e%?vT8ml zz2arX&@m~bP7~%#JNpvE~(C@favt-w}EhIOs4PllYxhksWGB3^gCx&jT zti+~J+w)R!x6C(`<1&ma93(pZJ9{C46uJx?P2w2R4EMFHx8Vv|U3IN>zU8>D%`ILs zJ??JWTzma|^+QWmZ0?4{3b{WD)uYnV z9^cM_f#bhqVM%E(V>roR_yNx66F7xl1E-TXgR>0thY<`PoS};X^3+mmv?y27V|bZ; zv9MPYfi;Kfa14FquyW07SNepPuyZM{4U90f0#Re`h z3>;Kjjj|jZO<)iiUG-Upp&;!o3%QUh8pt!u{Cl_wb#b#C^h3?%RQ*>E25eUTxgtY! z)~RsH&;oVZg8r!)6wi!}E>t4Sr+ zc~N+RVm#d#D-@StknNus6bi-PC^OXYGu)D-E3xF%XbR2=ox4;c4OeuNF-$a#MiXtj zeg;#6VXCR%R?C@qU+Rhp{3XjeA9GJFa$Q>FS8`rT?#|kl=W!4HH22%UkfW8>v!A{m z5jwYHnCO@3Hb;9K?TP#+u>9~x81L!Uh7tNt5ie+TM@cr0Zd{?;$$%RsuF{R*X^eehoBIy%LJLqW-&gYg^qUrNQ=wIH$P7^B7 zL+CU((2F6Q4&i4S_!*4jIziU)$9?<>+^FM^`R)SoFBn`Q7l+;vL*QKpNdbMn)}()q n*R|dxx_-Sydla{Qi2^iLe|9HnzeDRTttfE~;-3GaPyX*GX?-|d delta 282 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 DfGx=I literal 9276 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 diff --git a/target/classes/org/blueline/api/model/User.class b/target/classes/org/blueline/api/model/User.class index 98fa61397e6c8b732a5a3861a4d63c43b49b9670..62ccdb537005f5ae77c224e981d5a08b0f4f3150 100644 GIT binary patch literal 5994 zcmc&&`Fj(`8GcvR;bVh^i2=tD?%)FufshnrXuvU`)CP(jr#NlN+FIv;k7mrb&Njo~M6I|AMyZ`|XU3EUhE`*5~nPXTJI7oo~MRuJJ$r zd;1Ll$MN?RVg~xX@_c5tP_YY+YiF#IlPP+6yO4Rzx67R{5I2!P(m?y1<&IscSOo*U z1LK#iB`Z^~-1*Ge*~|7^kR6;6=5`Yu=roX+w}V{XK+nKn4ePk)&SyJe;;t0dnCKSR zbUl)Rq%GU{!noE%57rTP!SWYIDVKq^fx(JYCQ>k!o-;>{hU^n(9fB8ztG!>GbY*+ z;-rZwJjRXg}o~2dGsZV?sN8%f%~EyqtPNnqz{?+ zu*fMQ9S<);s85=(FsrCa$t?(~Z$sASfl7s6YZH?aVR2F_g`U19QTs~53s^gRp; zizW&v8rV>@7VS~bowI_+oq(R_(lSC|o~k7n)gs0KC?W6>%HR;FZ^ea8=Mch1h_@>zO)ZdW72I#?YAIZidt~g)RBq}5!&KxRqh6s>bcr2Tv8F@Cjf)bXk`>pvQnA~3hXyxy zD}{o^L&=zCu{9EJjFCYJP0U1S#0KtNl~s$GoSZtJJ9CO#7HoGuSl|}1e{oC4zi6od z&@t)EyNn}c8e)cU%oJ548(Gce!bqGwMnR%wov_@iQ=a^aZi`l_Wam#%w`PgACMs;cCg^(&sk+Aihx$d)T=Ep=PpIqV9$8LE6#e0 z61(qQ)4B7x35E?L#|tRiex(r5+rtg-ulMK{l%4QZo?h4B)q~SiE@x|E_lmTy-ALHF z$W{&6YE%u>YE%u+R1Z1Bpv(xzZZ#AS46JqoE0UM&CCgN}RDBium|&YgD88&fh!8qe z#Rq1D(oM?B=-8w6QMKD};rf*}M6cCXd!UZHsp3O10D3m2mO)08VbePM1?@Kvh)eh3nUtqlm+_jxMARF*=E4WA2s%DU9MkbP_0nH z37v4IU#&-^AC@7~nGeef=}}%56Vmg;;y`-3B#>@xy>-+W>`vlVCtky!Q@AZD=XFNO zJB6U*E_sV~MyW?-p_A5J;Fam{FVtG9W>n`oZMpe^?e84-yv0f>s{&70qH0^Mp{!UK z2DX|>&Hh9UsaoXLkTk@oRVYk4fdp_f=h9I}sX*WMQ+R`ARKuB~L`~sM#@%;krMIhd zVH@MQ$*A6i82j5fayeSdk;iX|<104G@JV2Gs;9g74cIC^i7}}~8z%-i!Fp6rq{9_4 zS+RttX7CZtvM_C&&EV<`KFV2EsCu?HT-zpV52#rUz>-z7tmlG{vHx)!Pva9+*$$D) z+Enw@h;SIssY|K#~Jbrr+2q3%REpJDr~kI$i>9X?FZ zhR>7bWiloB9UUHe35odi;kRJCh(z1<;WuGS>>0X=ws6Q!>dYSg9EtCJ8(n?ze_-u3 zn0x!;H_;Vx#IF;k4PW4Il3M_4@J0SUpy2T(e3>I|gs)FJcT+G-WKu0vAV5V{W5M- zG^K$Ixr8q<^eVQzybPwUeSHm3Rlh5N_-u**+EEYG*A$5FsR*Fm^+21N0`UPB0W?$( zw52H!Uu+RTd+UL=HwEGoF9K+PJ%-_rp9lW6$8t-`AsK*MJm;C~tozrR&@RYYj`0}b#$!_T9x_F${s+%LlrKr*M*DUEr(W{Rj*{$ ztdtdOC)iH1<=DpA&a$0<4b%L5knN)C&ar!m-P^b<6; n2usCoo?H-Kp%|=g|A;?v(9Xe4wpZ~Nj^y8Pf5qSM7W)1N05QZE literal 5975 zcmc&&`Fm7X8Gi3%wj{S?2n~=18rG5xhC*qD30O!2Oichuno6qDo6IG-Wai#s?wwTB zTI#-3rLMK^TX8`}El{b}`ip%Y|0cD*XPIP{o6h{^^W@Au=R4nd&v(A_Z72Ww@4dGG z9LL)UL=5zKrG@ldzHH|m*G^kSCtdJzc0T=-Z zaa(NT3&!;(y0L+{i`*hKD8$8~~Za$+WdUIXhTWI1s1>C;>` zWd|MDjGlJNx5dO(JZQkY?D4d@@nzc$d;?udNV$aU8Ns^E#CGg35OH{pRQ)Lg$1W4Q z@sNS`OHRoTT&qCox~mwcf|A1n3)TS>gMu}gw-l!!4x1Q}#7iZ|c5~UBAnr4fmc*jv z`_FkLNqpGE0YQw}10$1841G^{IxJTP97yR@o$tGoapNYc)EY2$o ztdc;>m>9)TlKj95%2Y@bv?<9Z(cW+3aU3_0aQx%Bg5w%6q$P}*cmm_pG%exes($XT zF$k?8VL7KvoEH3@DWX5a}Z(J}TrC zA|00(A*j!qurQ~nO35tg(VYt z6bx)CSWEVp=gwQf(@sFob7>jURoGmh`f z(Q(o9@W5RVk}vuz)BbGlJEF|?atfjY%W8u51pb_*(EEmb2Gvg$d>d2FlVG+1GDls(#v?C^4ZF5<7ImGI^C-Pol&RY`Edi7$?ozG4(P#9U> za>@3~`GEc{H+;C>hg(o~%vYItLxUd=&QQ6Wt%}w)iC#O7VCy7XB}l7LB{Zv1B_LBh zjrAlmDo!xQvp)-RkUMDZ33bAG6F&fp_5Z|U{+ANNU4p7-C7?N zyOj$!*4hyLRA23ZI_{>5mtq87C1rF>dF9f)ebNz@+AgeapYWQsJb#>lDiD6q7wVl- zD9*;p1frFtPIz7*;!jaopr6E_4LmQl8E|q(jgf6D6^j^DY7#l2Q>^G$DiG0^g@@=Y z$U;K&C@;$g(eq_FAbPq85Z&qu>!>l<702xk+`-)h-VjOVFN}+;rJv(2drNj&sYm6V zlh%CTmFV!VR9mWMROdQvxw*XU@0#$urE)Q&0*@?8wXIfBY8J{+RyC>FpQ$2M%GxTD zh8VN*`6(w5A=}Klbks2_(6{{r{>s9s;Y?woCh!&`?R&G*+ts+ z9IfZb+cK8ZG8$L&tTV#syJ2pJ@8e-9# z!~cNsDq?Lnhu?-VIWlwyZF0y?;>-wtjzssqgU;UQKe7G>%)PzQJLr@g(VK*6!{_-M z=N7;^e1X4@D0qAkU*d=x;mi06=Xe-`P!qqXPmg;@mum#`5 zx43T%58&JEMbN>6e@CiAC}Afe?_v|8RDxkp0sOqnQ?8C9hEbyjL%a~Rq{ql9ZXz{w z7xxdniOm=8;(^=f`y**eB^~^7oprLx8{-!{Do`nGItq0oC6o~J+ z5TO0_K)ag)@eLRPbg&+1uqhB$2b1n5XT(Eg@CeD#F@9jkZvgH3_> zT-1@H+OqrVC3-Xj=$f?t@7B5CEBKxUJVFVDi^YeAZ=-+J@szKmB%+E%@s(H<*BS3R z*D=y!R79KQ&Y{z$9vs`%0HqYd!C#A^-keYbI}Rd`i5Y52z*;BVkpOoSrEPgHxYOqbP# z&01b73NHh{=6aPzZT-E1}h9!7DOrMwAt7C)iH1W!Wa! z&a$0<12g=5nC+tK&a-=o-Fvt!D!NK?uZ}TFqKu9)FOz4ra8d#Dcd}IMqIkdO>>nrw c3)@=|x=nKMI@_Ock0bFvw>R-O{2jgj15NzBA^-pY diff --git a/target/classes/org/blueline/api/service/AuthService.class b/target/classes/org/blueline/api/service/AuthService.class index 24cb67d9f22cbf62af02ca9a4caae43b0c4325c9..56da77efd34a56ce13c96b50ec5c757a80ae9b61 100644 GIT binary patch delta 928 zcmY+CNlz3}6otRnQ$bN+ivvLd7Bmp-#;7q43~D#d2sk6{jM73Y-Gvl5E6%7SZYpk! zi7QtwlpsyOL|tj(579s1k{E9lgy5p;zB+a5obTONFH7H-=|BI@d;t=Sjq7jxT{>lK zHrT>eg_gDxJ(+Y?;jdeBGLJ?@UD6shdn_|+4Viu2*HY#{YA}+)OJsj?w;;!<#fD$x6_-gMR@o_G~% z_C?}LLfqEzamQc~JB%&Dh|YZ;XgoA{#N&xSdCOE_R8ix+%`Y#qtJ^H=%J6{3b47(K zZM7AlIgv`SYLQv_DW-;6Srro`jHW^J;#VTui3op<_r4{DW%T`pl2g0q^4Y_5qac2T_uu44hN zV*xI{2reJrED?b-|EIe`zdHhlk}RF1gLg?>5Xu$*XE&MwS=NG{Y>2A?!MbVz}j2D#t11QpwSO5S3 delta 643 zcmY+B*-leY6o$WjdIoxWT51_g)6l5322VvK#(`QD6bEpg&?<@}PAJZ^7StPWyxF(D zfH5(WS`bW(*WUOFK7fy4jB6J{;>Fr)?SKFK|JU04L*{MT|M7SB8_>(^N&j6W8Dl*g zJT?Y&Q&g9(PB;PIl>^cG2w*7+{ZX zbi&v^_In%%ILIO0=ad(fI1;dyql!rT%E1N(s~?>%RsHQ2tz5Cu<(`kl{ebI?CK(mr zk-N=O`f|q>EfK zCQ_G0#*&;5Kcggag&Ec<# z<$T_9M2JdW?uu9$%ORngnVQ3C4u^#rxo}lY%sXl{O$xqX&@7vH-65R2U>vj^Us^HB z@d$a6I1()qR|PUnU4x`=K0#{Zwyge6;D4o~oH{m5ml;C+H-ya%!7OevHm`C^@@STD$@P>gAs78R{0>ee1ZdD+@!f=&=dGb=pOvz{lPKH>ZTpWWG dLjr>yLn4DQLlT1pL&9ctDRD-|#L4#3s{j~a8L0pO