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()
}
}
1 Answer 1
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.
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 tryrequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization"). NotePostmancan generate the Swift code for you.dataTask(with: url)->dataTask(with: request)you aren't using theURLRequestwhich has the token...s256as 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 usesASWebAuthenticationSession.