How to retrieve an authenticated user data with Firestore in iOS? - ios

I'm playing around with Firebase and I'm using their authentication framework plus Firestore database.
My database model is quite simple: I want to keep a collection "users" which will hold a document -identified by the user's uid- for each registered user. This user document will hold a collection of documents representing user expenses.
Following their documentation I've set up the rules in the firebase console as shown beneath:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId=**} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}
And in my iOS app, when user logs in, I want to retrieve the user's expenses with the following code:
func retrieveUserExpenses() {
guard let dbRef = userExpensesDbRef() else {
print("Attempting to retrieve expense for unlogged user")
return
}
dbRef.getDocuments { [unowned self] (snapshot, err) in
if let err = err {
print("Error retrieving expenses \(err)")
} else {
print("Retrieved expenses!")
snapshot?.documents.forEach {
if let expense = self.createExpenseFromDocument($0) {
self.expenses.append(expense)
}
}
self.loadedExpenses = true
}
}
}
private func userExpensesDbRef() -> CollectionReference? {
guard let loggedInUser = AuthHandler.shared.loggedInUser() else {
return nil
}
return db.collection("users").document(loggedInUser.uid).collection("expenses")
}
The thing is that the call to getDocuments prints the error line:
Error retrieving expenses Error Domain=FIRFirestoreErrorDomain Code=7
"Missing or insufficient permissions."
I've checked by printing that this: loggedInUser.uid is the same one I see on the Firebase Console in the Authentication section in the Users tab.
Any guidance or tip on where I'm failing to securely retrieve the data is greatly appreciated.

Go in Database -> Rules ->
Change allow read, write: if false; to true;
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if true;
allow write: if false;
}
}
}

Related

How to get user specific data from firebase in iOS app?

