0

I am attempting to make an API call to Frappe ERPNext database in Swift. No matter how I build the API call I get a PermissionError. I have rebuilt the API call multiple times and still can't get it to work. I have built the API call inside Portman and it works just fine. I have posted my code and full error down below.

{"exception":"frappe.exceptions.PermissionError","exc_type":"PermissionError","exc":"["Traceback (most recent call last):\n File \"apps/frappe/frappe/app.py\", line 114, in application\n response = frappe.api.handle(request)\n File \"apps/frappe/frappe/api/init.py\", line 49, in handle\n data = endpoint(**arguments)\n File \"apps/frappe/frappe/api/v1.py\", line 27, in document_list\n return frappe.call(frappe.client.get_list, doctype, **frappe.form_dict)\n File \"apps/frappe/frappe/init.py\", line 1781, in call\n return fn(*args, **newargs)\n File \"apps/frappe/frappe/utils/typing_validations.py\", line 31, in wrapper\n return func(*args, **kwargs)\n File \"apps/frappe/frappe/client.py\", line 67, in get_list\n return frappe.get_list(**args)\n File \"apps/frappe/frappe/init.py\", line 2045, in get_list\n return frappe.model.db_query.DatabaseQuery(doctype).execute(*args, **kwargs)\n File \"apps/frappe/frappe/model/db_query.py\", line 114, in execute\n self.check_read_permission(self.doctype, parent_doctype=parent_doctype)\n File \"apps/frappe/frappe/model/db_query.py\", line 511, in check_read_permission\n self._set_permission_map(doctype, parent_doctype)\n File \"apps/frappe/frappe/model/db_query.py\", line 517, in _set_permission_map\n frappe.has_permission(\n File \"apps/frappe/frappe/init.py\", line 1103, in has_permission\n raise frappe.PermissionError\nfrappe.exceptions.PermissionError\n"]","_server_messages":"["{\"message\": \"User Guest does not have doctype access via role permission for document Item\", \"title\": \"Message\"}"]","_error_message":"No permission for Item"}

import Foundation
import SwiftUI
struct ItemResponse: Decodable {
 let data: [Item]
}
struct Item: Decodable, Identifiable {
 let name: String
 var id: String { name }
}
var itemDataResponse: String = ""
struct ShopAPIView: View {
 @ObservedObject var viewModel = itemapiresponse.itemGlobal
 
 var body: some View {
 VStack {
 if viewModel.itemList.isEmpty {
 Text(viewModel.itemDataResponse)
 } else {
 List(viewModel.itemList) { item in
 Text(item.name)
 }
 }
 }
 .onAppear {
 viewModel.callItemAPI()
 }
 }
}
final class itemapiresponse: ObservableObject {
 static let itemGlobal = itemapiresponse()
 
 @Published var itemDataResponse: String = "loading"
 @Published var itemList: [Item] = []
 
 func callItemAPI() {
 let itemapiURL = "https://myurl"
 
 guard let url = URL(string: itemapiURL) else {
 DispatchQueue.main.async {
 self.itemDataResponse = "Invalid URL"
 }
 return
 }
 
 var request = URLRequest(url: url)
 request.httpMethod = "GET"
 request.addValue("my token", forHTTPHeaderField: "Authorization")
 
 URLSession.shared.dataTask(with: url) { data, response, error in
 if let error = error {
 DispatchQueue.main.async {
 self.itemDataResponse = "Error fetching data: \(error.localizedDescription)"
 }
 return
 }
 
 guard let data = data else {
 DispatchQueue.main.async {
 self.itemDataResponse = "No data returned"
 }
 return
 }
 
 print(String(data: data, encoding: .utf8) ?? "No readable data")
 
 do {
 let itemInfo = try JSONDecoder().decode(ItemResponse.self, from: data)
 DispatchQueue.main.async {
 self.itemList = itemInfo.data
 self.itemDataResponse = "Success"
 }
 } catch {
 DispatchQueue.main.async {
 self.itemDataResponse = "Error parsing JSON: \(error.localizedDescription)"
 }
 }
 }.resume()
 }
}
#Preview {
 ShopAPIView()
}

Edit: I have this call working in Postman. Here is the code exported from Postman.

var request = URLRequest(url: URL(string: "\(MyURL)")!,timeoutInterval: Double.infinity)
request.addValue("token \(MyToken)", forHTTPHeaderField: "Authorization")
request.addValue("full_name=Guest; sid=Guest; system_user=no; user_id=Guest; user_image=", forHTTPHeaderField: "Cookie")
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in 
 guard let data = data else {
 print(String(describing: error))
 return
 }
 print(String(data: data, encoding: .utf8)!)
}
task.resume()

