Nextcloud login flow v2 support (not in working state yet)
This commit is contained in:
@@ -67,3 +67,5 @@ struct RecipeImage {
|
|||||||
var thumb: UIImage?
|
var thumb: UIImage?
|
||||||
var full: UIImage?
|
var full: UIImage?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,7 @@ class DataStore {
|
|||||||
func recipeDetailExists(recipeId: Int) -> Bool {
|
func recipeDetailExists(recipeId: Int) -> Bool {
|
||||||
let filePath = "recipe\(recipeId).data"
|
let filePath = "recipe\(recipeId).data"
|
||||||
guard let folderPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.path() else { return false }
|
guard let folderPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.path() else { return false }
|
||||||
let exists = fileManager.fileExists(atPath: folderPath + filePath)
|
return fileManager.fileExists(atPath: folderPath + filePath)
|
||||||
print("Path: ", folderPath + filePath)
|
|
||||||
print("Recipe detail with id \(recipeId)", exists ? "exists" : "does not exist")
|
|
||||||
return exists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearAll() -> Bool {
|
func clearAll() -> Bool {
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ public enum NetworkError: String, Error {
|
|||||||
class NetworkController {
|
class NetworkController {
|
||||||
var userSettings: UserSettings
|
var userSettings: UserSettings
|
||||||
var authString: String
|
var authString: String
|
||||||
var urlString: String
|
var cookBookUrlString: String
|
||||||
|
|
||||||
let apiVersion = "1"
|
let apiVersion = "1"
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
print("Initializing NetworkController.")
|
print("Initializing NetworkController.")
|
||||||
self.userSettings = UserSettings()
|
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 loginString = "\(userSettings.username):\(userSettings.token)"
|
||||||
let loginData = loginString.data(using: String.Encoding.utf8)!
|
let loginData = loginString.data(using: String.Encoding.utf8)!
|
||||||
@@ -38,7 +38,7 @@ class NetworkController {
|
|||||||
|
|
||||||
func fetchData(path: String) async throws -> Data? {
|
func fetchData(path: String) async throws -> Data? {
|
||||||
|
|
||||||
let url = URL(string: "\(urlString)/\(path)")!
|
let url = URL(string: "\(cookBookUrlString)/\(path)")!
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
|
|
||||||
@@ -61,9 +61,9 @@ class NetworkController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendHTTPRequest(path: String, _ requestWrapper: RequestWrapper) async throws -> (Data?, NetworkError?) {
|
func sendHTTPRequest(_ requestWrapper: RequestWrapper) async throws -> (Data?, NetworkError?) {
|
||||||
print("Sending \(requestWrapper.method.rawValue) request (path: \(path)) ...")
|
print("Sending \(requestWrapper.method.rawValue) request (path: \(requestWrapper.prepend(cookBookPath: cookBookUrlString))) ...")
|
||||||
let urlStringSanitized = "\(urlString)/\(path)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
let urlStringSanitized = requestWrapper.prepend(cookBookPath: cookBookUrlString).addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||||
let url = URL(string: urlStringSanitized!)!
|
let url = URL(string: urlStringSanitized!)!
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.setValue(
|
request.setValue(
|
||||||
@@ -123,7 +123,7 @@ class NetworkController {
|
|||||||
|
|
||||||
func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
|
func sendDataRequest<D: Decodable>(_ request: RequestWrapper) async -> (D?, Error?) {
|
||||||
do {
|
do {
|
||||||
let (data, error) = try await sendHTTPRequest(path: request.path, request)
|
let (data, error) = try await sendHTTPRequest(request)
|
||||||
if let data = data {
|
if let data = data {
|
||||||
return (decodeData(data), error)
|
return (decodeData(data), error)
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ class NetworkController {
|
|||||||
|
|
||||||
func sendRequest(_ request: RequestWrapper) async -> Error? {
|
func sendRequest(_ request: RequestWrapper) async -> Error? {
|
||||||
do {
|
do {
|
||||||
return try await sendHTTPRequest(path: request.path, request).1
|
return try await sendHTTPRequest(request).1
|
||||||
} catch {
|
} catch {
|
||||||
print("An unknown network error occured.")
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ enum AcceptHeader: String {
|
|||||||
|
|
||||||
struct RequestWrapper {
|
struct RequestWrapper {
|
||||||
let method: RequestMethod
|
let method: RequestMethod
|
||||||
let path: String
|
var path: String
|
||||||
let accept: AcceptHeader
|
let accept: AcceptHeader
|
||||||
let body: Codable?
|
let body: Codable?
|
||||||
|
|
||||||
@@ -31,6 +31,24 @@ struct RequestWrapper {
|
|||||||
self.body = body
|
self.body = body
|
||||||
self.accept = accept
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ extension MainViewModel {
|
|||||||
do {
|
do {
|
||||||
let networkPath = networkImagePath(recipeId, full)
|
let networkPath = networkImagePath(recipeId, full)
|
||||||
let request = RequestWrapper(method: .GET, path: networkPath, accept: .IMAGE)
|
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 {
|
guard let data = data else {
|
||||||
print("Error receiving or decoding data.")
|
print("Error receiving or decoding data.")
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ struct WelcomeTab: View {
|
|||||||
struct LoginTab: View {
|
struct LoginTab: View {
|
||||||
@ObservedObject var userSettings: UserSettings
|
@ObservedObject var userSettings: UserSettings
|
||||||
|
|
||||||
|
enum LoginMethod {
|
||||||
|
case v2, token
|
||||||
|
}
|
||||||
|
@State var selectedLoginMethod: LoginMethod = .v2
|
||||||
|
@State var loginRequest: LoginV2Request? = nil
|
||||||
|
|
||||||
enum Field {
|
enum Field {
|
||||||
case server
|
case server
|
||||||
case username
|
case username
|
||||||
@@ -73,43 +79,96 @@ struct LoginTab: View {
|
|||||||
.padding()
|
.padding()
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
LoginLabel(text: "Server address")
|
Picker("Login Method", selection: $selectedLoginMethod) {
|
||||||
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress)
|
Text("Nextcloud Login").tag(LoginMethod.v2)
|
||||||
.focused($focusedField, equals: .server)
|
Text("App Token Login").tag(LoginMethod.token)
|
||||||
.textContentType(.URL)
|
}
|
||||||
.submitLabel(.next)
|
.pickerStyle(.segmented)
|
||||||
.padding(.bottom)
|
.foregroundColor(.white)
|
||||||
|
if selectedLoginMethod == .token {
|
||||||
LoginLabel(text: "User name")
|
LoginLabel(text: "Server address")
|
||||||
LoginTextField(example: "username", text: $userSettings.username)
|
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress)
|
||||||
.focused($focusedField, equals: .username)
|
.focused($focusedField, equals: .server)
|
||||||
.textContentType(.username)
|
.textContentType(.URL)
|
||||||
.submitLabel(.next)
|
.submitLabel(.next)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
|
|
||||||
|
LoginLabel(text: "User name")
|
||||||
LoginLabel(text: "App Token")
|
LoginTextField(example: "username", text: $userSettings.username)
|
||||||
LoginTextField(example: "can be generated in security settings of your nextcloud", text: $userSettings.token)
|
.focused($focusedField, equals: .username)
|
||||||
.focused($focusedField, equals: .token)
|
.textContentType(.username)
|
||||||
.textContentType(.password)
|
.submitLabel(.next)
|
||||||
.submitLabel(.join)
|
.padding(.bottom)
|
||||||
HStack{
|
|
||||||
Spacer()
|
|
||||||
Button {
|
LoginLabel(text: "App Token")
|
||||||
userSettings.onboarding = false
|
LoginTextField(example: "can be generated in security settings of your nextcloud", text: $userSettings.token)
|
||||||
} label: {
|
.focused($focusedField, equals: .token)
|
||||||
Text("Submit")
|
.textContentType(.password)
|
||||||
.foregroundColor(.white)
|
.submitLabel(.join)
|
||||||
.font(.headline)
|
HStack{
|
||||||
.padding()
|
Spacer()
|
||||||
.background(
|
Button {
|
||||||
RoundedRectangle(cornerRadius: 10)
|
userSettings.onboarding = false
|
||||||
.stroke(Color.white, lineWidth: 2)
|
} label: {
|
||||||
.foregroundColor(.clear)
|
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()
|
Spacer()
|
||||||
}
|
}
|
||||||
@@ -127,6 +186,26 @@ struct LoginTab: View {
|
|||||||
.padding()
|
.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 {
|
struct LoginLabel: View {
|
||||||
|
|||||||
Reference in New Issue
Block a user