Move Auth to a Component
This commit is contained in:
61
src/main/kotlin/component/auth/SsoManager.kt
Normal file
61
src/main/kotlin/component/auth/SsoManager.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package fr.dcproject.component.auth
|
||||
|
||||
import com.sendgrid.helpers.mail.Mail
|
||||
import com.sendgrid.helpers.mail.objects.Content
|
||||
import com.sendgrid.helpers.mail.objects.Email
|
||||
import fr.dcproject.JwtConfig
|
||||
import fr.dcproject.component.citizen.CitizenBasicI
|
||||
import fr.dcproject.component.citizen.CitizenRepository
|
||||
import fr.dcproject.messages.Mailer
|
||||
import io.ktor.http.*
|
||||
|
||||
/**
|
||||
* Send an email to the citizen with a link to automatically connect
|
||||
*/
|
||||
class SsoManager(
|
||||
private val mailer: Mailer,
|
||||
private val domain: String,
|
||||
private val citizenRepo: CitizenRepository
|
||||
) {
|
||||
fun sendEmail(email: String, url: String) {
|
||||
val citizen = citizenRepo.findByEmail(email) ?: noEmail(email)
|
||||
sendEmail(citizen, url)
|
||||
}
|
||||
|
||||
fun sendEmail(citizen: CitizenBasicI, url: String) {
|
||||
mailer.sendEmail {
|
||||
Mail(
|
||||
Email("sso@$domain"),
|
||||
"Connection",
|
||||
Email(citizen.email),
|
||||
Content("text/plain", generateContent(citizen, url))
|
||||
).apply {
|
||||
addContent(Content("text/html", generateHtmlContent(citizen, url)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO pass token to the function to avoid double generations
|
||||
*/
|
||||
private fun generateHtmlContent(citizen: CitizenBasicI, url: String): String? {
|
||||
val urlObject = URLBuilder(url)
|
||||
urlObject.parameters.append("token", JwtConfig.makeToken(citizen.user))
|
||||
return "Click <a href=\"${urlObject.buildString()}\">here</a> for connect to $domain"
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO pass token to the function to avoid double generations
|
||||
*/
|
||||
private fun generateContent(citizen: CitizenBasicI, url: String): String {
|
||||
val urlObject = URLBuilder(url)
|
||||
urlObject.parameters.append("token", JwtConfig.makeToken(citizen.user))
|
||||
return "Copy this link into your browser for connect to $domain: \n${urlObject.buildString()}"
|
||||
}
|
||||
|
||||
class EmailNotFound(val email: String) : Exception() {
|
||||
override val message: String = "No Citizen with this email : $email"
|
||||
}
|
||||
|
||||
private fun noEmail(email: String): Nothing = throw EmailNotFound(email)
|
||||
}
|
||||
49
src/main/kotlin/component/auth/User.kt
Normal file
49
src/main/kotlin/component/auth/User.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
package fr.dcproject.component.auth
|
||||
|
||||
import fr.dcproject.component.auth.UserI.Roles
|
||||
import fr.postgresjson.entity.*
|
||||
import io.ktor.auth.*
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
class User(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
username: String,
|
||||
blockedAt: DateTime? = null,
|
||||
override var plainPassword: String? = null,
|
||||
override var roles: List<Roles> = emptyList()
|
||||
) : UserFull, UserBasic(id, username, blockedAt),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityUpdatedAt by EntityUpdatedAtImp()
|
||||
|
||||
@Deprecated("")
|
||||
open class UserBasic(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override var username: String,
|
||||
override var blockedAt: DateTime? = null
|
||||
) : UserBasicI, UserRef(id)
|
||||
|
||||
open class UserRef(
|
||||
id: UUID = UUID.randomUUID()
|
||||
) : UserI, UuidEntity(id)
|
||||
|
||||
interface UserI : UuidEntityI, Principal {
|
||||
enum class Roles { ROLE_USER, ROLE_ADMIN }
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
interface UserBasicI : UserI {
|
||||
var username: String
|
||||
var blockedAt: DateTime?
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
interface UserFull : UserBasicI, EntityCreatedAt, EntityUpdatedAt {
|
||||
var plainPassword: String?
|
||||
var roles: List<Roles>
|
||||
}
|
||||
|
||||
interface UserForAuthI : UserI {
|
||||
var roles: List<Roles>
|
||||
var blockedAt: DateTime?
|
||||
}
|
||||
42
src/main/kotlin/component/auth/UserRepository.kt
Normal file
42
src/main/kotlin/component/auth/UserRepository.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package fr.dcproject.component.auth
|
||||
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import io.ktor.auth.UserPasswordCredential
|
||||
import java.util.*
|
||||
import fr.dcproject.component.auth.User as UserEntity
|
||||
|
||||
class UserRepository(override var requester: Requester) : RepositoryI {
|
||||
fun findByCredentials(credentials: UserPasswordCredential): UserEntity? {
|
||||
return requester
|
||||
.getFunction("check_user")
|
||||
.selectOne(
|
||||
"username" to credentials.name,
|
||||
"plain_password" to credentials.password
|
||||
)
|
||||
}
|
||||
|
||||
fun findById(id: UUID): UserEntity {
|
||||
return requester
|
||||
.getFunction("find_user_by_id")
|
||||
.selectOne(
|
||||
"id" to id
|
||||
) ?: throw UserNotFound(id)
|
||||
}
|
||||
|
||||
fun insert(user: UserEntity): UserEntity? {
|
||||
return requester
|
||||
.getFunction("insert_user")
|
||||
.selectOne("resource" to user)
|
||||
}
|
||||
|
||||
fun changePassword(user: UserFull) {
|
||||
requester
|
||||
.getFunction("change_user_password")
|
||||
.sendQuery("resource" to user)
|
||||
}
|
||||
|
||||
class UserNotFound(override val message: String?, override val cause: Throwable?) : Throwable(message, cause) {
|
||||
constructor(id: UUID) : this("No User with ID $id", null)
|
||||
}
|
||||
}
|
||||
32
src/main/kotlin/component/auth/routes/Login.kt
Normal file
32
src/main/kotlin/component/auth/routes/Login.kt
Normal file
@@ -0,0 +1,32 @@
|
||||
package fr.dcproject.component.auth.routes
|
||||
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException
|
||||
import fr.dcproject.JwtConfig
|
||||
import fr.dcproject.component.auth.UserRepository
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.util.*
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@Location("/login")
|
||||
private class LoginRequest
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@KtorExperimentalAPI
|
||||
fun Route.authLogin(userRepo: UserRepository) {
|
||||
post<LoginRequest> {
|
||||
try {
|
||||
val credentials = call.receive<UserPasswordCredential>()
|
||||
userRepo.findByCredentials(credentials)?.let { user ->
|
||||
call.respondText(JwtConfig.makeToken(user))
|
||||
} ?: call.respond(HttpStatusCode.BadRequest, "Username not exist or password is wrong")
|
||||
} catch (e: MismatchedInputException) {
|
||||
call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request")
|
||||
}
|
||||
}
|
||||
}
|
||||
69
src/main/kotlin/component/auth/routes/Register.kt
Normal file
69
src/main/kotlin/component/auth/routes/Register.kt
Normal file
@@ -0,0 +1,69 @@
|
||||
package fr.dcproject.component.auth.routes
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
|
||||
import fr.dcproject.JwtConfig
|
||||
import fr.dcproject.component.auth.routes.RegisterRequest.Input
|
||||
import fr.dcproject.component.citizen.Citizen
|
||||
import fr.dcproject.component.citizen.CitizenI
|
||||
import fr.dcproject.component.citizen.CitizenRepository
|
||||
import fr.dcproject.component.auth.User
|
||||
import fr.dcproject.component.auth.UserI
|
||||
import io.ktor.application.*
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.util.*
|
||||
import org.joda.time.DateTime
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@Location("/register")
|
||||
private class RegisterRequest {
|
||||
data class Input(
|
||||
val name: Name,
|
||||
val email: String,
|
||||
val birthday: DateTime,
|
||||
val voteAnonymous: Boolean = true,
|
||||
val followAnonymous: Boolean = true,
|
||||
val user: User
|
||||
) {
|
||||
data class Name(
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val civility: String? = null
|
||||
)
|
||||
data class User(
|
||||
val username: String,
|
||||
val plainPassword: String? = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@KtorExperimentalAPI
|
||||
fun Route.authRegister(citizenRepo: CitizenRepository) {
|
||||
fun Input.toCitizen(): Citizen = Citizen(
|
||||
name = CitizenI.Name(name.firstName, name.lastName, name.civility),
|
||||
birthday = birthday,
|
||||
email = email,
|
||||
followAnonymous = followAnonymous,
|
||||
voteAnonymous = voteAnonymous,
|
||||
user = User(
|
||||
username = user.username,
|
||||
plainPassword = user.plainPassword,
|
||||
roles = listOf(UserI.Roles.ROLE_USER)
|
||||
)
|
||||
)
|
||||
|
||||
post<RegisterRequest> {
|
||||
try {
|
||||
val citizen = call.receive<Input>().toCitizen()
|
||||
val createdCitizen = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request")
|
||||
call.respondText(JwtConfig.makeToken(createdCitizen))
|
||||
} catch (e: MissingKotlinParameterException) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/main/kotlin/component/auth/routes/Sso.kt
Normal file
35
src/main/kotlin/component/auth/routes/Sso.kt
Normal file
@@ -0,0 +1,35 @@
|
||||
package fr.dcproject.component.auth.routes
|
||||
|
||||
import fr.dcproject.component.auth.routes.SsoRequest.Input
|
||||
import fr.dcproject.component.auth.SsoManager
|
||||
import io.ktor.application.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.request.*
|
||||
import io.ktor.response.*
|
||||
import io.ktor.routing.*
|
||||
import io.ktor.util.*
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@Location("/sso")
|
||||
private class SsoRequest {
|
||||
data class Input(val email: String, val url: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email to the citizen with a link to automatically connect
|
||||
*/
|
||||
@KtorExperimentalLocationsAPI
|
||||
@KtorExperimentalAPI
|
||||
fun Route.authSso(ssoManager: SsoManager) {
|
||||
post<SsoRequest> {
|
||||
call.receive<Input>().run {
|
||||
try {
|
||||
ssoManager.sendEmail(email, url)
|
||||
} catch (e: SsoManager.EmailNotFound) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
package fr.dcproject.component.citizen
|
||||
|
||||
import fr.dcproject.component.citizen.CitizenI.Name
|
||||
import fr.dcproject.entity.User
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.dcproject.entity.UserRef
|
||||
import fr.dcproject.component.auth.User
|
||||
import fr.dcproject.component.auth.UserI
|
||||
import fr.dcproject.component.auth.UserRef
|
||||
import fr.dcproject.entity.WorkgroupSimple
|
||||
import fr.postgresjson.entity.*
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
@Deprecated("")
|
||||
class Citizen(
|
||||
override val id: UUID = UUID.randomUUID(),
|
||||
override val name: Name,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package fr.dcproject.component.citizen
|
||||
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.dcproject.component.auth.UserI
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
|
||||
@@ -5,7 +5,7 @@ import fr.dcproject.citizen
|
||||
import fr.dcproject.citizenOrNull
|
||||
import fr.dcproject.component.citizen.Citizen
|
||||
import fr.dcproject.component.citizen.CitizenVoter
|
||||
import fr.dcproject.repository.User
|
||||
import fr.dcproject.component.auth.UserRepository
|
||||
import fr.dcproject.voter.assert
|
||||
import io.ktor.application.*
|
||||
import io.ktor.auth.*
|
||||
@@ -23,7 +23,7 @@ class ChangePasswordCitizenRequest(val citizen: Citizen) {
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.changeMyPassword(voter: CitizenVoter, userRepository: User) {
|
||||
fun Route.changeMyPassword(voter: CitizenVoter, userRepository: UserRepository) {
|
||||
put<ChangePasswordCitizenRequest> {
|
||||
voter.assert { canChangePassword(it.citizen, citizenOrNull) }
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user