How can I compare two values at two separate locations in a Firebase Realtime Database using the security rules language?

Viewed 53

I want to compare data at one node with data located at an entirely different node.

Please know, in advance I cannot change any front end codes, by instruction of my project lead.

Context: I'm building for a client which will be an upscale chatting app for people who want to find new roommates on college campuses. There are two user types, leasees (leasee) and leasors (leasor).

leasor users add listings and have profiles, while leasee users also have profiles and initiate communication by reaching out to leasor users first with a roommateRequest - it's a chat message but its basically like a Skype, Facebook or LinkedIn request if you will.

I want both user types to be able to write to or remove UIDs from the roommates node (their roommates list on the app), similar to a Facebook friends list. However, when a leasee has no roommates they cannot write to the roommates node because they send a roommateRequest to leasor users and leasor users cannot send a roommateRequest to a leasee user profile.

On the frontend iOS client, the leasor user accepts a roommateRequest - which simultaneously writes to both the leasor user's roommates node & the requesting leasee user's roommates node.

Here is the front end iOS client code related to the database data I will show you next:

import Foundation
import FirebaseAuth
import FirebaseDatabase

class ChatFunctionService {
    
    func sendRoommateRequestToId(text: String, _ id: String, completionHandler: @escaping() -> Void) {
        
        let timestamp = Date().timeIntervalSince1970
        guard let uid = Auth.auth().currentUser?.uid,
        let expiration = Calendar.current.date(byAdding: .day, value: 7, to: Date())?.timeIntervalSince1970 else {
            return
        }
        var values: [String: Any] = ["expiration": expiration, "status": "pending"]
        values["receiverProfileType"] = otherUserTypeTextString
        
        Database.database().reference().child("roommateRequests").childByAutoId().setValue(values) { _, ref in
            
            let message = UserChatModel(
                dictionary: [
                    "text": text,
                    "senderId": uid,
                    "receiverId": id,
                    "timestamp": timestamp,
                    "roommateRequestId": ref.key!,
                    "receiverProfileType": self.otherUserTypeTextString
                ]
            )
            self.sendChat(message, toUser: id) {
                completionHandler()
            }
        }
    }
    
    ///Send message:
    private func sendChat(_ message: UserChatModel, toUser receiverId: String, completion: @escaping() -> Void) {
        
        guard let uid = Auth.auth().currentUser?.uid,
        let partnerId = message.getPartnerId() else { return }
        
        Database.database().reference().child("messages").childByAutoId().updateChildValues(message.data) { _, ref in
            
            let messageUID = ref.key!
            let senderRef = Database.database().reference().child("conversations").child(uid).child(self.userTypeTextString).child(receiverId)
            let receiverRef = Database.database().reference().child("conversations").child(receiverId).child(self.otherUserTypeTextString).child(uid)
            
            ref.updateChildValues(["uid": messageUID])
            senderRef.updateChildValues([messageUID: 1])
            receiverRef.updateChildValues([messageUID: 1])
            
            self.updateChatsData(chat: message, partnerUID: partnerId, messageUID: messageUID)
            completion()
        }
    }
    
    func acceptRoommateRequest(roommateRequestId: String, userTypeTextString: String, receiverUID: String, otherUserTypeTextString: String, completion: @escaping () -> Void) {
        
        guard let uid = Auth.auth().currentUser?.uid else {
            completion()
            return
        }
        Database.database().reference().child("roommateRequests").child(roommateRequestId).child("status").setValue("accepted")
        
        let values = [
            "/\(uid)/\(userTypeTextString)/\(receiverUID)": 1,
            "/\(receiverUID)/\(otherUserTypeTextString)/\(uid)": 1
        ]
        Database.database().reference().child("roommates").updateChildValues(values)
        
        updateChatsMetaDataForNewRoommate(userTypeTextString: userTypeTextString, receiverUID: receiverUID, otherUserTypeTextString: otherUserTypeTextString) {
            completion()
        }
    }
    
