diff --git a/src/main/java/android/net/Uri.kt b/src/main/java/android/net/Uri.kt index 851faa2..c14bc76 100644 --- a/src/main/java/android/net/Uri.kt +++ b/src/main/java/android/net/Uri.kt @@ -3,13 +3,16 @@ package android.net import java.net.URI import java.util.Collections -class Uri(private val uri: URI) { - +class Uri( + private val uri: URI, +) { companion object { @JvmStatic fun parse(uriString: String) = Uri(URI.create(uriString)) } + override fun toString(): String = uri.toString() + val scheme get() = uri.scheme val port get() = uri.port val host get() = uri.host diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt index ee3abd0..484afc6 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt @@ -1,5 +1,6 @@ package com.google.firebase.auth +import android.net.Uri import android.util.Log import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.TaskCompletionSource @@ -41,19 +42,17 @@ import java.util.Base64 import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit -val jsonParser = Json { ignoreUnknownKeys = true } +internal val jsonParser = Json { ignoreUnknownKeys = true } class UrlFactory( private val app: FirebaseApp, - private val emulatorUrl: String? = null + private val emulatorUrl: String? = null, ) { - fun buildUrl(uri: String): String { - return "${emulatorUrl ?: "https://"}$uri?key=${app.options.apiKey}" - } + fun buildUrl(uri: String): String = "${emulatorUrl ?: "https://"}$uri?key=${app.options.apiKey}" } @Serializable -class FirebaseUserImpl private constructor( +class FirebaseUserImpl internal constructor( @Transient private val app: FirebaseApp = FirebaseApp.getInstance(), override val isAnonymous: Boolean, @@ -62,22 +61,38 @@ class FirebaseUserImpl private constructor( val refreshToken: String, val expiresIn: Int, val createdAt: Long, + override val email: String?, + override val photoUrl: String?, + override val displayName: String?, @Transient - private val urlFactory: UrlFactory = UrlFactory(app) + private val urlFactory: UrlFactory = UrlFactory(app), ) : FirebaseUser() { - - constructor(app: FirebaseApp, data: JsonObject, isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false, urlFactory: UrlFactory = UrlFactory(app)) : this( - app, - isAnonymous, - data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull ?: data["localId"]?.jsonPrimitive?.contentOrNull ?: "", - data["idToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("id_token").jsonPrimitive.content, - data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content, - data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int, - data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis(), - urlFactory + constructor( + app: FirebaseApp, + data: JsonObject, + isAnonymous: Boolean = data["isAnonymous"]?.jsonPrimitive?.booleanOrNull ?: false, + email: String? = data.getOrElse("email") { null }?.jsonPrimitive?.contentOrNull, + photoUrl: String? = data.getOrElse("photoUrl") { null }?.jsonPrimitive?.contentOrNull, + displayName: String? = data.getOrElse("displayName") { null }?.jsonPrimitive?.contentOrNull, + urlFactory: UrlFactory = UrlFactory(app), + ) : this( + app = app, + isAnonymous = isAnonymous, + uid = + data["uid"]?.jsonPrimitive?.contentOrNull ?: data["user_id"]?.jsonPrimitive?.contentOrNull + ?: data["localId"]?.jsonPrimitive?.contentOrNull + ?: "", + idToken = data["idToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("id_token").jsonPrimitive.content, + refreshToken = data["refreshToken"]?.jsonPrimitive?.contentOrNull ?: data.getValue("refresh_token").jsonPrimitive.content, + expiresIn = data["expiresIn"]?.jsonPrimitive?.intOrNull ?: data.getValue("expires_in").jsonPrimitive.int, + createdAt = data["createdAt"]?.jsonPrimitive?.longOrNull ?: System.currentTimeMillis(), + email = email, + photoUrl = photoUrl ?: data["photo_url"]?.jsonPrimitive?.contentOrNull, + displayName = displayName ?: data["display_name"]?.jsonPrimitive?.contentOrNull, + urlFactory = urlFactory, ) - val claims: Map by lazy { + internal val claims: Map by lazy { jsonParser .parseToJsonElement(String(Base64.getUrlDecoder().decode(idToken.split(".")[1]))) .jsonObject @@ -85,71 +100,207 @@ class FirebaseUserImpl private constructor( .orEmpty() } - val JsonElement.value get(): Any? = when (this) { - is JsonNull -> null - is JsonArray -> map { it.value } - is JsonObject -> jsonObject.mapValues { (_, it) -> it.value } - is JsonPrimitive -> booleanOrNull ?: doubleOrNull ?: content - else -> TODO() - } + internal val JsonElement.value get(): Any? = + when (this) { + is JsonNull -> null + is JsonArray -> map { it.value } + is JsonObject -> jsonObject.mapValues { (_, it) -> it.value } + is JsonPrimitive -> booleanOrNull ?: doubleOrNull ?: content + else -> TODO() + } override fun delete(): Task { val source = TaskCompletionSource() val body = RequestBody.create(FirebaseAuth.getInstance(app).json, JsonObject(mapOf("idToken" to JsonPrimitive(idToken))).toString()) - val request = Request.Builder() - .url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount")) - .post(body) - .build() - FirebaseAuth.getInstance(app).client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } + val request = + Request + .Builder() + .url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/deleteAccount")) + .post(body) + .build() + FirebaseAuth.getInstance(app).client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - FirebaseAuth.getInstance(app).signOut() - source.setException( - FirebaseAuth.getInstance(app).createAuthInvalidUserException( - "deleteAccount", - request, - response + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + FirebaseAuth.getInstance(app).signOut() + source.setException( + FirebaseAuth.getInstance(app).createAuthInvalidUserException( + "deleteAccount", + request, + response, + ), ) - ) - } else { - source.setResult(null) + } else { + source.setResult(null) + } } - } - }) + }, + ) return source.task } + override fun updateEmail(email: String): Task = FirebaseAuth.getInstance(app).updateEmail(email) + override fun reload(): Task { val source = TaskCompletionSource() FirebaseAuth.getInstance(app).refreshToken(this, source) { null } return source.task } + // TODO implement ActionCodeSettings and pass it to the url + override fun verifyBeforeUpdateEmail( + newEmail: String, + actionCodeSettings: ActionCodeSettings?, + ): Task { + val source = TaskCompletionSource() + val body = + RequestBody.create( + FirebaseAuth.getInstance(app).json, + JsonObject( + mapOf( + "idToken" to JsonPrimitive(idToken), + "email" to JsonPrimitive(email), + "newEmail" to JsonPrimitive(newEmail), + "requestType" to JsonPrimitive(OobRequestType.VERIFY_AND_CHANGE_EMAIL.name), + ), + ).toString(), + ) + val request = + Request + .Builder() + .url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:sendOobCode")) + .post(body) + .build() + FirebaseAuth.getInstance(app).client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + e.printStackTrace() + } + + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + FirebaseAuth.getInstance(app).signOut() + source.setException( + FirebaseAuth.getInstance(app).createAuthInvalidUserException( + "verifyEmail", + request, + response, + ), + ) + } else { + source.setResult(null) + } + } + }, + ) + return source.task + } + override fun getIdToken(forceRefresh: Boolean) = FirebaseAuth.getInstance(app).getAccessToken(forceRefresh) + + override fun updateProfile(request: UserProfileChangeRequest): Task = FirebaseAuth.getInstance(app).updateProfile(request) + + fun updateProfile( + displayName: String?, + photoUrl: String?, + ): Task { + val request = + UserProfileChangeRequest + .Builder() + .apply { setDisplayName(displayName) } + .apply { setPhotoUri(photoUrl?.let { Uri.parse(it) }) } + .build() + return FirebaseAuth.getInstance(app).updateProfile(request) + } } -class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { +class FirebaseAuth constructor( + val app: FirebaseApp, +) : InternalAuthProvider { + internal val json = MediaType.parse("application/json; charset=utf-8") + internal val client: OkHttpClient = + OkHttpClient + .Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .build() - val json = MediaType.parse("application/json; charset=utf-8") - val client: OkHttpClient = OkHttpClient.Builder() - .connectTimeout(60, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .writeTimeout(60, TimeUnit.SECONDS) - .build() + private fun enqueueAuthPost( + url: String, + body: RequestBody, + setResult: (responseBody: String) -> FirebaseUserImpl?, + ): TaskCompletionSource { + val source = TaskCompletionSource() + val request = + Request + .Builder() + .url(urlFactory.buildUrl(url)) + .post(body) + .build() + + client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } - companion object { + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + source.setException( + createAuthInvalidUserException("accounts", request, response), + ) + } else { + if (response.body()?.use { it.string() }?.also { responseBody -> + user = setResult(responseBody) + source.setResult(AuthResult { user }) + } == null + ) { + source.setException( + createAuthInvalidUserException("accounts", request, response), + ) + } + } + } + }, + ) + return source + } + companion object { @JvmStatic fun getInstance(): FirebaseAuth = getInstance(FirebaseApp.getInstance()) @JvmStatic fun getInstance(app: FirebaseApp): FirebaseAuth = app.get(FirebaseAuth::class.java) + + private const val REFRESH_TOKEN_TAG = "refresh_token_tag" } private val internalIdTokenListeners = CopyOnWriteArrayList() @@ -161,10 +312,11 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { val FirebaseApp.key get() = "com.google.firebase.auth.FIREBASE_USER${"[$name]".takeUnless { isDefaultApp }.orEmpty()}" - private var user: FirebaseUserImpl? = FirebasePlatform.firebasePlatform - .runCatching { retrieve(app.key)?.let { FirebaseUserImpl(app, jsonParser.parseToJsonElement(it).jsonObject) } } - .onFailure { it.printStackTrace() } - .getOrNull() + private var user: FirebaseUserImpl? = + FirebasePlatform.firebasePlatform + .runCatching { retrieve(app.key)?.let { FirebaseUserImpl(app, data = jsonParser.parseToJsonElement(it).jsonObject) } } + .onFailure { it.printStackTrace() } + .getOrNull() private set(value) { if (field != value) { @@ -199,119 +351,186 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { private var urlFactory = UrlFactory(app) fun signInAnonymously(): Task { - val source = TaskCompletionSource() - val body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString()) - val request = Request.Builder() - .url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:signUp")) - .post(body) - .build() - client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } - - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - source.setException( - createAuthInvalidUserException("accounts:signUp", request, response) - ) - } else { - val body = response.body()!!.use { it.string() } - user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject, true) - source.setResult(AuthResult { user }) - } - } - }) + val source = + enqueueAuthPost( + url = "identitytoolkit.googleapis.com/v1/accounts:signUp", + body = RequestBody.create(json, JsonObject(mapOf("returnSecureToken" to JsonPrimitive(true))).toString()), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject, isAnonymous = true) + }, + ) return source.task } fun signInWithCustomToken(customToken: String): Task { + val source = + enqueueAuthPost( + url = "www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken", + body = + RequestBody.create( + json, + JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString(), + ), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) + }, + ).task.addOnSuccessListener { + updateByGetAccountInfo() + } + return source + } + + internal fun updateByGetAccountInfo(): Task { val source = TaskCompletionSource() - val body = RequestBody.create( - json, - JsonObject(mapOf("token" to JsonPrimitive(customToken), "returnSecureToken" to JsonPrimitive(true))).toString() - ) - val request = Request.Builder() - .url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/verifyCustomToken")) - .post(body) - .build() - client.newCall(request).enqueue(object : Callback { - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } + val body = + RequestBody.create( + json, + JsonObject(mapOf("idToken" to JsonPrimitive(user?.idToken))).toString(), + ) + val request = + Request + .Builder() + .url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/getAccountInfo")) + .post(body) + .build() + + client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - source.setException( - createAuthInvalidUserException("verifyCustomToken", request, response) - ) - } else { - val body = response.body()!!.use { it.string() } - val user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject) - refreshToken(user, source) { AuthResult { it } } + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + source.setException( + createAuthInvalidUserException("updateWithAccountInfo", request, response), + ) + } else { + val newBody = + jsonParser + .parseToJsonElement( + response.body()?.use { it.string() } ?: "", + ).jsonObject + + user?.let { prev -> + user = + FirebaseUserImpl( + app = app, + isAnonymous = prev.isAnonymous, + uid = prev.uid, + idToken = prev.idToken, + refreshToken = prev.refreshToken, + expiresIn = prev.expiresIn, + createdAt = newBody["createdAt"]?.jsonPrimitive?.longOrNull ?: prev.createdAt, + email = newBody["email"]?.jsonPrimitive?.contentOrNull ?: prev.email, + photoUrl = newBody["photoUrl"]?.jsonPrimitive?.contentOrNull ?: prev.photoUrl, + displayName = newBody["displayName"]?.jsonPrimitive?.contentOrNull ?: prev.displayName, + ) + source.setResult(AuthResult { user }) + } + source.setResult(null) + } } - } - }) + }, + ) return source.task } - fun signInWithEmailAndPassword(email: String, password: String): Task { - val source = TaskCompletionSource() - val body = RequestBody.create( - json, - JsonObject(mapOf("email" to JsonPrimitive(email), "password" to JsonPrimitive(password), "returnSecureToken" to JsonPrimitive(true))).toString() - ) - val request = Request.Builder() - .url(urlFactory.buildUrl("www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword")) - .post(body) - .build() - client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(FirebaseException(e.toString(), e)) - } - - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - if (!response.isSuccessful) { - source.setException( - createAuthInvalidUserException("verifyPassword", request, response) + fun createUserWithEmailAndPassword( + email: String, + password: String, + ): Task { + val source = + enqueueAuthPost( + url = "www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser", + body = + RequestBody.create( + json, + JsonObject( + mapOf( + "email" to JsonPrimitive(email), + "password" to JsonPrimitive(password), + "returnSecureToken" to JsonPrimitive(true), + ), + ).toString(), + ), + setResult = { responseBody -> + FirebaseUserImpl( + app = app, + data = jsonParser.parseToJsonElement(responseBody).jsonObject, ) - } else { - val body = response.body()!!.use { it.string() } - val user = FirebaseUserImpl(app, jsonParser.parseToJsonElement(body).jsonObject) - refreshToken(user, source) { AuthResult { it } } - } - } - }) + }, + ) + return source.task + } + + fun signInWithEmailAndPassword( + email: String, + password: String, + ): Task { + val source = + enqueueAuthPost( + url = "www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword", + body = + RequestBody.create( + json, + JsonObject( + mapOf( + "email" to JsonPrimitive(email), + "password" to JsonPrimitive(password), + "returnSecureToken" to JsonPrimitive(true), + ), + ).toString(), + ), + setResult = { responseBody -> + FirebaseUserImpl(app, jsonParser.parseToJsonElement(responseBody).jsonObject) + }, + ) return source.task } internal fun createAuthInvalidUserException( action: String, request: Request, - response: Response + response: Response, ): FirebaseAuthInvalidUserException { val body = response.body()!!.use { it.string() } val jsonObject = jsonParser.parseToJsonElement(body).jsonObject return FirebaseAuthInvalidUserException( - jsonObject["error"]?.jsonObject - ?.get("message")?.jsonPrimitive + jsonObject["error"] + ?.jsonObject + ?.get("message") + ?.jsonPrimitive ?.contentOrNull ?: "UNKNOWN_ERROR", "$action API returned an error, " + "with url [${request.method()}] ${request.url()} ${request.body()} -- " + - "response [${response.code()}] ${response.message()} $body" + "response [${response.code()}] ${response.message()} $body", ) } fun signOut() { - // todo cancel token refresher + // cancel token refresher + client + .dispatcher() + .queuedCalls() + .find { it.request().tag() == REFRESH_TOKEN_TAG } + ?.cancel() ?: { + client + .dispatcher() + .runningCalls() + .find { it.request().tag() == REFRESH_TOKEN_TAG } + ?.cancel() + } user = null } @@ -330,7 +549,11 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { private var refreshSource = TaskCompletionSource().apply { setException(Exception()) } - internal fun refreshToken(user: FirebaseUserImpl, source: TaskCompletionSource, map: (user: FirebaseUserImpl) -> T?) { + internal fun refreshToken( + user: FirebaseUserImpl, + source: TaskCompletionSource, + map: (user: FirebaseUserImpl) -> T?, + ) { refreshSource = refreshSource .takeUnless { it.task.isComplete } ?: enqueueRefreshTokenCall(user) @@ -340,59 +563,221 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { private fun enqueueRefreshTokenCall(user: FirebaseUserImpl): TaskCompletionSource { val source = TaskCompletionSource() - val body = RequestBody.create( - json, - JsonObject( - mapOf( - "refresh_token" to JsonPrimitive(user.refreshToken), - "grant_type" to JsonPrimitive("refresh_token") - ) - ).toString() - ) - val request = Request.Builder() - .url(urlFactory.buildUrl("securetoken.googleapis.com/v1/token")) - .post(body) - .build() - - client.newCall(request).enqueue(object : Callback { - - override fun onFailure(call: Call, e: IOException) { - source.setException(e) - } + val body = + RequestBody.create( + json, + JsonObject( + mapOf( + "refresh_token" to JsonPrimitive(user.refreshToken), + "grant_type" to JsonPrimitive("refresh_token"), + ), + ).toString(), + ) + val request = + Request + .Builder() + .url(urlFactory.buildUrl("securetoken.googleapis.com/v1/token")) + .post(body) + .tag(REFRESH_TOKEN_TAG) + .build() + + client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(e) + } - @Throws(IOException::class) - override fun onResponse(call: Call, response: Response) { - val body = response.body()?.use { it.string() } - if (!response.isSuccessful) { - signOutAndThrowInvalidUserException(body.orEmpty(), "token API returned an error: $body") - } else { - jsonParser.parseToJsonElement(body!!).jsonObject.apply { - val user = FirebaseUserImpl(app, this, user.isAnonymous) - if (user.claims["aud"] != app.options.projectId) { - signOutAndThrowInvalidUserException( - user.claims.toString(), - "Project ID's do not match ${user.claims["aud"]} != ${app.options.projectId}" - ) - } else { - this@FirebaseAuth.user = user - source.setResult(user) + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + val responseBody = response.body()?.use { it.string() } + + if (!response.isSuccessful || responseBody == null) { + signOutAndThrowInvalidUserException(responseBody.orEmpty(), "token API returned an error: $body") + } else { + jsonParser.parseToJsonElement(responseBody).jsonObject.apply { + val newUser = FirebaseUserImpl(app, this, user.isAnonymous, user.email) + if (newUser.claims["aud"] != app.options.projectId) { + signOutAndThrowInvalidUserException( + newUser.claims.toString(), + "Project ID's do not match ${newUser.claims["aud"]} != ${app.options.projectId}", + ) + } else { + this@FirebaseAuth.user = newUser + source.setResult(newUser) + } } } } - } - private fun signOutAndThrowInvalidUserException(body: String, message: String) { - signOut() - source.setException(FirebaseAuthInvalidUserException(body, message)) - } - }) + private fun signOutAndThrowInvalidUserException( + body: String, + message: String, + ) { + signOut() + source.setException(FirebaseAuthInvalidUserException(body, message)) + } + }, + ) return source } - override fun getUid(): String? { - return user?.uid + internal fun updateEmail(email: String): Task { + val source = TaskCompletionSource() + + val body = + RequestBody.create( + json, + JsonObject( + mapOf( + "idToken" to JsonPrimitive(user?.idToken), + "email" to JsonPrimitive(email), + "returnSecureToken" to JsonPrimitive(true), + ), + ).toString(), + ) + val request = + Request + .Builder() + .url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:update")) + .post(body) + .build() + + client.newCall(request).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } + + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + signOut() + source.setException( + createAuthInvalidUserException( + "updateEmail", + request, + response, + ), + ) + } else { + val newBody = + jsonParser + .parseToJsonElement( + response.body()?.use { it.string() } ?: "", + ).jsonObject + + user?.let { prev -> + user = + FirebaseUserImpl( + app = app, + isAnonymous = prev.isAnonymous, + uid = prev.uid, + idToken = newBody["idToken"]?.jsonPrimitive?.contentOrNull ?: prev.idToken, + refreshToken = newBody["refreshToken"]?.jsonPrimitive?.contentOrNull ?: prev.refreshToken, + expiresIn = newBody["expiresIn"]?.jsonPrimitive?.intOrNull ?: prev.expiresIn, + createdAt = prev.createdAt, + email = newBody["newEmail"]?.jsonPrimitive?.contentOrNull ?: prev.email, + photoUrl = newBody["photoUrl"]?.jsonPrimitive?.contentOrNull ?: prev.photoUrl, + displayName = newBody["displayName"]?.jsonPrimitive?.contentOrNull ?: prev.displayName, + ) + } + source.setResult(null) + } + } + }, + ) + return source.task } + internal fun updateProfile(request: UserProfileChangeRequest): Task { + val source = TaskCompletionSource() + + val body = + RequestBody.create( + json, + JsonObject( + mapOf( + "idToken" to JsonPrimitive(user?.idToken), + "displayName" to JsonPrimitive(request.displayName), + "photoUrl" to JsonPrimitive(request.photoUrl), + "returnSecureToken" to JsonPrimitive(true), + ), + ).toString(), + ) + val req = + Request + .Builder() + .url(urlFactory.buildUrl("identitytoolkit.googleapis.com/v1/accounts:update")) + .post(body) + .build() + + client.newCall(req).enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + source.setException(FirebaseException(e.toString(), e)) + } + + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + if (!response.isSuccessful) { + signOut() + source.setException( + createAuthInvalidUserException( + "updateProfile", + req, + response, + ), + ) + } else { + val newBody = + jsonParser + .parseToJsonElement( + response.body()?.use { it.string() } ?: "", + ).jsonObject + + user?.let { prev -> + user = + FirebaseUserImpl( + app = app, + isAnonymous = prev.isAnonymous, + uid = prev.uid, + idToken = newBody["idToken"]?.jsonPrimitive?.contentOrNull ?: prev.idToken, + refreshToken = newBody["refreshToken"]?.jsonPrimitive?.contentOrNull ?: prev.refreshToken, + expiresIn = newBody["expiresIn"]?.jsonPrimitive?.intOrNull ?: prev.expiresIn, + createdAt = prev.createdAt, + email = newBody["newEmail"]?.jsonPrimitive?.contentOrNull ?: prev.email, + photoUrl = newBody["photoUrl"]?.jsonPrimitive?.contentOrNull ?: prev.photoUrl, + displayName = newBody["displayName"]?.jsonPrimitive?.contentOrNull ?: prev.displayName, + ) + } + source.setResult(null) + } + } + }, + ) + return source.task + } + + override fun getUid(): String? = user?.uid + override fun addIdTokenListener(listener: com.google.firebase.auth.internal.IdTokenListener) { internalIdTokenListeners.addIfAbsent(listener) GlobalScope.launch(Dispatchers.Main) { @@ -438,23 +823,46 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider { idTokenListeners.remove(listener) } - fun sendPasswordResetEmail(email: String, settings: ActionCodeSettings?): Task = TODO() - fun createUserWithEmailAndPassword(email: String, password: String): Task = TODO() - fun signInWithCredential(authCredential: AuthCredential): Task = TODO() + fun sendPasswordResetEmail( + email: String, + settings: ActionCodeSettings?, + ): Task = TODO() + fun checkActionCode(code: String): Task = TODO() - fun confirmPasswordReset(code: String, newPassword: String): Task = TODO() + + fun confirmPasswordReset( + code: String, + newPassword: String, + ): Task = TODO() + fun fetchSignInMethodsForEmail(email: String): Task = TODO() - fun sendSignInLinkToEmail(email: String, actionCodeSettings: ActionCodeSettings): Task = TODO() + + fun sendSignInLinkToEmail( + email: String, + actionCodeSettings: ActionCodeSettings, + ): Task = TODO() + fun verifyPasswordResetCode(code: String): Task = TODO() + fun updateCurrentUser(user: FirebaseUser): Task = TODO() + fun applyActionCode(code: String): Task = TODO() + val languageCode: String get() = TODO() + fun isSignInWithEmailLink(link: String): Boolean = TODO() - fun signInWithEmailLink(email: String, link: String): Task = TODO() + + fun signInWithEmailLink( + email: String, + link: String, + ): Task = TODO() fun setLanguageCode(value: String): Nothing = TODO() - fun useEmulator(host: String, port: Int) { + fun useEmulator( + host: String, + port: Int, + ) { urlFactory = UrlFactory(app, "http://$host:$port/") } } diff --git a/src/main/java/com/google/firebase/auth/FirebaseUser.kt b/src/main/java/com/google/firebase/auth/FirebaseUser.kt index 3f61dea..0932518 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUser.kt +++ b/src/main/java/com/google/firebase/auth/FirebaseUser.kt @@ -4,29 +4,46 @@ import com.google.android.gms.tasks.Task abstract class FirebaseUser { abstract val uid: String + abstract val email: String? + abstract val photoUrl: String? + abstract val displayName: String? abstract val isAnonymous: Boolean + abstract fun delete(): Task + abstract fun reload(): Task - val email: String get() = TODO() - val displayName: String get() = TODO() + abstract fun verifyBeforeUpdateEmail( + newEmail: String, + actionCodeSettings: ActionCodeSettings?, + ): Task + + abstract fun updateEmail(email: String): Task + + abstract fun getIdToken(forceRefresh: Boolean): Task + + abstract fun updateProfile(request: UserProfileChangeRequest): Task + val phoneNumber: String get() = TODO() - val photoUrl: String? get() = TODO() val isEmailVerified: Boolean get() = TODO() val metadata: FirebaseUserMetadata get() = TODO() val multiFactor: MultiFactor get() = TODO() val providerData: List get() = TODO() val providerId: String get() = TODO() - abstract fun getIdToken(forceRefresh: Boolean): Task + fun linkWithCredential(credential: AuthCredential): Task = TODO() + fun sendEmailVerification(): Task = TODO() + fun sendEmailVerification(actionCodeSettings: ActionCodeSettings): Task = TODO() + fun unlink(provider: String): Task = TODO() - fun updateEmail(email: String): Task = TODO() + fun updatePassword(password: String): Task = TODO() + fun updatePhoneNumber(credential: AuthCredential): Task = TODO() - fun updateProfile(request: UserProfileChangeRequest): Task = TODO() - fun verifyBeforeUpdateEmail(newEmail: String, actionCodeSettings: ActionCodeSettings?): Task = TODO() + fun reauthenticate(credential: AuthCredential): Task = TODO() + fun reauthenticateAndRetrieveData(credential: AuthCredential): Task = TODO() } diff --git a/src/main/java/com/google/firebase/auth/OobRequestType.kt b/src/main/java/com/google/firebase/auth/OobRequestType.kt new file mode 100644 index 0000000..51a77e2 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/OobRequestType.kt @@ -0,0 +1,8 @@ +package com.google.firebase.auth + +internal enum class OobRequestType { + PASSWORD_RESET, + EMAIL_SIGNIN, + VERIFY_EMAIL, + VERIFY_AND_CHANGE_EMAIL +} diff --git a/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.java b/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.java deleted file mode 100644 index 437f98d..0000000 --- a/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.google.firebase.auth; - -import android.net.Uri; -import kotlin.NotImplementedError; - -public class UserProfileChangeRequest { - public static class Builder { - public Builder setDisplayName(String name) { - throw new NotImplementedError(); - } - public Builder setPhotoUri(Uri uri) { - throw new NotImplementedError(); - } - public UserProfileChangeRequest build() { - throw new NotImplementedError(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.kt b/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.kt new file mode 100644 index 0000000..c60bb83 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserProfileChangeRequest.kt @@ -0,0 +1,47 @@ +package com.google.firebase.auth + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +class UserProfileChangeRequest private constructor( + internal val displayName: String?, + internal val photoUrl: String?, +) : Parcelable { + override fun describeContents(): Int = displayName.hashCode() + photoUrl.hashCode() + + override fun writeToParcel( + dest: Parcel, + flags: Int, + ) { + dest.writeString(displayName) + dest.writeString(photoUrl) + } + + internal companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): UserProfileChangeRequest { + val displayName = parcel.readString() + val photoUri = parcel.readString() + return UserProfileChangeRequest(displayName, photoUri) + } + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + + class Builder { + private var displayName: String? = null + private var photoUri: Uri? = null + + fun setDisplayName(name: String?): Builder { + this.displayName = name + return this + } + + fun setPhotoUri(uri: Uri?): Builder { + this.photoUri = uri + return this + } + + fun build(): UserProfileChangeRequest = UserProfileChangeRequest(displayName, photoUri?.toString()) + } +} diff --git a/src/test/kotlin/AppTest.kt b/src/test/kotlin/AppTest.kt index d017e38..2e5697f 100644 --- a/src/test/kotlin/AppTest.kt +++ b/src/test/kotlin/AppTest.kt @@ -8,20 +8,34 @@ import org.junit.Test class AppTest : FirebaseTest() { @Test fun testInitialize() { - FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() { - val storage = mutableMapOf() - override fun store(key: String, value: String) = storage.set(key, value) - override fun retrieve(key: String) = storage[key] - override fun clear(key: String) { storage.remove(key) } - override fun log(msg: String) = println(msg) - }) - val options = FirebaseOptions.Builder() - .setProjectId("my-firebase-project") - .setApplicationId("1:27992087142:android:ce3b6448250083d1") - .setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw") - // setDatabaseURL(...) - // setStorageBucket(...) - .build() + FirebasePlatform.initializeFirebasePlatform( + object : FirebasePlatform() { + val storage = mutableMapOf() + + override fun store( + key: String, + value: String, + ) = storage.set(key, value) + + override fun retrieve(key: String) = storage[key] + + override fun clear(key: String) { + storage.remove(key) + } + + override fun log(msg: String) = println(msg) + }, + ) + val options = + FirebaseOptions + .Builder() + .setProjectId("fir-java-sdk") + .setApplicationId("1:341458593155:web:bf8e1aa37efe01f32d42b6") + .setApiKey("AIzaSyCvVHjTJHyeStnzIE7J9LLtHqWk6reGM08") + .setDatabaseUrl("https://fir-java-sdk-default-rtdb.firebaseio.com") + .setStorageBucket("fir-java-sdk.appspot.com") + .setGcmSenderId("341458593155") + .build() val app = Firebase.initialize(Application(), options) } } diff --git a/src/test/kotlin/AuthTest.kt b/src/test/kotlin/AuthTest.kt index 7bdb90c..d603d5f 100644 --- a/src/test/kotlin/AuthTest.kt +++ b/src/test/kotlin/AuthTest.kt @@ -1,47 +1,112 @@ -import com.google.firebase.auth.FirebaseAuth +import android.net.Uri import com.google.firebase.auth.FirebaseAuthInvalidUserException import kotlinx.coroutines.runBlocking import kotlinx.coroutines.tasks.await import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test +import java.util.UUID class AuthTest : FirebaseTest() { - private fun createAuth(): FirebaseAuth { - return FirebaseAuth(app).apply { + private val email = "email${UUID.randomUUID()}@example.com" + + @Before + fun initialize() { + auth.apply { useEmulator("localhost", 9099) } } @Test - fun `should authenticate via anonymous auth`() = runTest { - val auth = createAuth() + fun `should authenticate via anonymous auth`() = + runTest { + auth.signInAnonymously().await() - auth.signInAnonymously().await() + assertEquals(true, auth.currentUser?.isAnonymous) + } - assertEquals(true, auth.currentUser?.isAnonymous) - } + @Test + fun `should create user via email and password`() = + runTest { + val createResult = auth.createUserWithEmailAndPassword(email, "test123").await() + assertNotEquals(null, createResult.user?.uid) + assertEquals(null, createResult.user?.displayName) + // assertEquals(null, createResult.user?.phoneNumber) + assertEquals(false, createResult.user?.isAnonymous) + assertEquals(email, createResult.user?.email) + assertNotEquals("", createResult.user!!.email) + + val signInResult = auth.signInWithEmailAndPassword(email, "test123").await() + assertEquals(createResult.user?.uid, signInResult.user?.uid) + } @Test - fun `should authenticate via email and password`() = runTest { - val auth = createAuth() + fun `should authenticate via email and password`() = + runTest { + auth.createUserWithEmailAndPassword(email, "test123").await() - auth.signInWithEmailAndPassword("email@example.com", "securepassword").await() + auth.signInWithEmailAndPassword(email, "test123").await() - assertEquals(false, auth.currentUser?.isAnonymous) - } + assertEquals(false, auth.currentUser?.isAnonymous) + } + + /*@Test + fun `should authenticate via custom token`() = + runTest { + val user = auth.createUserWithEmailAndPassword(email, "test123").await() + auth + .signInWithCustomToken( + user.user + .getIdToken(false) + .await() + .token ?: "", + ).await() + + assertEquals(false, auth.currentUser?.isAnonymous) + }*/ @Test - fun `should throw exception on invalid password`() { - val auth = createAuth() + fun `should update displayName and photoUrl`() = + runTest { + auth + .createUserWithEmailAndPassword(email, "test123") + .await() + .user + auth.currentUser + ?.updateProfile( + com.google.firebase.auth.UserProfileChangeRequest + .Builder() + .setDisplayName("testDisplayName") + .setPhotoUri(Uri.parse("https://picsum.photos/100")) + .build(), + )?.await() + assertEquals("testDisplayName", auth.currentUser?.displayName) + assertEquals("https://picsum.photos/100", auth.currentUser?.photoUrl) + } - val exception = assertThrows(FirebaseAuthInvalidUserException::class.java) { - runBlocking { - auth.signInWithEmailAndPassword("email@example.com", "wrongpassword").await() - } + @Test + fun `should sign in anonymously`() = + runTest { + val signInResult = auth.signInAnonymously().await() + assertNotEquals("", signInResult.user!!.email) + assertEquals(true, signInResult.user?.isAnonymous) } - assertEquals("INVALID_PASSWORD", exception.errorCode) - } + @Test + fun `should throw exception on invalid password`() = + runTest { + auth.createUserWithEmailAndPassword(email, "test123").await() + + val exception = + assertThrows(FirebaseAuthInvalidUserException::class.java) { + runBlocking { + auth.signInWithEmailAndPassword(email, "wrongpassword").await() + } + } + + assertEquals("INVALID_PASSWORD", exception.errorCode) + } } diff --git a/src/test/kotlin/FirebaseAuthTest.kt b/src/test/kotlin/FirebaseAuthTest.kt new file mode 100644 index 0000000..b2000f2 --- /dev/null +++ b/src/test/kotlin/FirebaseAuthTest.kt @@ -0,0 +1,57 @@ + +import android.net.Uri +import com.google.firebase.auth.FirebaseUser +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import java.util.UUID + +internal class FirebaseAuthTest : FirebaseTest() { + @Test + fun testCreateUserWithEmailAndPassword() = + runTest { + val email = "test+${UUID.randomUUID()}@test.com" + val createResult = auth.createUserWithEmailAndPassword(email, "test123").await() + assertNotEquals(null, createResult.user?.uid) + // assertEquals(null, createResult.user?.displayName) + // assertEquals(null, createResult.user?.phoneNumber) + assertEquals(false, createResult.user?.isAnonymous) + assertEquals(email, createResult.user?.email) + assertNotEquals("", createResult.user!!.email) + + val signInResult = auth.signInWithEmailAndPassword(email, "test123").await() + assertEquals(createResult.user?.uid, signInResult.user?.uid) + } + + @Test + fun testUpdateProfile() = + runTest { + val user = createUser() + user + ?.updateProfile( + com.google.firebase.auth.UserProfileChangeRequest + .Builder() + .setDisplayName("testDisplayName") + .setPhotoUri(Uri.parse("https://picsum.photos/100")) + .build(), + )?.await() + assertEquals("testDisplayName", auth.currentUser?.displayName) + assertEquals("https://picsum.photos/100", auth.currentUser?.photoUrl) + } + + @Test + fun testSignInAnonymously() = + runTest { + val signInResult = auth.signInAnonymously().await() + assertNotEquals("", signInResult.user!!.email) + assertEquals(true, signInResult.user?.isAnonymous) + } + + private suspend fun createUser(email: String = "test+${UUID.randomUUID()}@test.com"): FirebaseUser? = + auth + .createUserWithEmailAndPassword(email, "test123") + .await() + .user +} diff --git a/src/test/kotlin/FirebaseTest.kt b/src/test/kotlin/FirebaseTest.kt index 77aa858..a714f8c 100644 --- a/src/test/kotlin/FirebaseTest.kt +++ b/src/test/kotlin/FirebaseTest.kt @@ -3,31 +3,72 @@ import com.google.firebase.Firebase import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.FirebasePlatform +import com.google.firebase.auth.FirebaseAuth import com.google.firebase.initialize +import com.google.firebase.ktx.initialize +import org.junit.After import org.junit.Before import java.io.File abstract class FirebaseTest { + protected lateinit var auth: FirebaseAuth + protected val app: FirebaseApp get() { - val options = FirebaseOptions.Builder() - .setProjectId("my-firebase-project") - .setApplicationId("1:27992087142:android:ce3b6448250083d1") - .setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw") - .build() + val options = + FirebaseOptions + .Builder() + .setProjectId("fir-java-sdk") + .setApplicationId("1:341458593155:web:bf8e1aa37efe01f32d42b6") + .setApiKey("AIzaSyCvVHjTJHyeStnzIE7J9LLtHqWk6reGM08") + .setDatabaseUrl("https://fir-java-sdk-default-rtdb.firebaseio.com") + .setStorageBucket("fir-java-sdk.appspot.com") + .setGcmSenderId("341458593155") + .build() return Firebase.initialize(Application(), options) } @Before fun beforeEach() { - FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() { - val storage = mutableMapOf() - override fun store(key: String, value: String) = storage.set(key, value) - override fun retrieve(key: String) = storage[key] - override fun clear(key: String) { storage.remove(key) } - override fun log(msg: String) = println(msg) - override fun getDatabasePath(name: String) = File("./build/$name") - }) + FirebasePlatform.initializeFirebasePlatform( + object : FirebasePlatform() { + val storage = mutableMapOf() + + override fun store( + key: String, + value: String, + ) = storage.set(key, value) + + override fun retrieve(key: String) = storage[key] + + override fun clear(key: String) { + storage.remove(key) + } + + override fun log(msg: String) = println(msg) + + override fun getDatabasePath(name: String) = File("./build/$name") + }, + ) + val options = + FirebaseOptions + .Builder() + .setProjectId("fir-java-sdk") + .setApplicationId("1:341458593155:web:bf8e1aa37efe01f32d42b6") + .setApiKey("AIzaSyCvVHjTJHyeStnzIE7J9LLtHqWk6reGM08") + .setDatabaseUrl("https://fir-java-sdk-default-rtdb.firebaseio.com") + .setStorageBucket("fir-java-sdk.appspot.com") + .setGcmSenderId("341458593155") + .build() + + val firebaseApp = Firebase.initialize(Application(), options) + auth = FirebaseAuth.getInstance(app = firebaseApp) + FirebaseApp.clearInstancesForTest() } + + @After + fun clear() { + auth.currentUser?.delete() + } }