├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── src ├── main │ └── kotlin │ │ └── io │ │ └── supabase │ │ └── gotrue │ │ ├── types │ │ ├── GoTrueVerifyType.kt │ │ ├── GoTrueUserAttributes.kt │ │ ├── GoTrueTokenResponse.kt │ │ ├── GoTrueUserResponse.kt │ │ └── GoTrueSettings.kt │ │ ├── http │ │ ├── GoTrueHttpException.kt │ │ ├── GoTrueHttpClient.kt │ │ └── GoTrueHttpClientApache.kt │ │ ├── json │ │ ├── GoTrueJsonConverter.kt │ │ └── GoTrueJsonConverterJackson.kt │ │ └── GoTrueClient.kt └── test │ ├── resources │ └── fixtures │ │ ├── token-response.json │ │ ├── settings-response.json │ │ ├── user-response.json │ │ └── user-response-email-disabled.json │ └── kotlin │ └── io │ └── supabase │ └── gotrue │ ├── types │ └── CustomGoTrueUserResponse.kt │ ├── json │ └── GoTrueJsonConverterJackonTest.kt │ ├── CustomGoTrueClientIntegrationTest.kt │ ├── http │ └── GoTrueHttpClientApacheTest.kt │ └── GoTrueClientIntegrationTest.kt ├── .gitattributes ├── .github └── workflows │ ├── gradle.yml │ └── gradle-publish.yml ├── gradlew.bat ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'gotrue-kt' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supabase-community/gotrue-kt/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | 7 | .idea -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/types/GoTrueVerifyType.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.types 2 | 3 | enum class GoTrueVerifyType { 4 | SIGNUP, 5 | RECOVERY 6 | } 7 | 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /src/test/resources/fixtures/token-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xyz.abc", 3 | "token_type": "bearer", 4 | "expires_in": 3600, 5 | "refresh_token": "abc", 6 | "user": null 7 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/types/GoTrueUserAttributes.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.types 2 | 3 | data class GoTrueUserAttributes( 4 | val email: String? = null, 5 | val password: String? = null, 6 | val data: Map? = null 7 | ) -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/types/GoTrueTokenResponse.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.types 2 | 3 | data class GoTrueTokenResponse( 4 | val accessToken: String, 5 | val tokenType: String, 6 | val expiresIn: Long, 7 | val refreshToken: String 8 | ) 9 | -------------------------------------------------------------------------------- /src/test/resources/fixtures/settings-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "external": { 3 | "bitbucket": false, 4 | "github": false, 5 | "gitlab": false, 6 | "google": false, 7 | "facebook": false, 8 | "email": true, 9 | "saml": false 10 | }, 11 | "external_labels": {}, 12 | "disable_signup": false, 13 | "autoconfirm": true 14 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/types/GoTrueUserResponse.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.types 2 | 3 | import java.time.OffsetDateTime 4 | 5 | data class GoTrueUserResponse( 6 | val id: String, 7 | val email: String, 8 | val confirmationSentAt: OffsetDateTime, 9 | val createdAt: OffsetDateTime, 10 | val updatedAt: OffsetDateTime 11 | ) -------------------------------------------------------------------------------- /src/test/kotlin/io/supabase/gotrue/types/CustomGoTrueUserResponse.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.types 2 | 3 | import java.util.* 4 | 5 | data class CustomGoTrueUserResponse( 6 | val accessToken: String, 7 | val tokenType: String, 8 | val refreshToken: String, 9 | val user: User 10 | ) 11 | 12 | data class User( 13 | val id: UUID, 14 | val email: String, 15 | val phone: String 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/types/GoTrueSettings.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.types 2 | 3 | data class GoTrueSettings( 4 | val external: External, 5 | val disableSignup: Boolean, 6 | val autoconfirm: Boolean 7 | ) { 8 | 9 | data class External( 10 | val bitbucket: Boolean, 11 | val github: Boolean, 12 | val gitlab: Boolean, 13 | val google: Boolean 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/http/GoTrueHttpException.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.http 2 | 3 | /** 4 | * Exception is used when a bad status code (> 301) is returned. 5 | * 6 | * If you implement your custom GoTrueHttpClient, you need to handle exceptions on your own. 7 | * 8 | * @property[status] HTTP status code 9 | * @property[data] Response body as [String] if available 10 | */ 11 | class GoTrueHttpException(val status: Int, val data: String?) : RuntimeException("Unexpected response status: $status") -------------------------------------------------------------------------------- /src/test/resources/fixtures/user-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "efcb01d5-512d-45cc-9b0c-d04b5e3d9697", 3 | "aud": "authenticated", 4 | "role": "authenticated", 5 | "email": "foo@bar.com", 6 | "confirmed_at": "2021-01-25T20:04:11.859221Z", 7 | "invited_at": "2021-01-25T20:02:10.588452Z", 8 | "confirmation_sent_at": "2021-01-25T20:02:10.588452Z", 9 | "recovery_sent_at": "2021-01-25T20:03:20.335112Z", 10 | "last_sign_in_at": "2021-01-25T20:04:11.859637Z", 11 | "app_metadata": { 12 | "provider": "email" 13 | }, 14 | "user_metadata": { 15 | "admin": true 16 | }, 17 | "created_at": "2021-01-25T20:02:10.586299Z", 18 | "updated_at": "2021-01-25T20:05:21.572341Z" 19 | } -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 8 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: 8 23 | distribution: corretto 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | - name: Build with Gradle 27 | run: ./gradlew build 28 | -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/json/GoTrueJsonConverter.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.json 2 | 3 | /** 4 | * Interface used by the GoTrueClient, allows replacing the default JSON converter. 5 | * 6 | * Overwrite it to replace the default Jackson FasterXML implementation. 7 | */ 8 | interface GoTrueJsonConverter { 9 | 10 | /** 11 | * Serializes [data] as JSON string. 12 | * 13 | * @param[data] the data to serialize 14 | * 15 | * @return JSON string 16 | */ 17 | fun serialize(data: Any): String 18 | 19 | /** 20 | * Deserializes a JSON [text] to the corresponding [responseType]. 21 | * 22 | * @param[text] The JSON text to convert 23 | * @param[responseType] The response type as Java class 24 | */ 25 | fun deserialize(text: String, responseType: Class): T 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java#publishing-using-gradle 3 | 4 | name: Gradle Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up JDK 8 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: 8 21 | distribution: corretto 22 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 23 | settings-path: ${{ github.workspace }} # location for the settings.xml file 24 | 25 | - name: Build with Gradle 26 | run: gradle build 27 | 28 | - name: Publish to Bintray 29 | env: 30 | bintray_user: ${{ secrets.BINTRAY_USER }} 31 | bintray_key: ${{ secrets.BINTRAY_KEY }} 32 | run: gradle bintrayUpload 33 | -------------------------------------------------------------------------------- /src/test/kotlin/io/supabase/gotrue/json/GoTrueJsonConverterJackonTest.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.json 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import org.junit.jupiter.api.TestInstance 6 | import org.junit.jupiter.params.ParameterizedTest 7 | import org.junit.jupiter.params.provider.MethodSource 8 | import java.util.stream.Stream 9 | 10 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 11 | class GoTrueJsonConverterJackonTest { 12 | 13 | private val converter = GoTrueJsonConverterJackson() 14 | 15 | @ParameterizedTest 16 | @MethodSource("serializeData") 17 | fun `should serialize and deserialize`(data: Any) { 18 | val serialized = converter.serialize(data) 19 | 20 | val deserialized = converter.deserialize(serialized, data.javaClass) 21 | 22 | assertThat(deserialized).isEqualTo(data) 23 | } 24 | 25 | @Suppress("unused") 26 | private fun serializeData(): Stream { 27 | return Stream.of( 28 | "5", 29 | mapOf("foo" to "bar", "number" to 5), 30 | ConverterTestDto("bar", 5) 31 | ) 32 | } 33 | } 34 | 35 | data class ConverterTestDto( 36 | val prop: String, 37 | val otherProp: Int 38 | ) -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/json/GoTrueJsonConverterJackson.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.json 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude 4 | import com.fasterxml.jackson.databind.DeserializationFeature 5 | import com.fasterxml.jackson.databind.ObjectMapper 6 | import com.fasterxml.jackson.databind.PropertyNamingStrategies 7 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule 8 | import com.fasterxml.jackson.module.kotlin.KotlinModule 9 | 10 | /** 11 | * Default implementation of the [GoTrueJsonConverter] used by the GoTrueDefaultClient. 12 | * 13 | * Uses Jackson FasterXML for JSON (de)-serialization. 14 | */ 15 | class GoTrueJsonConverterJackson : GoTrueJsonConverter { 16 | 17 | private val objectMapper = ObjectMapper() 18 | .registerModule(KotlinModule.Builder().build()) 19 | .registerModule(JavaTimeModule()) 20 | .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) 21 | .setSerializationInclusion(JsonInclude.Include.NON_NULL) 22 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 23 | 24 | override fun serialize(data: Any): String { 25 | return objectMapper.writeValueAsString(data) 26 | } 27 | 28 | override fun deserialize(text: String, responseType: Class): T { 29 | return objectMapper.readValue(text, responseType) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/test/resources/fixtures/user-response-email-disabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", 3 | "token_type": "bearer", 4 | "expires_in": 3600, 5 | "refresh_token": "nGAyjPONelHS6XG-asAsnQ", 6 | "user": { 7 | "id": "b04343d0-8164-4c92-9aaa-cdbbb9188358", 8 | "aud": "authenticated", 9 | "role": "authenticated", 10 | "email": "foo@bar.com", 11 | "email_confirmed_at": "2022-02-09T10:36:38.750745355Z", 12 | "phone": "", 13 | "confirmation_sent_at": "2022-02-09T10:34:22.105331Z", 14 | "last_sign_in_at": "2022-02-09T10:36:38.754228834Z", 15 | "app_metadata": { 16 | "provider": "email", 17 | "providers": [ 18 | "email" 19 | ] 20 | }, 21 | "user_metadata": {}, 22 | "identities": [ 23 | { 24 | "id": "b04343d0-8164-4c92-9aaa-cdbbb9188358", 25 | "user_id": "b04343d0-8164-4c92-9aaa-cdbbb9188358", 26 | "identity_data": { 27 | "sub": "b04343d0-8164-4c92-9aaa-cdbbb9188358" 28 | }, 29 | "provider": "email", 30 | "last_sign_in_at": "2022-02-08T22:25:10.211158Z", 31 | "created_at": "2022-02-08T22:25:10.211215Z", 32 | "updated_at": "2022-02-08T22:25:10.211219Z" 33 | } 34 | ], 35 | "created_at": "2022-02-08T22:25:10.207882Z", 36 | "updated_at": "2022-02-09T10:36:38.756078Z" 37 | } 38 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/http/GoTrueHttpClient.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.http 2 | 3 | /** 4 | * Interface used by the GoTrueClient, allows replacing the default HTTP client. 5 | * 6 | * Overwrite it to replace the default Apache HTTP Client implementation. 7 | */ 8 | interface GoTrueHttpClient { 9 | 10 | /** 11 | * Executes a HTTP POST request. 12 | * 13 | * @param[url] The path that will be added to the base uri 14 | * 15 | * @param[headers] The custom headers that will be added to the default headers. 16 | * Custom headers replace default headers if duplicate 17 | * 18 | * @param[data] The data that will be JSON-encoded and submitted as POST body 19 | * 20 | * @return The response body as [String] 21 | */ 22 | fun post(url: String, headers: Map = emptyMap(), data: Any? = null): String? 23 | 24 | /** 25 | * Executes a HTTP PUT request. 26 | * 27 | * @param[url] The path that will be added to the base uri 28 | * 29 | * @param[headers] The custom headers that will be added to the default headers. 30 | * Custom headers replace default headers if duplicate 31 | * 32 | * @param[data] The data that will be JSON-encoded and submitted as POST body 33 | * 34 | * @return The response body as [String] 35 | */ 36 | fun put(url: String, headers: Map = emptyMap(), data: Any): String 37 | 38 | /** 39 | * Executes a HTTP GET request. 40 | * 41 | * @param[url] The path that will be added to the base uri 42 | * 43 | * @param[headers] The custom headers that will be added to the default headers. 44 | * Custom headers replace default headers if duplicate 45 | * 46 | * @return The response body as [String] 47 | */ 48 | fun get(url: String, headers: Map = emptyMap()): String 49 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/supabase/gotrue/CustomGoTrueClientIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer 4 | import com.github.tomakehurst.wiremock.client.WireMock.* 5 | import io.supabase.gotrue.types.CustomGoTrueUserResponse 6 | import io.supabase.gotrue.types.GoTrueTokenResponse 7 | import org.junit.jupiter.api.AfterEach 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | 11 | internal class CustomGoTrueClientIntegrationTest { 12 | 13 | private var wireMockServer: WireMockServer = WireMockServer(0) 14 | 15 | private var goTrueClient: GoTrueClient? = null 16 | 17 | @BeforeEach 18 | fun proxyToWireMock() { 19 | wireMockServer.start() 20 | goTrueClient = GoTrueClient.customApacheJacksonGoTrueClient( 21 | url = "http://localhost:${wireMockServer.port()}", 22 | headers = emptyMap() 23 | ) 24 | } 25 | 26 | @AfterEach 27 | fun noMoreWireMock() { 28 | wireMockServer.stop() 29 | wireMockServer.resetAll() 30 | } 31 | 32 | @Test 33 | fun `should sign up when email confirmation disabled`() { 34 | wireMockServer.stubFor( 35 | post("/signup") 36 | .withRequestBody( 37 | equalToJson( 38 | """{ 39 | "email": "foo@bar.de", 40 | "password": "foobar" 41 | } 42 | """ 43 | ) 44 | ) 45 | .willReturn( 46 | aResponse() 47 | .withStatus(200) 48 | .withBody(fixture("/fixtures/user-response-email-disabled.json")) 49 | ) 50 | ) 51 | 52 | val response = goTrueClient!!.signUpWithEmail( 53 | email = "foo@bar.de", 54 | password = "foobar" 55 | ) 56 | println(response) 57 | } 58 | 59 | private fun fixture(path: String): String { 60 | return CustomGoTrueClientIntegrationTest::class.java.getResource(path).readText() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Client for GoTrue 2 | 3 | > **Warning** 4 | > This repository is archived. Use [supabase-kt](https://github.com/supabase-community/supabase-kt) instead to use Supabase in your Kotlin projects. 5 | 6 | 7 | Kotlin JVM client for [Netlify's GoTrue API](https://github.com/netlify/gotrue). 8 | 9 | Comes with DTOs for the responses to enable type-safe access. 10 | 11 | ![Java CI with Gradle](https://img.shields.io/github/workflow/status/supabase/gotrue-kt/Java%20CI%20with%20Gradle?label=BUILD&style=for-the-badge) 12 | ![Gradle Package](https://img.shields.io/github/workflow/status/supabase/gotrue-kt/Gradle%20Package?label=PUBLISH&style=for-the-badge) 13 | ![Bintray](https://img.shields.io/bintray/v/supabase/supabase/gotrue-kt?style=for-the-badge) 14 | 15 | ## Installation 16 | 17 | Maven 18 | 19 | ```xml 20 | 21 | io.supabase 22 | gotrue-kt 23 | {version} 24 | pom 25 | 26 | ``` 27 | 28 | Gradle 29 | 30 | ```groovy 31 | implementation 'io.supabase:gotrue-kt:{version}' 32 | ``` 33 | 34 | ## Usage 35 | 36 | ```kotlin 37 | val goTrueClient = GoTrueClient.defaultGoTrueClient( 38 | url = "", 39 | headers = mapOf("Authorization" to "foo", "apiKey" to "bar") 40 | ) 41 | 42 | try { 43 | goTrueClient.invite("e@ma.il") 44 | 45 | val updatedUser = goTrueClient.updateUser( 46 | accessToken = "eyJ...", // read from request header 47 | data = mapOf( 48 | "admin" = true 49 | ) 50 | ) 51 | 52 | println(updatedUser.updatedAt) 53 | } catch (exc: GoTrueHttpException) { 54 | // Exception is thrown on bad status (anything above 300) 55 | println("Oops, status: ${exc.status}, body:\n${exc.httpBody}") 56 | } 57 | ``` 58 | 59 | You can also customize the DTO for example if you turn off email verification 60 | 61 | ```kotlin 62 | data class CustomGoTrueUserResponse( 63 | val accessToken: String, 64 | val tokenType: String, 65 | val refreshToken: String, 66 | val user: User 67 | ) 68 | 69 | data class User( 70 | val id: UUID, 71 | val email: String, 72 | val phone: String 73 | 74 | ) 75 | 76 | GoTrueClient.customApacheJacksonGoTrueClient( 77 | url = "", 78 | headers = mapOf("Authorization" to "foo", "apiKey" to "bar") 79 | ) 80 | ``` 81 | 82 | If you are using [supabase](https://supabase.io/), the base URL will be `https://.supabase.co/auth/v1` 83 | 84 | ## HTTP / (De)-Serialization 85 | 86 | The Apache Http-Client (5.x) is used for executing HTTP calls, Jackson is used to convert responses to DTOs. 87 | 88 | If you want to change that, you need to implement the `GoTrueHttpClient` and the `GoTrueJsonConverter` interface. 89 | 90 | See [GoTrueHttpClientApache](src/main/kotlin/io/supabase/gotrue/http/GoTrueHttpClientApache.kt) and [GoTrueJsonConverterJackson](src/main/kotlin/io/supabase/gotrue/json/GoTrueJsonConverterJackson.kt). 91 | 92 | ```kotlin 93 | GoTrueClient.goTrueClient( 94 | goTrueHttpClient = { customHttpClient() }, 95 | goTrueJsonConverter = customConverter() 96 | ) 97 | ``` 98 | -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/http/GoTrueHttpClientApache.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.http 2 | 3 | import io.supabase.gotrue.json.GoTrueJsonConverter 4 | import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase 5 | import org.apache.hc.client5.http.impl.classic.CloseableHttpClient 6 | import org.apache.hc.core5.http.ClassicHttpResponse 7 | import org.apache.hc.core5.http.HttpStatus 8 | import org.apache.hc.core5.http.Method 9 | import org.apache.hc.core5.http.io.HttpClientResponseHandler 10 | import org.apache.hc.core5.http.io.entity.EntityUtils 11 | import org.apache.hc.core5.http.io.entity.StringEntity 12 | import java.net.URI 13 | 14 | /** 15 | * Default implementation of the [GoTrueHttpClient] used by the GoTrueDefaultClient. 16 | * 17 | * Uses closable apache HTTP-Client 5.x. 18 | */ 19 | class GoTrueHttpClientApache( 20 | private val url: String, 21 | private val headers: Map, 22 | private val httpClient: () -> CloseableHttpClient, 23 | private val goTrueJsonConverter: GoTrueJsonConverter 24 | ): GoTrueHttpClient { 25 | 26 | override fun post(url: String, headers: Map, data: Any?): String? { 27 | return execute( 28 | method = Method.POST, 29 | path = url, 30 | headers = headers, 31 | data = data 32 | ) 33 | } 34 | 35 | override fun put(url: String, headers: Map, data: Any): String { 36 | return execute( 37 | method = Method.PUT, 38 | path = url, 39 | headers = headers, 40 | data = data 41 | )!! 42 | } 43 | 44 | override fun get(url: String, headers: Map): String { 45 | return execute( 46 | method = Method.GET, 47 | path = url, 48 | headers = headers 49 | )!! 50 | } 51 | 52 | private fun execute(method: Method, path: String, data: Any? = null, headers: Map = emptyMap()): String? { 53 | return httpClient().use { httpClient -> 54 | val httpRequest = HttpUriRequestBase(method.name, URI(url + path)) 55 | data?.apply { 56 | val dataAsString = goTrueJsonConverter.serialize(data) 57 | httpRequest.entity = StringEntity(dataAsString) 58 | } 59 | val allHeaders = this.headers.filter { !headers.containsKey(it.key) } + headers 60 | allHeaders.forEach { (name, value) -> httpRequest.addHeader(name, value) } 61 | 62 | return@use httpClient.execute(httpRequest, responseHandler()) 63 | } 64 | } 65 | 66 | private fun responseHandler(): HttpClientResponseHandler { 67 | return HttpClientResponseHandler { response -> 68 | throwIfError(response) 69 | 70 | return@HttpClientResponseHandler response.entity?.let { EntityUtils.toString(it) } 71 | } 72 | } 73 | 74 | private fun throwIfError(response: ClassicHttpResponse) { 75 | val status = response.code 76 | val statusSuccessful = status >= HttpStatus.SC_SUCCESS && status < HttpStatus.SC_REDIRECTION 77 | 78 | if (!statusSuccessful) { 79 | val entityAsString = response.entity?.let { EntityUtils.toString(it) } 80 | 81 | throw GoTrueHttpException(status, entityAsString) 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/supabase/gotrue/http/GoTrueHttpClientApacheTest.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue.http 2 | 3 | import assertk.assertAll 4 | import assertk.assertThat 5 | import assertk.assertions.hasSize 6 | import assertk.assertions.isEqualTo 7 | import io.mockk.CapturingSlot 8 | import io.mockk.every 9 | import io.mockk.mockk 10 | import io.mockk.slot 11 | import io.supabase.gotrue.json.GoTrueJsonConverterJackson 12 | import org.apache.hc.client5.http.impl.classic.CloseableHttpClient 13 | import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse 14 | import org.apache.hc.core5.http.ClassicHttpRequest 15 | import org.apache.hc.core5.http.HttpHeaders 16 | import org.apache.hc.core5.http.io.HttpClientResponseHandler 17 | import org.apache.hc.core5.http.io.entity.StringEntity 18 | import org.junit.jupiter.api.Nested 19 | import org.junit.jupiter.api.TestInstance 20 | import org.junit.jupiter.api.assertThrows 21 | import org.junit.jupiter.params.ParameterizedTest 22 | import org.junit.jupiter.params.provider.MethodSource 23 | import java.util.stream.Stream 24 | 25 | internal class GoTrueHttpClientApacheTest { 26 | 27 | private val url = "https://test.com" 28 | private val httpClientMock = mockk() 29 | private val headers = mapOf( 30 | HttpHeaders.AUTHORIZATION to "Bearer foobar" 31 | ) 32 | 33 | private val goTrueHttpClient = GoTrueHttpClientApache( 34 | url = url, 35 | headers = headers, 36 | httpClient = { httpClientMock }, 37 | goTrueJsonConverter = GoTrueJsonConverterJackson() 38 | ) 39 | 40 | init { 41 | every { httpClientMock.close() }.returns(Unit) 42 | } 43 | 44 | @Nested 45 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 46 | inner class SetHeaders { 47 | 48 | @ParameterizedTest 49 | @MethodSource("headersTestData") 50 | fun `should be able to replace default header`(testData: HeadersTestData) { 51 | val httpResponse = mockk() 52 | every { httpResponse.code } returns 200 53 | every { httpResponse.entity } returns null 54 | 55 | val requestCapture = mockHttpCallWithGetRequest(httpResponse) 56 | 57 | goTrueHttpClient.post( 58 | url = "/anywhere", 59 | headers = testData.customHeaders 60 | ) 61 | 62 | val request = requestCapture.captured 63 | 64 | assertAll { 65 | assertThat(request.headers).hasSize(testData.expectedRequestHeaders.size) 66 | testData.expectedRequestHeaders.forEach { (name, value) -> 67 | assertThat(request.getHeader(name).value).isEqualTo(value) 68 | } 69 | } 70 | } 71 | 72 | @Suppress("unused") 73 | fun headersTestData(): Stream { 74 | return Stream.of( 75 | HeadersTestData( 76 | customHeaders = emptyMap(), 77 | expectedRequestHeaders = headers 78 | ), 79 | HeadersTestData( 80 | customHeaders = mapOf(HttpHeaders.AUTHORIZATION to "something else"), 81 | expectedRequestHeaders = mapOf(HttpHeaders.AUTHORIZATION to "something else") 82 | ), 83 | HeadersTestData( 84 | customHeaders = mapOf("foo" to "bar"), 85 | expectedRequestHeaders = mapOf( 86 | HttpHeaders.AUTHORIZATION to "Bearer foobar", 87 | "foo" to "bar" 88 | ) 89 | ) 90 | ) 91 | } 92 | } 93 | 94 | data class HeadersTestData(val customHeaders: Map, val expectedRequestHeaders: Map) 95 | 96 | @Nested 97 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 98 | inner class ThrowHttpException { 99 | 100 | @ParameterizedTest 101 | @MethodSource("exceptionTestData") 102 | fun `should throw http exception when status is above 300`(testData: ExceptionTestData) { 103 | val httpResponse = mockk() 104 | val responseCode = testData.status 105 | val httpBody = testData.body 106 | 107 | every { httpResponse.code } returns responseCode 108 | every { httpResponse.entity } returns httpBody?.let { StringEntity(it) } 109 | 110 | mockHttpCallWithGetRequest(httpResponse) 111 | 112 | val exception = assertThrows { 113 | goTrueHttpClient.get( 114 | url = "/anywhere" 115 | ) 116 | } 117 | 118 | assertThat(exception.status).isEqualTo(responseCode) 119 | assertThat(exception.data).isEqualTo(httpBody) 120 | } 121 | 122 | @Suppress("unused") 123 | private fun exceptionTestData(): Stream { 124 | return Stream.of( 125 | ExceptionTestData(301, "httpbody"), 126 | ExceptionTestData(301, null), 127 | ExceptionTestData(400, "httpbody") 128 | ) 129 | } 130 | } 131 | 132 | data class ExceptionTestData( 133 | val status: Int, 134 | val body: String? 135 | ) 136 | 137 | private fun mockHttpCallWithGetRequest(httpResponse: CloseableHttpResponse): CapturingSlot { 138 | val slot = slot() 139 | every { httpClientMock.execute(capture(slot), any>()) }.answers { 140 | val handler = args[1] as HttpClientResponseHandler<*> 141 | handler.handleResponse(httpResponse) 142 | } 143 | 144 | return slot 145 | } 146 | 147 | } -------------------------------------------------------------------------------- /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 | 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 | -------------------------------------------------------------------------------- /src/test/kotlin/io/supabase/gotrue/GoTrueClientIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer 4 | import com.github.tomakehurst.wiremock.client.WireMock.* 5 | import io.supabase.gotrue.types.GoTrueTokenResponse 6 | import io.supabase.gotrue.types.GoTrueUserAttributes 7 | import io.supabase.gotrue.types.GoTrueUserResponse 8 | import io.supabase.gotrue.types.GoTrueVerifyType 9 | import org.apache.hc.core5.http.HttpHeaders 10 | import org.junit.jupiter.api.AfterEach 11 | import org.junit.jupiter.api.BeforeEach 12 | import org.junit.jupiter.api.Test 13 | 14 | internal class GoTrueClientIntegrationTest { 15 | 16 | private var wireMockServer: WireMockServer = WireMockServer(0) 17 | 18 | private var goTrueClient: GoTrueClient? = null 19 | 20 | @BeforeEach 21 | fun proxyToWireMock() { 22 | wireMockServer.start() 23 | goTrueClient = GoTrueClient.defaultGoTrueClient( 24 | url = "http://localhost:${wireMockServer.port()}", 25 | headers = emptyMap() 26 | ) 27 | } 28 | 29 | @AfterEach 30 | fun noMoreWireMock() { 31 | wireMockServer.stop() 32 | wireMockServer.resetAll() 33 | } 34 | 35 | @Test 36 | fun `should get settings`() { 37 | wireMockServer.stubFor( 38 | get("/settings").willReturn( 39 | aResponse() 40 | .withStatus(200) 41 | .withBody(fixture("/fixtures/settings-response.json")) 42 | ) 43 | ) 44 | 45 | goTrueClient!!.settings() 46 | } 47 | 48 | @Test 49 | fun `should sign up`() { 50 | wireMockServer.stubFor( 51 | post("/signup") 52 | .withRequestBody( 53 | equalToJson( 54 | """{ 55 | "email": "foo@bar.de", 56 | "password": "foobar" 57 | } 58 | """ 59 | ) 60 | ) 61 | .willReturn( 62 | aResponse() 63 | .withStatus(200) 64 | .withBody(fixture("/fixtures/user-response.json")) 65 | ) 66 | ) 67 | 68 | goTrueClient!!.signUpWithEmail( 69 | email = "foo@bar.de", 70 | password = "foobar" 71 | ) 72 | } 73 | 74 | @Test 75 | fun `should invite`() { 76 | wireMockServer.stubFor( 77 | post("/invite") 78 | .withRequestBody(equalToJson("""{"email": "foo@bar.de"}""")) 79 | .willReturn( 80 | aResponse() 81 | .withStatus(200) 82 | .withBody(fixture("/fixtures/user-response.json")) 83 | ) 84 | ) 85 | 86 | goTrueClient!!.inviteUserByEmail("foo@bar.de") 87 | } 88 | 89 | @Test 90 | fun `should verify`() { 91 | wireMockServer.stubFor( 92 | post("/verify") 93 | .withRequestBody( 94 | equalToJson( 95 | """{ 96 | "type": "recovery", 97 | "token": "123" 98 | } 99 | """ 100 | ) 101 | ) 102 | .willReturn( 103 | aResponse() 104 | .withStatus(200) 105 | .withBody(fixture("/fixtures/token-response.json")) 106 | ) 107 | ) 108 | 109 | goTrueClient!!.verify( 110 | type = GoTrueVerifyType.RECOVERY, 111 | token = "123" 112 | ) 113 | } 114 | 115 | @Test 116 | fun `should recover`() { 117 | wireMockServer.stubFor( 118 | post("/recover") 119 | .withRequestBody(equalToJson("""{"email": "foo@bar.de"}""")) 120 | .willReturn( 121 | aResponse() 122 | .withStatus(200) 123 | ) 124 | ) 125 | 126 | goTrueClient!!.resetPasswordForEmail("foo@bar.de") 127 | } 128 | 129 | @Test 130 | fun `should update user`() { 131 | wireMockServer.stubFor( 132 | put("/user") 133 | .withRequestBody( 134 | equalToJson( 135 | """{ 136 | "data": { 137 | "admin": true 138 | } 139 | } 140 | """ 141 | ) 142 | ) 143 | .withHeader(HttpHeaders.AUTHORIZATION, matching("Bearer token")) 144 | .willReturn( 145 | aResponse() 146 | .withStatus(200) 147 | .withBody(fixture("/fixtures/user-response.json")) 148 | ) 149 | ) 150 | 151 | goTrueClient!!.updateUser(jwt = "token", attributes = GoTrueUserAttributes(data = mapOf("admin" to true))) 152 | } 153 | 154 | @Test 155 | fun `should get user`() { 156 | wireMockServer.stubFor( 157 | get("/user") 158 | .withHeader(HttpHeaders.AUTHORIZATION, matching("Bearer token")) 159 | .willReturn( 160 | aResponse() 161 | .withStatus(200) 162 | .withBody(fixture("/fixtures/user-response.json")) 163 | ) 164 | ) 165 | 166 | goTrueClient!!.getUser("token") 167 | } 168 | 169 | @Test 170 | fun `should refresh access token`() { 171 | wireMockServer.stubFor( 172 | post("/token?grant_type=refresh_token") 173 | .withRequestBody( 174 | equalToJson( 175 | """{ 176 | "refresh_token": "refreshToken" 177 | } 178 | """ 179 | ) 180 | ) 181 | .willReturn( 182 | aResponse() 183 | .withStatus(200) 184 | .withBody(fixture("/fixtures/token-response.json")) 185 | ) 186 | ) 187 | 188 | goTrueClient!!.refreshAccessToken("refreshToken") 189 | } 190 | 191 | @Test 192 | fun `should issue token with email and password`() { 193 | wireMockServer.stubFor( 194 | post("/token?grant_type=password") 195 | .withRequestBody( 196 | equalToJson( 197 | """{ 198 | "email": "foo@bar.de", 199 | "password": "pw" 200 | } 201 | """ 202 | ) 203 | ) 204 | .willReturn( 205 | aResponse() 206 | .withStatus(200) 207 | .withBody(fixture("/fixtures/token-response.json")) 208 | ) 209 | ) 210 | 211 | goTrueClient!!.signInWithEmail("foo@bar.de", "pw") 212 | } 213 | 214 | @Test 215 | fun `should sign out user`() { 216 | wireMockServer.stubFor( 217 | post("/logout") 218 | .withHeader(HttpHeaders.AUTHORIZATION, matching("Bearer token")) 219 | .willReturn( 220 | aResponse() 221 | .withStatus(200) 222 | ) 223 | ) 224 | 225 | goTrueClient!!.signOut("token") 226 | } 227 | 228 | @Test 229 | fun `should send magic link`() { 230 | wireMockServer.stubFor( 231 | post("/magiclink") 232 | .withRequestBody(equalToJson("""{"email": "foo@bar.de"}""")) 233 | .willReturn( 234 | aResponse() 235 | .withStatus(200) 236 | ) 237 | ) 238 | 239 | goTrueClient!!.sendMagicLinkEmail("foo@bar.de") 240 | } 241 | 242 | private fun fixture(path: String): String { 243 | return GoTrueClientIntegrationTest::class.java.getResource(path).readText() 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/main/kotlin/io/supabase/gotrue/GoTrueClient.kt: -------------------------------------------------------------------------------- 1 | package io.supabase.gotrue 2 | 3 | import io.supabase.gotrue.http.GoTrueHttpClient 4 | import io.supabase.gotrue.http.GoTrueHttpClientApache 5 | import io.supabase.gotrue.json.GoTrueJsonConverter 6 | import io.supabase.gotrue.json.GoTrueJsonConverterJackson 7 | import io.supabase.gotrue.types.* 8 | import org.apache.hc.client5.http.impl.classic.HttpClients 9 | import java.util.Locale 10 | 11 | open class GoTrueClient( 12 | val goTrueHttpClient: GoTrueHttpClient, 13 | val goTrueJsonConverter: GoTrueJsonConverter, 14 | private val goTrueUserResponseClass: Class, 15 | private val goTrueTokenResponseClass: Class 16 | ) { 17 | companion object { 18 | 19 | inline fun goTrueClient( 20 | goTrueHttpClient: GoTrueHttpClient, 21 | goTrueJsonConverter: GoTrueJsonConverter 22 | ): GoTrueClient = 23 | GoTrueClient(goTrueHttpClient, goTrueJsonConverter, UserResponseClass::class.java, TokenResponseClass::class.java) 24 | 25 | inline fun customApacheJacksonGoTrueClient( 26 | url: String, 27 | headers: Map 28 | ): GoTrueClient = 29 | GoTrueClient( 30 | goTrueHttpClient = GoTrueHttpClientApache( 31 | url = url, 32 | headers = headers, 33 | httpClient = { HttpClients.createDefault() }, 34 | goTrueJsonConverter = GoTrueJsonConverterJackson() 35 | ), 36 | goTrueJsonConverter = GoTrueJsonConverterJackson(), 37 | UserResponseClass::class.java, 38 | TokenResponseClass::class.java 39 | ) 40 | fun defaultGoTrueClient( 41 | url: String, 42 | headers: Map 43 | ): GoTrueClient = 44 | GoTrueClient( 45 | goTrueHttpClient = GoTrueHttpClientApache( 46 | url = url, 47 | headers = headers, 48 | httpClient = { HttpClients.createDefault() }, 49 | goTrueJsonConverter = GoTrueJsonConverterJackson() 50 | ), 51 | goTrueJsonConverter = GoTrueJsonConverterJackson(), 52 | GoTrueUserResponse::class.java, 53 | GoTrueTokenResponse::class.java 54 | ) 55 | } 56 | 57 | /** 58 | * @return the publicly available settings for this GoTrue instance. 59 | */ 60 | fun settings(): GoTrueSettings { 61 | val response = goTrueHttpClient.get( 62 | url = "/settings" 63 | ) 64 | 65 | return goTrueJsonConverter.deserialize(response, GoTrueSettings::class.java) 66 | } 67 | 68 | /** 69 | * Creates a new user using their [email] address. 70 | * 71 | * @param[email] The email address of the user. 72 | * @param[password] The password of the user. 73 | */ 74 | fun signUpWithEmail(email: String, password: String): UserResponseClass { 75 | val response = goTrueHttpClient.post( 76 | url = "/signup", 77 | data = mapOf("email" to email, "password" to password) 78 | )!! 79 | 80 | return goTrueJsonConverter.deserialize(response, goTrueUserResponseClass) 81 | } 82 | 83 | /** 84 | * Sends an invite link to an [email] address. 85 | * 86 | * @param[email] The email address of the user. 87 | */ 88 | fun inviteUserByEmail(email: String): UserResponseClass { 89 | val response = goTrueHttpClient.post( 90 | url = "/invite", 91 | data = mapOf("email" to email) 92 | )!! 93 | 94 | return goTrueJsonConverter.deserialize(response, goTrueUserResponseClass) 95 | } 96 | 97 | /** 98 | * Verify a registration or a password recovery. 99 | * Type can be signup or recovery and the token is a token returned from either /signup or /recover. 100 | */ 101 | fun verify(type: GoTrueVerifyType, token: String, password: String? = null): TokenResponseClass { 102 | val response = goTrueHttpClient.post( 103 | url = "/verify", 104 | data = mapOf("type" to type.name.lowercase(Locale.getDefault()), "token" to token, "password" to password), 105 | )!! 106 | 107 | return goTrueJsonConverter.deserialize(response, goTrueTokenResponseClass) 108 | } 109 | 110 | /** 111 | * Sends a reset request to an [email] address. 112 | * 113 | * ### Notes 114 | * 115 | * Sends a reset request to an email address. 116 | * 117 | * When the user clicks the reset link in the email they will be forwarded to: 118 | * *#access_token=x&refresh_token=y&expires_in=z&token_type=bearer&type=recovery* 119 | * 120 | * Your app must detect type=recovery in the fragment and display a password reset form to the user. 121 | * You should then use the access_token in the url and new password to update the using: 122 | * 123 | * See [updateUser], example usage: 124 | * 125 | * ``` 126 | * goTrueClient.updateUser( 127 | * jwt = accessToken, 128 | * attributes = GoTrueUserAttributes( 129 | * password = "newPassword" 130 | * ) 131 | * ) 132 | * ``` 133 | * 134 | * @param[email] The email address of the user. 135 | */ 136 | fun resetPasswordForEmail(email: String) { 137 | goTrueHttpClient.post( 138 | url = "/recover", 139 | data = mapOf("email" to email) 140 | ) 141 | } 142 | 143 | /** 144 | * Updates the user data. 145 | * Apart from changing email/password, this method can be used to set custom user data. 146 | * 147 | * @param[jwt] A valid, logged-in JWT. 148 | * @param[attributes] Custom user attributes you want to update 149 | */ 150 | fun updateUser(jwt: String, attributes: GoTrueUserAttributes): UserResponseClass { 151 | val response = goTrueHttpClient.put( 152 | url = "/user", 153 | headers = mapOf("Authorization" to "Bearer $jwt"), 154 | data = mapOf( 155 | "email" to attributes.email, 156 | "password" to attributes.password, 157 | "data" to attributes.data 158 | ) 159 | ) 160 | 161 | return goTrueJsonConverter.deserialize(response, goTrueUserResponseClass) 162 | } 163 | 164 | /** 165 | * Gets the user details. 166 | * 167 | * @param[jwt] A valid, logged-in JWT. 168 | */ 169 | fun getUser(jwt: String): UserResponseClass { 170 | val response = goTrueHttpClient.get( 171 | url = "/user", 172 | headers = mapOf("Authorization" to "Bearer $jwt") 173 | ) 174 | 175 | return goTrueJsonConverter.deserialize(response, goTrueUserResponseClass) 176 | } 177 | 178 | /** 179 | * Logs in an existing user using their [email] address. 180 | * 181 | * @param[email] The email address of the user. 182 | * @param[password] The password of the user. 183 | */ 184 | fun signInWithEmail(email: String, password: String): TokenResponseClass { 185 | val response = goTrueHttpClient.post( 186 | url = "/token?grant_type=password", 187 | data = mapOf("email" to email, "password" to password), 188 | )!! 189 | 190 | return goTrueJsonConverter.deserialize(response, goTrueTokenResponseClass) 191 | } 192 | 193 | /** 194 | * Generates a new JWT. 195 | * 196 | * @param[refreshToken] A valid refresh token that was returned on login. 197 | */ 198 | fun refreshAccessToken(refreshToken: String): TokenResponseClass { 199 | val response = goTrueHttpClient.post( 200 | url = "/token?grant_type=refresh_token", 201 | data = mapOf("refresh_token" to refreshToken), 202 | )!! 203 | 204 | return goTrueJsonConverter.deserialize(response, goTrueTokenResponseClass) 205 | } 206 | 207 | /** 208 | * Removes a logged-in session. 209 | * 210 | * This will revoke all refresh tokens for the user. 211 | * Remember that the JWT tokens will still be valid for stateless auth until they expire. 212 | * 213 | * @param[jwt] A valid, logged-in JWT. 214 | */ 215 | fun signOut(jwt: String) { 216 | goTrueHttpClient.post( 217 | url = "/logout", 218 | headers = mapOf("Authorization" to "Bearer $jwt") 219 | ) 220 | } 221 | 222 | /** 223 | * Sends a magic login (passwordless) link to an [email] address. 224 | * 225 | * @param[email] The email address of the user. 226 | */ 227 | fun sendMagicLinkEmail(email: String) { 228 | goTrueHttpClient.post( 229 | url = "/magiclink", 230 | data = mapOf("email" to email) 231 | ) 232 | } 233 | } 234 | --------------------------------------------------------------------------------