diff --git a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/Index.kt b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/Index.kt index 4355ffc37..b8f45e96b 100644 --- a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/Index.kt +++ b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/Index.kt @@ -5,6 +5,7 @@ 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.processClientRequest import com.supercilex.robotscouter.server.functions.sanitizeDeletionRequest import com.supercilex.robotscouter.server.functions.transferUserData import com.supercilex.robotscouter.server.functions.updateDefaultTemplates @@ -29,26 +30,33 @@ fun main() { exports.updateDefaultTemplates = functions.pubsub.topic("update-default-templates") .onPublish { _, _ -> updateDefaultTemplates() } - val cleanupRuntime = json("timeoutSeconds" to 300, "memory" to "512MB") - exports.emptyTrash = functions.runWith(cleanupRuntime).https - .onCall { data: Array?, context -> emptyTrash(data, context) } + val cleanupRuntime = json("timeoutSeconds" to 540, "memory" to "512MB") exports.cleanup = functions.runWith(cleanupRuntime).pubsub.topic("daily-tick") .onPublish { _, _ -> emptyTrash() } exports.deleteUnusedData = functions.runWith(cleanupRuntime).pubsub.topic("daily-tick") .onPublish { _, _ -> deleteUnusedData() } exports.sanitizeDeletionQueue = functions.firestore.document("${deletionQueue.id}/{uid}") .onWrite { event, _ -> sanitizeDeletionRequest(event) } - - val transferRuntime = json("timeoutSeconds" to 540, "memory" to "1GB") - exports.transferUserData = functions.runWith(transferRuntime).https - .onCall { data: Json, context -> transferUserData(data, context) } - exports.updateOwners = functions.runWith(transferRuntime).https - .onCall { data: Json, context -> updateOwners(data, context) } exports.mergeDuplicateTeams = functions - .runWith(json("timeoutSeconds" to 300, "memory" to "256MB")) + .runWith(json("timeoutSeconds" to 540, "memory" to "1GB")) .firestore.document("${duplicateTeams.id}/{uid}") .onWrite { event, _ -> mergeDuplicateTeams(event) } + // TODO remove once we stop getting API requests + exports.emptyTrash = functions.runWith(cleanupRuntime).https + .onCall { data: Array?, context -> + emptyTrash(json("ids" to data), context) + } + exports.transferUserData = functions + .runWith(json("timeoutSeconds" to 540, "memory" to "1GB")) + .https.onCall { data: Json, context -> transferUserData(data, context) } + exports.updateOwners = functions + .runWith(json("timeoutSeconds" to 540, "memory" to "1GB")) + .https.onCall { data: Json, context -> updateOwners(data, context) } + + exports.clientApi = functions.runWith(json("timeoutSeconds" to 540, "memory" to "2GB")) + .https.onCall { data: Json, context -> processClientRequest(data, context) } + exports.initUser = functions.auth.user().onCreate { user -> initUser(user) } diff --git a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Cleanup.kt b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Cleanup.kt index e5d8e6e10..e568bda1a 100644 --- a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Cleanup.kt +++ b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Cleanup.kt @@ -32,6 +32,7 @@ import com.supercilex.robotscouter.server.utils.templates import com.supercilex.robotscouter.server.utils.toMap import com.supercilex.robotscouter.server.utils.toTeamString import com.supercilex.robotscouter.server.utils.toTemplateString +import com.supercilex.robotscouter.server.utils.types.AuthContext import com.supercilex.robotscouter.server.utils.types.CallableContext import com.supercilex.robotscouter.server.utils.types.Change import com.supercilex.robotscouter.server.utils.types.CollectionReference @@ -103,8 +104,14 @@ fun emptyTrash(): Promise<*>? = GlobalScope.async { ).processInBatches(10) { processDeletion(it) } }.asPromise() -fun emptyTrash(data: Array?, context: CallableContext): Promise<*>? { +fun emptyTrash(data: Json, context: CallableContext): Promise<*>? { val auth = context.auth ?: throw HttpsError("unauthenticated") + return emptyTrash(auth, data) +} + +fun emptyTrash(auth: AuthContext, data: Json): Promise<*>? { + @Suppress("UNCHECKED_CAST") + val ids = data["ids"] as? Array? console.log("Emptying trash for ${auth.uid}.") return GlobalScope.async { @@ -115,7 +122,7 @@ fun emptyTrash(data: Array?, context: CallableContext): Promise<*>? { return@async } - processDeletion(requests, data.orEmpty().toList()) + processDeletion(requests, ids.orEmpty().toList()) }.asPromise() } diff --git a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/ClientApi.kt b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/ClientApi.kt new file mode 100644 index 000000000..0f72a9218 --- /dev/null +++ b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/ClientApi.kt @@ -0,0 +1,32 @@ +package com.supercilex.robotscouter.server.functions + +import com.supercilex.robotscouter.server.utils.types.CallableContext +import com.supercilex.robotscouter.server.utils.types.HttpsError +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.asPromise +import kotlinx.coroutines.async +import kotlin.js.Json +import kotlin.js.Promise + +fun processClientRequest(data: Json, context: CallableContext): Promise { + val auth = context.auth ?: throw HttpsError("unauthenticated") + val rawOperation = data["operation"] as? String + console.log( + "Processing '$rawOperation' operation for user '${auth.uid}' with args: ", + JSON.stringify(data) + ) + + if (rawOperation == null) { + throw HttpsError("invalid-argument", "An operation must be supplied.") + } + val operation = rawOperation.toUpperCase().replace("-", "_") + + return GlobalScope.async { + when (operation) { + "EMPTY_TRASH" -> emptyTrash(auth, data) + "TRANSFER_USER_DATA" -> transferUserData(auth, data) + "UPDATE_OWNERS" -> updateOwners(auth, data) + else -> throw HttpsError("invalid-argument", "Unknown operation: $rawOperation") + } + }.asPromise() +} diff --git a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Transfer.kt b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Transfer.kt index d2b47634a..8279c91e8 100644 --- a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Transfer.kt +++ b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/functions/Transfer.kt @@ -29,6 +29,7 @@ import com.supercilex.robotscouter.server.utils.processInBatches import com.supercilex.robotscouter.server.utils.teams import com.supercilex.robotscouter.server.utils.toMap import com.supercilex.robotscouter.server.utils.toTeamString +import com.supercilex.robotscouter.server.utils.types.AuthContext import com.supercilex.robotscouter.server.utils.types.CallableContext import com.supercilex.robotscouter.server.utils.types.Change import com.supercilex.robotscouter.server.utils.types.DeltaDocumentSnapshot @@ -52,11 +53,14 @@ import kotlin.js.Promise import kotlin.js.json fun transferUserData(data: Json, context: CallableContext): Promise<*>? { - val auth = context.auth + val auth = context.auth ?: throw HttpsError("unauthenticated") + return transferUserData(auth, data) +} + +fun transferUserData(auth: AuthContext, data: Json): Promise<*>? { val token = data[FIRESTORE_TOKEN] as? String val prevUid = data[FIRESTORE_PREV_UID] as? String - if (auth == null) throw HttpsError("unauthenticated") if (token == null || prevUid == null) throw HttpsError("invalid-argument") if (prevUid == auth.uid) { throw HttpsError("already-exists", "Cannot add and remove the same user") @@ -132,12 +136,15 @@ fun transferUserData(data: Json, context: CallableContext): Promise<*>? { } fun updateOwners(data: Json, context: CallableContext): Promise<*>? { - val auth = context.auth + val auth = context.auth ?: throw HttpsError("unauthenticated") + return updateOwners(auth, data) +} + +fun updateOwners(auth: AuthContext, data: Json): Promise<*>? { val token = data[FIRESTORE_TOKEN] as? String val path = data[FIRESTORE_REF] as? String val prevUid = data[FIRESTORE_PREV_UID] - if (auth == null) throw HttpsError("unauthenticated") if (token == null || path == null) throw HttpsError("invalid-argument") if (prevUid != null) { if (prevUid !is String) { diff --git a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/utils/types/Https.kt b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/utils/types/Https.kt index 263971c80..08510a70c 100644 --- a/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/utils/types/Https.kt +++ b/app/server/functions/src/main/kotlin/com/supercilex/robotscouter/server/utils/types/Https.kt @@ -11,10 +11,9 @@ package com.supercilex.robotscouter.server.utils.types import kotlin.js.Promise @Suppress("FunctionName", "UNUSED_PARAMETER", "UNUSED_VARIABLE") // Fake class -fun HttpsError(code: String, message: String? = null, details: Any? = null): Nothing { +fun HttpsError(code: String, message: String? = null, details: Any? = null): Throwable { val functions = functions - js("throw new functions.https.HttpsError(code, message, details)") - throw Exception() // Never going to get called + return js("new functions.https.HttpsError(code, message, details)") } external class Https { diff --git a/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Cleanup.kt b/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Cleanup.kt index 05808cbd6..a1d185d10 100644 --- a/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Cleanup.kt +++ b/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Cleanup.kt @@ -19,6 +19,6 @@ fun cleanup() { } fun emptyTrash(ids: List? = null) = Firebase.functions - .getHttpsCallable("emptyTrash") - .call(ids) + .getHttpsCallable("clientApi") + .call(mapOf("operation" to "empty-trash", "ids" to ids)) .logFailures("emptyTrash", ids) diff --git a/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Links.kt b/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Links.kt index b8b6828ce..5e8025dba 100644 --- a/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Links.kt +++ b/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/Links.kt @@ -98,8 +98,9 @@ suspend fun updateOwner( newValue: (DocumentReference) -> Any ) = refs.map { ref -> Firebase.functions - .getHttpsCallable("updateOwners") + .getHttpsCallable("clientApi") .call(mapOf( + "operation" to "update-owners", FIRESTORE_TOKEN to token, FIRESTORE_REF to ref.path, FIRESTORE_PREV_UID to prevUid, diff --git a/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/model/Users.kt b/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/model/Users.kt index 4a8f939ca..932c2ae79 100644 --- a/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/model/Users.kt +++ b/library/core-data/src/main/java/com/supercilex/robotscouter/core/data/model/Users.kt @@ -19,8 +19,12 @@ val userPrefs get() = getUserPrefs(checkNotNull(uid)) val userDeletionQueue get() = deletionQueueRef.document(checkNotNull(uid)) fun transferUserData(prevUid: String, token: String) = Firebase.functions - .getHttpsCallable("transferUserData") - .call(mapOf(FIRESTORE_PREV_UID to prevUid, FIRESTORE_TOKEN to token)) + .getHttpsCallable("clientApi") + .call(mapOf( + "operation" to "transfer-user-data", + FIRESTORE_PREV_UID to prevUid, + FIRESTORE_TOKEN to token + )) .logFailures("transferUserData", prevUid, token) private fun getUserRef(uid: String) = usersRef.document(uid)