Nextcloud login flow v2 support (not in working state yet)

This commit is contained in:
Vicnet
2023-09-19 12:04:59 +02:00
parent 154cd9a51f
commit 0f16b164d6
6 changed files with 233 additions and 49 deletions

View File

@@ -67,3 +67,5 @@ struct RecipeImage {
var thumb: UIImage?
var full: UIImage?
}

View File

@@ -59,10 +59,7 @@ class DataStore {
func recipeDetailExists(recipeId: Int) -> Bool {
let filePath = "recipe\(recipeId).data"
guard let folderPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.path() else { return false }
let exists = fileManager.fileExists(atPath: folderPath + filePath)
print("Path: ", folderPath + filePath)
print("Recipe detail with id \(recipeId)", exists ? "exists" : "does not exist")
return exists
return fileManager.fileExists(atPath: folderPath + filePath)
}
func clearAll() -> Bool {

View File

@@ -22,14 +22,14 @@ public enum NetworkError: String, Error {
class NetworkController {
var userSettings: UserSettings
var authString: String
var urlString: String
var cookBookUrlString: String
let apiVersion = "1"
init() {
print("Initializing NetworkController.")
self.userSettings = UserSettings()
self.urlString = "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v\(apiVersion)"
self.cookBookUrlString = "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v\(apiVersion)/"
let loginString = "\(userSettings.username):\(userSettings.token)"
let loginData = loginString.data(using: String.Encoding.utf8)!
@@ -38,7 +38,7 @@ class NetworkController {
func fetchData(path: String) async throws -> Data? {
let url = URL(string: "\(urlString)/\(path)")!
let url = URL(string: "\(cookBookUrlString)/\(path)")!
var request = URLRequest(url: url)
@@ -61,9 +61,9 @@ class NetworkController {
}
}
func sendHTTPRequest(path: String, _ requestWrapper: RequestWrapper) async throws -> (Data?, NetworkError?) {
print("Sending \(requestWrapper.method.rawValue) request (path: \(path)) ...")
let urlStringSanitized = "\(urlString)/\(path)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
func sendHTTPRequest(_ requestWrapper: RequestWrapper) async throws -> (Data?, NetworkError?) {
print("Sending \(requestWrapper.method.rawValue) request (path: \(requestWrapper.prepend(cookBookPath: cookBookUrlString))) ...")
let urlStringSanitized = requestWrapper.prepend(cookBookPath: cookBookUrlString).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let url = URL(string: urlStringSanitized!)!
var request = URLRequest(url: url)
request.setValue(
@@ -123,7 +123,7 @@ class NetworkController {
func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
do {
let (data, error) = try await sendHTTPRequest(path: request.path, request)
let (data, error) = try await sendHTTPRequest(request)
if let data = data {
return (decodeData(data), error)
}
@@ -136,7 +136,7 @@ class NetworkController {
func sendRequest(_ request: RequestWrapper) async -> Error? {
do {
return try await sendHTTPRequest(path: request.path, request).1
return try await sendHTTPRequest(request).1
} catch {
print("An unknown network error occured.")
}
@@ -158,3 +158,91 @@ class NetworkController {
struct NetworkHandler {
static func sendHTTPRequest(_ requestWrapper: RequestWrapper, authString: String? = nil) async throws -> (Data?, NetworkError?) {
print("Sending \(requestWrapper.method.rawValue) request (path: \(requestWrapper.path)) ...")
let urlStringSanitized = requestWrapper.path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let url = URL(string: urlStringSanitized!)!
var request = URLRequest(url: url)
request.setValue(
"true",
forHTTPHeaderField: "OCS-APIRequest"
)
if let authString = authString {
request.setValue(
"Basic \(authString)",
forHTTPHeaderField: "Authorization"
)
}
request.setValue(
requestWrapper.accept.rawValue,
forHTTPHeaderField: "Accept"
)
request.httpMethod = requestWrapper.method.rawValue
switch requestWrapper.method {
case .GET: break
case .POST, .PUT:
guard let httpBody = requestWrapper.body else { break }
do {
print("Encoding request ...")
request.httpBody = try JSONEncoder().encode(httpBody)
print("Request body: \(String(data: request.httpBody ?? Data(), encoding: .utf8) ?? "nil")")
} catch {
throw error
}
case .DELETE: throw NotImplementedError.notImplemented
}
var data: Data? = nil
var response: URLResponse? = nil
do {
(data, response) = try await URLSession.shared.data(for: request)
print("Response: ", response)
return (data, nil)
} catch {
return (nil, decodeURLResponse(response: response as? HTTPURLResponse))
}
}
static func decodeURLResponse(response: HTTPURLResponse?) -> NetworkError? {
guard let response = response else {
return NetworkError.unknownError
}
switch response.statusCode {
case 200...299: return (nil)
case 300...399: return (NetworkError.redirectionError)
case 400...499: return (NetworkError.clientError)
case 500...599: return (NetworkError.serverError)
case 600: return (NetworkError.invalidRequest)
default: return (NetworkError.unknownError)
}
}
static func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
do {
let (data, error) = try await sendHTTPRequest(request)
if let data = data {
print(String(data: data, encoding: .utf8))
return (decodeData(data), error)
}
return (nil, error)
} catch {
print("An unknown network error occured.")
}
return (nil, NetworkError.unknownError)
}
private static func decodeData<D: Decodable>(_ data: Data) -> D? {
let decoder = JSONDecoder()
do {
print("Decoding type ", D.self, " ...")
return try decoder.decode(D.self, from: data)
} catch (let error) {
print("DataController - decodeData(): Failed to decode data.")
print("Error: ", error)
return nil
}
}
}

View File

@@ -21,7 +21,7 @@ enum AcceptHeader: String {
struct RequestWrapper {
let method: RequestMethod
let path: String
var path: String
let accept: AcceptHeader
let body: Codable?
@@ -31,6 +31,24 @@ struct RequestWrapper {
self.body = body
self.accept = accept
}
func prepend(cookBookPath: String) -> String {
return cookBookPath + self.path
}
}
struct LoginV2Request: Codable {
let poll: LoginV2Poll
let login: String
}
struct LoginV2Poll: Codable {
let token: String
let endpoint: String
}
struct LoginV2Response: Codable {
let server: String
let loginName: String
let appPassword: String
}

View File

@@ -233,7 +233,7 @@ extension MainViewModel {
do {
let networkPath = networkImagePath(recipeId, full)
let request = RequestWrapper(method: .GET, path: networkPath, accept: .IMAGE)
let (data, _): (Data?, Error?) = try await networkController.sendHTTPRequest(path: request.path, request)
let (data, _): (Data?, Error?) = try await networkController.sendHTTPRequest(request)
guard let data = data else {
print("Error receiving or decoding data.")
return nil

View File

@@ -54,6 +54,12 @@ struct WelcomeTab: View {
struct LoginTab: View {
@ObservedObject var userSettings: UserSettings
enum LoginMethod {
case v2, token
}
@State var selectedLoginMethod: LoginMethod = .v2
@State var loginRequest: LoginV2Request? = nil
enum Field {
case server
case username
@@ -73,43 +79,96 @@ struct LoginTab: View {
.padding()
Spacer()
}
LoginLabel(text: "Server address")
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress)
.focused($focusedField, equals: .server)
.textContentType(.URL)
.submitLabel(.next)
.padding(.bottom)
LoginLabel(text: "User name")
LoginTextField(example: "username", text: $userSettings.username)
.focused($focusedField, equals: .username)
.textContentType(.username)
.submitLabel(.next)
.padding(.bottom)
Picker("Login Method", selection: $selectedLoginMethod) {
Text("Nextcloud Login").tag(LoginMethod.v2)
Text("App Token Login").tag(LoginMethod.token)
}
.pickerStyle(.segmented)
.foregroundColor(.white)
if selectedLoginMethod == .token {
LoginLabel(text: "Server address")
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress)
.focused($focusedField, equals: .server)
.textContentType(.URL)
.submitLabel(.next)
.padding(.bottom)
LoginLabel(text: "App Token")
LoginTextField(example: "can be generated in security settings of your nextcloud", text: $userSettings.token)
.focused($focusedField, equals: .token)
.textContentType(.password)
.submitLabel(.join)
HStack{
Spacer()
Button {
userSettings.onboarding = false
} label: {
Text("Submit")
.foregroundColor(.white)
.font(.headline)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white, lineWidth: 2)
.foregroundColor(.clear)
)
LoginLabel(text: "User name")
LoginTextField(example: "username", text: $userSettings.username)
.focused($focusedField, equals: .username)
.textContentType(.username)
.submitLabel(.next)
.padding(.bottom)
LoginLabel(text: "App Token")
LoginTextField(example: "can be generated in security settings of your nextcloud", text: $userSettings.token)
.focused($focusedField, equals: .token)
.textContentType(.password)
.submitLabel(.join)
HStack{
Spacer()
Button {
userSettings.onboarding = false
} label: {
Text("Submit")
.foregroundColor(.white)
.font(.headline)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white, lineWidth: 2)
.foregroundColor(.clear)
)
}
.padding()
Spacer()
}
}
else if selectedLoginMethod == .v2 {
LoginLabel(text: "Server address")
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress)
.focused($focusedField, equals: .server)
.textContentType(.URL)
.submitLabel(.done)
.padding(.bottom)
.onSubmit {
if userSettings.serverAddress == "" { return }
Task {
await sendLoginV2Request()
if let loginRequest = loginRequest {
await UIApplication.shared.open(URL(string: loginRequest.login)!)
}
}
}
Text("Submitting will open a web browser. Please follow the login instructions provided there.\nAfter a successfull login, return to this application and press 'Done'.")
.font(.subheadline)
.padding(.bottom)
.tint(.white)
HStack{
Spacer()
Button {
// fetch login v2 response
Task {
guard let res = await fetchLoginV2Response() else { return }
print(res.loginName)
}
} label: {
Text("Done")
.foregroundColor(.white)
.font(.headline)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white, lineWidth: 2)
.foregroundColor(.clear)
)
}
.disabled(loginRequest == nil ? true : false)
.padding()
Spacer()
}
.padding()
Spacer()
}
Spacer()
}
@@ -127,6 +186,26 @@ struct LoginTab: View {
.padding()
}
}
func sendLoginV2Request() async {
let request = RequestWrapper(
method: .POST,
path: "https://\(userSettings.serverAddress)/index.php/login/v2"
)
let (loginReq, _): (LoginV2Request?, Error?) = await NetworkHandler.sendDataRequest(request)
self.loginRequest = loginReq
}
func fetchLoginV2Response() async -> LoginV2Response? {
guard let loginRequest = loginRequest else { return nil }
let request = RequestWrapper(
method: .POST,
path: loginRequest.poll.endpoint,
body: "token=\(loginRequest.poll.token)"
)
let (loginRes, _): (LoginV2Response?, Error?) = await NetworkHandler.sendDataRequest(request)
return loginRes
}
}
struct LoginLabel: View {