diff --git a/backend/app/build.gradle.kts b/backend/app/build.gradle.kts index b681cee..244c007 100644 --- a/backend/app/build.gradle.kts +++ b/backend/app/build.gradle.kts @@ -22,7 +22,6 @@ repositories { } dependencies { - implementation("io.ktor:ktor-server-sessions-jvm:2.3.6") val ktor_version = "2.3.6" val logback_version = "1.4.11" val slf4j_version = "2.0.9" @@ -31,16 +30,14 @@ dependencies { val flyway_version = "10.6.0" val hikari_version = "5.1.0" val postgres_version = "42.7.1" + val exposed_version = "0.41.1" + val h2_version = "2.1.214" // Use the Kotlin JUnit 5 integration. testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") // Use the JUnit 5 integration. testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3") - - implementation("io.ktor:ktor-server-core-jvm:$ktor_version") - implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") - implementation("io.ktor:ktor-server-content-negotiation:$ktor_version") testImplementation("io.ktor:ktor-server-test-host:$ktor_version") runtimeOnly("ch.qos.logback:logback-classic:$logback_version") @@ -49,13 +46,37 @@ dependencies { // Auth implementation("io.ktor:ktor-server-auth:$ktor_version") - implementation("io.ktor:ktor-server-auth-jvm:$ktor_version") - implementation("io.ktor:ktor-server-sessions:$ktor_version") + implementation("io.ktor:ktor-server-auth:jvm$ktor_version") + implementation("io.ktor:ktor-server-auth-jwt:$ktor_version") + + // Ktor server dependencies + implementation("io.ktor:ktor-server-core-jvm:$ktor_version") + implementation("io.ktor:ktor-server-netty-jvm:$ktor_version") + implementation("io.ktor:ktor-server-resources:$ktor_version") + implementation("io.ktor:ktor-server-metrics-jvm:$ktor_version") + implementation("io.ktor:ktor-server-config-yaml:$ktor_version") + implementation("io.ktor:ktor-server-cors-jvm:$ktor_version") + implementation("io.ktor:ktor-server-sessions-jvm:$ktor_version") + //Ktor client dependencies + implementation("io.ktor:ktor-client-core:$ktor_version") + implementation("io.ktor:ktor-client-java:$ktor_version") + implementation("io.ktor:ktor-client-encoding:$ktor_version") + // Serialization - implementation("io.ktor:ktor-server-content-negotiation:$ktor_version") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") + implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version") + + // Logging + implementation("io.ktor:ktor-server-call-logging-jvm:$ktor_version") + implementation("io.ktor:ktor-client-logging:$ktor_version") + implementation("ch.qos.logback:logback-classic:$logback_version") + + // Auth + implementation("io.ktor:ktor-server-auth:$ktor_version") + implementation("io.ktor:ktor-server-auth-jwt:$ktor_version") // Kotlin Query implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") @@ -66,7 +87,9 @@ dependencies { // https://mvnrepository.com/artifact/org.flywaydb/flyway-core - database migrations implementation("org.flywaydb:flyway-core:$flyway_version") implementation("org.flywaydb:flyway-database-postgresql:$flyway_version") - + implementation("org.jetbrains.exposed:exposed-core:$exposed_version") + implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version") + implementation("com.h2database:h2:$h2_version") // https://mvnrepository.com/artifact/com.zaxxer/HikariCP - connection pooling implementation("com.zaxxer:HikariCP:$hikari_version") // https://mvnrepository.com/artifact/org.postgresql/postgresql - database driver diff --git a/backend/app/src/main/kotlin/backend/App.kt b/backend/app/src/main/kotlin/backend/App.kt index 0ade1cb..906b34d 100644 --- a/backend/app/src/main/kotlin/backend/App.kt +++ b/backend/app/src/main/kotlin/backend/App.kt @@ -1,45 +1,34 @@ package backend -import backend.admin.AdminRepository -import backend.admin.adminRoutes -import backend.user.UserRepository -import backend.user.userRoutes +import backend.config.configureAuth +import backend.route.adminRoutes +import backend.route.userRoutes +import backend.repository.AdminRepository +import backend.repository.UserRepository +import com.inventy.plugins.DatabaseFactory import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.engine.* -import io.ktor.server.netty.* import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun server() = embeddedServer(factory = Netty, port = 8080) { - createServer() -} -private fun Application.createServer() { - runMigration() +fun main(args: Array): Unit = + io.ktor.server.netty.EngineMain.main(args) + +fun Application.module() { + DatabaseFactory( + dbHost = environment.config.property("database.host").getString(), + dbPort = environment.config.property("database.port").getString(), + dbUser = environment.config.property("database.user").getString(), + dbPassword = environment.config.property("database.password").getString(), + databaseName = environment.config.property("database.databaseName").getString(), + embedded = environment.config.property("database.embedded").getString().toBoolean(), + ).init() configureAuth() configureRouting() } -class CustomPrincipal(val userId: Int) : Principal - -fun Application.configureAuth() { - authentication { - basic(name = "basic") { - realm = "Ktor Server" - validate { credentials -> - if (credentials.name == "user" && credentials.password == "password") { - CustomPrincipal(1) - } else { - null - } - } - } - } -} - fun Application.configureRouting() { val userRepository = UserRepository() @@ -50,7 +39,7 @@ fun Application.configureRouting() { } routing { userRoutes(userRepository) - adminRoutes(adminRepository) + adminRoutes(adminRepository, userRepository) healthz() get { @@ -69,6 +58,3 @@ private fun Routing.healthz() { } } -fun main() { - server().start(wait = true) -} diff --git a/backend/app/src/main/kotlin/backend/ViewRegisteredWorkshops.kt b/backend/app/src/main/kotlin/backend/ViewRegisteredWorkshops.kt index 0734d60..404c343 100644 --- a/backend/app/src/main/kotlin/backend/ViewRegisteredWorkshops.kt +++ b/backend/app/src/main/kotlin/backend/ViewRegisteredWorkshops.kt @@ -1,7 +1,7 @@ package backend -import backend.domain.WorkshopRegistration -import backend.user.UserRepository +import backend.model.WorkshopRegistration +import backend.repository.UserRepository class ViewRegisteredWorkshops(private val userRepository: UserRepository) { fun viewRegisteredWorkshops(userId: Int): List { diff --git a/backend/app/src/main/kotlin/backend/admin/AdminWorkshopRegistration.kt b/backend/app/src/main/kotlin/backend/admin/AdminWorkshopRegistration.kt deleted file mode 100644 index 132af11..0000000 --- a/backend/app/src/main/kotlin/backend/admin/AdminWorkshopRegistration.kt +++ /dev/null @@ -1,7 +0,0 @@ -package backend.admin - -import backend.domain.WorkshopRegistrationState -import kotlinx.serialization.Serializable - -@Serializable -data class AdminWorkshopRegistration(val firstName: String, val lastName: String, val email: String, val state: WorkshopRegistrationState) \ No newline at end of file diff --git a/backend/app/src/main/kotlin/backend/config/Auth.kt b/backend/app/src/main/kotlin/backend/config/Auth.kt new file mode 100644 index 0000000..26e23a4 --- /dev/null +++ b/backend/app/src/main/kotlin/backend/config/Auth.kt @@ -0,0 +1,51 @@ +package backend.config + +import backend.repository.UserRepository +import com.auth0.jwk.JwkProviderBuilder +import com.auth0.jwt.interfaces.Payload +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.auth.jwt.* +import java.util.concurrent.TimeUnit + +class CustomPrincipal(payload: Payload, val userId: Int): Principal, JWTPayloadHolder(payload) + +fun Application.configureAuth() { + fun validateCreds(credential: JWTCredential): CustomPrincipal? { + val containsAudience = credential.payload.audience.contains(environment.config.property("auth0.audience").getString()) + + if (containsAudience) { + val userRepository = UserRepository() + + val subject = credential.payload.subject + val providerId = subject.split("|")[1] + val provider = userRepository.findProviderById(providerId) ?: throw Exception("Provider not found") + return CustomPrincipal(credential.payload, provider.userId) + } + + return null + } + + val issuer = environment.config.property("auth0.issuer").getString() + val jwkProvider = JwkProviderBuilder(issuer) + .cached(10, 24, TimeUnit.HOURS) + .rateLimited(10, 1, TimeUnit.MINUTES) + .build() + + install(Authentication) { + jwt("auth0") { + verifier(jwkProvider, issuer) + validate { credential -> validateCreds(credential) } + } + basic("basic-auth0") { + realm = "Used by auth0 for creating users" + validate { credentials -> + if (credentials.name == "auth0" && credentials.password == "password") { + UserIdPrincipal(credentials.name) + } else { + null + } + } + } + } +} diff --git a/backend/app/src/main/kotlin/backend/config/Client.kt b/backend/app/src/main/kotlin/backend/config/Client.kt new file mode 100644 index 0000000..f3f0488 --- /dev/null +++ b/backend/app/src/main/kotlin/backend/config/Client.kt @@ -0,0 +1,33 @@ +package backend.config + +import io.ktor.client.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.http.* +import io.ktor.http.HttpHeaders.ContentEncoding +import io.ktor.serialization.kotlinx.json.* +import io.netty.handler.codec.compression.StandardCompressionOptions.deflate +import io.netty.handler.codec.compression.StandardCompressionOptions.gzip +import kotlinx.serialization.json.Json + +fun HttpClientConfig<*>.defaultClient() { + install(ContentNegotiation) { + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) + } + + install(ContentEncoding){ + gzip() + deflate() + } + + install(Logging) { + logger = Logger.DEFAULT + level = LogLevel.ALL + sanitizeHeader { header -> header == HttpHeaders.Authorization } + } + // Add all the common configuration here. +} diff --git a/backend/app/src/main/kotlin/backend/config/DatabaseFactory.kt b/backend/app/src/main/kotlin/backend/config/DatabaseFactory.kt new file mode 100644 index 0000000..3518b4e --- /dev/null +++ b/backend/app/src/main/kotlin/backend/config/DatabaseFactory.kt @@ -0,0 +1,66 @@ +package com.inventy.plugins + +import com.zaxxer.hikari.HikariDataSource +import java.util.concurrent.TimeUnit.MINUTES +import org.jetbrains.exposed.sql.* +import kotlinx.coroutines.* +import org.flywaydb.core.Flyway +import org.postgresql.ds.PGSimpleDataSource +import org.h2.jdbcx.JdbcDataSource +import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction + +class DatabaseFactory( + private val dbHost: String, + private val dbPort: String, + private val dbUser: String, + private val dbPassword: String, + private val databaseName: String, + private val embedded: Boolean = false + ) { + + companion object { + suspend fun dbQuery(block: suspend () -> T): T = + newSuspendedTransaction(Dispatchers.IO) { block() } + } + + fun init() { + Database.connect(hikari()) + val flyway = Flyway.configure() + .dataSource(hikari()) + .load() + flyway.migrate() + } + + private fun hikari(): HikariDataSource { + if (embedded) { + return HikariDataSource().apply { + dataSourceClassName = JdbcDataSource::class.qualifiedName + addDataSourceProperty("url", "jdbc:h2:mem:inventy;DB_CLOSE_DELAY=-1") + addDataSourceProperty("user", "root") + addDataSourceProperty("password", "") + maximumPoolSize = 10 + minimumIdle = 1 + idleTimeout = 100000 + connectionTimeout = 100000 + maxLifetime = MINUTES.toMillis(30) + } + } else { + return HikariDataSource().apply { + dataSourceClassName = PGSimpleDataSource::class.qualifiedName + addDataSourceProperty("serverName", dbHost) + addDataSourceProperty("portNumber", dbPort) + addDataSourceProperty("user", dbUser) + addDataSourceProperty("password", dbPassword) + addDataSourceProperty("databaseName", databaseName) + maximumPoolSize = 10 + minimumIdle = 1 + idleTimeout = 100000 + connectionTimeout = 100000 + maxLifetime = MINUTES.toMillis(30) + } + } + + + } + +} diff --git a/backend/app/src/main/kotlin/backend/config/TestData.kt b/backend/app/src/main/kotlin/backend/config/TestData.kt new file mode 100644 index 0000000..dda837e --- /dev/null +++ b/backend/app/src/main/kotlin/backend/config/TestData.kt @@ -0,0 +1,43 @@ +package backend.config + +import backend.model.* + + +class TestData { + companion object { + val provider1 = listOf(Provider(1, 1, ProviderType.GOOGLE.id, "1234567890")) + + val user1 = User(1, "John", "Doe", "john.doe@example.com", "", provider1) + val user2 = User(2, "Jane", "Doe", "jane.doe@example.com","", provider1) + val user3 = User(3, "John", "Smith", "john.smith@example.com","", provider1) + + val workshop1 = Workshop(1, "Kotlin", "John Doe") + val workshop2 = Workshop(2, "Ktor", "Jane Doe") + val workshop3 = Workshop(3, "Kotlin Multiplatform", "John Doe") + + + val userMap = + mutableMapOf( + 1 to user1, + 2 to user2, + 3 to user3, + ) + + val registrationMap = + mutableMapOf( + 1 to WorkshopRegistration(1, user1, workshop1, WorkshopRegistrationState.APPROVED), + 2 to WorkshopRegistration(2, user1, workshop2, WorkshopRegistrationState.APPROVED), + 3 to WorkshopRegistration(3, user2, workshop1, WorkshopRegistrationState.APPROVED), + 4 to WorkshopRegistration(4, user3, workshop3, WorkshopRegistrationState.APPROVED), + ) + + val workshopMap: MutableMap = + mutableMapOf( + 1 to workshop1, + 2 to workshop2, + 3 to workshop3, + ) + + } +} + diff --git a/backend/app/src/main/kotlin/backend/domain/User.kt b/backend/app/src/main/kotlin/backend/domain/User.kt deleted file mode 100644 index e5a1743..0000000 --- a/backend/app/src/main/kotlin/backend/domain/User.kt +++ /dev/null @@ -1,3 +0,0 @@ -package backend.domain - -class User(val id: Int, val firstName: String, val lastName: String, val email: String) diff --git a/backend/app/src/main/kotlin/backend/domain/Workshop.kt b/backend/app/src/main/kotlin/backend/domain/Workshop.kt deleted file mode 100644 index 36968b4..0000000 --- a/backend/app/src/main/kotlin/backend/domain/Workshop.kt +++ /dev/null @@ -1,3 +0,0 @@ -package backend.domain - -class Workshop(val id: Int, val title: String, val teacherName: String) diff --git a/backend/app/src/main/kotlin/backend/admin/AdminWorkshopDTO.kt b/backend/app/src/main/kotlin/backend/dto/AdminWorkshopDTO.kt similarity index 52% rename from backend/app/src/main/kotlin/backend/admin/AdminWorkshopDTO.kt rename to backend/app/src/main/kotlin/backend/dto/AdminWorkshopDTO.kt index d31f6ea..99e7b1f 100644 --- a/backend/app/src/main/kotlin/backend/admin/AdminWorkshopDTO.kt +++ b/backend/app/src/main/kotlin/backend/dto/AdminWorkshopDTO.kt @@ -1,10 +1,11 @@ -package backend.admin +package backend.dto +import backend.dto.AdminWorkshopRegistrationDTO import kotlinx.serialization.Serializable @Serializable data class AdminWorkshopDTO( val title: String, val teacherName: String, - val registrations: List, + val registrations: List, ) diff --git a/backend/app/src/main/kotlin/backend/dto/AdminWorkshopRegistrationDTO.kt b/backend/app/src/main/kotlin/backend/dto/AdminWorkshopRegistrationDTO.kt new file mode 100644 index 0000000..47bf80b --- /dev/null +++ b/backend/app/src/main/kotlin/backend/dto/AdminWorkshopRegistrationDTO.kt @@ -0,0 +1,7 @@ +package backend.dto + +import backend.model.WorkshopRegistrationState +import kotlinx.serialization.Serializable + +@Serializable +data class AdminWorkshopRegistrationDTO(val firstName: String, val lastName: String, val email: String, val state: WorkshopRegistrationState) diff --git a/backend/app/src/main/kotlin/backend/dto/DTO.kt b/backend/app/src/main/kotlin/backend/dto/DTO.kt new file mode 100644 index 0000000..200267e --- /dev/null +++ b/backend/app/src/main/kotlin/backend/dto/DTO.kt @@ -0,0 +1,6 @@ +package backend.dto + + +interface DTO { + val id: Long? +} diff --git a/backend/app/src/main/kotlin/backend/dto/ProviderDTO.kt b/backend/app/src/main/kotlin/backend/dto/ProviderDTO.kt new file mode 100644 index 0000000..beecf9b --- /dev/null +++ b/backend/app/src/main/kotlin/backend/dto/ProviderDTO.kt @@ -0,0 +1,7 @@ +package backend.dto + +import backend.model.ProviderType +import kotlinx.serialization.Serializable + +@Serializable +data class ProviderDTO(override val id: Long? = null, val providerType: ProviderType, val providerId: String) : DTO diff --git a/backend/app/src/main/kotlin/backend/dto/UserDTO.kt b/backend/app/src/main/kotlin/backend/dto/UserDTO.kt new file mode 100644 index 0000000..dc0b10a --- /dev/null +++ b/backend/app/src/main/kotlin/backend/dto/UserDTO.kt @@ -0,0 +1,14 @@ +package backend.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class UserDTO( + override val id: Long? = 0, + val firstName: String, + val lastName: String, + val email: String, + val imageUrl: String, + val providers: List +) : DTO + diff --git a/backend/app/src/main/kotlin/backend/user/WorkshopDTO.kt b/backend/app/src/main/kotlin/backend/dto/WorkshopDTO.kt similarity index 85% rename from backend/app/src/main/kotlin/backend/user/WorkshopDTO.kt rename to backend/app/src/main/kotlin/backend/dto/WorkshopDTO.kt index d6ba5ea..c509b20 100644 --- a/backend/app/src/main/kotlin/backend/user/WorkshopDTO.kt +++ b/backend/app/src/main/kotlin/backend/dto/WorkshopDTO.kt @@ -1,8 +1,7 @@ -package backend.user +package backend.dto import kotlinx.serialization.Serializable @Serializable data class WorkshopDTO(val title: String, val teacherName: String) - diff --git a/backend/app/src/main/kotlin/backend/model/Model.kt b/backend/app/src/main/kotlin/backend/model/Model.kt new file mode 100644 index 0000000..460b5ec --- /dev/null +++ b/backend/app/src/main/kotlin/backend/model/Model.kt @@ -0,0 +1,5 @@ +package backend.model + +interface Model { + val id: Int? +} diff --git a/backend/app/src/main/kotlin/backend/model/Provider.kt b/backend/app/src/main/kotlin/backend/model/Provider.kt new file mode 100644 index 0000000..13f20aa --- /dev/null +++ b/backend/app/src/main/kotlin/backend/model/Provider.kt @@ -0,0 +1,9 @@ +package backend.model + +class Provider( + override val id: Int, + val userId: Int, + val providerType: Int, + val providerId: String, +) : Model { +} diff --git a/backend/app/src/main/kotlin/backend/model/ProviderType.kt b/backend/app/src/main/kotlin/backend/model/ProviderType.kt new file mode 100644 index 0000000..4f7adfb --- /dev/null +++ b/backend/app/src/main/kotlin/backend/model/ProviderType.kt @@ -0,0 +1,24 @@ +package backend.model + +import kotlinx.serialization.SerialName + +enum class ProviderType (val id: Int) { + @SerialName("auth0") + AUTH0(0), + @SerialName("google-oauth2") + GOOGLE(1), + @SerialName("facebook") + FACEBOOK(2); + + companion object { + fun fromId(id: Int): ProviderType { + values().forEach { + if (it.id == id) { + return it + } + } + throw IllegalArgumentException("No AuthProvider with id $id found") + } + } + +} diff --git a/backend/app/src/main/kotlin/backend/model/User.kt b/backend/app/src/main/kotlin/backend/model/User.kt new file mode 100644 index 0000000..3adaebf --- /dev/null +++ b/backend/app/src/main/kotlin/backend/model/User.kt @@ -0,0 +1,4 @@ +package backend.model + +class User(override val id: Int?, val firstName: String, val lastName: String, val email: String, val imageUrl: String, + var providers: List) : Model diff --git a/backend/app/src/main/kotlin/backend/model/Workshop.kt b/backend/app/src/main/kotlin/backend/model/Workshop.kt new file mode 100644 index 0000000..6060232 --- /dev/null +++ b/backend/app/src/main/kotlin/backend/model/Workshop.kt @@ -0,0 +1,3 @@ +package backend.model + +class Workshop(override val id: Int, val title: String, val teacherName: String) : Model diff --git a/backend/app/src/main/kotlin/backend/domain/WorkshopRegistration.kt b/backend/app/src/main/kotlin/backend/model/WorkshopRegistration.kt similarity index 88% rename from backend/app/src/main/kotlin/backend/domain/WorkshopRegistration.kt rename to backend/app/src/main/kotlin/backend/model/WorkshopRegistration.kt index bb961c4..effd972 100644 --- a/backend/app/src/main/kotlin/backend/domain/WorkshopRegistration.kt +++ b/backend/app/src/main/kotlin/backend/model/WorkshopRegistration.kt @@ -1,4 +1,4 @@ -package backend.domain +package backend.model class WorkshopRegistration( val id: Int, diff --git a/backend/app/src/main/kotlin/backend/domain/WorkshopRegistrationState.kt b/backend/app/src/main/kotlin/backend/model/WorkshopRegistrationState.kt similarity index 79% rename from backend/app/src/main/kotlin/backend/domain/WorkshopRegistrationState.kt rename to backend/app/src/main/kotlin/backend/model/WorkshopRegistrationState.kt index 204ee1a..f70aec1 100644 --- a/backend/app/src/main/kotlin/backend/domain/WorkshopRegistrationState.kt +++ b/backend/app/src/main/kotlin/backend/model/WorkshopRegistrationState.kt @@ -1,4 +1,4 @@ -package backend.domain +package backend.model enum class WorkshopRegistrationState { diff --git a/backend/app/src/main/kotlin/backend/admin/AdminRepository.kt b/backend/app/src/main/kotlin/backend/repository/AdminRepository.kt similarity index 50% rename from backend/app/src/main/kotlin/backend/admin/AdminRepository.kt rename to backend/app/src/main/kotlin/backend/repository/AdminRepository.kt index a75eb12..fbf8411 100644 --- a/backend/app/src/main/kotlin/backend/admin/AdminRepository.kt +++ b/backend/app/src/main/kotlin/backend/repository/AdminRepository.kt @@ -1,40 +1,13 @@ -package backend.admin +package backend.repository -import backend.domain.User -import backend.domain.Workshop -import backend.domain.WorkshopRegistration -import backend.domain.WorkshopRegistrationState - -val user1 = User(1, "John", "Doe", "john.doe@example.com") -val user2 = User(2, "Jane", "Doe", "jane.doe@example.com") -val user3 = User(3, "John", "Smith", "john.smith@example.com") - -val workshop1 = Workshop(1, "Kotlin", "John Doe") -val workshop2 = Workshop(2, "Ktor", "Jane Doe") -val workshop3 = Workshop(3, "Kotlin Multiplatform", "John Doe") +import backend.config.TestData +import backend.dto.AdminWorkshopDTO +import backend.dto.AdminWorkshopRegistrationDTO class AdminRepository { - val userMap = - mutableMapOf( - 1 to user1, - 2 to user2, - 3 to user3, - ) - - val registrationMap = - mutableMapOf( - 1 to WorkshopRegistration(1, user1, workshop1, WorkshopRegistrationState.APPROVED), - 2 to WorkshopRegistration(2, user1, workshop2, WorkshopRegistrationState.APPROVED), - 3 to WorkshopRegistration(3, user2, workshop1, WorkshopRegistrationState.APPROVED), - 4 to WorkshopRegistration(4, user3, workshop3, WorkshopRegistrationState.APPROVED), - ) - - val workshopMap = - mutableMapOf( - 1 to workshop1, - 2 to workshop2, - 3 to workshop3, - ) + val userMap = TestData.userMap + val registrationMap = TestData.registrationMap + val workshopMap = TestData.workshopMap fun getWorkshops(): List { return workshopMap.map { workshop -> @@ -42,7 +15,7 @@ class AdminRepository { registrationMap.filter { it.value.workshop.id == workshop.key } .map { it.value } .map { - AdminWorkshopRegistration( + AdminWorkshopRegistrationDTO( userMap[it.user.id]!!.firstName, userMap[it.user.id]!!.lastName, userMap[it.user.id]!!.email, @@ -59,7 +32,7 @@ class AdminRepository { registrationMap.filter { it.value.workshop.id == workshop.id } .map { it.value } .map { - AdminWorkshopRegistration( + AdminWorkshopRegistrationDTO( userMap[it.user.id]!!.firstName, userMap[it.user.id]!!.lastName, userMap[it.user.id]!!.email, diff --git a/backend/app/src/main/kotlin/backend/repository/UserRepository.kt b/backend/app/src/main/kotlin/backend/repository/UserRepository.kt new file mode 100644 index 0000000..3f5f7ff --- /dev/null +++ b/backend/app/src/main/kotlin/backend/repository/UserRepository.kt @@ -0,0 +1,89 @@ +package backend.repository + +import backend.config.TestData +import backend.dto.ProviderDTO +import backend.dto.UserDTO +import backend.model.Provider +import backend.model.User +import backend.model.WorkshopRegistration +import backend.model.WorkshopRegistrationState + +class UserRepository{ + val userMap = TestData.userMap + val registrationMap = TestData.registrationMap + val workshopMap = TestData.workshopMap + + fun create(user: UserDTO): User { + val maxId = userMap.keys.max() + 1 + userMap.put(maxId, User( + maxId, + user.firstName, + user.lastName, + user.email, + user.imageUrl, + mutableListOf(), + )) + return userMap[maxId]!! + } + + fun getUsers(): List { + return userMap.values.toList() + } + + fun readByEmail(email: String): User? { + return userMap.values.firstOrNull { it.email == email } + } + + fun updateProviders(userId: Int, existingProviders: List, providers: List) { + // add provider + providers.filter { + existingProviders.none { provider -> provider.providerType == it.providerType.id } + }.forEach( + fun (provider: ProviderDTO) { + val maxId = userMap[userId]!!.providers.maxOf { it.id!! } + 1 + userMap[userId]!!.providers += Provider( + maxId, + userId, + provider.providerType.id, + provider.providerId, + ) + } + ) + } + + fun findProviderById(providerId: String): Provider? { + return userMap.values.flatMap { it.providers }.firstOrNull { it.providerId == providerId } + } + fun getWorkShopRegistrations(userId: Int): List { + return registrationMap.filter { + it.value.user.id == userId + }.values.toList() + } + + fun addWorkshopRegistrations( + userId: Int, + workshopId: Int, + ) { + workshopMap.get(workshopId) ?: throw RuntimeException("Workshop does not exist") + val maxId = registrationMap.keys.max() + 1 + registrationMap.put( + maxId, + WorkshopRegistration( + maxId, + userMap[userId]!!, + workshopMap[workshopId]!!, + WorkshopRegistrationState.PENDING, + ), + ) + } + + fun cancelWorkshopRegistration( + userId: Int, + workshopId: Int, + ) { + val registration = + registrationMap.filter { it.value.user.id == userId && it.value.workshop.id == workshopId } + .values.firstOrNull() ?: throw RuntimeException("Workshop registration does not exist") + registration.state = WorkshopRegistrationState.CANCELED + } +} diff --git a/backend/app/src/main/kotlin/backend/workshop/Repository.kt b/backend/app/src/main/kotlin/backend/repository/WorkshopRepository.kt similarity index 86% rename from backend/app/src/main/kotlin/backend/workshop/Repository.kt rename to backend/app/src/main/kotlin/backend/repository/WorkshopRepository.kt index 0173985..8aaab79 100644 --- a/backend/app/src/main/kotlin/backend/workshop/Repository.kt +++ b/backend/app/src/main/kotlin/backend/repository/WorkshopRepository.kt @@ -1,9 +1,9 @@ -package backend.workshop +package backend.repository -import backend.domain.Workshop +import backend.model.Workshop -class Repository { +class WorkshopRepository { var map = mutableMapOf( 1 to Workshop(1, "Kotlin", "John Doe"), 2 to Workshop(2, "Ktor", "Jane Doe"), diff --git a/backend/app/src/main/kotlin/backend/admin/Routing.kt b/backend/app/src/main/kotlin/backend/route/AdminRoute.kt similarity index 64% rename from backend/app/src/main/kotlin/backend/admin/Routing.kt rename to backend/app/src/main/kotlin/backend/route/AdminRoute.kt index 16273e8..39d4291 100644 --- a/backend/app/src/main/kotlin/backend/admin/Routing.kt +++ b/backend/app/src/main/kotlin/backend/route/AdminRoute.kt @@ -1,11 +1,12 @@ -package backend.admin +package backend.route +import backend.repository.AdminRepository +import backend.repository.UserRepository import io.ktor.server.application.* import io.ktor.server.response.* import io.ktor.server.routing.* - -fun Routing.adminRoutes(adminRepository: AdminRepository) { +fun Routing.adminRoutes(adminRepository: AdminRepository, userRepository: UserRepository) { get("/admin/workshop") { call.respond(adminRepository.getWorkshops()) } @@ -16,4 +17,7 @@ fun Routing.adminRoutes(adminRepository: AdminRepository) { call.respondText("Workshop not found", status = io.ktor.http.HttpStatusCode.NotFound) } } + get("/admin/user") { + call.respond(userRepository.getUsers()) + } } diff --git a/backend/app/src/main/kotlin/backend/route/UserRoute.kt b/backend/app/src/main/kotlin/backend/route/UserRoute.kt new file mode 100644 index 0000000..51b9de7 --- /dev/null +++ b/backend/app/src/main/kotlin/backend/route/UserRoute.kt @@ -0,0 +1,57 @@ +package backend.route + +import backend.config.CustomPrincipal +import backend.dto.UserDTO +import backend.repository.UserRepository +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + + +fun Application.configureUserRoutes(userRepository: UserRepository) { + routing { + userRoutes(userRepository) + } +} + +fun Routing.userRoutes(userRepository: UserRepository) { + get("/user/workshop") { + // Should be based on the logged in user + val userId = call.authentication.principal()?.userId!! + call.respond(userRepository.getWorkShopRegistrations(userId)) + } + + post("/user/workshop/{workshopId}") { + try { + val userId = call.authentication.principal()?.userId!! + userRepository.addWorkshopRegistrations(userId, call.parameters["workshopId"]!!.toInt()) + call.respondText("Workshop added") + } catch (e: Exception) { + call.respondText("Workshop not found", status = io.ktor.http.HttpStatusCode.NotFound) + } + } + + put("/user/workshop/{workshopId}/cancel") { + val userId = call.authentication.principal()?.userId!! + userRepository.cancelWorkshopRegistration(userId, call.parameters["workshopId"]!!.toInt()) + call.respondText("Workshop cancelled") + } + authenticate ("basic-auth0") { + post("/user") { + val userDTO = call.receive() + val user = userRepository.readByEmail(userDTO.email) + if (user?.id != null) { + userRepository.updateProviders(user.id, user.providers, userDTO.providers) + call.respond(HttpStatusCode.OK) + return@post + } else { + val create = userRepository.create(userDTO) + call.respond(HttpStatusCode.Created, create) + return@post + } + } + } +} diff --git a/backend/app/src/main/kotlin/backend/user/Routing.kt b/backend/app/src/main/kotlin/backend/user/Routing.kt deleted file mode 100644 index b780889..0000000 --- a/backend/app/src/main/kotlin/backend/user/Routing.kt +++ /dev/null @@ -1,35 +0,0 @@ -package backend.user - -import backend.CustomPrincipal -import backend.ViewRegisteredWorkshops -import io.ktor.server.application.* -import io.ktor.server.auth.* -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun Routing.userRoutes(userRepository: UserRepository) { - authenticate("basic") { - get("/user/workshop") { - // Should be based on the logged in user - val userId = call.authentication.principal()?.userId!! - val workshops = ViewRegisteredWorkshops(userRepository).viewRegisteredWorkshops(userId) - call.respond(workshops) - } - - post("/user/workshop/{workshopId}") { - try { - val userId = call.authentication.principal()?.userId!! - userRepository.addWorkshopRegistrations(userId, call.parameters["workshopId"]!!.toInt()) - call.respondText("Workshop added") - } catch (e: Exception) { - call.respondText("Workshop not found", status = io.ktor.http.HttpStatusCode.NotFound) - } - } - - put("/user/workshop/{workshopId}/cancel") { - val userId = call.authentication.principal()?.userId!! - userRepository.cancelWorkshopRegistration(userId, call.parameters["workshopId"]!!.toInt()) - call.respondText("Workshop cancelled") - } - } -} diff --git a/backend/app/src/main/kotlin/backend/user/UserRepository.kt b/backend/app/src/main/kotlin/backend/user/UserRepository.kt deleted file mode 100644 index 8111c4d..0000000 --- a/backend/app/src/main/kotlin/backend/user/UserRepository.kt +++ /dev/null @@ -1,87 +0,0 @@ -package backend.user - -import backend.domain.User -import backend.domain.Workshop -import backend.domain.WorkshopRegistration -import backend.domain.WorkshopRegistrationState - -class UserRepository( - var userMap: MutableMap = - mutableMapOf( - 1 to backend.admin.user1, - 2 to backend.admin.user2, - 3 to backend.admin.user3, - ), - var registrationMap: MutableMap = - - mutableMapOf( - 1 to - WorkshopRegistration( - 1, - backend.admin.user1, - backend.admin.workshop1, - WorkshopRegistrationState.APPROVED, - ), - 2 to - WorkshopRegistration( - 2, - backend.admin.user1, - backend.admin.workshop2, - WorkshopRegistrationState.APPROVED, - ), - 3 to - WorkshopRegistration( - 3, - backend.admin.user2, - backend.admin.workshop1, - WorkshopRegistrationState.APPROVED, - ), - 4 to - WorkshopRegistration( - 4, - backend.admin.user3, - backend.admin.workshop3, - WorkshopRegistrationState.APPROVED, - ), - ), - var workshopMap: MutableMap = - - mutableMapOf( - 1 to backend.admin.workshop1, - 2 to backend.admin.workshop2, - 3 to backend.admin.workshop3, - ), -) { - fun getWorkShopRegistrations(userId: Int): List { - return registrationMap.filter { - it.value.user.id == userId - }.values.toList() - } - - fun addWorkshopRegistrations( - userId: Int, - workshopId: Int, - ) { - workshopMap.get(workshopId) ?: throw RuntimeException("Workshop does not exist") - val maxId = registrationMap.keys.max() + 1 - registrationMap.put( - maxId, - WorkshopRegistration( - maxId, - userMap[userId]!!, - workshopMap[workshopId]!!, - WorkshopRegistrationState.PENDING, - ), - ) - } - - fun cancelWorkshopRegistration( - uesrId: Int, - workshopId: Int, - ) { - val registration = - registrationMap.filter { it.value.user.id == uesrId && it.value.workshop.id == workshopId } - .values.firstOrNull() ?: throw RuntimeException("Workshop registration does not exist") - registration.state = WorkshopRegistrationState.CANCELED - } -} diff --git a/backend/app/src/main/resources/application.yaml b/backend/app/src/main/resources/application.yaml new file mode 100644 index 0000000..f919a55 --- /dev/null +++ b/backend/app/src/main/resources/application.yaml @@ -0,0 +1,20 @@ +ktor: + application: + modules: + - backend.AppKt.module + development: true + deployment: + port: 8080 + +database: + embedded: true + host: localhost + port: 5432 + databaseName: workshop + user: postgres + password: example + + +auth0: + issuer: 'https://damoad.eu.auth0.com/' + audience: workshop-wizard diff --git a/backend/app/src/main/resources/logback.xml b/backend/app/src/main/resources/logback.xml index 0ad1cd7..bdbb64e 100644 --- a/backend/app/src/main/resources/logback.xml +++ b/backend/app/src/main/resources/logback.xml @@ -1,11 +1,12 @@ - - + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + - - - - - + + - \ No newline at end of file + + + diff --git a/backend/app/src/test/kotlin/Fixtures.kt b/backend/app/src/test/kotlin/Fixtures.kt index 7ec44e2..46edd86 100644 --- a/backend/app/src/test/kotlin/Fixtures.kt +++ b/backend/app/src/test/kotlin/Fixtures.kt @@ -1,13 +1,13 @@ -import backend.domain.User -import backend.domain.Workshop -import backend.domain.WorkshopRegistration -import backend.domain.WorkshopRegistrationState +import backend.model.User +import backend.model.Workshop +import backend.model.WorkshopRegistration +import backend.model.WorkshopRegistrationState object Fixtures { val WORKSHOP_1_KOTLIN_WITH_KARI = Workshop(id = 1, title = "Kotlin Workshop for new developers", teacherName = "Kari Nordmann") val USER_1_OLA_NORDMANN = - User(id = 1, firstName = "Ola", lastName = "Nordmann", email = "ola.nordmann@example.net") + User(id = 1, firstName = "Ola", lastName = "Nordmann", email = "ola.nordmann@example.net", imageUrl = "", providers = listOf()) val WORKSHOP_REGISTRATION_1_USER_1_TO_WORKSHOP_1_APPROVED = WorkshopRegistration( id = 1, diff --git a/backend/app/src/test/kotlin/backend/ViewRegisteredWorkshopsTest.kt b/backend/app/src/test/kotlin/backend/ViewRegisteredWorkshopsTest.kt index da2dfbc..538c561 100644 --- a/backend/app/src/test/kotlin/backend/ViewRegisteredWorkshopsTest.kt +++ b/backend/app/src/test/kotlin/backend/ViewRegisteredWorkshopsTest.kt @@ -5,7 +5,7 @@ import backend.domain.User import backend.domain.Workshop import backend.domain.WorkshopRegistration import backend.domain.WorkshopRegistrationState -import backend.user.UserRepository +import backend.repository.UserRepository import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test