From 839a90a5d34e22950a3d42f7486ccde6b9c4aeff Mon Sep 17 00:00:00 2001 From: IsraaMohamed Date: Fri, 20 Feb 2026 21:36:05 +0200 Subject: [PATCH 1/3] feat(identity): setup base entities, DTOs, and JWT security config --- app/build.gradle.kts | 2 + .../org/spendoo/app/SpendooApplication.kt | 4 ++ app/src/main/resources/application.properties | 30 +++++++++ identity/build.gradle.kts | 14 ++++ .../org/spendoo/identity/dto/AuthResponse.kt | 6 ++ .../identity/dto/ForgotPasswordRequest.kt | 6 ++ .../org/spendoo/identity/dto/LoginRequest.kt | 6 ++ .../spendoo/identity/dto/RegisterRequest.kt | 9 +++ .../identity/dto/ResetPasswordRequest.kt | 7 ++ .../spendoo/identity/dto/VerifyOtpRequest.kt | 6 ++ .../identity/entity/EmailVerification.kt | 34 ++++++++++ .../org/spendoo/identity/entity/User.kt | 36 ++++++++++ .../org/spendoo/identity/mapper/UserMapper.kt | 12 ++++ .../security/CustomUserDetailsService.kt | 25 +++++++ .../spendoo/identity/security/JwtFilter.kt | 39 +++++++++++ .../org/spendoo/identity/security/JwtUtil.kt | 65 +++++++++++++++++++ .../identity/security/SecurityConfig.kt | 43 ++++++++++++ 17 files changed, 344 insertions(+) create mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/entity/User.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/security/CustomUserDetailsService.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/security/JwtFilter.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/security/JwtUtil.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/security/SecurityConfig.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3dbc241..e3e7ec8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,6 +12,8 @@ repositories { dependencies { implementation(projects.identity) implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + runtimeOnly("org.postgresql:postgresql") implementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") diff --git a/app/src/main/kotlin/org/spendoo/app/SpendooApplication.kt b/app/src/main/kotlin/org/spendoo/app/SpendooApplication.kt index e75330d..5628920 100644 --- a/app/src/main/kotlin/org/spendoo/app/SpendooApplication.kt +++ b/app/src/main/kotlin/org/spendoo/app/SpendooApplication.kt @@ -3,9 +3,13 @@ package org.spendoo.app import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.ComponentScan +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.boot.persistence.autoconfigure.EntityScan @SpringBootApplication @ComponentScan(basePackages = ["org.spendoo"]) +@EnableJpaRepositories(basePackages = ["org.spendoo"]) +@EntityScan(basePackages = ["org.spendoo"]) class SpendooApplication fun main(args: Array) { diff --git a/app/src/main/resources/application.properties b/app/src/main/resources/application.properties index 1e6ca6c..237492e 100644 --- a/app/src/main/resources/application.properties +++ b/app/src/main/resources/application.properties @@ -1 +1,31 @@ spring.application.name=app + +# DATABASE +# =============================== +spring.datasource.url=${DATABASE_URL} +spring.datasource.username=${DATABASE_USERNAME} +spring.datasource.password=${DATABASE_PASSWORD} +spring.datasource.driver-class-name=org.postgresql.Driver + +# =============================== +# JPA +# =============================== +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.hbm2ddl.create_namespaces=true + +# =============================== +# MAIL CONFIGURATION +# =============================== +spring.mail.host=smtp.gmail.com +spring.mail.port=587 +spring.mail.username=${MAIL_USERNAME} +spring.mail.password=${MAIL_PASSWORD} +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true + +# =============================== +# JWT SECRET KEY +# =============================== +jwt.secret-key=${JWT_SECRET} diff --git a/identity/build.gradle.kts b/identity/build.gradle.kts index 192f9a5..b6e3fa0 100644 --- a/identity/build.gradle.kts +++ b/identity/build.gradle.kts @@ -11,8 +11,18 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + runtimeOnly("org.postgresql:postgresql") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-mail") implementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.springframework.boot:spring-boot-starter-test") + implementation("org.springframework.boot:spring-boot-starter-mail") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } @@ -20,3 +30,7 @@ dependencies { tasks.withType { useJUnitPlatform() } +kotlin { + jvmToolchain(21) +} + diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt new file mode 100644 index 0000000..0f36d54 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt @@ -0,0 +1,6 @@ +package org.spendoo.identity.dto + +data class AuthResponse( + val token: String, + val message: String +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt new file mode 100644 index 0000000..3cbaeec --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt @@ -0,0 +1,6 @@ +package org.spendoo.identity.dto + + +data class ForgotPasswordRequest ( + val email: String +) diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt new file mode 100644 index 0000000..e615a9a --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt @@ -0,0 +1,6 @@ +package org.spendoo.identity.dto + +data class LoginRequest( + val email: String, + val password: String +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt new file mode 100644 index 0000000..de183b6 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt @@ -0,0 +1,9 @@ +package org.spendoo.identity.dto + +data class RegisterRequest( + val fullName: String, + val email: String, + val password: String, + val gender: String, + val age: Int +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt new file mode 100644 index 0000000..d130ad6 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt @@ -0,0 +1,7 @@ +package org.spendoo.identity.dto + +data class ResetPasswordRequest( + val email: String, + val otp: String, + val newPassword: String +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt new file mode 100644 index 0000000..6cabac9 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt @@ -0,0 +1,6 @@ +package org.spendoo.identity.dto + +data class VerifyOtpRequest( + val email: String, + val otp: String +) diff --git a/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt b/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt new file mode 100644 index 0000000..0a503d1 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt @@ -0,0 +1,34 @@ +package org.spendoo.identity.entity + +import jakarta.persistence.* +import java.time.LocalDateTime + +@Entity +@Table(name = "email_verification", schema = "identity") +class EmailVerification( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "verification_id") + val id: Long = 0, + + @Column(name = "verification_code", nullable = false) + var verificationCode: String = "", + + @Column(nullable = false) + var email: String = "", + + @Column(name = "sent_at", nullable = false) + val sentAt: LocalDateTime = LocalDateTime.now(), + + @Column(name = "is_used", nullable = false) + var isUsed: Boolean = false, + + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + var user: User = User() +) { + fun isExpired(): Boolean { + return sentAt.plusMinutes(1).isBefore(LocalDateTime.now()) + } +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt b/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt new file mode 100644 index 0000000..386d343 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt @@ -0,0 +1,36 @@ +package org.spendoo.identity.entity + +import jakarta.persistence.* +import jakarta.validation.constraints.Email +import java.time.LocalDateTime + +@Entity +@Table(name = "users", schema = "identity" ) +class User( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + val id: Long = 0, + + @Column(name = "full_name", nullable = false) + var fullName: String = "", + + @Column(nullable = false, unique = true) + @Email + var email: String = "", + + @Column(name = "password_hash", nullable = false) + var passwordHash: String = "", + + @Column(nullable = false) + var gender: String = "", + + @Column(nullable = false) + var age: Int = 0, + + @Column(name = "created_at", nullable = false) + val createdAt: LocalDateTime = LocalDateTime.now(), + + @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) + var emailVerifications: MutableList = mutableListOf() +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt b/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt new file mode 100644 index 0000000..40c2b65 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt @@ -0,0 +1,12 @@ +package org.spendoo.identity.mapper +import org.spendoo.identity.dto.RegisterRequest +import org.spendoo.identity.entity.User + +fun RegisterRequest.toEntity(hashedPassword: String): User { + return User( + fullName = this.fullName, + email = this.email, + passwordHash = hashedPassword, + gender = this.gender, + age = this.age ) +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/security/CustomUserDetailsService.kt b/identity/src/main/kotlin/org/spendoo/identity/security/CustomUserDetailsService.kt new file mode 100644 index 0000000..35f98eb --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/security/CustomUserDetailsService.kt @@ -0,0 +1,25 @@ +package org.spendoo.identity.security + +import org.spendoo.identity.repository.UserRepository +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.stereotype.Service + +@Service +class CustomUserDetailsService( + private val userRepository: UserRepository +) : UserDetailsService { + + override fun loadUserByUsername(email: String): UserDetails { + val user = userRepository.findByEmail(email) + .orElseThrow {UsernameNotFoundException("User not found with email: $email") } + + return User.builder() + .username(user.email) + .password(user.passwordHash) + .roles("USER") + .build() + } +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/security/JwtFilter.kt b/identity/src/main/kotlin/org/spendoo/identity/security/JwtFilter.kt new file mode 100644 index 0000000..2405458 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/security/JwtFilter.kt @@ -0,0 +1,39 @@ +package org.spendoo.identity.security + +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Component +import org.springframework.web.filter.OncePerRequestFilter + +@Component +class JwtFilter( + private val jwtUtil: JwtUtil, + private val userDetailsService: CustomUserDetailsService +) : OncePerRequestFilter() { + + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val authHeader = request.getHeader("Authorization") + if (authHeader != null && authHeader.startsWith("Bearer ")) { + val token = authHeader.substring(7) + val username = jwtUtil.extractUsername(token) + + if (username != null && SecurityContextHolder.getContext().authentication == null) { + val userDetails = userDetailsService.loadUserByUsername(username) + if (jwtUtil.validateToken(token, userDetails.username)) { + val authToken = UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.authorities + ) + SecurityContextHolder.getContext().authentication = authToken + } + } + } + filterChain.doFilter(request, response) + } +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/security/JwtUtil.kt b/identity/src/main/kotlin/org/spendoo/identity/security/JwtUtil.kt new file mode 100644 index 0000000..7837bbf --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/security/JwtUtil.kt @@ -0,0 +1,65 @@ +package org.spendoo.identity.security + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.security.Keys +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.util.Base64 +import java.util.Date +import java.time.Instant +import javax.crypto.SecretKey +import java.time.Duration + +@Component +class JwtUtil( + @Value("\${jwt.secret-key}") private val secret: String +){ + + private val accessExpiration: Duration = Duration.ofHours(1) + private val secretKey: SecretKey by lazy { + Keys.hmacShaKeyFor(Base64.getDecoder().decode(secret)) + } + + fun generateToken(username: String): String { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(Date()) + .setExpiration(Date.from(Instant.now().plus(accessExpiration))) + .signWith(secretKey) + .compact() + } + + fun extractUsername(token: String): String? { + return try { + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .body + .subject + } catch (e: Exception) { + null + } + } + + + fun validateToken(token: String, username: String): Boolean { + val extracted = extractUsername(token) + return extracted == username && !isTokenExpired(token) + } + + private fun isTokenExpired(token: String): Boolean { + return try { + val expiration = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .body + .expiration + expiration.before(Date()) + } catch (e: Exception) { + true + } + } +} + diff --git a/identity/src/main/kotlin/org/spendoo/identity/security/SecurityConfig.kt b/identity/src/main/kotlin/org/spendoo/identity/security/SecurityConfig.kt new file mode 100644 index 0000000..6d7582c --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/security/SecurityConfig.kt @@ -0,0 +1,43 @@ +package org.spendoo.identity.security + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +@EnableWebSecurity +class SecurityConfig( + private val jwtFilter: JwtFilter +) { + + @Bean + fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf { it.disable() } + .authorizeHttpRequests { + it.requestMatchers("/api/auth/**").permitAll() + it.anyRequest().authenticated() + } + .sessionManagement { + it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + } + http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) + return http.build() + } + + @Bean + fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder() + + @Bean + fun authenticationManager(authConfig: AuthenticationConfiguration): AuthenticationManager { + return authConfig.authenticationManager + } + +} \ No newline at end of file From 3c68ee9cc3d01043bf079219065f2fdd6df0a157 Mon Sep 17 00:00:00 2001 From: IsraaMohamed Date: Tue, 24 Feb 2026 03:39:49 +0200 Subject: [PATCH 2/3] feat: Implement Auth system (Sign Up, Login, Refresh Token, Logout) --- identity/build.gradle.kts | 2 +- .../identity/controller/AuthController.kt | 47 ++++++++ .../org/spendoo/identity/dto/AuthResponse.kt | 4 +- .../identity/dto/ForgotPasswordRequest.kt | 7 +- .../org/spendoo/identity/dto/LoginRequest.kt | 7 ++ .../identity/dto/RefreshTokenRequest.kt | 9 ++ .../spendoo/identity/dto/RegisterRequest.kt | 27 ++++- .../identity/dto/ResetPasswordRequest.kt | 11 ++ .../spendoo/identity/dto/VerifyOtpRequest.kt | 9 ++ .../identity/entity/EmailVerification.kt | 27 ++--- .../spendoo/identity/entity/RefreshToken.kt | 23 ++++ .../org/spendoo/identity/entity/User.kt | 39 ++++--- .../org/spendoo/identity/enums/Gender.kt | 6 + .../exception/GlobalExceptionHandler.kt | 51 +++++++++ .../exception/InvalidCredentialsException.kt | 4 + .../exception/TokenExpiredException.kt | 4 + .../exception/UnauthorizedException.kt | 4 + .../exception/UserAlreadyExistsException.kt | 4 + .../org/spendoo/identity/mapper/UserMapper.kt | 7 +- .../repository/RefreshTokenRepository.kt | 14 +++ .../identity/repository/UserRepository.kt | 10 ++ .../identity/response/ErrorResponse.kt | 7 ++ .../spendoo/identity/security/JwtFilter.kt | 31 +++-- .../org/spendoo/identity/security/JwtUtil.kt | 72 ++++++++---- .../spendoo/identity/service/AuthService.kt | 106 ++++++++++++++++++ 25 files changed, 461 insertions(+), 71 deletions(-) create mode 100644 identity/src/main/kotlin/org/spendoo/identity/controller/AuthController.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/RefreshTokenRequest.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/entity/RefreshToken.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/enums/Gender.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/GlobalExceptionHandler.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/InvalidCredentialsException.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/TokenExpiredException.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/UnauthorizedException.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/UserAlreadyExistsException.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/repository/RefreshTokenRepository.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/repository/UserRepository.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/response/ErrorResponse.kt create mode 100644 identity/src/main/kotlin/org/spendoo/identity/service/AuthService.kt diff --git a/identity/build.gradle.kts b/identity/build.gradle.kts index b6e3fa0..0f22d8a 100644 --- a/identity/build.gradle.kts +++ b/identity/build.gradle.kts @@ -3,6 +3,7 @@ plugins { kotlin("plugin.spring") version "2.2.21" id("org.springframework.boot") version "4.0.1" id("io.spring.dependency-management") version "1.1.7" + kotlin("plugin.jpa") version "1.9.22" } repositories { @@ -22,7 +23,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-mail") implementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.springframework.boot:spring-boot-starter-test") - implementation("org.springframework.boot:spring-boot-starter-mail") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") } diff --git a/identity/src/main/kotlin/org/spendoo/identity/controller/AuthController.kt b/identity/src/main/kotlin/org/spendoo/identity/controller/AuthController.kt new file mode 100644 index 0000000..ce877d5 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/controller/AuthController.kt @@ -0,0 +1,47 @@ +package org.spendoo.identity.controller + +import jakarta.validation.Valid +import org.spendoo.identity.dto.AuthResponse +import org.spendoo.identity.dto.LoginRequest +import org.spendoo.identity.dto.RefreshTokenRequest +import org.spendoo.identity.dto.RegisterRequest +import org.spendoo.identity.service.AuthService +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + + +@RestController +@RequestMapping("/api/auth") +class AuthController ( + private val authService: AuthService +) { + + @PostMapping("/signup") + fun register (@Valid @RequestBody request: RegisterRequest): ResponseEntity { + val response = authService.register(request) + return ResponseEntity.status(HttpStatus.CREATED).body(response) + } + + @PostMapping("/login") + fun login (@Valid @RequestBody request: LoginRequest): ResponseEntity { + val response = authService.login(request) + return ResponseEntity.ok(response) + } + + @PostMapping("/refresh") + fun refresh (@Valid @RequestBody request: RefreshTokenRequest): ResponseEntity { + val response = authService.refreshToken(request) + return ResponseEntity.ok(response) + } + + @PostMapping("/logout") + fun logout(@Valid @RequestBody request: RefreshTokenRequest): ResponseEntity> { + authService.logout(request) + return ResponseEntity.ok(mapOf("message" to "Logged out successfully")) + } + +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt index 0f36d54..59807e2 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt @@ -1,6 +1,6 @@ package org.spendoo.identity.dto data class AuthResponse( - val token: String, - val message: String + val accessToken: String, + val refreshToken: String ) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt index 3cbaeec..75d6a5b 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt @@ -1,6 +1,11 @@ package org.spendoo.identity.dto +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank -data class ForgotPasswordRequest ( + +data class ForgotPasswordRequest( + @field:NotBlank(message = "Email is required") + @field:Email(message = "Invalid email format") val email: String ) diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt index e615a9a..8f33da1 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt @@ -1,6 +1,13 @@ package org.spendoo.identity.dto +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank + data class LoginRequest( + @field:NotBlank(message = "Email is required") + @field:Email(message = "Invalid email format") val email: String, + + @field:NotBlank(message = "Password is required") val password: String ) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/RefreshTokenRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/RefreshTokenRequest.kt new file mode 100644 index 0000000..a556396 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/RefreshTokenRequest.kt @@ -0,0 +1,9 @@ +package org.spendoo.identity.dto + +import jakarta.validation.constraints.NotBlank + +data class RefreshTokenRequest( + + @field:NotBlank(message = "Refresh token is required") + val refreshToken: String +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt index de183b6..c8f34eb 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt @@ -1,9 +1,32 @@ package org.spendoo.identity.dto +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Past +import jakarta.validation.constraints.Pattern +import org.spendoo.identity.enums.Gender +import java.time.LocalDate + data class RegisterRequest( + @field:NotBlank(message = "Full name is required") val fullName: String, + + @field:NotBlank(message = "Email is required") + @field:Email(message = "Please provide a valid email address") val email: String, + + @field:NotBlank(message = "Password is required") + @field:Pattern( + regexp = """^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!]).{8,}$""", + message = "Password must contain at least 8 characters, one uppercase, one lowercase, one number and one special character" + ) val password: String, - val gender: String, - val age: Int + + @field:NotNull(message = "Gender is required") + val gender: Gender, + + @field:NotNull(message = "Birth date is required") + @field:Past(message = "Birth date must be in the past") + val birthDate: LocalDate ) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt index d130ad6..79fecff 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt @@ -1,7 +1,18 @@ package org.spendoo.identity.dto +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size + data class ResetPasswordRequest( + @field:NotBlank(message = "Email is required") + @field:Email(message = "Invalid email format") val email: String, + + @field:NotBlank(message = "OTP is required") val otp: String, + + @field:NotBlank(message = "New password is required") + @field:Size(min = 8, message = "Password must be at least 8 characters long") val newPassword: String ) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt index 6cabac9..ca50226 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt @@ -1,6 +1,15 @@ package org.spendoo.identity.dto +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Size + data class VerifyOtpRequest( + @field:NotBlank(message = "Email is required") + @field:Email(message = "Invalid email format") val email: String, + + @field:NotBlank(message = "OTP is required") + @field:Size(min = 4, max = 4, message = "OTP must be exactly 4 characters") val otp: String ) diff --git a/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt b/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt index 0a503d1..f96e3f0 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt @@ -2,31 +2,32 @@ package org.spendoo.identity.entity import jakarta.persistence.* import java.time.LocalDateTime +import java.util.UUID @Entity @Table(name = "email_verification", schema = "identity") -class EmailVerification( +data class EmailVerification( @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "verification_id") - val id: Long = 0, + @GeneratedValue(strategy = GenerationType.UUID) + @Column(columnDefinition = "uuid", updatable = false, nullable = false) + val id: UUID? = null, - @Column(name = "verification_code", nullable = false) - var verificationCode: String = "", + @Column(nullable = false) + val verificationCode: String, @Column(nullable = false) - var email: String = "", + val email: String, - @Column(name = "sent_at", nullable = false) - val sentAt: LocalDateTime = LocalDateTime.now(), + @Column(nullable = false) + val sentAt: LocalDateTime, - @Column(name = "is_used", nullable = false) - var isUsed: Boolean = false, + @Column(nullable = false) + val isUsed: Boolean, @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - var user: User = User() + @JoinColumn(nullable = false) + val user: User ) { fun isExpired(): Boolean { return sentAt.plusMinutes(1).isBefore(LocalDateTime.now()) diff --git a/identity/src/main/kotlin/org/spendoo/identity/entity/RefreshToken.kt b/identity/src/main/kotlin/org/spendoo/identity/entity/RefreshToken.kt new file mode 100644 index 0000000..04b3b73 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/entity/RefreshToken.kt @@ -0,0 +1,23 @@ +package org.spendoo.identity.entity + +import jakarta.persistence.* +import java.time.LocalDateTime +import java.util.UUID + +@Entity +@Table(name = "refresh_token", schema = "identity") +data class RefreshToken( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + @Column(nullable = false, unique = true) + val token: String, + + @Column(nullable = false) + val expiryDate: LocalDateTime, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + val user: User +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt b/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt index 386d343..0b632ec 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt @@ -1,36 +1,41 @@ package org.spendoo.identity.entity import jakarta.persistence.* -import jakarta.validation.constraints.Email +import org.spendoo.identity.enums.Gender +import java.time.LocalDate import java.time.LocalDateTime +import java.util.UUID @Entity @Table(name = "users", schema = "identity" ) -class User( +data class User( @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "user_id") - val id: Long = 0, + @GeneratedValue(strategy = GenerationType.UUID) + @Column(columnDefinition = "uuid", updatable = false, nullable = false) + val id: UUID? = null, - @Column(name = "full_name", nullable = false) - var fullName: String = "", + @Column(nullable = false) + val fullName: String, @Column(nullable = false, unique = true) - @Email - var email: String = "", + val email: String, - @Column(name = "password_hash", nullable = false) - var passwordHash: String = "", + @Column(nullable = false) + val passwordHash: String, + @Enumerated(EnumType.STRING) @Column(nullable = false) - var gender: String = "", + val gender: Gender, @Column(nullable = false) - var age: Int = 0, + val birthDate: LocalDate, - @Column(name = "created_at", nullable = false) - val createdAt: LocalDateTime = LocalDateTime.now(), + @Column(nullable = false, updatable = false) + val createdAt: LocalDateTime, @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) - var emailVerifications: MutableList = mutableListOf() -) \ No newline at end of file + val emailVerifications: List = emptyList(), + + @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true) + val refreshTokens: List = emptyList() +) diff --git a/identity/src/main/kotlin/org/spendoo/identity/enums/Gender.kt b/identity/src/main/kotlin/org/spendoo/identity/enums/Gender.kt new file mode 100644 index 0000000..e3dbe5b --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/enums/Gender.kt @@ -0,0 +1,6 @@ +package org.spendoo.identity.enums + +enum class Gender { + MALE, + FEMALE +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/GlobalExceptionHandler.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/GlobalExceptionHandler.kt new file mode 100644 index 0000000..8445a0f --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/exception/GlobalExceptionHandler.kt @@ -0,0 +1,51 @@ +package org.spendoo.identity.exception + +import org.spendoo.identity.response.ErrorResponse +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + + +@RestControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler(UserAlreadyExistsException::class) + fun handleUserAlreadyExists(ex: UserAlreadyExistsException): ResponseEntity { + val error = ErrorResponse(ex.message ?: "Conflict", HttpStatus.CONFLICT.value()) + return ResponseEntity.status(HttpStatus.CONFLICT).body(error) + } + + @ExceptionHandler(InvalidCredentialsException::class, BadCredentialsException::class) + fun handleInvalidCredentials(ex: Exception): ResponseEntity { + val error = ErrorResponse("Invalid email or password", HttpStatus.UNAUTHORIZED.value()) + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error) + } + + @ExceptionHandler(TokenExpiredException::class) + fun handleTokenExpired(ex: TokenExpiredException): ResponseEntity { + val error = ErrorResponse(ex.message ?: "Token expired", HttpStatus.UNAUTHORIZED.value()) + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error) + } + + @ExceptionHandler(UnauthorizedException::class) + fun handleUnauthorized(ex: UnauthorizedException): ResponseEntity { + val error = ErrorResponse(ex.message ?: "Unauthorized", HttpStatus.FORBIDDEN.value()) + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error) + } + + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleValidationExceptions(ex: MethodArgumentNotValidException): ResponseEntity { + val errorMessage = ex.bindingResult.allErrors.firstOrNull()?.defaultMessage ?: "Validation failed" + val error = ErrorResponse(errorMessage, HttpStatus.BAD_REQUEST.value()) + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error) + } + + @ExceptionHandler(Exception::class) + fun handleGenericException(ex: Exception): ResponseEntity { + val error = ErrorResponse(ex.message ?: "Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR.value()) // 500 + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error) + } +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/InvalidCredentialsException.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/InvalidCredentialsException.kt new file mode 100644 index 0000000..0cb0f04 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/exception/InvalidCredentialsException.kt @@ -0,0 +1,4 @@ +package org.spendoo.identity.exception + +class InvalidCredentialsException( + message: String = "Invalid email or password") : RuntimeException(message) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/TokenExpiredException.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/TokenExpiredException.kt new file mode 100644 index 0000000..b50be31 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/exception/TokenExpiredException.kt @@ -0,0 +1,4 @@ +package org.spendoo.identity.exception + +class TokenExpiredException ( + message: String = "Token has expired. Please login again.") : RuntimeException(message) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/UnauthorizedException.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/UnauthorizedException.kt new file mode 100644 index 0000000..73e74a4 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/exception/UnauthorizedException.kt @@ -0,0 +1,4 @@ +package org.spendoo.identity.exception + +class UnauthorizedException ( + message: String = "Unauthorized access") : RuntimeException(message) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/UserAlreadyExistsException.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/UserAlreadyExistsException.kt new file mode 100644 index 0000000..3ecc10b --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/exception/UserAlreadyExistsException.kt @@ -0,0 +1,4 @@ +package org.spendoo.identity.exception + +class UserAlreadyExistsException( + message: String = "User already exists") : RuntimeException(message) diff --git a/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt b/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt index 40c2b65..2b3320b 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt @@ -1,6 +1,7 @@ package org.spendoo.identity.mapper import org.spendoo.identity.dto.RegisterRequest import org.spendoo.identity.entity.User +import java.time.LocalDateTime fun RegisterRequest.toEntity(hashedPassword: String): User { return User( @@ -8,5 +9,9 @@ fun RegisterRequest.toEntity(hashedPassword: String): User { email = this.email, passwordHash = hashedPassword, gender = this.gender, - age = this.age ) + birthDate = this.birthDate, + createdAt = LocalDateTime.now(), + emailVerifications = emptyList(), + refreshTokens = emptyList() + ) } \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/repository/RefreshTokenRepository.kt b/identity/src/main/kotlin/org/spendoo/identity/repository/RefreshTokenRepository.kt new file mode 100644 index 0000000..75a40c0 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/repository/RefreshTokenRepository.kt @@ -0,0 +1,14 @@ +package org.spendoo.identity.repository + +import org.spendoo.identity.entity.RefreshToken +import org.spendoo.identity.entity.User +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.util.Optional + +@Repository +interface RefreshTokenRepository : JpaRepository { + + fun findByToken(token: String): Optional + fun findByUser(user: User): Optional +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/repository/UserRepository.kt b/identity/src/main/kotlin/org/spendoo/identity/repository/UserRepository.kt new file mode 100644 index 0000000..90488e2 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/repository/UserRepository.kt @@ -0,0 +1,10 @@ +package org.spendoo.identity.repository + +import org.spendoo.identity.entity.User +import org.springframework.data.jpa.repository.JpaRepository +import java.util.Optional + +interface UserRepository : JpaRepository { + fun findByEmail(email: String): Optional + fun existsByEmail(email: String): Boolean +} \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/response/ErrorResponse.kt b/identity/src/main/kotlin/org/spendoo/identity/response/ErrorResponse.kt new file mode 100644 index 0000000..2c8403f --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/response/ErrorResponse.kt @@ -0,0 +1,7 @@ +package org.spendoo.identity.response + + +data class ErrorResponse( + val message: String, + val status: Int +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/security/JwtFilter.kt b/identity/src/main/kotlin/org/spendoo/identity/security/JwtFilter.kt index 2405458..554e688 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/security/JwtFilter.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/security/JwtFilter.kt @@ -5,6 +5,7 @@ import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource import org.springframework.stereotype.Component import org.springframework.web.filter.OncePerRequestFilter @@ -20,20 +21,30 @@ class JwtFilter( filterChain: FilterChain ) { val authHeader = request.getHeader("Authorization") - if (authHeader != null && authHeader.startsWith("Bearer ")) { - val token = authHeader.substring(7) - val username = jwtUtil.extractUsername(token) - if (username != null && SecurityContextHolder.getContext().authentication == null) { + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response) + return + } + + val token = authHeader.substring(7) + val username = jwtUtil.extractUsername(token) + + if (username != null && SecurityContextHolder.getContext().authentication == null) { + if (jwtUtil.validateAccessToken(token) && + jwtUtil.validateTokenForUser(token, username) + ) { val userDetails = userDetailsService.loadUserByUsername(username) - if (jwtUtil.validateToken(token, userDetails.username)) { - val authToken = UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.authorities - ) - SecurityContextHolder.getContext().authentication = authToken - } + val authToken = UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.authorities + ) + authToken.details = WebAuthenticationDetailsSource().buildDetails(request) + SecurityContextHolder.getContext().authentication = authToken } } + filterChain.doFilter(request, response) } } \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/security/JwtUtil.kt b/identity/src/main/kotlin/org/spendoo/identity/security/JwtUtil.kt index 7837bbf..21e7bba 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/security/JwtUtil.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/security/JwtUtil.kt @@ -1,12 +1,12 @@ package org.spendoo.identity.security +import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import java.util.Base64 import java.util.Date -import java.time.Instant import javax.crypto.SecretKey import java.time.Duration @@ -15,51 +15,75 @@ class JwtUtil( @Value("\${jwt.secret-key}") private val secret: String ){ - private val accessExpiration: Duration = Duration.ofHours(1) private val secretKey: SecretKey by lazy { Keys.hmacShaKeyFor(Base64.getDecoder().decode(secret)) } + private val accessExpiration = Duration.ofMinutes(30) + private val refreshExpiration = Duration.ofDays(14) - fun generateToken(username: String): String { + // ================== Generate ================== + + private fun createToken(email: String, expirationTimeMillis: Long, type: String): String { return Jwts.builder() - .setSubject(username) - .setIssuedAt(Date()) - .setExpiration(Date.from(Instant.now().plus(accessExpiration))) + .setSubject(email) + .claim("type", type) + .setIssuedAt(Date(System.currentTimeMillis())) + .setExpiration(Date(System.currentTimeMillis() + expirationTimeMillis)) .signWith(secretKey) .compact() } - fun extractUsername(token: String): String? { - return try { + fun generateAccessToken(email: String): String { + return createToken(email, accessExpiration.toMillis(), "access") + } + + fun generateRefreshToken(email: String): String { + return createToken(email, refreshExpiration.toMillis(), "refresh") + } + + // ================== Parsing ================== + + private fun parseAllClaims(token: String) = + try { Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .body - .subject } catch (e: Exception) { null } + + fun extractUsername(token: String): String? { + return parseAllClaims(token)?.subject } + // ================== Validation ================== - fun validateToken(token: String, username: String): Boolean { - val extracted = extractUsername(token) - return extracted == username && !isTokenExpired(token) + private fun isTokenExpired(claims: Claims): Boolean { + return claims.expiration.before(Date()) } - private fun isTokenExpired(token: String): Boolean { - return try { - val expiration = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build() - .parseClaimsJws(token) - .body - .expiration - expiration.before(Date()) - } catch (e: Exception) { - true - } + private fun validateTokenInternal(token: String): Claims? { + val claims = parseAllClaims(token) ?: return null + if (isTokenExpired(claims)) return null + return claims + } + + fun validateAccessToken(token: String): Boolean { + val claims = validateTokenInternal(token) ?: return false + return claims["type"] == "access" } + + fun validateRefreshToken(token: String): Boolean { + val claims = validateTokenInternal(token) ?: return false + return claims["type"] == "refresh" + } + + fun validateTokenForUser(token: String, username: String): Boolean { + val claims = validateTokenInternal(token) ?: return false + return claims.subject == username + } + } diff --git a/identity/src/main/kotlin/org/spendoo/identity/service/AuthService.kt b/identity/src/main/kotlin/org/spendoo/identity/service/AuthService.kt new file mode 100644 index 0000000..f2be674 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/service/AuthService.kt @@ -0,0 +1,106 @@ +package org.spendoo.identity.service + +import org.spendoo.identity.dto.AuthResponse +import org.spendoo.identity.dto.LoginRequest +import org.spendoo.identity.dto.RefreshTokenRequest +import org.spendoo.identity.dto.RegisterRequest +import org.spendoo.identity.entity.RefreshToken +import org.spendoo.identity.entity.User +import org.spendoo.identity.exception.TokenExpiredException +import org.spendoo.identity.exception.UnauthorizedException +import org.spendoo.identity.exception.UserAlreadyExistsException +import org.spendoo.identity.mapper.toEntity +import org.spendoo.identity.repository.RefreshTokenRepository +import org.spendoo.identity.repository.UserRepository +import org.spendoo.identity.security.JwtUtil +import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import java.time.LocalDateTime + +@Service +class AuthService( + private val userRepository: UserRepository, + private val refreshTokenRepository: RefreshTokenRepository, + private val passwordEncoder: PasswordEncoder, + private val jwtUtil: JwtUtil, + private val authenticationManager: AuthenticationManager +) { + + fun register(request: RegisterRequest): AuthResponse { + if (userRepository.findByEmail(request.email).isPresent) { + throw UserAlreadyExistsException("Email is already registered!") + } + + + val user = request.toEntity(passwordEncoder.encode(request.password)!!) + + userRepository.save(user) + + val accessToken = jwtUtil.generateAccessToken(user.email) + val refreshToken = jwtUtil.generateRefreshToken(user.email) + + saveRefreshToken(user, refreshToken) + + return AuthResponse(accessToken, refreshToken) + } + + + fun login(request: LoginRequest): AuthResponse { + authenticationManager.authenticate( + UsernamePasswordAuthenticationToken(request.email, request.password) + ) + + val user = userRepository.findByEmail(request.email).get() + val accessToken = jwtUtil.generateAccessToken(request.email) + val refreshToken = jwtUtil.generateRefreshToken(request.email) + + saveRefreshToken(user, refreshToken) + + return AuthResponse(accessToken, refreshToken) + } + + + fun refreshToken(request: RefreshTokenRequest): AuthResponse { + + val refreshTokenEntity = refreshTokenRepository.findByToken(request.refreshToken) + .orElseThrow { UnauthorizedException("Invalid refresh token") } + + val user = refreshTokenEntity.user + + refreshTokenRepository.delete(refreshTokenEntity) + + if (refreshTokenEntity.expiryDate.isBefore(LocalDateTime.now())) { + throw TokenExpiredException("Refresh token is expired. Please login again.") + } + + if (jwtUtil.validateRefreshToken(request.refreshToken) && + jwtUtil.validateTokenForUser(request.refreshToken, user.email)) { + + val newAccessToken = jwtUtil.generateAccessToken(user.email) + val newRefreshToken = jwtUtil.generateRefreshToken(user.email) + + + saveRefreshToken(user, newRefreshToken) + + return AuthResponse(newAccessToken, newRefreshToken) + } else { + throw UnauthorizedException("Invalid refresh token") + } + } + + private fun saveRefreshToken(user: User, token: String) { + val expiryDate = LocalDateTime.now().plusDays(14) + + refreshTokenRepository.save( + RefreshToken(token = token, expiryDate = expiryDate, user = user) + ) + } + + fun logout(request: RefreshTokenRequest) { + refreshTokenRepository.findByToken(request.refreshToken).ifPresent { tokenEntity -> + refreshTokenRepository.delete(tokenEntity) + } + } +} \ No newline at end of file From d06b3d209f801ed3078c48374bffa5ee83c972d0 Mon Sep 17 00:00:00 2001 From: IsraaMohamed Date: Tue, 24 Feb 2026 14:02:48 +0200 Subject: [PATCH 3/3] chore: minor refactoring and validation updates --- .../identity/controller/AuthController.kt | 10 +++++----- .../org/spendoo/identity/dto/LoginRequest.kt | 13 ------------- .../dto/{ => request}/ForgotPasswordRequest.kt | 5 ++--- .../identity/dto/request/LoginRequest.kt | 18 ++++++++++++++++++ .../dto/{ => request}/RefreshTokenRequest.kt | 2 +- .../dto/{ => request}/RegisterRequest.kt | 2 +- .../dto/{ => request}/ResetPasswordRequest.kt | 9 +++++++-- .../dto/{ => request}/VerifyOtpRequest.kt | 4 ++-- .../dto/{ => response}/AuthResponse.kt | 2 +- .../identity/entity/EmailVerification.kt | 2 +- .../spendoo/identity/entity/RefreshToken.kt | 2 +- .../kotlin/org/spendoo/identity/entity/User.kt | 2 +- .../identity/exception/AuthExceptions.kt | 16 ++++++++++++++++ .../exception/InvalidCredentialsException.kt | 4 ---- .../exception/TokenExpiredException.kt | 4 ---- .../exception/UnauthorizedException.kt | 4 ---- .../exception/UserAlreadyExistsException.kt | 4 ---- .../org/spendoo/identity/mapper/UserMapper.kt | 2 +- .../identity/repository/UserRepository.kt | 3 ++- .../spendoo/identity/service/AuthService.kt | 8 ++++---- 20 files changed, 63 insertions(+), 53 deletions(-) delete mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt rename identity/src/main/kotlin/org/spendoo/identity/dto/{ => request}/ForgotPasswordRequest.kt (85%) create mode 100644 identity/src/main/kotlin/org/spendoo/identity/dto/request/LoginRequest.kt rename identity/src/main/kotlin/org/spendoo/identity/dto/{ => request}/RefreshTokenRequest.kt (80%) rename identity/src/main/kotlin/org/spendoo/identity/dto/{ => request}/RegisterRequest.kt (96%) rename identity/src/main/kotlin/org/spendoo/identity/dto/{ => request}/ResetPasswordRequest.kt (52%) rename identity/src/main/kotlin/org/spendoo/identity/dto/{ => request}/VerifyOtpRequest.kt (91%) rename identity/src/main/kotlin/org/spendoo/identity/dto/{ => response}/AuthResponse.kt (66%) create mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/AuthExceptions.kt delete mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/InvalidCredentialsException.kt delete mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/TokenExpiredException.kt delete mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/UnauthorizedException.kt delete mode 100644 identity/src/main/kotlin/org/spendoo/identity/exception/UserAlreadyExistsException.kt diff --git a/identity/src/main/kotlin/org/spendoo/identity/controller/AuthController.kt b/identity/src/main/kotlin/org/spendoo/identity/controller/AuthController.kt index ce877d5..d22f676 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/controller/AuthController.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/controller/AuthController.kt @@ -1,10 +1,10 @@ package org.spendoo.identity.controller import jakarta.validation.Valid -import org.spendoo.identity.dto.AuthResponse -import org.spendoo.identity.dto.LoginRequest -import org.spendoo.identity.dto.RefreshTokenRequest -import org.spendoo.identity.dto.RegisterRequest +import org.spendoo.identity.dto.response.AuthResponse +import org.spendoo.identity.dto.request.LoginRequest +import org.spendoo.identity.dto.request.RefreshTokenRequest +import org.spendoo.identity.dto.request.RegisterRequest import org.spendoo.identity.service.AuthService import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -15,7 +15,7 @@ import org.springframework.web.bind.annotation.RestController @RestController -@RequestMapping("/api/auth") +@RequestMapping("/api/v1/auth") class AuthController ( private val authService: AuthService ) { diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt deleted file mode 100644 index 8f33da1..0000000 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/LoginRequest.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.spendoo.identity.dto - -import jakarta.validation.constraints.Email -import jakarta.validation.constraints.NotBlank - -data class LoginRequest( - @field:NotBlank(message = "Email is required") - @field:Email(message = "Invalid email format") - val email: String, - - @field:NotBlank(message = "Password is required") - val password: String -) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/request/ForgotPasswordRequest.kt similarity index 85% rename from identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt rename to identity/src/main/kotlin/org/spendoo/identity/dto/request/ForgotPasswordRequest.kt index 75d6a5b..2db185b 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/ForgotPasswordRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/request/ForgotPasswordRequest.kt @@ -1,11 +1,10 @@ -package org.spendoo.identity.dto +package org.spendoo.identity.dto.request import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotBlank - data class ForgotPasswordRequest( @field:NotBlank(message = "Email is required") @field:Email(message = "Invalid email format") val email: String -) +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/request/LoginRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/request/LoginRequest.kt new file mode 100644 index 0000000..b2a3dba --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/request/LoginRequest.kt @@ -0,0 +1,18 @@ +package org.spendoo.identity.dto.request + +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern + +data class LoginRequest( + @field:NotBlank(message = "Email is required") + @field:Email(message = "Invalid email format") + val email: String, + + @field:NotBlank(message = "Password is required") + @field:Pattern( + regexp = """^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!]).{8,}$""", + message = "Password must contain at least 8 characters, one uppercase, one lowercase, one number and one special character" + ) + val password: String +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/RefreshTokenRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/request/RefreshTokenRequest.kt similarity index 80% rename from identity/src/main/kotlin/org/spendoo/identity/dto/RefreshTokenRequest.kt rename to identity/src/main/kotlin/org/spendoo/identity/dto/request/RefreshTokenRequest.kt index a556396..43321b2 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/RefreshTokenRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/request/RefreshTokenRequest.kt @@ -1,4 +1,4 @@ -package org.spendoo.identity.dto +package org.spendoo.identity.dto.request import jakarta.validation.constraints.NotBlank diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/request/RegisterRequest.kt similarity index 96% rename from identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt rename to identity/src/main/kotlin/org/spendoo/identity/dto/request/RegisterRequest.kt index c8f34eb..0f05169 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/RegisterRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/request/RegisterRequest.kt @@ -1,4 +1,4 @@ -package org.spendoo.identity.dto +package org.spendoo.identity.dto.request import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotBlank diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/request/ResetPasswordRequest.kt similarity index 52% rename from identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt rename to identity/src/main/kotlin/org/spendoo/identity/dto/request/ResetPasswordRequest.kt index 79fecff..c6d9328 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/ResetPasswordRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/request/ResetPasswordRequest.kt @@ -1,7 +1,8 @@ -package org.spendoo.identity.dto +package org.spendoo.identity.dto.request import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotBlank +import jakarta.validation.constraints.Pattern import jakarta.validation.constraints.Size data class ResetPasswordRequest( @@ -10,9 +11,13 @@ data class ResetPasswordRequest( val email: String, @field:NotBlank(message = "OTP is required") + @field:Size(min = 4, max = 4, message = "OTP must be exactly 4 characters") val otp: String, @field:NotBlank(message = "New password is required") - @field:Size(min = 8, message = "Password must be at least 8 characters long") + @field:Pattern( + regexp = """^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=!]).{8,}$""", + message = "Password must contain at least 8 characters, one uppercase, one lowercase, one number and one special character" + ) val newPassword: String ) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/request/VerifyOtpRequest.kt similarity index 91% rename from identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt rename to identity/src/main/kotlin/org/spendoo/identity/dto/request/VerifyOtpRequest.kt index ca50226..663f05c 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/VerifyOtpRequest.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/request/VerifyOtpRequest.kt @@ -1,4 +1,4 @@ -package org.spendoo.identity.dto +package org.spendoo.identity.dto.request import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotBlank @@ -12,4 +12,4 @@ data class VerifyOtpRequest( @field:NotBlank(message = "OTP is required") @field:Size(min = 4, max = 4, message = "OTP must be exactly 4 characters") val otp: String -) +) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt b/identity/src/main/kotlin/org/spendoo/identity/dto/response/AuthResponse.kt similarity index 66% rename from identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt rename to identity/src/main/kotlin/org/spendoo/identity/dto/response/AuthResponse.kt index 59807e2..682ef69 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/dto/AuthResponse.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/dto/response/AuthResponse.kt @@ -1,4 +1,4 @@ -package org.spendoo.identity.dto +package org.spendoo.identity.dto.response data class AuthResponse( val accessToken: String, diff --git a/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt b/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt index f96e3f0..21994ea 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/entity/EmailVerification.kt @@ -10,7 +10,7 @@ data class EmailVerification( @Id @GeneratedValue(strategy = GenerationType.UUID) @Column(columnDefinition = "uuid", updatable = false, nullable = false) - val id: UUID? = null, + val id: UUID = UUID.randomUUID(), @Column(nullable = false) val verificationCode: String, diff --git a/identity/src/main/kotlin/org/spendoo/identity/entity/RefreshToken.kt b/identity/src/main/kotlin/org/spendoo/identity/entity/RefreshToken.kt index 04b3b73..b4ed30b 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/entity/RefreshToken.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/entity/RefreshToken.kt @@ -9,7 +9,7 @@ import java.util.UUID data class RefreshToken( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long? = null, + val id: Long = 0, @Column(nullable = false, unique = true) val token: String, diff --git a/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt b/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt index 0b632ec..0d05c20 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/entity/User.kt @@ -12,7 +12,7 @@ data class User( @Id @GeneratedValue(strategy = GenerationType.UUID) @Column(columnDefinition = "uuid", updatable = false, nullable = false) - val id: UUID? = null, + val id: UUID = UUID.randomUUID(), @Column(nullable = false) val fullName: String, diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/AuthExceptions.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/AuthExceptions.kt new file mode 100644 index 0000000..55f12e0 --- /dev/null +++ b/identity/src/main/kotlin/org/spendoo/identity/exception/AuthExceptions.kt @@ -0,0 +1,16 @@ +package org.spendoo.identity.exception + +class InvalidCredentialsException( + message: String = "Invalid email or password") : RuntimeException(message) + + +class TokenExpiredException ( + message: String = "Token has expired. Please login again.") : RuntimeException(message) + + +class UnauthorizedException ( + message: String = "Unauthorized access") : RuntimeException(message) + +class UserAlreadyExistsException( + message: String = "User already exists") : RuntimeException(message) + diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/InvalidCredentialsException.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/InvalidCredentialsException.kt deleted file mode 100644 index 0cb0f04..0000000 --- a/identity/src/main/kotlin/org/spendoo/identity/exception/InvalidCredentialsException.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.spendoo.identity.exception - -class InvalidCredentialsException( - message: String = "Invalid email or password") : RuntimeException(message) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/TokenExpiredException.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/TokenExpiredException.kt deleted file mode 100644 index b50be31..0000000 --- a/identity/src/main/kotlin/org/spendoo/identity/exception/TokenExpiredException.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.spendoo.identity.exception - -class TokenExpiredException ( - message: String = "Token has expired. Please login again.") : RuntimeException(message) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/UnauthorizedException.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/UnauthorizedException.kt deleted file mode 100644 index 73e74a4..0000000 --- a/identity/src/main/kotlin/org/spendoo/identity/exception/UnauthorizedException.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.spendoo.identity.exception - -class UnauthorizedException ( - message: String = "Unauthorized access") : RuntimeException(message) \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/exception/UserAlreadyExistsException.kt b/identity/src/main/kotlin/org/spendoo/identity/exception/UserAlreadyExistsException.kt deleted file mode 100644 index 3ecc10b..0000000 --- a/identity/src/main/kotlin/org/spendoo/identity/exception/UserAlreadyExistsException.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.spendoo.identity.exception - -class UserAlreadyExistsException( - message: String = "User already exists") : RuntimeException(message) diff --git a/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt b/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt index 2b3320b..f4d2afb 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/mapper/UserMapper.kt @@ -1,5 +1,5 @@ package org.spendoo.identity.mapper -import org.spendoo.identity.dto.RegisterRequest +import org.spendoo.identity.dto.request.RegisterRequest import org.spendoo.identity.entity.User import java.time.LocalDateTime diff --git a/identity/src/main/kotlin/org/spendoo/identity/repository/UserRepository.kt b/identity/src/main/kotlin/org/spendoo/identity/repository/UserRepository.kt index 90488e2..a47fd69 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/repository/UserRepository.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/repository/UserRepository.kt @@ -3,8 +3,9 @@ package org.spendoo.identity.repository import org.spendoo.identity.entity.User import org.springframework.data.jpa.repository.JpaRepository import java.util.Optional +import java.util.UUID -interface UserRepository : JpaRepository { +interface UserRepository : JpaRepository { fun findByEmail(email: String): Optional fun existsByEmail(email: String): Boolean } \ No newline at end of file diff --git a/identity/src/main/kotlin/org/spendoo/identity/service/AuthService.kt b/identity/src/main/kotlin/org/spendoo/identity/service/AuthService.kt index f2be674..248b9d9 100644 --- a/identity/src/main/kotlin/org/spendoo/identity/service/AuthService.kt +++ b/identity/src/main/kotlin/org/spendoo/identity/service/AuthService.kt @@ -1,9 +1,9 @@ package org.spendoo.identity.service -import org.spendoo.identity.dto.AuthResponse -import org.spendoo.identity.dto.LoginRequest -import org.spendoo.identity.dto.RefreshTokenRequest -import org.spendoo.identity.dto.RegisterRequest +import org.spendoo.identity.dto.response.AuthResponse +import org.spendoo.identity.dto.request.LoginRequest +import org.spendoo.identity.dto.request.RefreshTokenRequest +import org.spendoo.identity.dto.request.RegisterRequest import org.spendoo.identity.entity.RefreshToken import org.spendoo.identity.entity.User import org.spendoo.identity.exception.TokenExpiredException