Skip to content

Commit

Permalink
Move user profile update logic to server (fixes #260)
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
  • Loading branch information
SUPERCILEX committed Feb 1, 2020
1 parent ec9936b commit bf45873
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.supercilex.robotscouter.server

import com.supercilex.robotscouter.server.functions.deleteUnusedData
import com.supercilex.robotscouter.server.functions.emptyTrash
import com.supercilex.robotscouter.server.functions.initUser
import com.supercilex.robotscouter.server.functions.logUserData
import com.supercilex.robotscouter.server.functions.mergeDuplicateTeams
import com.supercilex.robotscouter.server.functions.sanitizeDeletionRequest
Expand Down Expand Up @@ -47,4 +48,8 @@ fun main() {
.runWith(json("timeoutSeconds" to 300, "memory" to "256MB"))
.firestore.document("${duplicateTeams.id}/{uid}")
.onWrite { event, _ -> mergeDuplicateTeams(event) }

exports.initUser = functions.auth.user().onCreate { user ->
initUser(user)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import com.supercilex.robotscouter.common.FIRESTORE_BASE_TIMESTAMP
import com.supercilex.robotscouter.common.FIRESTORE_CONTENT_ID
import com.supercilex.robotscouter.common.FIRESTORE_LAST_LOGIN
import com.supercilex.robotscouter.common.FIRESTORE_METRICS
import com.supercilex.robotscouter.common.FIRESTORE_NAME
import com.supercilex.robotscouter.common.FIRESTORE_OWNERS
import com.supercilex.robotscouter.common.FIRESTORE_SCOUTS
import com.supercilex.robotscouter.common.FIRESTORE_SHARE_TYPE
import com.supercilex.robotscouter.common.FIRESTORE_TIMESTAMP
import com.supercilex.robotscouter.common.FIRESTORE_TYPE
import com.supercilex.robotscouter.server.utils.FIRESTORE_EMAIL
import com.supercilex.robotscouter.server.utils.FIRESTORE_PHONE_NUMBER
import com.supercilex.robotscouter.server.utils.FIRESTORE_PHOTO_URL
import com.supercilex.robotscouter.server.utils.auth
import com.supercilex.robotscouter.server.utils.batch
import com.supercilex.robotscouter.server.utils.delete
Expand Down Expand Up @@ -51,6 +53,7 @@ import kotlinx.coroutines.asPromise
import kotlinx.coroutines.async
import kotlinx.coroutines.await
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
Expand All @@ -75,7 +78,7 @@ fun deleteUnusedData(): Promise<*>? = GlobalScope.async {
))
}
val anonymousUser = async {
deleteUnusedData(users.where(
deleteAnonymousUsers(users.where(
FIRESTORE_LAST_LOGIN,
"<",
Timestamps.fromDate(
Expand Down Expand Up @@ -142,9 +145,37 @@ fun sanitizeDeletionRequest(event: Change<DeltaDocumentSnapshot>): Promise<*>? {
}
}

private suspend fun CoroutineScope.deleteUnusedData(
private suspend fun deleteAnonymousUsers(
userQuery: Query
) = userQuery.processInBatches(10) { user ->
val userRecord = try {
auth.getUser(user.id).await()
} catch (t: Throwable) {
if (t.asDynamic().code != "auth/user-not-found") throw t else null
}

if (userRecord == null || userRecord.providerData.orEmpty().isEmpty()) {
purgeUser(user)
} else {
console.log("Correcting user inadvertently marked as anonymous: " +
JSON.stringify(userRecord.toJSON()))

val payload = json()
userRecord.email?.let { payload[FIRESTORE_EMAIL] = it }
userRecord.displayName?.let { payload[FIRESTORE_NAME] = it }
userRecord.phoneNumber?.let { payload[FIRESTORE_PHONE_NUMBER] = it }
userRecord.photoURL?.let { payload[FIRESTORE_PHOTO_URL] = it }
user.ref.set(payload, SetOptions.merge).await()
}
}

private suspend fun deleteUnusedData(
userQuery: Query
) = userQuery.processInBatches(10) { user ->
purgeUser(user)
}

private suspend fun purgeUser(user: DocumentSnapshot) = coroutineScope {
console.log("Deleting all data for user:\n${JSON.stringify(user.data())}")

val userId = user.id
Expand Down Expand Up @@ -264,8 +295,7 @@ private suspend fun deleteUser(user: DocumentSnapshot) {
try {
auth.deleteUser(user.id).await()
} catch (t: Throwable) {
@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") // It's a JS object
if ((t as Json)["code"] != "auth/user-not-found") throw t
if (t.asDynamic().code != "auth/user-not-found") throw t
}

deletionQueue.doc(user.id).delete().await()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.supercilex.robotscouter.server.functions

import com.supercilex.robotscouter.common.FIRESTORE_LAST_LOGIN
import com.supercilex.robotscouter.common.FIRESTORE_NAME
import com.supercilex.robotscouter.server.utils.FIRESTORE_EMAIL
import com.supercilex.robotscouter.server.utils.FIRESTORE_PHONE_NUMBER
import com.supercilex.robotscouter.server.utils.FIRESTORE_PHOTO_URL
import com.supercilex.robotscouter.server.utils.types.SetOptions
import com.supercilex.robotscouter.server.utils.types.Timestamps
import com.supercilex.robotscouter.server.utils.types.UserInfo
import com.supercilex.robotscouter.server.utils.users
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asPromise
import kotlinx.coroutines.async
import kotlinx.coroutines.await
import kotlin.js.Date
import kotlin.js.Promise
import kotlin.js.json

fun initUser(user: UserInfo): Promise<*>? = GlobalScope.async {
console.log("Initializing user: ${JSON.stringify(user.toJSON())}")

users.doc(user.uid).set(json(
FIRESTORE_LAST_LOGIN to Timestamps.fromDate(Date()),
FIRESTORE_EMAIL to user.email,
FIRESTORE_NAME to user.displayName,
FIRESTORE_PHONE_NUMBER to user.phoneNumber,
FIRESTORE_PHOTO_URL to user.photoURL
), SetOptions.merge).await()
}.asPromise()
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import kotlin.js.Date

const val FIRESTORE_EMAIL = "email"
const val FIRESTORE_PHONE_NUMBER = "phoneNumber"
const val FIRESTORE_PHOTO_URL = "photoUrl"

val firestore by lazy { admin.firestore() }
val auth by lazy { admin.auth() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ package com.supercilex.robotscouter.server.utils.types

import kotlin.js.Promise

external class FunctionsAuth {
fun user(): UserBuilder = definedExternally
}

external class UserBuilder {
fun onCreate(handler: (UserInfo) -> Promise<*>?): dynamic = definedExternally
}

external interface UserMetadata {
val lastSignInTime: String
val creationTime: String
Expand All @@ -28,14 +36,14 @@ external interface UserInfo {

external interface UserRecord {
val uid: String
val email: String
val email: String?
val emailVerified: Boolean
val displayName: String
val phoneNumber: String
val photoURL: String
val displayName: String?
val phoneNumber: String?
val photoURL: String?
val disabled: Boolean
val metadata: UserMetadata
val providerData: Array<UserInfo>
val providerData: Array<UserInfo>?
val passwordHash: String? get() = definedExternally
val passwordSalt: String? get() = definedExternally
val customClaims: Any? get() = definedExternally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ external class Functions {
val firestore: NamespaceBuilder = definedExternally
val pubsub: Pubsub = definedExternally
val https: Https = definedExternally
val auth: FunctionsAuth = definedExternally
fun runWith(options: dynamic): Functions
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import com.google.firebase.firestore.SetOptions
import com.google.firebase.firestore.WriteBatch
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
import com.google.gson.Gson
import com.supercilex.robotscouter.common.DeletionType
import com.supercilex.robotscouter.common.FIRESTORE_CONTENT_ID
import com.supercilex.robotscouter.common.FIRESTORE_LAST_LOGIN
Expand All @@ -35,7 +34,6 @@ import com.supercilex.robotscouter.core.CrashLogger
import com.supercilex.robotscouter.core.data.client.retrieveLocalMedia
import com.supercilex.robotscouter.core.data.client.retrieveShouldUpload
import com.supercilex.robotscouter.core.data.client.startUploadMediaJob
import com.supercilex.robotscouter.core.data.model.add
import com.supercilex.robotscouter.core.data.model.fetchLatestData
import com.supercilex.robotscouter.core.data.model.forceUpdate
import com.supercilex.robotscouter.core.data.model.isStale
Expand All @@ -48,15 +46,11 @@ import com.supercilex.robotscouter.core.data.model.userRef
import com.supercilex.robotscouter.core.isMain
import com.supercilex.robotscouter.core.logBreadcrumb
import com.supercilex.robotscouter.core.mainHandler
import com.supercilex.robotscouter.core.model.User
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.invoke
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.tasks.await
import java.io.File
import java.lang.reflect.Field
import java.util.Calendar
import java.util.concurrent.CopyOnWriteArrayList
Expand Down Expand Up @@ -161,29 +155,15 @@ private val teamUpdater = object : ChangeEventListenerBase {
}
}

private val dbCacheLock = Mutex()

fun initDatabase() {
if (BuildConfig.DEBUG) FirebaseFirestore.setLoggingEnabled(true)
teams.addChangeEventListener(teamTemplateIdUpdater)
teams.addChangeEventListener(teamUpdater)

FirebaseAuth.getInstance().addAuthStateListener {
val user = it.currentUser
if (user == null) {
GlobalScope.launch(Dispatchers.IO) {
dbCacheLock.withLock { dbCache.deleteRecursively() }
}
} else {
if (user != null) {
updateLastLogin.run()

User(
user.uid,
user.email.nullOrFull(),
user.phoneNumber.nullOrFull(),
user.displayName.nullOrFull(),
user.photoUrl?.toString()
).smartWrite(userCache) { it.add() }
}
}
}
Expand Down Expand Up @@ -246,25 +226,6 @@ fun <T> ObservableSnapshotArray<T>.asLiveData(): LiveData<ObservableSnapshotArra
}
}

private inline fun <reified T> T.smartWrite(file: File, crossinline write: (t: T) -> Unit) {
val new = this
GlobalScope.launch(Dispatchers.IO) {
val cache = {
write(new)
file.safeCreateNewFile().writeText(Gson().toJson(new))
}

dbCacheLock.withLock {
if (file.exists()) {
val cached = Gson().fromJson(file.readText(), T::class.java)
if (new != cached) cache()
} else {
cache()
}
}
}
}

internal sealed class QueuedDeletion(
id: String,
type: DeletionType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import android.os.Build
import android.os.Environment
import androidx.annotation.RequiresPermission
import androidx.annotation.WorkerThread
import com.supercilex.robotscouter.core.RobotScouter
import java.io.File

const val MIME_TYPE_ANY = "*/*"
Expand All @@ -22,9 +21,6 @@ private val exports = Environment.getExternalStoragePublicDirectory(
private val media: File =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)

internal val dbCache = File(RobotScouter.cacheDir, "db")
internal val userCache = File(dbCache, "user.json")

@get:WorkerThread
@get:RequiresPermission(value = Manifest.permission.WRITE_EXTERNAL_STORAGE)
val exportsFolder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.supercilex.robotscouter.core.data.model

import com.google.firebase.firestore.SetOptions
import com.google.firebase.functions.ktx.functions
import com.google.firebase.ktx.Firebase
import com.supercilex.robotscouter.common.FIRESTORE_PREFS
Expand All @@ -11,7 +10,6 @@ import com.supercilex.robotscouter.core.data.deletionQueueRef
import com.supercilex.robotscouter.core.data.logFailures
import com.supercilex.robotscouter.core.data.uid
import com.supercilex.robotscouter.core.data.usersRef
import com.supercilex.robotscouter.core.model.User

val userRef get() = getUserRef(checkNotNull(uid))

Expand All @@ -25,11 +23,6 @@ fun transferUserData(prevUid: String, token: String) = Firebase.functions
.call(mapOf(FIRESTORE_PREV_UID to prevUid, FIRESTORE_TOKEN to token))
.logFailures("transferUserData", prevUid, token)

internal fun User.add() {
val ref = getUserRef(uid)
ref.set(this, SetOptions.merge()).logFailures("addUser", ref, this)
}

private fun getUserRef(uid: String) = usersRef.document(uid)

private fun getUserPrefs(uid: String) = getUserRef(uid).collection(FIRESTORE_PREFS)

This file was deleted.

0 comments on commit bf45873

Please sign in to comment.