I'm following the AWS Amplify tutorials on API and Authentication. While I've made progress to get both working (!), it's not clear to me how I might connect the two features of Amplify so as to separate/segregate (?) user data...
For instance, take this GraphQL Todo Model:
type Todo #model #auth(rules: [{allow: public}]) {
id: ID!
name: String!
description: String
completed: Boolean!
}
I can save and fetch these Todos with a ViewModel:
import Amplify
class TodoListViewModel: ObservableObject {
#Published var todos = [Todo]()
#Published var completedTodos = [Todo]()
func loadToDos() {
Amplify.DataStore.query(Todo.self) { result in
switch result {
case .success(let todos):
self.todos = todos.filter { !$0.completed }
self.completedTodos = todos.filter { $0.completed }
case .failure(let error):
print("Could not query DataStore: \(error)")
}
}
}
func createTodo(name: String, description: String?) {
let item = Todo(name: name, description: description, completed: false)
todos.append(item)
Amplify.DataStore.save(item) { result in
switch result {
case .success(let savedItem):
print("Saved item: \(savedItem.name)")
case .failure(let error):
print("Could not save item with error: \(error)")
}
}
}
}
But these methods seemingly allow any user to access any other users Todo data (?)
Reading through the docs, I think I need to setup authorization rules (?)
If I'm reading this correctly, to make sure that an arbitrary user can only see their data, is it really as simple as changing the GraphQL Todo model to:
type Todo #model #auth(rules: [{allow: owner}]) {
id: ID!
name: String!
description: String
completed: Boolean!
}
That can't be it?
...what other modifications will I need to implement in order to ensure that "Alice" can save and fetch her data and be sure that I'm not mixing it with "Bob's" data?
A fully worked example that uses an Authenticated (logged-in) user would be appreciated!
It really is that simple. That's the value of using Amplify. Every record that is saved to the database has a column called owner. The value of owner is the Id of the Cognito user that created the record.
AppSync's auto-generated resolvers are smart enough to verify that the user asking for data is the same user that owns the data.
Related
Many the tutorials around StoreKit 2 and even Apple's own Sample Code reference "StoreKit testing in Xcode so you can build and run the sample app without completing any setup in App Store Connect. The project defines in-app products for the StoreKit testing server in the Products.storekit file." I have an app with auto renewing subscriptions already set up in App Store Connect (migrating to StoreKit 2 from SwiftyStoreKit)...how can I set up this StoreKitManager class to check for an active subscription without needing to make a separate Products.plist file? This code below, largely based on Apple's sample code, results in Error Domain=ASDErrorDomain Code=509 "No active account" which is obvious as I can't figure out how to connect my products to the StoreKit 2 logic? 🤔
EDIT: here is a gist of my code
import Foundation
import StoreKit
typealias Transaction = StoreKit.Transaction
public enum StoreError: Error {
case failedVerification
}
#available(watchOSApplicationExtension 8.0, *)
class WatchStoreManager: ObservableObject {
var updateListenerTask: Task<Void, Error>? = nil
init() {
print("Init called in WatchStoreManager")
//Start a transaction listener as close to app launch as possible so you don't miss any transactions.
updateListenerTask = listenForTransactions()
}
deinit {
updateListenerTask?.cancel()
}
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
//Iterate through any transactions that don't come from a direct call to `purchase()`.
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
print("we have a verified transacction")
//Deliver products to the user.
//TODO:
//await self.updateCustomerProductStatus()
//Always finish a transaction.
await transaction.finish()
} catch {
//StoreKit has a transaction that fails verification. Don't deliver content to the user.
print("Transaction failed verification")
}
}
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
//Check whether the JWS passes StoreKit verification.
switch result {
case .unverified:
//StoreKit parses the JWS, but it fails verification.
throw StoreError.failedVerification
case .verified(let safe):
//The result is verified. Return the unwrapped value.
return safe
}
}
}
509: No active account
Means the user isn't signed in to the app store. Go to Settings -> Sign in to Your iPhone and sign in with a valid App Store or sandbox account
Edit: I see you are logged in on your device - that's strange. You mentioned you haven't linked your products. You need to fetch products from the App Store with something similar to the following. But I wouldn't expect you to see that specific error message...
enum AppProduct: String, CaseIterable, Identifiable {
case noAds = "MYUNIQUEIDENTIFIER_ESTABLISHEDIN_APPSTORECONNECT"
var id: String {
UUID().uuidString
} // to make it Identifiable for use in a List
static var allProductIds: [String] {
return Self.allCases.map { $0.rawValue }
} // convenience
}
#MainActor
#discardableResult func fetchProducts() async throws -> [Product] {
let products = try await Product.products(for: AppProduct.allProductIds) // this is the call that fetches products
guard products.first != nil else { throw PurchaseError.unknown } // custom error
self._storeProducts = products
let fetchedProducts: [AppProduct] = products.compactMap {
let appProduct = AppProduct(rawValue: $0.id)
return appProduct
}
self.fetchedProducts = fetchedProducts
try await checkPurchased()
return products
}
private func checkPurchased() async throws {
for product in _storeProducts {
guard let state = await product.currentEntitlement else { continue }
let transaction = try self.checkVerified(state)
//Always finish a transaction.
await transaction.finish()
}
}
I'm setting purchase state in checkVerified when verification passes...
I am using Firebase Firestore with Swift and SwiftUI. I have two collections Users and Tweets. Each Tweet store the userId of the user who tweeted. I want to fetch all the tweets along with the user information associated with tweet. Since both are stored in different collections at the root level, this poses a problem. Here is one of the solutions.
private func populateUserInfoIntoTweets(tweets: [Tweet]) {
var results = [Tweet]()
tweets.forEach { tweet in
var tweetCopy = tweet
self.db.collection("users").document(tweetCopy.userId).getDocument(as: UserInfo.self) { result in
switch result {
case .success(let userInfo):
tweetCopy.userInfo = userInfo
results.append(tweetCopy)
if results.count == tweets.count {
DispatchQueue.main.async {
self.tweets = results.sorted(by: { t1, t2 in
t1.dateUpdated > t2.dateUpdated
})
}
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
Kinda messy!
Another way is to store the documentRef of the User doc instead of the userId.
If I store the User document reference then I am using the following code:
init() {
db.collection("tweets").order(by: "dateUpdated", descending: true)
.addSnapshotListener { snapshot, error in
if let error {
print(error.localizedDescription)
return
}
snapshot?.documents.compactMap { document in
try? document.data(as: Tweet.self)
}.forEach { tweet in
var tweetCopy = tweet
tweetCopy.userDocRef?.getDocument(as: UserInfo.self, completion: { result in
switch result {
case .success(let userInfo):
tweetCopy.userInfo = userInfo
self.tweets.append(tweetCopy)
case .failure(let error):
print(error.localizedDescription)
}
})
}
}
}
The above code does not work as expected. I am wondering there must be a better way. I don't want to store all the user information with the Tweet because it will be storing a lot of extra data.
Recommendations?
The common alternative is to duplicate the necessary information of the user in their tweets, a process known as denormalization. If you come from a background in relational databases this may sounds like blasphemy, but it's actually quite common in NoSQL data modeling (and one of the reasons these databases scale to such massive numbers of readers).
If you want to learn more about such data modeling considerations, I recommend checking out NoSQL data modeling and watching Todd's excellent Get to know Cloud Firestore series.
If you're wondering how to keep the duplicated data up to date, this is probably also a good read: How to write denormalized data in Firebase
I'm developing a mobile app using Swift and Realm database.
I configured Realm Device Sync and tried to add custom user data to a cluster I created.
Even though I watched dozens of tutorials about realm permissions I still can't figure out what's wrong with the in-app permissions
here is the authentication function I am using to add Custom User Data
func login() {
isLoading = true
errorMessage = nil
let credentials = Credentials.emailPassword(email: username, password: password)
DispatchQueue.main.async {
app.login(credentials: credentials) { [weak self] result in
switch (result) {
case .failure(let error):
print(String(describing: error))
self?.errorMessage = error.localizedDescription
case .success(let user):
if user.customData.isEmpty {
let client = user.mongoClient("mongodb-atlas")
let database = client.database(named: "UserAPI")
let collection = database.collection(withName: "Users")
// Insert the custom user data object
let customUserData: Document = [
"_id": AnyBSON(user.id),
"email": .string(self!.email),
"province": .string(self!.province),
"_partition": .string(user.id)
]
collection.insertOne(customUserData) {result in
switch result {
case .failure(let error):
print("Failed to insert document: \(error.localizedDescription)")
case .success(let newObjectId):
print("Inserted custom user data document with object ID: \(newObjectId)")
}
}
}
}
self?.isLoading = false
}
}
}
But when I try to create a new user, it successfully creates one. The problem is, when it comes things comes to adding the Custom User Data it returns an error like this:
Failed to insert document: no rule exists for namespace 'UserAPI.Users'
and when I check the MongoDB logs, I can see the error in more detail:
my Custom User Data settings:
and my app permissions:
any help would be appriciated, I'm struggling with this error for 3 days, thanks in advance.
#GrandSirr - have you tried setting "users can read and write all data" permissions template (for development, at least)?
Also, what is your actual 'Users' collection? User custom data should be a separate collection in my opinion as size of user custom data is limited.
My flow - login users with email password - set database triggers to create a new user document with relevant fields that a user can later fill in eg 'profile settings' page of your app and another trigger to create a document in a separate user custom data collection to save some permission roles or subscription to notifications etc.
I am using Parse and created a new colums in the User. I set the field to "Required", but now I can't create a user anymore.
Error i get:
Login failed: ParseError code=142 error=myCustomColumn is required
This is how I did it:
do {
let currentUser = try User.signup(username: "user", password: "password")
print("Login succes: \(currentUser)")
} catch {
print("Login failed: \(error)")
}
How can I set my custom field? It's already created in the struct. I just need to set the value.
I am using ParseSwift.
https://github.com/parse-community/Parse-Swift
This can be done by using the instance version of signUp in the documentation. An example is shown in the playgrounds:
//: To add additional information when signing up a user,
//: you should create an instance of your user first.
var newUser = User(username: "parse", password: "aPassword*", email: "parse#parse.com")
//: Add any other additional information.
newUser.targetScore = .init(score: 40)
newUser.signup { result in
switch result {
case .success(let user):
guard let currentUser = User.current else {
assertionFailure("Error: current user not stored locally")
return
}
assert(currentUser.hasSameObjectId(as: user))
print("Successfully signed up as user: \(user)")
case .failure(let error):
print("Error logging in: \(error)")
}
}
Also, in my example above, I’m signing up asynchronously, which is most likely the way you want to signup. In your example, you are signing up synchronously, which can hold up the main queue and cause run-time warnings in Xcode
I am making an ordering app for customers to order their specific specs. When the user logs in they can go to a tab that contains a tableview with all their specs, once they click on a cell it will take them to a new view controller that will display more information on the spec. Once on this view controller they will have the ability to add x amount of pallets/rolls/etc of that item. I am able to add the spec to Firestore, but I cannot get it to an array in Firestore which I need. My goal is that on anther tab the user can view all the current specs they are trying to order until they hit submit. I am currently using the user.uid to get to that specific customers orders inside Firestore.
Code:
#IBAction func addPallet(_ sender: Any) {
// Get the current user
let user = Auth.auth().currentUser
if let user = user {
_ = user.uid
}
if spec != nil {
// Get the qty ordered for that spec
let totalQty: Int? = Int(palletsToAddTextField.text!)
let qty = spec!.palletCount * totalQty!
let specToAdd = Spec(specNumber: spec!.specNumber,
specDescription: spec!.specDescription,
palletCount: spec!.palletCount,
palletsOrdered: qty)
orderedArray.append(specToAdd)
let specAdded: [String: Any] = [
"SpecDesc": spec!.specDescription,
"SpecNum": spec!.specNumber,
"PalletCount": spec!.palletCount,
"PalletsOrder": qty
]
db.collection("orders").document(user?.uid ?? "error").setData(specAdded) { err in
if let err = err {
print("Error writing document: \(err)")
} else {
print("Document successfully written!")
}
}
}
}
code for spec:
struct Spec: Codable {
// Properties
var specNumber: String
var specDescription: String
var palletCount: Int
var palletsOrdered = 0
enum CodingKeys: String, CodingKey {
case specNumber
case specDescription
case palletCount
case palletsOrdered
}
}
I need something added like the below picture. The user will add x amount of pallets, then going to the next spec they want and add it to the array in Firestore as well.
Ok i guess i get what you want to do. Try:
db.collection("orders").document(userID).setData(["allMyData" : myArray])
"allMyData" will be the name of the field in which you want to save your array and myArray would be your array (specAdded). Thats what you are looking for?
If the document already exists you will want to use .updateData instead of .setData to keep all other fields that might already exist in that specific doc.
Kind regards