    func updateChatsMetaDataForNewRoommate(userTypeTextString: String, receiverUID: String, otherUserTypeTextString: String, completion: @escaping () -> Void ) {
        
        guard let uid = Auth.auth().currentUser?.uid else { return }
        
        Database.database().reference().child("chatsData").child(uid).child(userTypeTextString).child(receiverUID).child("roommates").setValue(true)
        Database.database().reference().child("chatsData").child(receiverUID).child(otherUserTypeTextString).child(uid).child("roommates").setValue(true)
        
        sendUserChat(text: "roommate request granted.", receiverUID: receiverUID) { (messageId) in
            completion()
        }
    }
    
    func sendUserChat(text: String, receiverUID: String, completion: @escaping(String?) -> Void) {
        
        let timestamp = Date().timeIntervalSince1970
        guard let uid = Auth.auth().currentUser?.uid else { return }
        
        var chatDictionary: [String: Any] = [
            "text": text, "timestamp": timestamp,
            "senderId": uid, "receiverId": receiverUID
        ]
        chatDictionary["receiverProfileType"] = "leasee"
        let chat = UserChatModel(dictionary: chatDictionary)
        
        guard let partnerUID = chat.getPartnerId() else {
            return
        }
        Database.database().reference().child("threads").childByAutoId().updateChildValues(chat.data) { _, ref in
            
            let messageUID = ref.key!
            ref.updateChildValues(["uid": messageUID])
            
            self.updateChatsData(
                chat: chat,
                partnerUID: partnerUID,
                messageUID: messageUID
            )
            completion(messageUID)
        }
    }
    
    func updateChatsData(chat: UserChatModel, partnerUID: String, messageUID: String) {
        
        guard let uid = Auth.auth().currentUser?.uid else {
            return
        }
        var chatData = [String: Any]()
        
        chatData["lastMessageSenderId"] = uid
        chatData["lastMessageId"] = messageUID
        chatData["lastMessage"] = chat.text ?? ""
        chatData["chatMemberIds"] = [uid, partnerUID]
        
        Database.database().reference().child("chatsMetaData").child(uid).child(userTypeTextString).child(partnerUID).setValue(chatData)
        Database.database().reference().child("chatsMetaData").child(partnerUID).child(otherUserTypeTextString).child(uid).setValue(chatData)
    }
}

class UserChatModel {
    
    var text: String?
    var senderId: String
    var receiverId: String
    var timeStamp: NSNumber
    var data: [String: Any]
    var roommateRequestId: String?
    
    init(dictionary: [String: Any]) {
        
        data = dictionary
        text = dictionary["text"] as? String
        senderId = dictionary["senderId"] as! String
        timeStamp = dictionary["timestamp"] as! NSNumber
        receiverId = dictionary["receiverId"] as? String ?? ""
        roommateRequestId = dictionary["roommateRequestId"] as? String
        
        super.init(dictionary: dictionary)
    }
    
    func getPartnerId() -> String? {
        
        guard let uid = Auth.auth().currentUser?.uid else {
            return nil
        }
        return senderId == uid ? receiverId : senderId
    }
}

Once the leasee has a single roommate, they should be able to remove that roommate from their roommates list (if they so wish). Continually, only leasor users can accept a roommateRequest which writes to both the roomateRequest sender (leasee) user's UID in the roommates node and the receiver (leasor) user's UID in the roommates node. Moreover, leasor users can also remove leasee users from their roommates node, at anytime.

The issue is I don't want just any user to be able to write or update the roommates node of any user unless it is removing themselves from it.

Here is the full realtime-database structure for the chat portion of the app, in two screenshot images:

top three nodes bottom two nodes

I've narrowed my works down (between the iOS front end, database structure and security rules) to matching the lastMessageId value from the chatsMetaData or chats node to then check the threads node to get the roommateRequestId and then compare the roommateRequestId to the 'roommateRequests' node to verify that the leasor can write to the leasee's node. And then I will provide an OR case and match the value at the roommates node to the value of auth.uid for after they are already on each other's roommate's list for when a user wants to write to their roommates node (removing a roommate) - but I have no idea on how to do any of this. I know what to do but I am not sure how to achieve it...

I think I can reference the chatsMetaData node all the way down to the 'lastMessageId' value : lastMessageId

With this value I think I should then search the threads node to get the roommateRequestId and then search the roommateRequests node to match it and allow a write along with some auth.uid checks to match uid values in the node's data. But how can I do all this if they are by autoId way?

enter image description here

0 Answers
Related