Skip to content
Merged
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
8 changes: 7 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ version = "1.0"

application {
mainClass.set("es.wokis.ApplicationKt")
mainClassName = "es.wokis.ApplicationKt"

val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
Expand Down Expand Up @@ -55,6 +54,13 @@ dependencies {

// BCrypt
implementation("org.mindrot:jbcrypt:0.4")

// Google auth
implementation("com.google.api-client:google-api-client:2.1.1")

// JavaMail
implementation("javax.mail:javax.mail-api:1.6.2")
implementation("com.sun.mail:javax.mail:1.6.2")
}

ktor {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ktor_version=2.1.3
ktor_version=2.2.1
kotlin_version=1.7.21
logback_version=1.4.4
kmongo_version=4.7.2
Expand Down
2 changes: 2 additions & 0 deletions src/main/kotlin/es/wokis/data/bo/user/UserBO.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package es.wokis.data.bo.user

import es.wokis.data.constants.ServerConstants.DEFAULT_LANG
import es.wokis.data.constants.ServerConstants.EMPTY_TEXT
import io.ktor.server.auth.*

Expand All @@ -9,5 +10,6 @@ data class UserBO(
val email: String,
val password: String,
val image: String = EMPTY_TEXT,
val lang: String = DEFAULT_LANG,
val devices: List<String> = emptyList()
) : Principal
10 changes: 10 additions & 0 deletions src/main/kotlin/es/wokis/data/bo/verification/VerificationBO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package es.wokis.data.bo.verification

import java.util.*

data class VerificationBO(
val id: Long? = null,
val email: String,
val verificationToken: String,
val timeStamp: Date = Date()
)
1 change: 1 addition & 0 deletions src/main/kotlin/es/wokis/data/constants/ServerConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ package es.wokis.data.constants

object ServerConstants {
const val EMPTY_TEXT = ""
const val DEFAULT_LANG = "en"
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface UserLocalDataSource {
suspend fun getUserByUsernameOrEmail(username: String, email: String = EMPTY_TEXT): UserBO?

suspend fun createUser(user: UserBO): Boolean
suspend fun updateUser(user: UserBO): Boolean
}

class UserLocalDataSourceImpl(private val userCollection: MongoCollection<UserDBO>) : UserLocalDataSource {
Expand Down Expand Up @@ -60,4 +61,9 @@ class UserLocalDataSourceImpl(private val userCollection: MongoCollection<UserDB
false
}
}

override suspend fun updateUser(user: UserBO): Boolean {
val bsonId: Id<UserDBO> = ObjectId(user.id).toId()
return userCollection.updateOne(UserDBO::id eq bsonId, user.toDBO()).wasAcknowledged()
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/es/wokis/data/dbo/user/UserDBO.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package es.wokis.data.dbo.user

import es.wokis.data.constants.ServerConstants
import org.bson.codecs.pojo.annotations.BsonId
import org.litote.kmongo.Id

Expand All @@ -9,5 +10,7 @@ data class UserDBO(
val username: String,
val email: String,
val password: String,
val lang: String,
val image: String = ServerConstants.EMPTY_TEXT,
val devices: List<String> = emptyList()
)
1 change: 1 addition & 0 deletions src/main/kotlin/es/wokis/data/dto/user/UserDTO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ data class UserDTO(
val username: String,
val email: String,
val image: String = ServerConstants.EMPTY_TEXT,
val lang: String,
val devices: List<String> = emptyList()
)
9 changes: 7 additions & 2 deletions src/main/kotlin/es/wokis/data/dto/user/auth/Auth.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package es.wokis.data.dto.user.auth

import es.wokis.data.constants.ServerConstants.DEFAULT_LANG

data class LoginDTO(
val username: String,
val password: String
val password: String,
val isGoogleAuth: Boolean = false
)

data class RegisterDTO(
val username: String,
val email: String,
val password: String
val password: String,
val lang: String = DEFAULT_LANG,
val isGoogleAuth: Boolean = false
)
9 changes: 8 additions & 1 deletion src/main/kotlin/es/wokis/data/mapper/user/UserMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.mindrot.jbcrypt.BCrypt
fun RegisterDTO.toBO() = UserBO(
username = username,
email = email,
lang = lang,
password = BCrypt.hashpw(password, BCrypt.gensalt()),
)

Expand All @@ -27,14 +28,17 @@ fun UserDTO.toBO() = UserBO(
email = email,
password = EMPTY_TEXT,
image = image,
lang = lang,
devices = devices
)

fun UserBO.toDBO() = UserDBO(
id = id?.let { ObjectId(it).toId() },
username = username,
email = email,
password = password.orEmpty(),
password = password,
lang = lang,
image = image,
devices = devices
)

Expand All @@ -43,6 +47,8 @@ fun UserDBO.toBO() = UserBO(
username = username,
email = email,
password = password,
image = image,
lang = lang,
devices = devices
)

Expand All @@ -53,5 +59,6 @@ fun UserBO.toDTO() = UserDTO(
username = username,
email = email,
image = image,
lang = lang,
devices = devices
)
65 changes: 63 additions & 2 deletions src/main/kotlin/es/wokis/data/repository/user/UserRepository.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
package es.wokis.data.repository.user

import com.google.firebase.auth.hash.Bcrypt
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import es.wokis.data.bo.user.UserBO
import es.wokis.data.constants.ServerConstants.DEFAULT_LANG
import es.wokis.data.constants.ServerConstants.EMPTY_TEXT
import es.wokis.data.datasource.UserLocalDataSource
import es.wokis.data.dto.user.auth.LoginDTO
import es.wokis.data.dto.user.auth.RegisterDTO
import es.wokis.data.mapper.user.toBO
import es.wokis.plugins.config
import es.wokis.plugins.makeToken
import es.wokis.utils.HashGenerator
import es.wokis.utils.generatePassword
import org.mindrot.jbcrypt.BCrypt
import kotlin.math.log

interface UserRepository {
suspend fun login(login: LoginDTO): String?
suspend fun loginWithGoogle(googleToken: String): String?
suspend fun register(register: RegisterDTO): String?
suspend fun getUsers(): List<UserBO>
suspend fun getUserById(id: String?): UserBO?
suspend fun getUserByUsername(name: String?): UserBO?
suspend fun getUserByEmail(email: String?): UserBO?
suspend fun updateUser(user: UserBO): Boolean
}

class UserRepositoryImpl(private val userLocalDataSource: UserLocalDataSource) : UserRepository {
Expand All @@ -33,6 +42,57 @@ class UserRepositoryImpl(private val userLocalDataSource: UserLocalDataSource) :
}
}

override suspend fun loginWithGoogle(googleToken: String): String? {
val verifier: GoogleIdTokenVerifier =
GoogleIdTokenVerifier.Builder(
NetHttpTransport(),
GsonFactory()
)
.setAudience(listOf(config.getString("google.clientId")))
.setIssuer("https://accounts.google.com")
.build()

return verifier.verify(googleToken)?.let {
val payload: GoogleIdToken.Payload = it.payload

// Print user identifier
val userId: String = payload.subject
println("User ID: $userId")

// Get profile information from payload
val email: String = payload.email
val imageUrl: String = (payload["picture"] as? String).orEmpty()
val locale: String = payload["locale"] as? String ?: DEFAULT_LANG
val username = email.split("@").firstOrNull() ?: HashGenerator.generateHash()
val user = getUserByEmail(email)
val token = if (user == null) {
val token = register(
RegisterDTO(
username = username,
email = email,
password = EMPTY_TEXT,
isGoogleAuth = true,
lang = locale
)
)
getUserByEmail(email)?.let { userNotNull ->
updateUser(
userNotNull.copy(
image = imageUrl
)
)
}
token

} else {
login(
LoginDTO(username = username, password = EMPTY_TEXT, isGoogleAuth = true)
)
}
token
}
}

override suspend fun register(register: RegisterDTO): String? {
val currentUser = userLocalDataSource.getUserByUsernameOrEmail(register.username, register.email)
val user = register.toBO()
Expand Down Expand Up @@ -65,4 +125,5 @@ class UserRepositoryImpl(private val userLocalDataSource: UserLocalDataSource) :
userLocalDataSource.getUserByEmail(it)
}

override suspend fun updateUser(user: UserBO): Boolean = userLocalDataSource.updateUser(user)
}
5 changes: 4 additions & 1 deletion src/main/kotlin/es/wokis/plugins/Security.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package es.wokis.plugins
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.google.firebase.auth.hash.Bcrypt
import es.wokis.data.bo.user.UserBO
import es.wokis.data.repository.user.UserRepository
import es.wokis.utils.orGeneratePassword
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import org.koin.ktor.ext.inject
import org.mindrot.jbcrypt.BCrypt
import java.util.*

private lateinit var jwtIssuer: String
Expand Down Expand Up @@ -59,6 +62,6 @@ fun makeToken(user: UserBO): String =
.withIssuer(jwtIssuer)
.withAudience(jwtAudience)
.withClaim("username", user.username)
.withClaim("password", user.password)
.withClaim("password", user.password.takeIf { it.isNotBlank() }.orGeneratePassword() )
.withClaim("timestamp", Date().time)
.sign(algorithm)
8 changes: 8 additions & 0 deletions src/main/kotlin/es/wokis/routing/AuthRouting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,12 @@ fun Routing.setUpAuthRouting() {
call.respond(HttpStatusCode.Conflict, "That user already exists")
}
}

post("/google-auth") {
val googleToken = call.receive<String>()
val token: String? = userRepository.loginWithGoogle(googleToken)
token?.let {
call.respond(HttpStatusCode.OK, it)
} ?: call.respond(HttpStatusCode.NotFound, "That user doesn't exists.")
}
}
61 changes: 61 additions & 0 deletions src/main/kotlin/es/wokis/services/EmailService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package es.wokis.services

