Skip to content

Commit bf45873

Browse files
committed
Move user profile update logic to server (fixes #260)
Signed-off-by: Alex Saveau <[email protected]>
1 parent ec9936b commit bf45873

File tree

10 files changed

+85
-91
lines changed

10 files changed

+85
-91
lines changed

app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/Index.kt

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.supercilex.robotscouter.server
22

33
import com.supercilex.robotscouter.server.functions.deleteUnusedData
44
import com.supercilex.robotscouter.server.functions.emptyTrash
5+
import com.supercilex.robotscouter.server.functions.initUser
56
import com.supercilex.robotscouter.server.functions.logUserData
67
import com.supercilex.robotscouter.server.functions.mergeDuplicateTeams
78
import com.supercilex.robotscouter.server.functions.sanitizeDeletionRequest
@@ -47,4 +48,8 @@ fun main() {
4748
.runWith(json("timeoutSeconds" to 300, "memory" to "256MB"))
4849
.firestore.document("${duplicateTeams.id}/{uid}")
4950
.onWrite { event, _ -> mergeDuplicateTeams(event) }
51+
52+
exports.initUser = functions.auth.user().onCreate { user ->
53+
initUser(user)
54+
}
5055
}

app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Cleanup.kt

+34-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import com.supercilex.robotscouter.common.FIRESTORE_BASE_TIMESTAMP
66
import com.supercilex.robotscouter.common.FIRESTORE_CONTENT_ID
77
import com.supercilex.robotscouter.common.FIRESTORE_LAST_LOGIN
88
import com.supercilex.robotscouter.common.FIRESTORE_METRICS
9+
import com.supercilex.robotscouter.common.FIRESTORE_NAME
910
import com.supercilex.robotscouter.common.FIRESTORE_OWNERS
1011
import com.supercilex.robotscouter.common.FIRESTORE_SCOUTS
1112
import com.supercilex.robotscouter.common.FIRESTORE_SHARE_TYPE
1213
import com.supercilex.robotscouter.common.FIRESTORE_TIMESTAMP
1314
import com.supercilex.robotscouter.common.FIRESTORE_TYPE
1415
import com.supercilex.robotscouter.server.utils.FIRESTORE_EMAIL
1516
import com.supercilex.robotscouter.server.utils.FIRESTORE_PHONE_NUMBER
17+
import com.supercilex.robotscouter.server.utils.FIRESTORE_PHOTO_URL
1618
import com.supercilex.robotscouter.server.utils.auth
1719
import com.supercilex.robotscouter.server.utils.batch
1820
import com.supercilex.robotscouter.server.utils.delete
@@ -51,6 +53,7 @@ import kotlinx.coroutines.asPromise
5153
import kotlinx.coroutines.async
5254
import kotlinx.coroutines.await
5355
import kotlinx.coroutines.awaitAll
56+
import kotlinx.coroutines.coroutineScope
5457
import kotlinx.coroutines.delay
5558
import kotlinx.coroutines.joinAll
5659
import kotlinx.coroutines.launch
@@ -75,7 +78,7 @@ fun deleteUnusedData(): Promise<*>? = GlobalScope.async {
7578
))
7679
}
7780
val anonymousUser = async {
78-
deleteUnusedData(users.where(
81+
deleteAnonymousUsers(users.where(
7982
FIRESTORE_LAST_LOGIN,
8083
"<",
8184
Timestamps.fromDate(
@@ -142,9 +145,37 @@ fun sanitizeDeletionRequest(event: Change<DeltaDocumentSnapshot>): Promise<*>? {
142145
}
143146
}
144147

145-
private suspend fun CoroutineScope.deleteUnusedData(
148+
private suspend fun deleteAnonymousUsers(
149+
userQuery: Query
150+
) = userQuery.processInBatches(10) { user ->
151+
val userRecord = try {
152+
auth.getUser(user.id).await()
153+
} catch (t: Throwable) {
154+
if (t.asDynamic().code != "auth/user-not-found") throw t else null
155+
}
156+
157+
if (userRecord == null || userRecord.providerData.orEmpty().isEmpty()) {
158+
purgeUser(user)
159+
} else {
160+
console.log("Correcting user inadvertently marked as anonymous: " +
161+
JSON.stringify(userRecord.toJSON()))
162+
163+
val payload = json()
164+
userRecord.email?.let { payload[FIRESTORE_EMAIL] = it }
165+
userRecord.displayName?.let { payload[FIRESTORE_NAME] = it }
166+
userRecord.phoneNumber?.let { payload[FIRESTORE_PHONE_NUMBER] = it }
167+
userRecord.photoURL?.let { payload[FIRESTORE_PHOTO_URL] = it }
168+
user.ref.set(payload, SetOptions.merge).await()
169+
}
170+
}
171+
172+
private suspend fun deleteUnusedData(
146173
userQuery: Query
147174
) = userQuery.processInBatches(10) { user ->
175+
purgeUser(user)
176+
}
177+
178+
private suspend fun purgeUser(user: DocumentSnapshot) = coroutineScope {
148179
console.log("Deleting all data for user:\n${JSON.stringify(user.data())}")
149180

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

271301
deletionQueue.doc(user.id).delete().await()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.supercilex.robotscouter.server.functions
2+
3+
import com.supercilex.robotscouter.common.FIRESTORE_LAST_LOGIN
4+
import com.supercilex.robotscouter.common.FIRESTORE_NAME
5+
import com.supercilex.robotscouter.server.utils.FIRESTORE_EMAIL
6+
import com.supercilex.robotscouter.server.utils.FIRESTORE_PHONE_NUMBER
7+
import com.supercilex.robotscouter.server.utils.FIRESTORE_PHOTO_URL
8+
import com.supercilex.robotscouter.server.utils.types.SetOptions
9+
import com.supercilex.robotscouter.server.utils.types.Timestamps
10+
import com.supercilex.robotscouter.server.utils.types.UserInfo
11+
import com.supercilex.robotscouter.server.utils.users
12+
import kotlinx.coroutines.GlobalScope
13+
import kotlinx.coroutines.asPromise
14+
import kotlinx.coroutines.async
15+
import kotlinx.coroutines.await
16+
import kotlin.js.Date
17+
import kotlin.js.Promise
18+
import kotlin.js.json
19+
20+
fun initUser(user: UserInfo): Promise<*>? = GlobalScope.async {
21+
console.log("Initializing user: ${JSON.stringify(user.toJSON())}")
22+
23+
users.doc(user.uid).set(json(
24+
FIRESTORE_LAST_LOGIN to Timestamps.fromDate(Date()),
25+
FIRESTORE_EMAIL to user.email,
26+
FIRESTORE_NAME to user.displayName,
27+
FIRESTORE_PHONE_NUMBER to user.phoneNumber,
28+
FIRESTORE_PHOTO_URL to user.photoURL
29+
), SetOptions.merge).await()
30+
}.asPromise()

app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/utils/Constants.kt

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import kotlin.js.Date
1818

1919
const val FIRESTORE_EMAIL = "email"
2020
const val FIRESTORE_PHONE_NUMBER = "phoneNumber"
21+
const val FIRESTORE_PHOTO_URL = "photoUrl"
2122

2223
val firestore by lazy { admin.firestore() }
2324
val auth by lazy { admin.auth() }

app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/utils/types/Auth.kt

+13-5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ package com.supercilex.robotscouter.server.utils.types
1010

1111
import kotlin.js.Promise
1212

13+
external class FunctionsAuth {
14+
fun user(): UserBuilder = definedExternally
15+
}
16+
17+
external class UserBuilder {
18+
fun onCreate(handler: (UserInfo) -> Promise<*>?): dynamic = definedExternally
19+
}
20+
1321
external interface UserMetadata {
1422
val lastSignInTime: String
1523
val creationTime: String
@@ -28,14 +36,14 @@ external interface UserInfo {
2836

2937
external interface UserRecord {
3038
val uid: String
31-
val email: String
39+
val email: String?
3240
val emailVerified: Boolean
33-
val displayName: String
34-
val phoneNumber: String
35-
val photoURL: String
41+
val displayName: String?
42+
val phoneNumber: String?
43+
val photoURL: String?
3644
val disabled: Boolean
3745
val metadata: UserMetadata
38-
val providerData: Array<UserInfo>
46+
val providerData: Array<UserInfo>?
3947
val passwordHash: String? get() = definedExternally
4048
val passwordSalt: String? get() = definedExternally
4149
val customClaims: Any? get() = definedExternally

app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/utils/types/Functions.kt

+1
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ external class Functions {
7777
val firestore: NamespaceBuilder = definedExternally
7878
val pubsub: Pubsub = definedExternally
7979
val https: Https = definedExternally
80+
val auth: FunctionsAuth = definedExternally
8081
fun runWith(options: dynamic): Functions
8182
}
8283

library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Database.kt

+1-40
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import com.google.firebase.firestore.SetOptions
2323
import com.google.firebase.firestore.WriteBatch
2424
import com.google.firebase.firestore.ktx.firestore
2525
import com.google.firebase.ktx.Firebase
26-
import com.google.gson.Gson
2726
import com.supercilex.robotscouter.common.DeletionType
2827
import com.supercilex.robotscouter.common.FIRESTORE_CONTENT_ID
2928
import com.supercilex.robotscouter.common.FIRESTORE_LAST_LOGIN
@@ -35,7 +34,6 @@ import com.supercilex.robotscouter.core.CrashLogger
3534
import com.supercilex.robotscouter.core.data.client.retrieveLocalMedia
3635
import com.supercilex.robotscouter.core.data.client.retrieveShouldUpload
3736
import com.supercilex.robotscouter.core.data.client.startUploadMediaJob
38-
import com.supercilex.robotscouter.core.data.model.add
3937
import com.supercilex.robotscouter.core.data.model.fetchLatestData
4038
import com.supercilex.robotscouter.core.data.model.forceUpdate
4139
import com.supercilex.robotscouter.core.data.model.isStale
@@ -48,15 +46,11 @@ import com.supercilex.robotscouter.core.data.model.userRef
4846
import com.supercilex.robotscouter.core.isMain
4947
import com.supercilex.robotscouter.core.logBreadcrumb
5048
import com.supercilex.robotscouter.core.mainHandler
51-
import com.supercilex.robotscouter.core.model.User
5249
import kotlinx.coroutines.Dispatchers
5350
import kotlinx.coroutines.GlobalScope
5451
import kotlinx.coroutines.invoke
5552
import kotlinx.coroutines.launch
56-
import kotlinx.coroutines.sync.Mutex
57-
import kotlinx.coroutines.sync.withLock
5853
import kotlinx.coroutines.tasks.await
59-
import java.io.File
6054
import java.lang.reflect.Field
6155
import java.util.Calendar
6256
import java.util.concurrent.CopyOnWriteArrayList
@@ -161,29 +155,15 @@ private val teamUpdater = object : ChangeEventListenerBase {
161155
}
162156
}
163157

164-
private val dbCacheLock = Mutex()
165-
166158
fun initDatabase() {
167159
if (BuildConfig.DEBUG) FirebaseFirestore.setLoggingEnabled(true)
168160
teams.addChangeEventListener(teamTemplateIdUpdater)
169161
teams.addChangeEventListener(teamUpdater)
170162

171163
FirebaseAuth.getInstance().addAuthStateListener {
172164
val user = it.currentUser
173-
if (user == null) {
174-
GlobalScope.launch(Dispatchers.IO) {
175-
dbCacheLock.withLock { dbCache.deleteRecursively() }
176-
}
177-
} else {
165+
if (user != null) {
178166
updateLastLogin.run()
179-
180-
User(
181-
user.uid,
182-
user.email.nullOrFull(),
183-
user.phoneNumber.nullOrFull(),
184-
user.displayName.nullOrFull(),
185-
user.photoUrl?.toString()
186-
).smartWrite(userCache) { it.add() }
187167
}
188168
}
189169
}
@@ -246,25 +226,6 @@ fun <T> ObservableSnapshotArray<T>.asLiveData(): LiveData<ObservableSnapshotArra
246226
}
247227
}
248228

249-
private inline fun <reified T> T.smartWrite(file: File, crossinline write: (t: T) -> Unit) {
250-
val new = this
251-
GlobalScope.launch(Dispatchers.IO) {
252-
val cache = {
253-
write(new)
254-
file.safeCreateNewFile().writeText(Gson().toJson(new))
255-
}
256-
257-
dbCacheLock.withLock {
258-
if (file.exists()) {
259-
val cached = Gson().fromJson(file.readText(), T::class.java)
260-
if (new != cached) cache()
261-
} else {
262-
cache()
263-
}
264-
}
265-
}
266-
}
267-
268229
internal sealed class QueuedDeletion(
269230
id: String,
270231
type: DeletionType,

library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Io.kt

-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import android.os.Build
55
import android.os.Environment
66
import androidx.annotation.RequiresPermission
77
import androidx.annotation.WorkerThread
8-
import com.supercilex.robotscouter.core.RobotScouter
98
import java.io.File
109

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

25-
internal val dbCache = File(RobotScouter.cacheDir, "db")
26-
internal val userCache = File(dbCache, "user.json")
27-
2824
@get:WorkerThread
2925
@get:RequiresPermission(value = Manifest.permission.WRITE_EXTERNAL_STORAGE)
3026
val exportsFolder

library/core-data/src/main/java/com/supercilex/robotscouter/core/data/model/Users.kt

-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.supercilex.robotscouter.core.data.model
22

3-
import com.google.firebase.firestore.SetOptions
43
import com.google.firebase.functions.ktx.functions
54
import com.google.firebase.ktx.Firebase
65
import com.supercilex.robotscouter.common.FIRESTORE_PREFS
@@ -11,7 +10,6 @@ import com.supercilex.robotscouter.core.data.deletionQueueRef
1110
import com.supercilex.robotscouter.core.data.logFailures
1211
import com.supercilex.robotscouter.core.data.uid
1312
import com.supercilex.robotscouter.core.data.usersRef
14-
import com.supercilex.robotscouter.core.model.User
1513

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

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

28-
internal fun User.add() {
29-
val ref = getUserRef(uid)
30-
ref.set(this, SetOptions.merge()).logFailures("addUser", ref, this)
31-
}
32-
3326
private fun getUserRef(uid: String) = usersRef.document(uid)
3427

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

library/core-model/src/main/java/com/supercilex/robotscouter/core/model/User.kt

-31
This file was deleted.

0 commit comments

Comments
 (0)