Firstly I've reviewed Firebase documentation, they only have two separate explanations on login user and CRUD operations but not really how both they can be connected with security rules. Despite that I've worked on it here is the security rules. In iOS code I fetch the list and also add the item with user id
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /contacts/{creatorsID} {
allow read, write: if request.auth != null && request.auth.uid == creatorsID;
}
}
}
ERROR: Write at contacts/wv6fMMhBpw3ROEOyUMBe failed: Missing or insufficient permissions
{ creatorsID: "", name: "" }
func fetchData() {
db.collection("contacts").addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.contacts = documents.map { (queryDocumentSnapshot) -> Contact in
let data = queryDocumentSnapshot.data()
let name = data["name"] as? String ?? ""
let userID = data["creatorsID"] as? String ?? ""
return Contact(name: name)
}
}
}
func addData(name: String) {
do {
_ = try db.collection("contacts").addDocument(data: ["name": name, "creatorsID": Auth.auth().currentUser?.uid])
}
catch {
print(error.localizedDescription)
}
}
The problem here is that I still receive all the items from all users instead of specific user's list(FYI, CreatorsId is as same as UID). I can't figure out if the issue is in rules or in iOS code.
You're getting all users because you read the data with:
db.collection("contacts").addSnapshotListener...
So this gives you all documents from the contacts collection. If you want to only read a single document, you'll have to identify that document either by its ID or by using a query.
Since you use addDocument to add the document, Firestore generates a unique ID for you. That means that with your current data structure, the way to get the user document is by querying on the createdID field:
db.collection("contacts")
.whereField("createdID", isEqualTo: Auth.auth().currentUser?.uid)
.addSnapshotListener { (querySnapshot, error) in
...
A more idiomatic way to store user profile documents is to use the UID of each user as the ID of the document too. That both guarantees that each user can only have one document (as document IDs are by definition unique within a collection), and it means you can look up the profile document for a user without a query:
db.collection("contacts")
.document(Auth.auth().currentUser?.uid)
.addSnapshotListener { (documentSnapshot, error) in
....
Doing this will also solve your security errors, as this code matches exactly with what you're checking here:
match /contacts/{createdID} {
if request.auth != null && request.auth.uid == createdID
In your current approach the value of createdID is the random ID that Firestore generates when you call addDocument, and not the user's UID.

Get Firestore Subcollections - Permission Denied

I am pulling my hair out at this problem. I am attempting to get documents from a subcollection, like so:
func getTransactions(completion: ((_ data: [Any]?) -> Void)? = nil ) {
if let fbUser = Auth.auth().currentUser {
let uid = fbUser.uid
//Make a call to the Database to load the user
db.collection("users").document(uid).collection("transactions")
.getDocuments(completion: { (snapshot, error) in
if error != nil {
print(error)
}
})
} else {
print("What")
}
}
This should theoretically work fine, especially since my rules are like this
match /users/{user_id} {
allow get, create: if request.auth.uid != null
allow update, delete: if request.auth.uid == user_id
}
match /users/{user_id}/transactions/{id=**} {
allow read, get, create: if request.auth.uid != null
allow update, delete: if request.auth.uid == user_id
}
match /users/{user_id}/secret {
allow read, write: if false
}
But when I run the function, I am getting the good ol' missing permissions error.
Error Domain=FIRFirestoreErrorDomain Code=7 "Missing or insufficient permissions." UserInfo={NSLocalizedDescription=Missing or insufficient permissions.}
Even more confusingly, I can //write// to the location just fine, as well as pull all information on the {user} doc, but I just can't... get access to the transaction (or testing, for that matter) subcollection for some reason.
Any help? Ideas? Input?

Firebase Realtime Database doesn't save data from sign up page

I am working on sign up page of application in Swift. The part of authentication in Firebase works well, but the database doesn't save any information I request. Can anyone help?
My code:
Auth.auth().createUser(withEmail: userEmail,password: userPassword, completion: {(User, error) in
if error != nil {
print(error as Any)
return
}
guard let uid = User?.user.uid else {return}
let ref = Database.database().reference(fromURL:"Database-URL")
let userReference = ref.child("users").child(uid)
let values = ["Firstname": userFirstName,"email": userEmail]
userReference.updateChildValues(values, withCompletionBlock: { (error, reference) in
if error != nil {
print(error as Any)
return
}
})
})
The console prints an error
Optional(Error Domain=com.firebase Code=1 "Permission denied"
UserInfo={NSLocalizedDescription=Permission denied})
By default the database in a project in the new Firebase Console is only readable/writeable by authenticated users:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
See the quickstart for the Firebase Database security rules.
Since you're not signing the user in from your code, the database denies you access to the data. To solve that you will either need to allow unauthenticated access to your database, or sign in the user before accessing the database.
Allow unauthenticated access to your database
The simplest workaround for the moment (until the tutorial gets updated) is to go into the Database panel in the console for you project, select the Rules tab and replace the contents with these rules:
{
"rules": {
".read": true,
".write": true
}
}
This makes your new database readable and writeable by everyone. Be certain to secure your database again before you go into production, otherwise somebody is likely to start abusing it.
I may not be sure but the completion for createUser doesnot give you User and error rather AuthResult and Error. So you have to get the user from result as below
Auth.auth().createUser(withEmail: email, password: password) { (authData, error) in
if let error = error {
debugPrint("FIREBASE ERROR : \(error.localizedDescription)")
} else {
if let authData = authData {
let user = authData.user //here get the user from result
self.saveToDB(user: user) . //save the user to database
}
}
}
This is the new code for firebase from may 2019. just change false to true like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}

CloudKit Sharing

I am having trouble understanding some of the CloudKit sharing concepts and the WWDC 2016 "What's new in CloudKit" video doesn't appear to explain everything that is required to allow users to share and access shared records.
I have successfully created an app that allows the user to create and edit a record in their private database.
I have also been able to create a Share record and share this using the provided sharing UIController. This can be successfully received and accepted by the participant user but I can't figure out how to query and display this shared record.
The app creates a "MainZone" in the users private database and then creates a CKRecord in this "MainZone". I then create and save a CKShare record and use this to display the UICloudSharingController.
How do I query the sharedDatabase in order to access this record ? I have tried using the same query as is used in the privateDatabase but get the following error:
"ShareDB can't be used to access local zone"
EDIT
I found the problem - I needed to process the accepted records in the AppDelegate. Now they appear in the CloudKit dashboard but I am still unable to query them. It seems I may need to fetch the sharedDatabase "MainZone" in order to query them.
Dude, I got it: First you need to get the CKRecordZone of that Shared Record. You do it by doing the following:
let sharedData = CKContainer.default().sharedCloudDatabase
sharedData.fetchAllRecordZones { (recordZone, error) in
if error != nil {
print(error?.localizedDescription)
}
if let recordZones = recordZone {
// Here you'll have an array of CKRecordZone that is in your SharedDB!
}
}
Now, with that array in hand, all you have to do is fetch normally:
func showData(id: CKRecordZoneID) {
ctUsers = [CKRecord]()
let sharedData = CKContainer.default().sharedCloudDatabase
let predicate = NSPredicate(format: "TRUEPREDICATE")
let query = CKQuery(recordType: "Elder", predicate: predicate)
sharedData.perform(query, inZoneWith: id) { results, error in
if let error = error {
DispatchQueue.main.async {
print("Cloud Query Error - Fetch Establishments: \(error)")
}
return
}
if let users = results {
print(results)
self.ctUsers = users
print("\nHow many shares in cloud: \(self.ctUsers.count)\n")
if self.ctUsers.count != 0 {
// Here you'll your Shared CKRecords!
}
else {
print("No shares in SharedDB\n")
}
}
}
}
I didn't understand quite well when you want to get those informations. I'm with the same problem as you, but I only can get the shared data by clicking the URL... To do that you'll need two functions. First one in AppDelegate:
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShareMetadata) {
let acceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [cloudKitShareMetadata])
acceptSharesOperation.perShareCompletionBlock = {
metadata, share, error in
if error != nil {
print(error?.localizedDescription)
} else {
let viewController: ViewController = self.window?.rootViewController as! ViewController
viewController.fetchShare(cloudKitShareMetadata)
}
}
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptSharesOperation)
}
in ViewConroller you have the function that will fetch this MetaData:
func fetchShare(_ metadata: CKShareMetadata) {
let operation = CKFetchRecordsOperation(recordIDs: [metadata.rootRecordID])
operation.perRecordCompletionBlock = { record, _, error in
if error != nil {
print(error?.localizedDescription)
}
if record != nil {
DispatchQueue.main.async() {
self.currentRecord = record
//now you have your Shared Record
}
}
}
operation.fetchRecordsCompletionBlock = { _, error in
if error != nil {
print(error?.localizedDescription)
}
}
CKContainer.default().sharedCloudDatabase.add(operation)
}
As I said before, I'm now trying to fetch the ShareDB without accessing the URL. I don't want to depend on the link once I already accepted the share. Hope this helps you!