And here is now I implemented it on the second attempt:

struct ContentView: View {
 @State private var responseData: String = "Loading..."
 
 var body: some View {
 ScrollView {
 Text(responseData)
 .padding()
 }
 .onAppear(perform: fetchData)
 }
 func fetchData() {
 var request = URLRequest(url: URL(string: "\(MyURL)")!, timeoutInterval: Double.infinity)
 request.addValue("token \(MyToken)", forHTTPHeaderField: "Authorization")
 request.addValue("full_name=Guest; sid=Guest; system_user=no; user_id=Guest; user_image=", forHTTPHeaderField: "Cookie")
 request.httpMethod = "GET"
 let task = URLSession.shared.dataTask(with: request) { data, response, error in
 guard let data = data else {
 print(String(describing: error))
 return
 }
 print(String(data: data, encoding: .utf8)!)
 } 
 task.resume()
 }
}
19
  • Don’t use the localized description of the error Commented Apr 10 at 1:14
  • Use the common practice of class names in PascalCase/UpperCamelCase, eg ItemApiResponse. I would not recommend using the singleton pattern with ObservedObject class. Declare @StateObject private var viewModel = ItemApiResponse(), see this Apple docs Monitoring data. Depending on what your server requires, you could try request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization"). Note Postman can generate the Swift code for you. Commented Apr 10 at 5:48
  • dataTask(with: url) -> dataTask(with: request) you aren't using the URLRequest which has the token... Commented Apr 10 at 7:44
  • Frappe supports a variety of "login" methods. I recommend using OAuth2 with PKCE as described in the docs docs.frappe.io/framework/user/en/guides/integration/rest_api/…. Ensure using s256 as code challenge method. Ensure you setup the HTTP request properly (your's not correct for a bearer token). You will need to read quite a bit of documentation and RFCs beyond Frappe's docs. I strongly recommend to utilise a third party package for a client side OAuth2 implementation which uses ASWebAuthenticationSession. Commented Apr 10 at 10:06
  • @workingdogsupportUkraine Thank you for the quick response. I tried changing the code according to your suggestions, however I am still having issues. I used Postman to generate the Swift code. Here is a link to an image of the Postman code. Commented Apr 10 at 18:21

1 Answer 1

0

I would use the actual user’s credentials to log in to Frappe. In almost all mobile applications, their lifecycle retains the session object. Once you log in using the session object, you don’t have to make any login call again before calling your apis.

Using user credentials also gives you an advantage in terms of the content they can view on the app. By default, the Frappe framework applies permissions to all the resources accessed by the user.

/*
 I will write my login method in Frappe Auth class like this.
*/
final class FrappeClient: NSObject{
 
 private let serverAddress: String = "your-server-url"
 private let APIPath: String = "api/resource"
 private let methodPath: String = "api/method"
 
 static let shared = FrappeClient()
 
 // this is to manage cookie
 private var sessionConfiguration = URLSessionConfiguration.default
 private var urlSession: URLSession?
 
 
 override init(){
 self.sessionConfiguration.httpShouldSetCookies = true
 self.sessionConfiguration.httpCookieStorage = HTTPCookieStorage.shared
 self.urlSession = URLSession(configuration: self.sessionConfiguration)
 }
 func login(username: String, password: String) async throws -> LoginResponse{
 
 do{
 let params = LoginRequest(usr: username, pwd: password)
 let (data, response) = try! await self.sendMethodRequest(methodPath: "login", requestMethod: "POST", args: params)
 let statusCode = (response as! HTTPURLResponse).statusCode
 if(statusCode == 401){
 throw FrappeClientError.invalidCredentials("Invalid username or password")
 }
 let message = try JSONDecoder().decode(LoginResponse.self, from: data)
 return message
 }catch let error{
 throw error
 }
 }
 /* you can define another method to check if the user is already logged in */
 func isUserLoggedIn() async throws -> Bool{
 
 let url = try! self.getMethodURLObject(methodPath: "<your method-path>")
 
 var request = URLRequest(url: url)
 request.httpMethod = "POST".uppercased()
 request.setValue("application/json", forHTTPHeaderField: "Content-Type")
 
 let (data, response) = try await self.urlSession!.data(for: request)
 let statusCode = (response as! HTTPURLResponse).statusCode
 let flag = try JSONDecoder().decode(LoggedInUserResponseModel.self, from: data)
 if(statusCode == 200){
 return flag.message
 }
 return false
 } 
}

Upon launching the application, you can verify if the user is already logged in. If so, redirect them to the home page. Otherwise, redirect them to the login screen.

answered Nov 10 at 22:38
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.