├── settings.gradle.kts ├── .DS_Store ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── README.md ├── src ├── main │ ├── kotlin │ │ └── dev │ │ │ └── baseio │ │ │ └── superapp │ │ │ ├── database │ │ │ ├── connection │ │ │ │ ├── DatabaseConnectionService.kt │ │ │ │ └── DatabaseConnectionServiceImpl.kt │ │ │ ├── entities │ │ │ │ ├── Departments.kt │ │ │ │ └── Employee.kt │ │ │ └── dao │ │ │ │ ├── EmployeeDao.kt │ │ │ │ └── EmployeeDaoImpl.kt │ │ │ ├── auth │ │ │ ├── FirebaseUser.kt │ │ │ ├── ApplicationCall+Extensions.kt │ │ │ ├── FirebaseAdmin.kt │ │ │ └── FirebaseAuth.kt │ │ │ ├── rest │ │ │ ├── models │ │ │ │ └── ApiResponse.kt │ │ │ └── SuperAppEndpoints.kt │ │ │ ├── plugins │ │ │ ├── Serialization.kt │ │ │ ├── Templating.kt │ │ │ ├── StatusPages.kt │ │ │ ├── HTTP.kt │ │ │ ├── Authentication.kt │ │ │ └── Routing.kt │ │ │ ├── Config.kt │ │ │ ├── SuperApp.kt │ │ │ └── koin │ │ │ └── KoinModules.kt │ └── resources │ │ ├── application.conf │ │ └── logback.xml └── test │ └── kotlin │ └── dev │ └── baseio │ └── superapp │ └── ApplicationTest.kt ├── gradlew.bat └── gradlew /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "PraxisKtor" -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/emploektor.baseio.dev/master/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /out 4 | /build 5 | *.iml 6 | *.ipr 7 | *.iws 8 | 9 | /src/main/resources/firebase-admin-sdk.json -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/emploektor.baseio.dev/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ktor_version=1.6.7 2 | kotlin_version=1.6.10 3 | logback_version=1.2.10 4 | exposedVersion=0.37.3 5 | kotlin.code.style=official 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SECRET_KEY=sdfsdf;DATABASE_NAME=superapp;DATABASE_HOST=localhost;DATABASE_PORT=5432;DATABASE_USER=postgres;DATABASE_PASSWORD=postgres 2 | 3 | https://www.sqlshack.com/setting-up-a-postgresql-database-on-mac/ -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/database/connection/DatabaseConnectionService.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.database.connection 2 | 3 | import org.ktorm.database.Database 4 | 5 | interface DatabaseConnectionService { 6 | fun connect(): Database 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/auth/FirebaseUser.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.auth 2 | 3 | import io.ktor.auth.* 4 | 5 | data class FirebaseUser( 6 | val userId: String?, 7 | val email: String?, 8 | val name: String?, 9 | val picture: String? 10 | ): Principal -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/rest/models/ApiResponse.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.rest.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class ApiResponse( 7 | val message: String, 8 | val data: T? = null 9 | ) 10 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/rest/SuperAppEndpoints.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.rest 2 | 3 | object SuperAppEndpoints { 4 | const val REGISTER_USER = "/registerUser" 5 | const val USERS = "/users" 6 | 7 | object Query { 8 | const val SEARCH: String = "search" 9 | } 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/auth/ApplicationCall+Extensions.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.auth 2 | 3 | import io.ktor.application.* 4 | import io.ktor.auth.* 5 | import javax.naming.AuthenticationException 6 | 7 | val ApplicationCall.applicationCallFirebaseUser: FirebaseUser 8 | get() = principal() ?: throw AuthenticationException() -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/database/entities/Departments.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.database.entities 2 | 3 | import org.ktorm.schema.Table 4 | import org.ktorm.schema.int 5 | import org.ktorm.schema.varchar 6 | 7 | object Departments : Table("t_department") { 8 | val id = int("id").primaryKey() 9 | val name = varchar("name") 10 | } -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8080 4 | port = ${?PORT} 5 | } 6 | application { 7 | modules = [ dev.baseio.superapp.SuperAppKt.module ] 8 | } 9 | } 10 | 11 | key { 12 | secret = ${SECRET_KEY} 13 | } 14 | 15 | database { 16 | host = ${DATABASE_HOST} 17 | port = ${DATABASE_PORT} 18 | name = ${DATABASE_NAME} 19 | user = ${DATABASE_USER} 20 | password = ${DATABASE_PASSWORD} 21 | } -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/plugins/Serialization.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.plugins 2 | 3 | import io.ktor.serialization.* 4 | import io.ktor.features.* 5 | import io.ktor.application.* 6 | import io.ktor.response.* 7 | import io.ktor.routing.* 8 | 9 | fun Application.configureSerialization() { 10 | install(ContentNegotiation) { 11 | json() 12 | } 13 | 14 | routing { 15 | get("/json/kotlinx-serialization") { 16 | call.respond(mapOf("hello" to "world")) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/test/kotlin/dev/baseio/superapp/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp 2 | 3 | import io.ktor.http.* 4 | import kotlin.test.* 5 | import io.ktor.server.testing.* 6 | import dev.baseio.superapp.plugins.configureRouting 7 | 8 | class ApplicationTest { 9 | @Test 10 | fun testRoot() { 11 | withTestApplication({ configureRouting() }) { 12 | handleRequest(HttpMethod.Get, "/").apply { 13 | assertEquals(HttpStatusCode.OK, response.status()) 14 | assertEquals("Hello World!", response.content) 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/database/dao/EmployeeDao.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.database.dao 2 | 3 | import dev.baseio.superapp.database.entities.Employee 4 | import java.time.LocalDate 5 | import java.util.* 6 | 7 | interface EmployeeDao { 8 | fun createUpdateEmployee( 9 | id: UUID?, 10 | name: String, 11 | job: String, 12 | managerId: UUID, 13 | hireDate: LocalDate, 14 | departmentId: UUID, 15 | location: UUID, 16 | ): Employee? 17 | 18 | fun findByUUID(uuid: UUID): Employee? 19 | fun exists(uuid: UUID): Boolean 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/database/entities/Employee.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.database.entities 2 | 3 | import org.ktorm.database.Database 4 | import org.ktorm.entity.sequenceOf 5 | import org.ktorm.schema.* 6 | 7 | object Employee : Table("t_users") { 8 | val id = uuid("id").primaryKey() 9 | val name = varchar("name") 10 | val job = varchar("job") 11 | val managerId = uuid("manager_id") 12 | val hireDate = date("hire_date") 13 | val departmentId = uuid("department_id") 14 | val location = uuid("location") 15 | } 16 | 17 | val Database.employees get() = this.sequenceOf(Employee) -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/database/connection/DatabaseConnectionServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.database.connection 2 | 3 | import dev.baseio.superapp.Config 4 | import org.ktorm.database.Database 5 | 6 | class DatabaseConnectionServiceImpl(private val databaseConfig: Config) : DatabaseConnectionService { 7 | override fun connect(): Database { 8 | return Database.connect( 9 | url = with(databaseConfig) { "jdbc:postgresql://$dbHost:$dbPort/$dbName" }, 10 | driver = "org.postgresql.Driver", 11 | user = databaseConfig.dbUser, 12 | password = databaseConfig.dbPassword 13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/plugins/Templating.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.plugins 2 | 3 | import io.ktor.html.* 4 | import kotlinx.html.* 5 | import io.ktor.application.* 6 | import io.ktor.routing.* 7 | 8 | fun Application.configureTemplating() { 9 | 10 | 11 | routing { 12 | get("/html-dsl") { 13 | call.respondHtml { 14 | body { 15 | h1 { +"HTML" } 16 | ul { 17 | for (n in 1..10) { 18 | li { +"$n" } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/Config.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp 2 | 3 | import io.ktor.config.* 4 | 5 | /** 6 | * Class containing Configuration values or secret key which will be provided from 7 | * application.conf (from environment variables). 8 | */ 9 | class Config constructor(config: ApplicationConfig) { 10 | val secretKey = config.property("key.secret").getString() 11 | val dbHost = config.property("database.host").getString() 12 | val dbPort = config.property("database.port").getString() 13 | val dbName = config.property("database.name").getString() 14 | val dbUser = config.property("database.user").getString() 15 | val dbPassword = config.property("database.password").getString() 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/auth/FirebaseAdmin.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.auth 2 | 3 | import com.google.auth.oauth2.GoogleCredentials 4 | import com.google.firebase.FirebaseApp 5 | import com.google.firebase.FirebaseOptions 6 | import java.io.InputStream 7 | 8 | /** 9 | * Initialization for Firebase application. 10 | */ 11 | object FirebaseAdmin { 12 | private val serviceAccount: InputStream? = 13 | this::class.java.classLoader.getResourceAsStream("firebase-admin-sdk.json") 14 | 15 | private val options: FirebaseOptions = FirebaseOptions.builder() 16 | .setCredentials(GoogleCredentials.fromStream(serviceAccount)) 17 | .build() 18 | 19 | fun init(): FirebaseApp = FirebaseApp.initializeApp(options) 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/plugins/StatusPages.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.plugins 2 | 3 | import io.ktor.application.* 4 | import io.ktor.content.* 5 | import io.ktor.features.* 6 | import io.ktor.http.* 7 | import io.ktor.response.* 8 | 9 | fun Application.configureStatusPages(){ 10 | install(StatusPages) { 11 | exception { cause -> 12 | call.respond(HttpStatusCode.InternalServerError, "Internal Server Error") 13 | throw cause 14 | } 15 | status(HttpStatusCode.NotFound) { 16 | call.respond( 17 | TextContent( 18 | "${it.value} ${it.description}", 19 | ContentType.Text.Plain.withCharset(Charsets.UTF_8), 20 | it 21 | ) 22 | ) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/plugins/HTTP.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.plugins 2 | 3 | import io.ktor.http.* 4 | import io.ktor.features.* 5 | import io.ktor.application.* 6 | 7 | fun Application.configureHTTP() { 8 | install(CORS) { 9 | method(HttpMethod.Options) 10 | method(HttpMethod.Put) 11 | method(HttpMethod.Delete) 12 | method(HttpMethod.Patch) 13 | header(HttpHeaders.Authorization) 14 | header("MyCustomHeader") 15 | anyHost() // @TODO: Don't do this in production if possible. Try to limit it. 16 | } 17 | install(PartialContent) { 18 | // Maximum number of ranges that will be accepted from a HTTP request. 19 | // If the HTTP request specifies more ranges, they will all be merged into a single range. 20 | maxRangeCount = 10 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/plugins/Authentication.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.plugins 2 | 3 | import com.google.firebase.FirebaseApp 4 | import dev.baseio.superapp.auth.FirebaseAdmin 5 | import dev.baseio.superapp.auth.FirebaseUser 6 | import dev.baseio.superapp.auth.firebase 7 | import io.ktor.application.* 8 | import io.ktor.auth.* 9 | 10 | const val FIREBASE_AUTH = "auth" 11 | 12 | fun Application.authentication() { 13 | val firebaseApp: FirebaseApp = FirebaseAdmin.init() 14 | authentication { 15 | firebase(FIREBASE_AUTH, firebaseApp) { 16 | validate { credential -> 17 | // return Principal using credential 18 | FirebaseUser( 19 | credential.token.uid, 20 | credential.token.email, 21 | credential.token.name, 22 | credential.token.picture 23 | ) 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/SuperApp.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp 2 | 3 | import dev.baseio.superapp.koin.DatabaseComponent 4 | import dev.baseio.superapp.koin.databaseModule 5 | import io.ktor.server.engine.* 6 | import io.ktor.server.netty.* 7 | import dev.baseio.superapp.plugins.* 8 | import io.ktor.application.* 9 | import org.koin.core.component.KoinComponent 10 | import org.koin.core.component.inject 11 | import org.koin.ktor.ext.Koin 12 | import org.koin.logger.SLF4JLogger 13 | 14 | 15 | fun main(args: Array): Unit = EngineMain.main(args) 16 | 17 | fun Application.module() { 18 | authentication() 19 | configureStatusPages() 20 | configureRouting() 21 | configureHTTP() 22 | configureTemplating() 23 | configureSerialization() 24 | // Declare Koin 25 | install(Koin) { 26 | SLF4JLogger() 27 | modules(databaseModule(environment.config)) 28 | } 29 | databaseComponent.provideDatabaseConnectionService().connect() 30 | } 31 | 32 | val databaseComponent by lazy { DatabaseComponent() } 33 | 34 | inline fun getKoinInstance() = 35 | object : KoinComponent { 36 | val value: T by inject() 37 | }.value -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/koin/KoinModules.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.koin 2 | 3 | import dev.baseio.superapp.Config 4 | import dev.baseio.superapp.database.connection.DatabaseConnectionService 5 | import dev.baseio.superapp.database.connection.DatabaseConnectionServiceImpl 6 | import dev.baseio.superapp.database.dao.EmployeeDao 7 | import dev.baseio.superapp.database.dao.EmployeeDaoImpl 8 | import io.ktor.config.* 9 | import org.koin.core.component.KoinComponent 10 | import org.koin.core.component.get 11 | import org.koin.core.module.Module 12 | import org.koin.dsl.module 13 | import org.ktorm.database.Database 14 | 15 | fun databaseModule(config: ApplicationConfig): Module = module { 16 | single { Config(config) } 17 | single { DatabaseConnectionServiceImpl(get()) } 18 | single { get().connect() } 19 | single { EmployeeDaoImpl(get()) } 20 | } 21 | 22 | class DatabaseComponent : KoinComponent { 23 | fun provideDatabaseConnectionService(): DatabaseConnectionService = get() // do we need this again ? 24 | fun provideDatabaseInstance(): Database = get() 25 | fun provideDatabaseConfig(): Config = get() // do we need to expose this again ? 26 | } 27 | 28 | class DaoComponent : KoinComponent { 29 | fun provideEmployeeDao(): EmployeeDao = get() 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/plugins/Routing.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.plugins 2 | 3 | import dev.baseio.superapp.auth.applicationCallFirebaseUser 4 | import dev.baseio.superapp.rest.SuperAppEndpoints 5 | import dev.baseio.superapp.rest.models.ApiResponse 6 | import io.ktor.routing.* 7 | import io.ktor.application.* 8 | import io.ktor.auth.* 9 | import io.ktor.http.* 10 | import io.ktor.response.* 11 | 12 | fun Application.configureRouting() { 13 | routing { 14 | authenticate(FIREBASE_AUTH) { 15 | registerUser() 16 | searchUsers() 17 | } 18 | rootRoute() 19 | } 20 | } 21 | 22 | 23 | private fun Routing.rootRoute() { 24 | get("/") { 25 | call.respond(ApiResponse(message = "I am alive!")) 26 | } 27 | } 28 | 29 | private fun Route.searchUsers() { 30 | get(SuperAppEndpoints.USERS) { 31 | val query = call.parameters[SuperAppEndpoints.Query.SEARCH] ?: return@get call.respond( 32 | status = HttpStatusCode.BadRequest, 33 | ApiResponse("Missing or malformed query") 34 | ) 35 | call.respond(ApiResponse(data = "", message = "Found users")) 36 | } 37 | } 38 | 39 | private fun Route.registerUser() { 40 | post(SuperAppEndpoints.REGISTER_USER) { 41 | val firebaseUser = call.applicationCallFirebaseUser 42 | firebaseUser.email?.let { 43 | call.respond(ApiResponse(message = "Registered successfully", data = null)) 44 | } ?: kotlin.run { 45 | call.respond(ApiResponse(message = "Email missing in request")) 46 | } 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/database/dao/EmployeeDaoImpl.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.database.dao 2 | 3 | import dev.baseio.superapp.database.entities.Employee 4 | import dev.baseio.superapp.database.entities.employees 5 | import org.ktorm.database.Database 6 | import org.ktorm.dsl.* 7 | import org.ktorm.entity.any 8 | import org.ktorm.entity.find 9 | import org.ktorm.support.postgresql.insertOrUpdateReturning 10 | import java.time.LocalDate 11 | import java.util.* 12 | 13 | class EmployeeDaoImpl(private val database: Database) : EmployeeDao { 14 | override fun createUpdateEmployee( 15 | id: UUID?, 16 | name: String, 17 | job: String, 18 | managerId: UUID, 19 | hireDate: LocalDate, 20 | departmentId: UUID, 21 | location: UUID 22 | ): Employee? { 23 | val uuid = database.insertOrUpdateReturning(Employee, Employee.id) { 24 | id?.let { 25 | this.set(Employee.id, id) 26 | } 27 | this.set(Employee.name, name) 28 | this.set(Employee.job, job) 29 | this.set(Employee.managerId, managerId) 30 | this.set(Employee.hireDate, hireDate) 31 | this.set(Employee.departmentId, departmentId) 32 | this.set(Employee.location, location) 33 | } 34 | uuid?.let { 35 | return database.employees.find { it.id eq uuid } 36 | } 37 | return null 38 | } 39 | 40 | override fun findByUUID(uuid: UUID): Employee? { 41 | return database.employees.find { it.id eq uuid } 42 | } 43 | 44 | override fun exists(uuid: UUID): Boolean { 45 | return database.employees.any { it.id eq uuid } 46 | } 47 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/baseio/superapp/auth/FirebaseAuth.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.superapp.auth 2 | 3 | import com.google.firebase.FirebaseApp 4 | import com.google.firebase.auth.FirebaseAuth 5 | import com.google.firebase.auth.FirebaseAuthException 6 | import com.google.firebase.auth.FirebaseToken 7 | import io.ktor.application.ApplicationCall 8 | import io.ktor.application.call 9 | import io.ktor.auth.Authentication 10 | import io.ktor.auth.AuthenticationContext 11 | import io.ktor.auth.AuthenticationFailedCause 12 | import io.ktor.auth.AuthenticationFunction 13 | import io.ktor.auth.AuthenticationPipeline 14 | import io.ktor.auth.AuthenticationProvider 15 | import io.ktor.auth.Credential 16 | import io.ktor.auth.UnauthorizedResponse 17 | import io.ktor.auth.parseAuthorizationHeader 18 | import io.ktor.http.auth.HttpAuthHeader 19 | import io.ktor.response.respond 20 | 21 | private const val FirebaseAuthKey = "FirebaseAuth" 22 | 23 | data class FirebaseCredential(val token: FirebaseToken) : Credential 24 | 25 | class FirebaseAuthenticationProvider internal constructor( 26 | config: Configuration, 27 | internal val firebaseApp: FirebaseApp 28 | ) : AuthenticationProvider(config) { 29 | 30 | internal val realm: String = config.realm 31 | internal val schemes = config.schemes 32 | internal val authHeader: (ApplicationCall) -> HttpAuthHeader? = config.authHeader 33 | internal val authenticationFunction: AuthenticationFunction = config.authenticationFunction 34 | 35 | class Configuration internal constructor( 36 | name: String?, 37 | private val firebaseApp: FirebaseApp 38 | ) : AuthenticationProvider.Configuration(name) { 39 | 40 | internal var authenticationFunction: AuthenticationFunction = { 41 | throw NotImplementedError( 42 | "Firebase auth validate function is not specified. Use firebaseAuth { validate { ... } } to fix." 43 | ) 44 | } 45 | 46 | internal var schemes = FirebaseAuthSchemes("Bearer") 47 | 48 | internal var authHeader: (ApplicationCall) -> HttpAuthHeader? = { call -> 49 | try { 50 | call.request.parseAuthorizationHeader() 51 | } catch (e: IllegalArgumentException) { 52 | null 53 | } 54 | } 55 | 56 | /** 57 | * JWT realm name that will be used during auth challenge 58 | */ 59 | var realm: String = "FirebaseIdToken" 60 | 61 | /** 62 | * Http auth header retrieval function. By default it does parse `Authorization` header content. 63 | */ 64 | fun authHeader(block: (ApplicationCall) -> HttpAuthHeader?) { 65 | authHeader = block 66 | } 67 | 68 | /** 69 | * @param [defaultScheme] default scheme that will be used to challenge the client when no valid auth is provided 70 | * @param [additionalSchemes] additional schemes that will be accepted when validating the authentication 71 | */ 72 | fun authSchemes(defaultScheme: String = "Bearer", vararg additionalSchemes: String) { 73 | schemes = FirebaseAuthSchemes(defaultScheme, *additionalSchemes) 74 | } 75 | 76 | fun validate(validate: AuthenticationFunction) { 77 | authenticationFunction = validate 78 | } 79 | 80 | internal fun build() = FirebaseAuthenticationProvider(this, firebaseApp) 81 | } 82 | } 83 | 84 | fun Authentication.Configuration.firebase( 85 | name: String? = null, 86 | firebaseApp: FirebaseApp, 87 | configure: FirebaseAuthenticationProvider.Configuration.() -> Unit 88 | ) { 89 | val provider = FirebaseAuthenticationProvider.Configuration(name, firebaseApp).apply(configure).build() 90 | val realm = provider.realm 91 | val schemes = provider.schemes 92 | val firebaseAuth = FirebaseAuth.getInstance(provider.firebaseApp) 93 | 94 | provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context -> 95 | val authHeader = provider.authHeader(call) ?: run { 96 | context.bearerChallenge(AuthenticationFailedCause.NoCredentials, realm, schemes) 97 | return@intercept 98 | } 99 | 100 | val token = authHeader.getBlob(provider.schemes)?.takeIf { it.isNotBlank() } ?: run { 101 | context.bearerChallenge(AuthenticationFailedCause.InvalidCredentials, realm, schemes) 102 | return@intercept 103 | } 104 | 105 | val firebaseToken = try { 106 | firebaseAuth.verifyIdToken(token) 107 | } catch (e: FirebaseAuthException) { 108 | context.bearerChallenge(AuthenticationFailedCause.InvalidCredentials, realm, schemes) 109 | return@intercept 110 | } catch (cause: Throwable) { 111 | val message = cause.message ?: cause.javaClass.simpleName 112 | context.error(FirebaseAuthKey, AuthenticationFailedCause.Error(message)) 113 | return@intercept 114 | } 115 | 116 | val principal = provider.authenticationFunction(call, FirebaseCredential(firebaseToken)) ?: run { 117 | context.bearerChallenge(AuthenticationFailedCause.InvalidCredentials, realm, schemes) 118 | return@intercept 119 | } 120 | 121 | context.principal(principal) 122 | } 123 | 124 | register(provider) 125 | } 126 | 127 | internal class FirebaseAuthSchemes(internal val defaultScheme: String, vararg additionalSchemes: String) { 128 | 129 | private val schemes = (arrayOf(defaultScheme) + additionalSchemes).map { it.toLowerCase() }.toSet() 130 | 131 | operator fun contains(scheme: String): Boolean = scheme.toLowerCase() in schemes 132 | } 133 | 134 | private fun AuthenticationContext.bearerChallenge( 135 | cause: AuthenticationFailedCause, 136 | realm: String, 137 | schemes: FirebaseAuthSchemes 138 | ) = challenge(FirebaseAuthKey, cause) { 139 | call.respond( 140 | UnauthorizedResponse( 141 | HttpAuthHeader.Parameterized( 142 | schemes.defaultScheme, 143 | mapOf(HttpAuthHeader.Parameters.Realm to realm) 144 | ) 145 | ) 146 | ) 147 | it.complete() 148 | } 149 | 150 | private fun HttpAuthHeader.getBlob(schemes: FirebaseAuthSchemes) = when { 151 | this is HttpAuthHeader.Single && authScheme.toLowerCase() in schemes -> blob 152 | else -> null 153 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | --------------------------------------------------------------------------------