Parse + Facebook Questions

I am trying to implement Parse + Facebook
Here is what I would like to do:
User Log In with Facebook
Login Authorized
A new User created from Facebook properties (gender, age, name, etc)
Below is my code which logs in to Facebook using PFFacebookUtils. The code successfully created a User on my Parse, but I don't have those details of the User from Facebook which I want to use.
I would like to get the user's name, gender, hometown, etc on Facebook.
How do I achieve that??
var permissionArray = ["public_profile","user_friends","email","user_birthday", "user_work_history", "user_education_history", "user_hometown", "user_location", "user_likes"]; on Facebook
PFFacebookUtils.logInInBackgroundWithReadPermissions(permissionArray) { (user: PFUser?, error: NSError?) -> Void in
if user != nil {
if user!.isNew {
println("New User");
}
else {
println("Old User");
}
println(user?.username);
}
else {
println("Login Cancel")
}
}
You need to request the users "me" data, and then you can use that to populate your user object. So, in your if user!.isNew { you'll need to do something along the lines of...
let fbDetailsRequest = FBRequest.requestForMe()
fbDetailsRequest.startWithCompletionHandler { connection, result, error in
if error != nil {
// Handle the error
}
if let graph = result as? FBGraphObject {
// Now you can fill out the data on your user object
user!.setObject(graph.objectForKey("email")!, forKey: "email")
user!.setObject(graph.objectForKey("gender")!, forKey: "gender")
// Don't forget to save afterwards...
user!.saveInBackgroundWithBlock(nil)
}
}
To see the fields you can grab, check out https://developers.facebook.com/docs/graph-api/reference/user If you need anything other than "core" items, you might have to have extra permissions.

Resources