Can login with SSO & change Password
This commit is contained in:
@@ -98,7 +98,7 @@ fun Application.module(env: Env = PROD) {
|
||||
decode { values, _ ->
|
||||
val id = values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
?: throw InternalError("Cannot convert $values to UUID")
|
||||
get<RepositoryCitizen>().findById(id) ?: throw InternalError("Citizen $values not found")
|
||||
get<RepositoryCitizen>().findById(id, true) ?: throw InternalError("Citizen $values not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,8 +156,8 @@ fun Application.module(env: Env = PROD) {
|
||||
// trace { application.log.trace(it.buildText()) }
|
||||
authenticate(optional = true) {
|
||||
article(get())
|
||||
auth(get(), get())
|
||||
citizen(get())
|
||||
auth(get(), get(), get())
|
||||
citizen(get(), get())
|
||||
constitution(get())
|
||||
followArticle(get())
|
||||
followConstitution(get())
|
||||
|
||||
@@ -12,6 +12,7 @@ class Config {
|
||||
private var config = ConfigFactory.load()
|
||||
val sqlFiles = File(this::class.java.getResource("/sql").toURI())
|
||||
val envName: String = config.getString("app.envName")
|
||||
val domain: String = config.getString("app.domain")
|
||||
|
||||
val host: String = config.getString("db.host")
|
||||
var database: String = config.getString("db.database")
|
||||
@@ -23,11 +24,12 @@ class Config {
|
||||
}
|
||||
|
||||
object JwtConfig {
|
||||
const val secret = "zAP5MBA4B4Ijz0MZaS48"
|
||||
private const val issuer = "dc-project.fr"
|
||||
private const val validityInMs = 36_000_00 * 10 // 10 hours
|
||||
private const val secret = "zAP5MBA4B4Ijz0MZaS48"
|
||||
const val issuer = "dc-project.fr"
|
||||
private const val validityInMs = 3_600_000 * 10 // 10 hours
|
||||
|
||||
// TODO change to RSA512
|
||||
private val algorithm = Algorithm.HMAC512(secret)
|
||||
val algorithm = Algorithm.HMAC512(secret)
|
||||
|
||||
val verifier: JWTVerifier = JWT
|
||||
.require(algorithm)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package fr.dcproject
|
||||
|
||||
import fr.dcproject.messages.Mailer
|
||||
import fr.dcproject.messages.SsoManager
|
||||
import fr.postgresjson.connexion.Connection
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.migration.Migrations
|
||||
@@ -50,4 +51,5 @@ val Module = module {
|
||||
single { Migrations(connection = get(), directory = config.sqlFiles) }
|
||||
|
||||
single { Mailer(config.sendGridKey) }
|
||||
single { SsoManager(get<Mailer>(), config.domain, get()) }
|
||||
}
|
||||
|
||||
@@ -4,20 +4,13 @@ import com.sendgrid.Method
|
||||
import com.sendgrid.Request
|
||||
import com.sendgrid.SendGrid
|
||||
import com.sendgrid.helpers.mail.Mail
|
||||
import com.sendgrid.helpers.mail.objects.Content
|
||||
import com.sendgrid.helpers.mail.objects.Email
|
||||
import java.io.IOException
|
||||
|
||||
class Mailer (
|
||||
private val key: String
|
||||
) {
|
||||
fun sendEmail(from: String, to: String, content: String, subject: String): Boolean {
|
||||
val mail = Mail(
|
||||
Email(from),
|
||||
subject,
|
||||
Email(to),
|
||||
Content("text/plain", content)
|
||||
)
|
||||
fun sendEmail(action: () -> Mail): Boolean {
|
||||
val mail = action()
|
||||
|
||||
val sg = SendGrid(key)
|
||||
val request = Request()
|
||||
|
||||
41
src/main/kotlin/fr/dcproject/messages/SsoManager.kt
Normal file
41
src/main/kotlin/fr/dcproject/messages/SsoManager.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
package fr.dcproject.messages
|
||||
|
||||
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 io.ktor.http.URLBuilder
|
||||
import fr.dcproject.entity.Citizen as CitizenEntity
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
|
||||
class SsoManager (
|
||||
private val mailer: Mailer,
|
||||
private val domain: String,
|
||||
private val citizenRepo: CitizenRepository
|
||||
) {
|
||||
fun sendMail(email: String, url: String) {
|
||||
val citizen = citizenRepo.findByEmail(email) ?: error("No Citizen with this email")
|
||||
mailer.sendEmail {
|
||||
Mail(
|
||||
Email("sso@$domain"),
|
||||
"Connection",
|
||||
Email(email),
|
||||
Content("text/plain", generateContent(citizen, url))
|
||||
).apply {
|
||||
addContent(Content("text/html", generateHtmlContent(citizen, url)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateHtmlContent(citizen: CitizenEntity, url: String): String? {
|
||||
val urlObject = URLBuilder(url)
|
||||
urlObject.parameters.append("token", JwtConfig.makeToken(citizen.user ?: error("Citizen must have User")))
|
||||
return "Click <a href=\"$urlObject\">here</a> for connect to $domain"
|
||||
}
|
||||
|
||||
private fun generateContent(citizen: CitizenEntity, url: String): String {
|
||||
val urlObject = URLBuilder(url)
|
||||
urlObject.parameters.append("token", JwtConfig.makeToken(citizen.user ?: error("Citizen must have User")))
|
||||
return "Copy this link into your browser for connect to $domain: \n$urlObject"
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,12 @@ class Citizen(override var requester: Requester) : RepositoryI<CitizenEntity> {
|
||||
.selectOne("username" to unsername)
|
||||
}
|
||||
|
||||
fun findByEmail(email: String): CitizenEntity? {
|
||||
return requester
|
||||
.getFunction("find_citizen_by_email")
|
||||
.selectOne("email" to email)
|
||||
}
|
||||
|
||||
fun find(
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
|
||||
@@ -32,6 +32,12 @@ class User(override var requester: Requester) : RepositoryI<UserEntity> {
|
||||
.selectOne("resource" to user)
|
||||
}
|
||||
|
||||
fun changePassword(user: UserEntity) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -3,13 +3,19 @@ package fr.dcproject.routes
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException
|
||||
import fr.dcproject.JwtConfig
|
||||
import fr.dcproject.entity.User
|
||||
import fr.dcproject.messages.SsoManager
|
||||
import fr.dcproject.routes.AuthPaths.LoginRequest
|
||||
import fr.dcproject.routes.AuthPaths.RegisterRequest
|
||||
import fr.dcproject.routes.AuthPaths.SsoRequest
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.UserPasswordCredential
|
||||
import io.ktor.features.BadRequestException
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
import io.ktor.locations.post
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.response.respondText
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
@@ -21,12 +27,19 @@ import fr.dcproject.repository.User as UserRepository
|
||||
object AuthPaths {
|
||||
@Location("/login") class LoginRequest
|
||||
@Location("/register") class RegisterRequest
|
||||
@Location("/sso") class SsoRequest {
|
||||
data class Content(val email: String, val url: String)
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@KtorExperimentalAPI
|
||||
fun Route.auth(userRepo: UserRepository, citizenRepo: CitizenRepository) {
|
||||
post <AuthPaths.LoginRequest> {
|
||||
fun Route.auth(
|
||||
userRepo: UserRepository,
|
||||
citizenRepo: CitizenRepository,
|
||||
ssoManager: SsoManager
|
||||
) {
|
||||
post <LoginRequest> {
|
||||
try {
|
||||
val credentials = call.receive<UserPasswordCredential>()
|
||||
val user = userRepo.findByCredentials(credentials) ?: throw BadRequestException("Username not exist or password is wrong")
|
||||
@@ -36,11 +49,18 @@ fun Route.auth(userRepo: UserRepository, citizenRepo: CitizenRepository) {
|
||||
}
|
||||
}
|
||||
|
||||
post <AuthPaths.RegisterRequest> {
|
||||
post <RegisterRequest> {
|
||||
val citizen = call.receive<CitizenEntity>()
|
||||
citizen.user?.roles = listOf(User.Roles.ROLE_USER)
|
||||
val created = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request")
|
||||
|
||||
call.respondText(JwtConfig.makeToken(created))
|
||||
}
|
||||
|
||||
post<SsoRequest> {
|
||||
val content = call.receive<SsoRequest.Content>()
|
||||
ssoManager.sendMail(content.email, content.url)
|
||||
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,46 +2,71 @@ package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.routes.CitizenPaths.ChangePasswordCitizenRequest
|
||||
import fr.dcproject.routes.CitizenPaths.CitizenRequest
|
||||
import fr.dcproject.routes.CitizenPaths.CitizensRequest
|
||||
import fr.dcproject.routes.CitizenPaths.CurrentCitizenRequest
|
||||
import fr.dcproject.security.voter.CitizenVoter.Action.CHANGE_PASSWORD
|
||||
import fr.dcproject.security.voter.CitizenVoter.Action.VIEW
|
||||
import fr.dcproject.security.voter.assertCan
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import fr.postgresjson.repository.RepositoryI.Direction
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
import io.ktor.locations.get
|
||||
import io.ktor.locations.put
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
import fr.dcproject.repository.User as UserRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object CitizenPaths {
|
||||
@Location("/citizens") class CitizensRequest(page: Int = 1, limit: Int = 50, val sort: String? = null, val direction: RepositoryI.Direction? = null, val search: String? = null) {
|
||||
@Location("/citizens") class CitizensRequest(page: Int = 1, limit: Int = 50, val sort: String? = null, val direction: Direction? = null, val search: String? = null) {
|
||||
val page: Int = if (page < 1) 1 else page
|
||||
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
|
||||
}
|
||||
@Location("/citizens/{citizen}") class CitizenRequest(val citizen: Citizen)
|
||||
@Location("/citizens/current") class CurrentCitizenRequest
|
||||
@Location("/citizens/{citizen}/follows/articles") class CitizenFollowArticleRequest(val citizen: Citizen)
|
||||
@Location("/citizens/{citizen}/follows/constitutions") class CitizenFollowConstitutionRequest(val citizen: Citizen)
|
||||
@Location("/citizens/{citizen}/password/change") class ChangePasswordCitizenRequest(val citizen: Citizen) {
|
||||
data class Content(val password: String)
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.citizen(repo: CitizenRepository) {
|
||||
get<CitizenPaths.CitizensRequest> {
|
||||
fun Route.citizen(
|
||||
repo: CitizenRepository,
|
||||
userRepository: UserRepository
|
||||
) {
|
||||
get<CitizensRequest> {
|
||||
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
|
||||
assertCan(VIEW, citizens.result)
|
||||
call.respond(citizens)
|
||||
}
|
||||
|
||||
get<CitizenPaths.CitizenRequest> {
|
||||
get<CitizenRequest> {
|
||||
assertCan(VIEW, it.citizen)
|
||||
|
||||
call.respond(it.citizen)
|
||||
}
|
||||
|
||||
get<CitizenPaths.CurrentCitizenRequest> {
|
||||
get<CurrentCitizenRequest> {
|
||||
assertCan(VIEW, citizen)
|
||||
|
||||
call.respond(citizen)
|
||||
}
|
||||
|
||||
put<ChangePasswordCitizenRequest> {
|
||||
assertCan(CHANGE_PASSWORD, it.citizen)
|
||||
val content = call.receive<ChangePasswordCitizenRequest.Content>()
|
||||
|
||||
val user = it.citizen.user ?: error("Citizen must have User")
|
||||
|
||||
user.plainPassword = content.password
|
||||
userRepository.changePassword(user)
|
||||
|
||||
call.respond(HttpStatusCode.Created)
|
||||
}
|
||||
}
|
||||
@@ -3,19 +3,24 @@ package fr.dcproject.security.voter
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.entity.User
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
class CitizenVoter: Voter {
|
||||
enum class Action: ActionI {
|
||||
CREATE,
|
||||
UPDATE,
|
||||
VIEW,
|
||||
DELETE
|
||||
DELETE,
|
||||
CHANGE_PASSWORD
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return (action is Action)
|
||||
&&
|
||||
(subject is List<*> || subject is Citizen?)
|
||||
&& (
|
||||
subject is List<*> ||
|
||||
subject is Citizen?
|
||||
)
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
@@ -52,6 +57,15 @@ class CitizenVoter: Voter {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.CHANGE_PASSWORD && user != null && subject is Citizen) {
|
||||
val userToChange = subject.user ?: error("Citizen must have User")
|
||||
return if (user.id == userToChange.id) {
|
||||
Vote.GRANTED
|
||||
} else {
|
||||
Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
|
||||
if (action is Action) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ ktor {
|
||||
|
||||
app {
|
||||
envName = prod
|
||||
domain = dc-project.fr
|
||||
}
|
||||
|
||||
db {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
create or replace function find_citizen_by_email(_email text, out resource json) language plpgsql as
|
||||
$$
|
||||
begin
|
||||
select to_json(t) into resource
|
||||
from (
|
||||
select
|
||||
z.*,
|
||||
find_user_by_id(z.user_id) as "user"
|
||||
from citizen as z
|
||||
where z.email = _email
|
||||
) as t;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- drop function if exists find_citizen_by_email(text, out json);
|
||||
@@ -19,7 +19,7 @@ create table citizen
|
||||
user_id uuid not null references "user" (id) unique,
|
||||
vote_anonymous boolean default true not null,
|
||||
follow_anonymous boolean default true not null,
|
||||
email text not null check ( email ~* '.+@.+\..+' )
|
||||
email text not null check ( email ~* '.+@.+\..+' ) unique
|
||||
);
|
||||
|
||||
create table workgroup
|
||||
|
||||
Reference in New Issue
Block a user