Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feature/auth
Browse files Browse the repository at this point in the history
  • Loading branch information
bdmendes committed Nov 23, 2022
2 parents edc8ff1 + b43981a commit 5a0f2e2
Show file tree
Hide file tree
Showing 24 changed files with 752 additions and 80 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,6 @@ gradle test
- `dto/` - Data Transfer Objects for creating and modifying entities
- `repository/` - Data access layer methods (Spring Data repositories)
- `service/` - Business logic for the controllers
- `annotations/` - Custom annotations used in the project
- `validation/` - Custom validations used across the different models
- `src/test/` - Self explanatory: unit tests, functional (end-to-end) tests, etc.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ dependencies {

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
freeCompilerArgs = listOf("-Xjsr305=strict", "-Xemit-jvm-type-annotations")
jvmTarget = "17"
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pt.up.fe.ni.website.backend.annotations.validation

import javax.validation.Constraint
import javax.validation.ConstraintValidator
import javax.validation.ConstraintValidatorContext
import javax.validation.Payload
import kotlin.reflect.KClass

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
@Constraint(validatedBy = [NullOrNotBlankValidator::class])
@MustBeDocumented
annotation class NullOrNotBlank(
val message: String = "must be null or not blank",
val groups: Array<KClass<*>> = [],
val payload: Array<KClass<Payload>> = []
)

class NullOrNotBlankValidator : ConstraintValidator<NullOrNotBlank, String?> {
override fun isValid(value: String?, context: ConstraintValidatorContext?): Boolean {
return value == null || value.isNotBlank()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ class AuthConfig(val rsaKeys: RSAKeyProperties) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
return http.csrf { csrf -> csrf.disable() }
.oauth2ResourceServer(OAuth2ResourceServerConfigurer<HttpSecurity>::jwt)
.sessionManagement { session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.httpBasic(Customizer.withDefaults()).build()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer<HttpSecurity>::jwt)
.sessionManagement { session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
.httpBasic(Customizer.withDefaults()).build()
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package pt.up.fe.ni.website.backend.config

import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey

@ConstructorBinding
@ConfigurationProperties(prefix = "rsa")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pt.up.fe.ni.website.backend.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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
import pt.up.fe.ni.website.backend.model.dto.AccountDto
import pt.up.fe.ni.website.backend.service.AccountService

@RestController
@RequestMapping("/accounts")
class AccountController(private val service: AccountService) {
@GetMapping
fun getAllAccounts() = service.getAllAccounts()

@GetMapping("/{id}")
fun getAccountById(@PathVariable id: Long) = service.getAccountById(id)

@PostMapping("/new")
fun createAccount(@RequestBody dto: AccountDto) = service.createAccount(dto)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package pt.up.fe.ni.website.backend.controller

import org.springframework.http.HttpStatus
import org.springframework.security.access.annotation.Secured
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation.GetMapping
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
package pt.up.fe.ni.website.backend.controller

import com.fasterxml.jackson.databind.exc.InvalidFormatException
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import org.springframework.boot.web.servlet.error.ErrorController
import org.springframework.http.HttpStatus
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.security.access.AccessDeniedException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.RestControllerAdvice
import javax.validation.ConstraintViolationException
import org.springframework.security.access.AccessDeniedException
import org.springframework.web.server.ResponseStatusException
import javax.servlet.http.HttpServletResponse
import javax.validation.ConstraintViolationException

data class SimpleError(
val message: String,
val param: String? = null,
val value: Any? = null
val message: String,
val param: String? = null,
val value: Any? = null
)

data class CustomError(val errors: List<SimpleError>)
Expand All @@ -37,11 +38,11 @@ class ErrorController : ErrorController {
val errors = mutableListOf<SimpleError>()
e.constraintViolations.forEach { violation ->
errors.add(
SimpleError(
violation.message,
violation.propertyPath.toString(),
violation.invalidValue
)
SimpleError(
violation.message,
violation.propertyPath.toString(),
violation.invalidValue
)
)
}
return CustomError(errors)
Expand All @@ -54,15 +55,22 @@ class ErrorController : ErrorController {
is InvalidFormatException -> {
val type = cause.targetType.simpleName.lowercase()
return wrapSimpleError(
"must be $type",
value = cause.value
"must be $type",
value = cause.value
)
}

is MissingKotlinParameterException -> {
return wrapSimpleError(
"required",
param = cause.parameter.name
"required",
param = cause.parameter.name
)
}

is MismatchedInputException -> {
return wrapSimpleError(
"must be ${cause.targetType.simpleName.lowercase()}",
param = cause.path.joinToString(".") { it.fieldName }
)
}
}
Expand All @@ -76,6 +84,19 @@ class ErrorController : ErrorController {
return wrapSimpleError(e.message ?: "element not found")
}

@ExceptionHandler(IllegalArgumentException::class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
fun illegalArgument(e: IllegalArgumentException): CustomError {
return wrapSimpleError(e.message ?: "invalid argument")
}

@ExceptionHandler(Exception::class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun unexpectedError(e: Exception): CustomError {
System.err.println(e)
return wrapSimpleError("unexpected error: " + e.message)
}

@ExceptionHandler(AccessDeniedException::class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
fun unauthorized(e: AccessDeniedException): CustomError {
Expand All @@ -88,14 +109,7 @@ class ErrorController : ErrorController {
return wrapSimpleError(e.reason ?: (e.message))
}

@ExceptionHandler(Exception::class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
fun unexpectedError(e: Exception): CustomError {
System.err.println(e)
return wrapSimpleError("unexpected error: " + e.message)
}

fun wrapSimpleError(msg: String, param: String? = null, value: Any? = null) = CustomError(
mutableListOf(SimpleError(msg, param, value))
mutableListOf(SimpleError(msg, param, value))
)
}
46 changes: 43 additions & 3 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Account.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,57 @@
package pt.up.fe.ni.website.backend.model

import com.fasterxml.jackson.annotation.JsonProperty
import org.hibernate.validator.constraints.URL
import pt.up.fe.ni.website.backend.annotations.validation.NullOrNotBlank
import java.util.Date
import javax.persistence.CascadeType
import javax.persistence.Column
import javax.persistence.Entity
import javax.persistence.FetchType
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.persistence.ManyToOne
import javax.persistence.JoinColumn
import javax.persistence.OneToMany
import javax.validation.Valid
import javax.validation.constraints.Email
import javax.validation.constraints.NotEmpty
import javax.validation.constraints.Past
import javax.validation.constraints.Size
import pt.up.fe.ni.website.backend.model.constants.AccountConstants as Constants

@Entity
class Account(
@Column(nullable = false)
val name: String,
@JsonProperty(required = true)
@field:Size(min = Constants.Name.minSize, max = Constants.Name.maxSize)
var name: String,

@JsonProperty(required = true)
@Column(unique = true)
@field:NotEmpty
@field:Email
var email: String,

@field:Size(min = Constants.Bio.minSize, max = Constants.Bio.maxSize)
var bio: String?,

@field:Past
var birthDate: Date?,

@field:NullOrNotBlank
@field:URL
var photoPath: String?,

@field:NullOrNotBlank
@field:URL
var linkedin: String?,

@field:NullOrNotBlank
@field:URL
var github: String?,

@JoinColumn
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
val websites: List<@Valid CustomWebsite>,

@Id @GeneratedValue
val id: Long? = null
Expand Down
24 changes: 24 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/CustomWebsite.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pt.up.fe.ni.website.backend.model

import com.fasterxml.jackson.annotation.JsonProperty
import org.hibernate.validator.constraints.URL
import pt.up.fe.ni.website.backend.annotations.validation.NullOrNotBlank
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.validation.constraints.NotEmpty

@Entity
class CustomWebsite(
@JsonProperty(required = true)
@field:NotEmpty
@field:URL
val url: String,

@field:NullOrNotBlank
@field:URL
val iconPath: String?,

@Id @GeneratedValue
val id: Long? = null
)
4 changes: 4 additions & 0 deletions src/main/kotlin/pt/up/fe/ni/website/backend/model/Post.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package pt.up.fe.ni.website.backend.model

import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonProperty
import org.hibernate.validator.constraints.URL
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
Expand All @@ -26,12 +28,14 @@ class Post(

@JsonProperty(required = true)
@field:NotEmpty
@field:URL
var thumbnailPath: String,

@CreatedDate
var publishDate: Date? = null,

@LastModifiedDate
@JsonFormat(pattern = "dd-MM-yyyy HH:mm:ss")
var lastUpdatedAt: Date? = null,

@Id @GeneratedValue
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package pt.up.fe.ni.website.backend.model.constants

object AccountConstants {
object Name {
const val minSize = 2
const val maxSize = 100
}

object Bio {
const val minSize = 5
const val maxSize = 500
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package pt.up.fe.ni.website.backend.model.dto

import pt.up.fe.ni.website.backend.model.Account
import java.util.Date

class AccountDto(
val name: String,
val email: String,
val bio: String?,
val birthDate: Date?,
val photoPath: String?,
val linkedin: String?,
val github: String?,
val websites: List<CustomWebsiteDto>
) : Dto<Account>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pt.up.fe.ni.website.backend.model.dto

import pt.up.fe.ni.website.backend.model.CustomWebsite

class CustomWebsiteDto(
val url: String,
val iconPath: String?
) : Dto<CustomWebsite>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package pt.up.fe.ni.website.backend.repository

import org.springframework.data.repository.CrudRepository
import pt.up.fe.ni.website.backend.model.Account

interface AccountRepository : CrudRepository<Account, Long> {
fun findByEmail(email: String): Account?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package pt.up.fe.ni.website.backend.service

import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import pt.up.fe.ni.website.backend.model.Account
import pt.up.fe.ni.website.backend.model.dto.AccountDto
import pt.up.fe.ni.website.backend.repository.AccountRepository

@Service
class AccountService(private val repository: AccountRepository) {
fun getAllAccounts(): List<Account> = repository.findAll().toList()

fun createAccount(dto: AccountDto): Account {
repository.findByEmail(dto.email)?.let {
throw IllegalArgumentException("email already exists")
}

val account = dto.create()
return repository.save(account)
}

fun getAccountById(id: Long): Account = repository.findByIdOrNull(id)
?: throw NoSuchElementException("account not found with id $id")
}
Loading

0 comments on commit 5a0f2e2

Please sign in to comment.