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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions authentication/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.coded.spring</groupId>
<artifactId>Ordering</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>authentication</artifactId>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
</dependencies>


</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package authentication

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class AuthenticationApplication

fun main(args: Array<String>) {
runApplication<AuthenticationApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package authentication.config

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.ContentCachingResponseWrapper

@Component
class LoggingFilter : OncePerRequestFilter() {

private val logger = LoggerFactory.getLogger(LoggingFilter::class.java)

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val cachedRequest = ContentCachingRequestWrapper(request)
val cachedResponse = ContentCachingResponseWrapper(response)

filterChain.doFilter(cachedRequest, cachedResponse)

logRequest(cachedRequest)
logResponse(cachedResponse)

cachedResponse.copyBodyToResponse()
}

private fun logRequest(request: ContentCachingRequestWrapper) {
val requestBody = String(request.contentAsByteArray)
logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody")
}

private fun logResponse(response: ContentCachingResponseWrapper) {
val responseBody = String(response.contentAsByteArray)
logger.info("Response: status=${response.status}, body=$responseBody")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package authentication.jwt

import jakarta.servlet.FilterChain
import jakarta.servlet.http.*
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthenticationFilter(
private val jwtService: JwtService,
private val userDetailsService: UserDetailsService
) : OncePerRequestFilter() {

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val authHeader = request.getHeader("Authorization")
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response)
return
}

val token = authHeader.substring(7)
val username = jwtService.extractUsername(token)

if (SecurityContextHolder.getContext().authentication == null) {
if (jwtService.isTokenValid(token, username)) {
val userDetails = userDetailsService.loadUserByUsername(username)
val authToken = UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.authorities
)
authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = authToken
}
}

filterChain.doFilter(request, response)
}
}
50 changes: 50 additions & 0 deletions authentication/src/main/kotlin/authentication/jwt/JwtService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package authentication.jwt

import io.jsonwebtoken.*
import io.jsonwebtoken.security.Keys
import org.springframework.stereotype.Component
import java.util.*
import javax.crypto.SecretKey


@Component
class JwtService {

private val secretKey: SecretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256)
private val expirationMs: Long = 1000 * 60 * 60

fun generateToken(username: String): String {
val now = Date()
val expiry = Date(now.time + expirationMs)

return Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(secretKey)
.compact()
}

fun extractUsername(token: String): String {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.body.subject
}

fun isTokenValid(token: String, username: String): Boolean {
return try {
val extractedUsername = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.body
.subject

extractedUsername == username
} catch (e: Exception) {
false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.coded.spring.ordering.exceptions

class InvalidProfileException(message: String) : Exception(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package authentication.profile

import com.coded.spring.ordering.exceptions.InvalidProfileException
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.security.Principal

@RestController
@RequestMapping("/profile")
@Tag(name = "PROFILE", description = "Endpoints for managing user profiles")
class ProfileController(
private val profileService: ProfileService
) {

@PostMapping
@Operation(summary = "Submit user profile", description = "Creates a new profile for the logged-in user")
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "Profile created successfully"),
ApiResponse(responseCode = "400", description = "Invalid profile data or already exists"),
ApiResponse(responseCode = "500", description = "Internal server error")
]
)
fun submitProfile(
@RequestBody request: ProfileRequest,
principal: Principal
): ResponseEntity<Any> {
return try {
profileService.saveProfile(principal.name, request)
ResponseEntity.ok(mapOf("message" to "Profile created successfully."))
} catch (e: InvalidProfileException) {
ResponseEntity.badRequest().body(mapOf("error" to e.message))
} catch (e: Exception) {
e.printStackTrace()
ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong."))
}
}

@GetMapping
@Operation(summary = "Get user profile", description = "Fetches the profile for the logged-in user")
@ApiResponses(
value = [
ApiResponse(responseCode = "200", description = "Profile fetched successfully"),
ApiResponse(responseCode = "400", description = "Profile not found or invalid request"),
ApiResponse(responseCode = "500", description = "Internal server error")
]
)
fun getProfile(principal: Principal): ResponseEntity<Any> {
return try {
val profile = profileService.getByUsername(principal.name)
ResponseEntity.ok(profile)
} catch (e: InvalidProfileException) {
ResponseEntity.badRequest().body(mapOf("error" to e.message))
} catch (e: Exception) {
e.printStackTrace()
ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong."))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package authentication.profile

data class ProfileRequest(
val firstName: String,
val lastName: String,
val phoneNumber: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package authentication.profile

import jakarta.inject.Named
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository

@Named
interface ProfileRepository : JpaRepository<ProfileEntity, Long> {
fun findByUserId(userId: Long): ProfileEntity?
}

@Entity
@Table(name = "profiles")
data class ProfileEntity(

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,

val firstName: String = "",
val lastName: String = "",
val phoneNumber: String = "",
val userId: Long = 0

)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package authentication.profile

import authentication.users.UsersRepository
import com.coded.spring.ordering.exceptions.InvalidProfileException
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
open class ProfileService(
private val profileRepository: ProfileRepository,
private val usersRepository: UsersRepository
) {

fun isValidName(name: String): Boolean {
return name.matches(Regex("^[A-Za-z]+$"))
}

fun isValidPhone(phone: String): Boolean {
return phone.matches(Regex("^[0-9]{8}$"))
}

@Transactional
fun saveProfile(username: String, request: ProfileRequest): ProfileEntity {
val user = usersRepository.findByUsername(username)
?: throw UsernameNotFoundException("User not found")

val existingProfile = profileRepository.findByUserId(user.id!!)
if (existingProfile != null) {
throw InvalidProfileException("Profile already exists for this user")
}

if (!isValidName(request.firstName)) {
throw InvalidProfileException("First name must contain only letters")
}

if (!isValidName(request.lastName)) {
throw InvalidProfileException("Last name must contain only letters")
}

if (!isValidPhone(request.phoneNumber)) {
throw InvalidProfileException("Phone number must be exactly 8 digits")
}

val profile = ProfileEntity(
firstName = request.firstName,
lastName = request.lastName,
phoneNumber = request.phoneNumber,
userId = user.id!!
)

return profileRepository.save(profile)
}

fun getByUsername(username: String): ProfileEntity {
val user = usersRepository.findByUsername(username)
if (user == null) {
throw UsernameNotFoundException("User not found")
}

val profile = profileRepository.findByUserId(user.id!!)
if (profile == null) {
throw InvalidProfileException("Profile not found for this user")
}

return profile
}
}
Loading