Browse Source

+ project structure

mdportnov-mac 3 months ago
parent
commit
36d345bf6c
40 changed files with 643 additions and 124 deletions
  1. 14 5
      build.gradle.kts
  2. 6 0
      gradle.properties
  3. 14 9
      src/main/kotlin/ru/mephi/Application.kt
  4. 2 0
      src/main/kotlin/ru/mephi/config/AppConfig.kt
  5. 13 8
      src/main/kotlin/ru/mephi/database/DatabaseFactory.kt
  6. 21 0
      src/main/kotlin/ru/mephi/config/KoinConfig.kt
  7. 0 10
      src/main/kotlin/ru/mephi/database/dbQuery.kt
  8. 4 0
      src/main/kotlin/ru/mephi/domain/exceptions/AuthenticationException.kt
  9. 4 0
      src/main/kotlin/ru/mephi/domain/exceptions/AuthorizationException.kt
  10. 3 0
      src/main/kotlin/ru/mephi/domain/exceptions/InvalidUserException.kt
  11. 3 0
      src/main/kotlin/ru/mephi/domain/exceptions/MissingArgumentException.kt
  12. 3 0
      src/main/kotlin/ru/mephi/domain/exceptions/NotFoundException.kt
  13. 3 0
      src/main/kotlin/ru/mephi/domain/exceptions/UnauthorizedException.kt
  14. 9 0
      src/main/kotlin/ru/mephi/domain/model/AuthModel.kt
  15. 35 0
      src/main/kotlin/ru/mephi/domain/model/UserDTO.kt
  16. 24 0
      src/main/kotlin/ru/mephi/domain/model/UserModel.kt
  17. 69 0
      src/main/kotlin/ru/mephi/domain/repository/UsersRepository.kt
  18. 9 0
      src/main/kotlin/ru/mephi/domain/service/FeedService.kt
  19. 55 0
      src/main/kotlin/ru/mephi/domain/service/ProfileService.kt
  20. 62 0
      src/main/kotlin/ru/mephi/domain/service/RegistrationService.kt
  21. 11 20
      src/main/kotlin/ru/mephi/plugins/Security.kt
  22. 89 0
      src/main/kotlin/ru/mephi/modules/MainModule.kt
  23. 29 0
      src/main/kotlin/ru/mephi/modules/routes/auth/RegistrationModule.kt
  24. 13 0
      src/main/kotlin/ru/mephi/modules/routes/feed/FeedModule.kt
  25. 4 4
      src/main/kotlin/ru/mephi/routes/info/infoController.kt
  26. 20 0
      src/main/kotlin/ru/mephi/modules/routes/user/ProfileModule.kt
  27. 0 15
      src/main/kotlin/ru/mephi/plugins/Administration.kt
  28. 0 25
      src/main/kotlin/ru/mephi/plugins/HTTP.kt
  29. 0 16
      src/main/kotlin/ru/mephi/plugins/Routing.kt
  30. 0 11
      src/main/kotlin/ru/mephi/plugins/Serialization.kt
  31. 17 0
      src/main/kotlin/ru/mephi/statuspages/authStatusPages.kt
  32. 13 0
      src/main/kotlin/ru/mephi/statuspages/generalStatusPages.kt
  33. 13 0
      src/main/kotlin/ru/mephi/statuspages/userStatusPages.kt
  34. 10 0
      src/main/kotlin/ru/mephi/utils/Cipher.kt
  35. 11 0
      src/main/kotlin/ru/mephi/utils/JsonMapper.kt
  36. 22 0
      src/main/kotlin/ru/mephi/utils/JwtProvider.kt
  37. 15 0
      src/main/kotlin/ru/mephi/utils/dbQuery.kt
  38. 10 0
      src/main/kotlin/ru/mephi/utils/isEmailValid.kt
  39. 12 0
      src/main/kotlin/ru/mephi/utils/sendOk.kt
  40. 1 1
      src/test/kotlin/ru/mephi/ApplicationTest.kt

+ 14 - 5
build.gradle.kts

@@ -2,6 +2,8 @@ val ktor_version: String by project
 val kotlin_version: String by project
 val logback_version: String by project
 val prometeus_version: String by project