import es.wokis.data.bo.user.UserBO
import es.wokis.data.bo.verification.VerificationBO
import es.wokis.plugins.config
import es.wokis.utils.HashGenerator
import java.util.*
import javax.mail.Message
import javax.mail.MessagingException
import javax.mail.Session
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage

class EmailService {
private val fromEmail = config.getString("mail.user")
private val fromPassword = config.getString("mail.pass")

private val about = "Invitacion"

fun sendEmail(user: UserBO): VerificationBO {
val emailHtml = this::class.java.getResource("/emails/${user.lang}/email-verify.html") ?: throw IllegalAccessException()

val properties: Properties = System.getProperties().apply {
put("mail.smtp.host", "email.wokis.es")
put("mail.smtp.user", fromEmail)
put("mail.smtp.clave", fromPassword)
put("mail.smtp.auth", "true")
put("mail.smtp.starttls.enable", "true")
put("mail.smtp.ssl.trust", "email.wokis.es");
put("mail.smtp.port", 587)
}
val hash = HashGenerator.generateHash(20)
val cuerpo = emailHtml.readText()

val session = Session.getDefaultInstance(properties)
val message = MimeMessage(session)

try {
with(message) {
setFrom(InternetAddress(fromEmail))
addRecipients(Message.RecipientType.TO, user.email)
subject = about
setContent(cuerpo, "text/html")
}

with(session.getTransport("smtp")) {
connect("email.wokis.es", fromEmail, fromPassword)
sendMessage(message, message.allRecipients)
close()
}

} catch (e: MessagingException) {
println(e.message)
}

return VerificationBO(
email = user.email,
verificationToken = hash
)
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/es/wokis/services/NotificationService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package es.wokis.services

import com.google.firebase.messaging.FirebaseMessaging
import com.google.firebase.messaging.MulticastMessage
import es.wokis.data.bo.user.UserBO


fun sendNotifications(users: List<UserBO>) {
val registrationTokens: List<String> = users.flatMap { it.devices }

val message = MulticastMessage.builder()
.putData("score", "850")
.putData("time", "2:45")
.addAllTokens(registrationTokens)
.build()
val response = FirebaseMessaging.getInstance().sendMulticast(message)
if (response.failureCount > 0) {
val responses = response.responses
val failedTokens: List<String> = responses.mapIndexedNotNull { index, sendResponse ->
if (!sendResponse.isSuccessful) registrationTokens[index] else null
}
println("List of tokens that caused failures: $failedTokens")
}
}
Loading