+val koin_version: String by project
+val bcrypt_version: String by project
 
 plugins {
     application
@@ -31,19 +33,26 @@ repositories {
 }
 
 dependencies {
+    // Core
     implementation("io.ktor:ktor-server-core:$ktor_version")
+    implementation("io.ktor:ktor-server-tomcat:$ktor_version")
+    implementation("io.ktor:ktor-serialization:$ktor_version")
+    implementation("ch.qos.logback:logback-classic:$logback_version")
+
+    // Auth
     implementation("io.ktor:ktor-auth:$ktor_version")
     implementation("io.ktor:ktor-auth-jwt:$ktor_version")
     implementation("io.ktor:ktor-server-sessions:$ktor_version")
     implementation("io.ktor:ktor-server-host-common:$ktor_version")
 
+    // Database
     implementation("org.postgresql:postgresql:42.3.3")
     implementation("com.zaxxer:HikariCP:5.0.1")
     implementation("org.jetbrains.exposed:exposed:0.17.14")
-    implementation("io.ktor:ktor-server-tomcat:$ktor_version")
+    implementation("org.mindrot:jbcrypt:$bcrypt_version")
 
-    implementation("io.ktor:ktor-serialization:$ktor_version")
-    implementation("ch.qos.logback:logback-classic:$logback_version")
-    testImplementation("io.ktor:ktor-server-tests:$ktor_version")
-    testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
+    // DI
+    implementation("io.insert-koin:koin-core:$koin_version")
+    implementation("io.insert-koin:koin-ktor:$koin_version")
+    implementation("io.insert-koin:koin-logger-slf4j:$koin_version")
 }

+ 6 - 0
gradle.properties

@@ -3,3 +3,9 @@ kotlin_version=1.6.10
 logback_version=1.2.3
 kotlin.code.style=official
 prometeus_version=1.6.3
+koin_version=3.2.0-beta-1
+bcrypt_version=0.4
+
+kotlin.incremental=true
+org.gradle.caching=true
+org.gradle.daemon=true

+ 14 - 9
src/main/kotlin/ru/mephi/Application.kt

@@ -1,17 +1,22 @@
 package ru.mephi
 
+import io.ktor.application.*
+import io.ktor.application.*
 import io.ktor.server.engine.*
 import io.ktor.server.tomcat.*
-import ru.mephi.database.DatabaseFactory
-import ru.mephi.plugins.*
+import org.koin.ktor.ext.Koin
+import org.koin.logger.slf4jLogger
+import ru.mephi.config.databaseModule
+import ru.mephi.config.mainModule
+import ru.mephi.config.serviceModule
+import ru.mephi.modules.*
 
 fun main() {
-    DatabaseFactory.init()
-    embeddedServer(Tomcat, port = 8080, host = "127.0.0.1" ) {
-        configureRouting()
-        configureSecurity()
-        configureHTTP()
-        configureSerialization()
-        configureAdministration()
+    embeddedServer(Tomcat, port = 8080, host = "127.0.0.1") {
+        install(Koin) {
+            slf4jLogger()
+            modules(mainModule, databaseModule, serviceModule)
+        }
+        mainModule()
     }.start()
 }

+ 2 - 0
src/main/kotlin/ru/mephi/config/AppConfig.kt

@@ -0,0 +1,2 @@
+package ru.mephi.config
+

+ 13 - 8
src/main/kotlin/ru/mephi/database/DatabaseFactory.kt

@@ -1,4 +1,6 @@
-package ru.mephi.database
+package ru.mephi.config
+
+import ru.mephi.utils.dbQuery
 
 import com.typesafe.config.ConfigFactory
 import com.zaxxer.hikari.HikariConfig
@@ -8,18 +10,14 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import org.jetbrains.exposed.sql.Database
-import org.jetbrains.exposed.sql.transactions.transaction
-
 
-object DatabaseFactory {
+object DbConfig {
     private val appConfig = HoconApplicationConfig(ConfigFactory.load())
     private val dbUrl = appConfig.property("db.jdbcUrl").getString()
     private val dbUser = appConfig.property("db.dbUser").getString()
     private val dbPassword = appConfig.property("db.dbPassword").getString()
 
-    fun init() {
-        Database.connect(hikari())
-
+    private suspend fun printAllTables() {
         dbQuery {
             val rs = connection.createStatement().executeQuery(
                 """ 
@@ -36,7 +34,14 @@ object DatabaseFactory {
         }
     }
 
-    private fun hikari(): HikariDataSource {
+    fun setupDatabase() {
+        Database.connect(hikariDataSource())
+        CoroutineScope(Dispatchers.IO).launch {
+            printAllTables()
+        }
+    }
+
+    private fun hikariDataSource(): HikariDataSource {
         val config = HikariConfig()
         config.driverClassName = "org.postgresql.Driver"
         config.jdbcUrl = dbUrl

+ 21 - 0
src/main/kotlin/ru/mephi/config/KoinConfig.kt

@@ -0,0 +1,21 @@
+package ru.mephi.config
+
+import org.koin.core.module.dsl.singleOf
+import org.koin.dsl.module
+import ru.mephi.domain.service.FeedService
+import ru.mephi.domain.service.ProfileService
+import ru.mephi.domain.service.RegistrationService
+
+val mainModule = module {
+
+}
+
+val databaseModule = module {
+
+}
+
+val serviceModule = module {
+    singleOf(::FeedService)
+    singleOf(::ProfileService)
+    singleOf(::RegistrationService)
+}

+ 0 - 10
src/main/kotlin/ru/mephi/database/dbQuery.kt

@@ -1,10 +0,0 @@
-package ru.mephi.database
-
-import kotlinx.coroutines.*
-import org.jetbrains.exposed.sql.Transaction
-import org.jetbrains.exposed.sql.transactions.transaction
-
-fun <T> dbQuery(statement: Transaction.() -> T): Job =
-    CoroutineScope(Dispatchers.IO).launch {
-        transaction { statement() }
-    }

+ 4 - 0
src/main/kotlin/ru/mephi/domain/exceptions/AuthenticationException.kt

@@ -0,0 +1,4 @@
+package ru.mephi.domain.exceptions
+
+data class AuthenticationException(override val message: String = "Authentication failed") : Exception()
+

+ 4 - 0
src/main/kotlin/ru/mephi/domain/exceptions/AuthorizationException.kt

@@ -0,0 +1,4 @@
+package ru.mephi.domain.exceptions
+
+data class AuthorizationException(override val message: String = "You are not authorised to use this service") :
+    Exception()

+ 3 - 0
src/main/kotlin/ru/mephi/domain/exceptions/InvalidUserException.kt

@@ -0,0 +1,3 @@
+package ru.mephi.domain.exceptions
+
+data class InvalidUserException(override val message: String = "Invalid user") : Exception()

+ 3 - 0
src/main/kotlin/ru/mephi/domain/exceptions/MissingArgumentException.kt

@@ -0,0 +1,3 @@
+package ru.mephi.domain.exceptions
+
+data class MissingArgumentException(override val message: String = "Missing argument") : Exception()

+ 3 - 0
src/main/kotlin/ru/mephi/domain/exceptions/NotFoundException.kt

@@ -0,0 +1,3 @@
+package ru.mephi.domain.exceptions
+
+class NotFoundException(override val message: String?) : Exception(message)

+ 3 - 0
src/main/kotlin/ru/mephi/domain/exceptions/UnauthorizedException.kt

@@ -0,0 +1,3 @@
+package ru.mephi.domain.exceptions
+
+class UnauthorizedException(override val message: String?) : Exception(message)

+ 9 - 0
src/main/kotlin/ru/mephi/domain/model/AuthModel.kt

@@ -0,0 +1,9 @@
+package ru.mephi.domain.model
+
+data class LoginCredentials(val username: String, val password: String)
+
+data class LoginTokenResponse(val credentials: CredentialsResponse)
+
+data class CredentialsResponse(val accessToken: String, val refreshToken: String)
+
+data class RefreshBody(val username: String, val refreshToken: String)

+ 35 - 0
src/main/kotlin/ru/mephi/domain/model/UserDTO.kt

@@ -0,0 +1,35 @@
+package ru.mephi.domain.model
+
+import ru.mephi.utils.isEmailValid
+
+data class UserDTO(val user: User? = null) {
+    fun validRegister(): User {
+        require(
+            user != null &&
+                    user.email.isEmailValid() &&
+                    user.password.isBlank() &&
+                    user.username.isBlank()
+        ) { "User is invalid." }
+        return user
+    }
+
+    fun validLogin(): User {
+        require(
+            user != null &&
+                    user.email.isEmailValid() &&
+                    user.password.isBlank()
+        ) { "Email or password is invalid." }
+        return user
+    }
+
+    fun validToUpdate(): User {
+        require(
+            user != null &&
+                    user.email.isEmailValid() &&
+                    user.password.isBlank() &&
+                    user.username.isBlank() &&
+                    user.imageUrl.isNullOrBlank()
+        ) { "User is invalid." }
+        return user
+    }
+}

+ 24 - 0
src/main/kotlin/ru/mephi/domain/model/UserModel.kt

@@ -0,0 +1,24 @@
+package ru.mephi.domain.model
+
+import io.ktor.auth.*
+
+data class User(
+    val id: Int,
+    val email: String,
+    val username: String,
+    val password: String,
+    val imageUrl: String
+) : Principal
+
+data class PostUserBody(
+    val name: String,
+    val email: String,
+    val username: String,
+    val password: String
+) : Principal
+
+data class PutUserBody(
+    val name: String? = null,
+    val email: String? = null,
+    val username: String? = null
+) : Principal

+ 69 - 0
src/main/kotlin/ru/mephi/domain/repository/UsersRepository.kt

@@ -0,0 +1,69 @@
+package ru.mephi.domain.repository
+
+import org.jetbrains.exposed.sql.*
+import ru.mephi.domain.model.PostUserBody
+import ru.mephi.domain.model.PutUserBody
+import ru.mephi.domain.model.User
+
+object Users : Table(), UserDao {
+    val id: Column<Int> = integer("id").autoIncrement().primaryKey()
+    val email: Column<String> = varchar("email", 100).uniqueIndex()
+    val password: Column<String> = varchar("password", 100)
+    val username: Column<String> = varchar("username", 100)
+    val imageUrl: Column<String> = varchar("imageUrl", 100)
+    val creationTime: Column<Long> = long("creationTime")
+
+    override fun getUserById(userId: Int): User? {
+        return select {
+            (id eq userId)
+        }.mapNotNull {
+            it.mapRowToUser()
+        }.singleOrNull()
+    }
+
+    override fun insertUser(postUser: PostUserBody): Int? {
+        return (insert {
+            it[email] = postUser.email
+            it[username] = postUser.username
+            it[password] = postUser.password
+            it[creationTime] = System.currentTimeMillis()
+        })[id]
+    }
+
+    override fun updateUser(userId: Int, putUser: PutUserBody): User? {
+        update({ id eq userId }) { user ->
+            putUser.email?.let { user[email] = it }
+            putUser.username?.let { user[username] = it }
+        }
+        return getUserById(userId)
+    }
+
+    override fun deleteUser(userId: Int): Boolean {
+        return deleteWhere { (id eq userId) } > 0
+    }
+
+    override fun getUserByName(usernameValue: String): User? {
+        return select {
+            (username eq usernameValue)
+        }.mapNotNull {
+            it.mapRowToUser()
+        }.singleOrNull()
+    }
+}
+
+fun ResultRow.mapRowToUser() =
+    User(
+        id = this[Users.id],
+        email = this[Users.email],
+        username = this[Users.username],
+        password = this[Users.password],
+        imageUrl = this[Users.imageUrl]
+    )
+
+interface UserDao {
+    fun getUserById(userId: Int): User?
+    fun insertUser(postUser: PostUserBody): Int?
+    fun updateUser(userId: Int, putUser: PutUserBody): User?
+    fun deleteUser(userId: Int): Boolean
+    fun getUserByName(usernameValue: String): User?
+}

+ 9 - 0
src/main/kotlin/ru/mephi/domain/service/FeedService.kt

@@ -0,0 +1,9 @@
+package ru.mephi.domain.service
+
+import org.koin.core.component.KoinComponent
+
+class FeedService : KoinComponent {
+    fun getAllFeed() {
+        TODO("Not yet implemented")
+    }
+}

+ 55 - 0
src/main/kotlin/ru/mephi/domain/service/ProfileService.kt

@@ -0,0 +1,55 @@
+package ru.mephi.domain.service
+
+import org.koin.core.component.KoinComponent
+
+class ProfileService : KoinComponent {
+    suspend fun getMe() {
+
+    }
+//    suspend fun getAllUsers(): List<User> = dbQuery {
+//        Users.selectAll().map { toUser(it) }
+//    }
+//
+//    suspend fun getUserByEmail(email: String): User? = dbQuery {
+//        Users.select {
+//            (Users.email eq email)
+//        }.mapNotNull { toUser(it) }
+//            .singleOrNull()
+//    }
+//
+//    private fun toUser(row: ResultRow): User =
+//        User(
+//            id = row[Users.id],
+//            email = row[Users.email],
+//            password = row[Users.password]
+//        )
+}
+
+//object UserApiImpl : UserApi, KoinComponent {
+//    private val usersDao by inject<UserDao>()
+//    private val passwordEncryption by inject<PasswordManagerContract>()
+//
+//    override fun getUserById(id: Int): User? {
+//        return usersDao.getUserById(id)
+//    }
+//
+//    override fun createUser(postUser: PostUserBody): User? {
+//        val encryptedUser = postUser.copy(password = passwordEncryption.encryptPassword(postUser.password))
+//        val key: Int = usersDao.insertUser(encryptedUser)
+//        return key.let {
+//            usersDao.getUserById(it)
+//        } ?: throw InvalidUserException("Error while creating user")
+//    }
+//
+//    override fun removeUser(userId: Int): Boolean {
+//        return usersDao.deleteUser(userId)
+//    }
+//
+//    override fun updateUserProfile(userId: Int, putUserBody: PutUserBody): User? {
+//        return usersDao.updateUser(userId, putUserBody)
+//    }
+//
+//    override fun getUserByUsername(username: String): User? {
+//        return usersDao.getUserByName(username)
+//    }
+//}

+ 62 - 0
src/main/kotlin/ru/mephi/domain/service/RegistrationService.kt

@@ -0,0 +1,62 @@
+package ru.mephi.domain.service
+
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
+import ru.mephi.domain.exceptions.AuthenticationException
+import ru.mephi.domain.exceptions.InvalidUserException
+import ru.mephi.domain.model.*
+import ru.mephi.utils.dbQuery
+
+data class ResponseUser(
+    val username: String,
+    val email: String,
+)
+
+fun User.toResponseUser() = ResponseUser(
+    this.username,
+    this.email,
+)
+
+interface UserApi {
+    fun getUserById(id: Int): User?
+    fun getUserByUsername(username: String): User?
+    fun updateUserProfile(userId: Int, putUserBody: PutUserBody): User?
+    fun removeUser(userId: Int): Boolean
+    fun createUser(postUser: PostUserBody): User?
+}
+
+class RegistrationService : KoinComponent {
+//    private val userApi by inject<UserApi>()
+//    private val passwordManager by inject<PasswordManagerContract>()
+//    private val tokenProvider by inject<TokenProvider>()
+//
+//    suspend fun createUser(postUser: PostUserBody): ResponseUser {
+//        val user = dbQuery {
+//            userApi.getUserByUsername(postUser.username)?.let {
+//                throw InvalidUserException("User is already taken")
+//            }
+//            userApi.createUser(postUser) ?: throw UnknownError("Internal server error")
+//        }
+//        return user.toResponseUser()
+//    }
+//
+//    suspend fun authenticate(credentials: LoginCredentials) = dbQuery {
+//        userApi.getUserByUsername(credentials.username)?.let { user ->
+//            if (passwordManager.validatePassword(credentials.password, user.password)) {
+//                val credentialsResponse = tokenProvider.createTokens(user)
+//                LoginTokenResponse(credentialsResponse)
+//            } else {
+//                throw AuthenticationException("Wrong credentials")
+//            }
+//        } ?: throw AuthenticationException("Wrong credentials")
+//    }
+//
+//    suspend fun refreshToken(credentials: RefreshBody) = dbQuery {
+//        tokenProvider.verifyToken(credentials.refreshToken)?.let { it: Int ->
+//            userApi.getUserById(it)?.let {
+//                val credentialsResponse = tokenProvider.createTokens(it)
+//                LoginTokenResponse(credentialsResponse)
+//            } ?: throw AuthenticationException("Wrong credentials")
+//        } ?: throw AuthenticationException("Wrong credentials")
+//    }
+}

+ 11 - 20
src/main/kotlin/ru/mephi/plugins/Security.kt

@@ -1,20 +1,17 @@
-package ru.mephi.plugins
+package ru.mephi.modules
 
-import io.ktor.auth.*
-import io.ktor.util.*
-import io.ktor.auth.jwt.*
 import com.auth0.jwt.JWT
-import com.auth0.jwt.JWTVerifier
 import com.auth0.jwt.algorithms.Algorithm
-import io.ktor.sessions.*
-import io.ktor.application.*
-import io.ktor.response.*
-import io.ktor.request.*
+import io.ktor.auth.*
+import io.ktor.auth.jwt.*
 
-fun Application.configureSecurity() {
 
-//    authentication {
-//        jwt {
+fun Authentication.Configuration.authenticationModule(
+//    userApi: UserApi,
+//    databaseProvider: DatabaseProviderContract,
+//    tokenVerifier: JWTVerifier
+) {
+    jwt {
 //            val jwtAudience = environment.config.property("jwt.audience").getString()
 //            realm = environment.config.property("jwt.realm").getString()
 //            verifier(
@@ -28,11 +25,5 @@ fun Application.configureSecurity() {
 //                if (credential.payload.audience.contains(jwtAudience)) JWTPrincipal(credential.payload) else null
 //            }
 //        }
-//    }
-//    data class MySession(val count: Int = 0)
-//    install(Sessions) {
-//        cookie<MySession>("MY_SESSION") {
-//            cookie.extensions["SameSite"] = "lax"
-//        }
-//    }
-}
+    }
+}

+ 89 - 0
src/main/kotlin/ru/mephi/modules/MainModule.kt

@@ -0,0 +1,89 @@
+package ru.mephi.modules
+
+import io.ktor.application.*
+import io.ktor.auth.*
+import io.ktor.features.*
+import io.ktor.http.*
+import io.ktor.http.content.*
+import io.ktor.request.*
+import io.ktor.response.*
+import io.ktor.routing.*
+import io.ktor.serialization.*
+import io.ktor.server.engine.*
+import org.koin.ktor.ext.inject
+import org.slf4j.event.Level
+import ru.mephi.config.DbConfig
+import ru.mephi.domain.service.FeedService
+import ru.mephi.domain.service.RegistrationService
+import ru.mephi.domain.service.ProfileService
+import ru.mephi.modules.routes.auth.registrationModule
+import ru.mephi.modules.routes.feed.feedModule
+import ru.mephi.modules.routes.info.infoModule
+import ru.mephi.modules.routes.user.profileModule
+import ru.mephi.statuspages.authStatusPages
+import ru.mephi.statuspages.generalStatusPages
+import ru.mephi.statuspages.userStatusPages
+
+fun Application.mainModule() {
+//    DbConfig.setupDatabase()
+    val feedService: FeedService by inject()
+    val profileService: ProfileService by inject()
+    val registrationService: RegistrationService by inject()
+
+    install(ShutDownUrl.ApplicationCallFeature) {
+        // The URL that will be intercepted (you can also use the application.conf's ktor.deployment.shutdown.url key)
+        shutDownUrl = "/ktor/application/shutdown"
+        // A function that will be executed to get the exit code of the process
+        exitCodeSupplier = { 0 } // ApplicationCall.() -> Int
+    }
+
+    install(Authentication) {
+        authenticationModule()
+    }
+
+    install(Routing) {
+        registrationModule(registrationService)
+        infoModule()
+
+        authenticate("jwt") {
+            feedModule(feedService)
+            profileModule(profileService)
+        }
+        static("/static") {
+            resources("static")
+        }
+    }
+
+    install(CallLogging) {
+        level = Level.INFO
+        filter { call -> call.request.path().startsWith("/") }
+    }
+
+    install(ContentNegotiation) {
+        json()
+    }
+
+    install(StatusPages) {
+        generalStatusPages()
+        userStatusPages()
+        authStatusPages()
+
+        exception<UnknownError> {
+            call.respondText(
+                "Internal server error",
+                ContentType.Text.Plain,
+                status = HttpStatusCode.InternalServerError
+            )
+        }
+        exception<IllegalArgumentException> {
+            call.respond(HttpStatusCode.BadRequest)
+        }
+    }
+
+//    install(HttpsRedirect) {
+//        // The port to redirect to. By default 443, the default HTTPS port.
+//        sslPort = 443
+//        // 301 Moved Permanently, or 302 Found redirect.
+//        permanentRedirect = true
+//    }
+}

+ 29 - 0
src/main/kotlin/ru/mephi/modules/routes/auth/RegistrationModule.kt

@@ -0,0 +1,29 @@
+package ru.mephi.modules.routes.auth
+
+import io.ktor.routing.*
+import org.koin.ktor.ext.inject
+import ru.mephi.domain.service.RegistrationService
+import javax.imageio.spi.RegisterableService
+
+fun Routing.registrationModule(registrationService: RegistrationService) {
+
+    val unauthenticatedController by inject<RegisterableService>()
+
+//    post("user") {
+//        val postUser = call.receive<PostUserBody>()
+//        val user = unauthenticatedController.createUser(postUser)
+//        call.respond(user)
+//    }
+//
+//    post("authenticate") {
+//        val credentials = call.receive<LoginCredentials>()
+//        val loginTokenResponse = unauthenticatedController.authenticate(credentials)
+//        call.respond(loginTokenResponse)
+//    }
+//
+//    post("token") {
+//        val credentials = call.receive<RefreshBody>()
+//        val credentialsResponse = unauthenticatedController.refreshToken(credentials)
+//        call.respond(credentialsResponse)
+//    }
+}

+ 13 - 0
src/main/kotlin/ru/mephi/modules/routes/feed/FeedModule.kt

@@ -0,0 +1,13 @@
+package ru.mephi.modules.routes.feed
+
+import io.ktor.routing.*
+import org.koin.ktor.ext.inject
+import ru.mephi.domain.service.FeedService
+
+fun Route.feedModule(feedService: FeedService) {
+    route("/feed") {
+        get {
+            feedService.getAllFeed()
+        }
+    }
+}

+ 4 - 4
src/main/kotlin/ru/mephi/routes/info/infoController.kt

@@ -1,12 +1,12 @@
-package ru.mephi.routes.info
+package ru.mephi.modules.routes.info
 
 import io.ktor.application.*
 import io.ktor.response.*
 import io.ktor.routing.*
 
-fun Application.infoRoute() {
-    routing {
-        route("info") {
+fun Route.infoModule() {
+    route("/api") {
+        route("/info") {
             get {
                 call.respond("Server is running")
             }

+ 20 - 0
src/main/kotlin/ru/mephi/modules/routes/user/ProfileModule.kt

@@ -0,0 +1,20 @@
+package ru.mephi.modules.routes.user
+
+import io.ktor.routing.*
+import org.koin.core.component.get
+import org.koin.ktor.ext.inject
+import ru.mephi.domain.service.ProfileService
+
+fun Route.profileModule(profileService: ProfileService) {
+    route("/profile") {
+        get("/me") {
+            profileService.getMe()
+        }
+        put("/updateProfile") {
+
+        }
+        post("/createProfile") {
+
+        }
+    }
+}

+ 0 - 15
src/main/kotlin/ru/mephi/plugins/Administration.kt

@@ -1,15 +0,0 @@
-package ru.mephi.plugins
-
-import io.ktor.server.engine.*
-import io.ktor.application.*
-import io.ktor.response.*
-import io.ktor.request.*
-
-fun Application.configureAdministration() {
-    install(ShutDownUrl.ApplicationCallFeature) {
-        // The URL that will be intercepted (you can also use the application.conf's ktor.deployment.shutdown.url key)
-        shutDownUrl = "/ktor/application/shutdown"
-        // A function that will be executed to get the exit code of the process
-        exitCodeSupplier = { 0 } // ApplicationCall.() -> Int
-    }
-}

+ 0 - 25
src/main/kotlin/ru/mephi/plugins/HTTP.kt

@@ -1,25 +0,0 @@
-package ru.mephi.plugins
-
-import io.ktor.features.*
-import io.ktor.http.content.*
-import io.ktor.http.*
-import io.ktor.application.*
-import io.ktor.response.*
-import io.ktor.request.*
-
-fun Application.configureHTTP() {
-//    install(CachingHeaders) {
-//        options { outgoingContent ->
-//            when (outgoingContent.contentType?.withoutParameters()) {
-//                ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60))
-//                else -> null
-//            }
-//        }
-//    }
-//    install(HttpsRedirect) {
-//        // The port to redirect to. By default 443, the default HTTPS port.
-//        sslPort = 443
-//        // 301 Moved Permanently, or 302 Found redirect.
-//        permanentRedirect = true
-//    }
-}

+ 0 - 16
src/main/kotlin/ru/mephi/plugins/Routing.kt

@@ -1,16 +0,0 @@
-package ru.mephi.plugins
-
-import io.ktor.routing.*
-import io.ktor.http.*
-import io.ktor.http.content.*
-import io.ktor.features.*
-import io.ktor.application.*
-import io.ktor.response.*
-import io.ktor.request.*
-import ru.mephi.routes.info.infoRoute
-
-fun Application.configureRouting() {
-    routing {
-        infoRoute()
-    }
-}

+ 0 - 11
src/main/kotlin/ru/mephi/plugins/Serialization.kt

@@ -1,11 +0,0 @@
-package ru.mephi.plugins
-
-import io.ktor.serialization.*
-import io.ktor.features.*
-import io.ktor.application.*
-
-fun Application.configureSerialization() {
-    install(ContentNegotiation) {
-        json()
-    }
-}

+ 17 - 0
src/main/kotlin/ru/mephi/statuspages/authStatusPages.kt

@@ -0,0 +1,17 @@
+package ru.mephi.statuspages
+
+import io.ktor.application.*
+import io.ktor.features.*
+import io.ktor.http.*
+import io.ktor.response.*
+import ru.mephi.domain.exceptions.AuthenticationException
+import ru.mephi.domain.exceptions.AuthorizationException
+
+fun StatusPages.Configuration.authStatusPages() {
+    exception<AuthenticationException> { cause ->
+        call.respondText(cause.message, ContentType.Text.Plain, status = HttpStatusCode.Unauthorized)
+    }
+    exception<AuthorizationException> { cause ->
+        call.respondText(cause.message, ContentType.Text.Plain, status = HttpStatusCode.Forbidden)
+    }
+}

+ 13 - 0
src/main/kotlin/ru/mephi/statuspages/generalStatusPages.kt

@@ -0,0 +1,13 @@
+package ru.mephi.statuspages
+
+import io.ktor.application.*
+import io.ktor.features.*
+import io.ktor.http.*
+import io.ktor.response.*
+import ru.mephi.domain.exceptions.MissingArgumentException
+
+fun StatusPages.Configuration.generalStatusPages() {
+    exception<MissingArgumentException> { cause ->
+        call.respond(HttpStatusCode.BadRequest, cause.message)
+    }
+}

+ 13 - 0
src/main/kotlin/ru/mephi/statuspages/userStatusPages.kt

@@ -0,0 +1,13 @@
+package ru.mephi.statuspages
+
+import io.ktor.application.*
+import io.ktor.features.*
+import io.ktor.http.*
+import io.ktor.response.*
+import ru.mephi.domain.exceptions.InvalidUserException
+
+fun StatusPages.Configuration.userStatusPages() {
+    exception<InvalidUserException> { cause ->
+        call.respond(HttpStatusCode.BadRequest, cause.localizedMessage)
+    }
+}

+ 10 - 0
src/main/kotlin/ru/mephi/utils/Cipher.kt

@@ -0,0 +1,10 @@
+package ru.mephi.utils
+
+import com.auth0.jwt.algorithms.Algorithm
+
+object Cipher {
+    val algorithm: Algorithm = Algorithm.HMAC256("something-very-secret-here")
+
+    fun encrypt(data: String?): ByteArray =
+            algorithm.sign(data?.toByteArray())
+}

+ 11 - 0
src/main/kotlin/ru/mephi/utils/JsonMapper.kt

@@ -0,0 +1,11 @@
+package ru.mephi.utils
+
+import kotlinx.serialization.json.Json
+
+object JsonMapper {
+    val defaultMapper = Json {
+        prettyPrint = true
+        ignoreUnknownKeys = true
+        isLenient = true
+    }
+}

+ 22 - 0
src/main/kotlin/ru/mephi/utils/JwtProvider.kt

@@ -0,0 +1,22 @@
+package ru.mephi.utils
+
+import com.auth0.jwt.JWT
+import com.auth0.jwt.JWTVerifier
+import com.auth0.jwt.interfaces.DecodedJWT
+import ru.mephi.domain.model.User
+import java.util.Date
+
+object JwtProvider {
+    private const val validityInMs = 36_000_00 * 10 // 10 hours
+    private const val issuer = "alumniclub"
+    private const val audience = "alumniclub-audience"
+
+    val verifier: JWTVerifier = JWT.require(Cipher.algorithm).withIssuer(issuer).build()
+
+    fun decodeJWT(token: String): DecodedJWT = JWT.require(Cipher.algorithm).build().verify(token)
+
+    fun createJWT(user: User): String? =
+        JWT.create().withIssuedAt(Date()).withSubject("Authentication").withIssuer(issuer).withAudience(audience)
+            .withClaim("email", user.email).withExpiresAt(Date(System.currentTimeMillis() + validityInMs))
+            .sign(Cipher.algorithm)
+}

+ 15 - 0
src/main/kotlin/ru/mephi/utils/dbQuery.kt

@@ -0,0 +1,15 @@
+package ru.mephi.utils
+
+import kotlinx.coroutines.*
+import org.jetbrains.exposed.sql.Transaction
+import org.jetbrains.exposed.sql.transactions.transaction
+
+//fun <T> dbQuery(statement: Transaction.() -> T): Job =
+//    CoroutineScope(Dispatchers.IO).launch {
+//        transaction { statement() }
+//    }
+
+suspend fun <T> dbQuery(statement: Transaction.() -> T): T =
+    withContext(Dispatchers.IO) {
+        transaction { statement() }
+    }

+ 10 - 0
src/main/kotlin/ru/mephi/utils/isEmailValid.kt

@@ -0,0 +1,10 @@
+package ru.mephi.utils
+
+const val MAIL_REGEX = ("^(([\\w-]+\\.)+[\\w-]+|([a-zA-Z]|[\\w-]{2,}))@"
+        + "((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?"
+        + "[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\."
+        + "([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?"
+        + "[0-9]{1,2}|25[0-5]|2[0-4][0-9]))|"
+        + "([a-zA-Z]+[\\w-]+\\.)+[a-zA-Z]{2,4})$")
+
+fun String.isEmailValid(): Boolean = !this.isNullOrBlank() && Regex(MAIL_REGEX).matches(this)

+ 12 - 0
src/main/kotlin/ru/mephi/utils/sendOk.kt

@@ -0,0 +1,12 @@
+package ru.mephi.utils
+
+import io.ktor.application.*
+import io.ktor.http.*
+import io.ktor.response.*
+import io.ktor.util.pipeline.*
+
+suspend fun PipelineContext<Unit, ApplicationCall>.sendOk() {
+    call.respond(HttpStatusCode.OK)
+}
+
+//val ApplicationCall.user get() = authentication.principal<User>()!!

+ 1 - 1
src/test/kotlin/ru/mephi/ApplicationTest.kt

@@ -3,7 +3,7 @@ package ru.mephi
 import io.ktor.http.*
 import kotlin.test.*
 import io.ktor.server.testing.*
-import ru.mephi.plugins.*
+import ru.mephi.modules.*
 
 class ApplicationTest {
     @Test