-
Notifications
You must be signed in to change notification settings - Fork 0
Login to the server #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
bc5fcba
Implement authentication on the server
mariana0412 eb5cb4c
Add codingKeys to AuthResponse
mariana0412 8adf85a
Add navigation from Login screen to setup screen
mariana0412 275bee3
Remove Back button from SetupView
mariana0412 9c11179
Decide the first screen based on token presence
mariana0412 8484032
Use expiration date to check if token is valid
mariana0412 30f4089
Introduce KeychainKey for consistency
mariana0412 b7f1f18
Resolve merge conflicts with main
mariana0412 a70d97b
Create ErrorResponse to handle errors received from the server
mariana0412 cff3e25
Create enum with auth errors and use them
mariana0412 6f08564
Show only error message
mariana0412 511d276
Implement loading state when Login button is tapped
mariana0412 0e1e904
Align error message to the left
mariana0412 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| // | ||
| // AuthService.swift | ||
| // IOSAccessAssessment | ||
| // | ||
| // Created by Mariana Piz on 08.11.2024. | ||
| // | ||
|
|
||
| import Foundation | ||
|
|
||
| enum AuthError: Error, LocalizedError { | ||
| case invalidURL | ||
| case noData | ||
| case invalidResponse | ||
| case serverError(message: String) | ||
| case decodingError | ||
| case unknownError | ||
|
|
||
| var errorDescription: String? { | ||
| switch self { | ||
| case .invalidURL: | ||
| return "Invalid URL." | ||
| case .noData: | ||
| return "No data received from the server." | ||
| case .invalidResponse: | ||
| return "Invalid response from the server." | ||
| case .serverError(let message): | ||
| return message | ||
| case .decodingError: | ||
| return "Failed to decode the response from the server." | ||
| case .unknownError: | ||
| return "An unknown error occurred." | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct AuthResponse: Decodable { | ||
| let accessToken: String | ||
| let refreshToken: String | ||
| let expiresIn: Int | ||
| let refreshExpiresIn: Int | ||
|
|
||
| private enum CodingKeys: String, CodingKey { | ||
| case accessToken = "access_token" | ||
| case refreshToken = "refresh_token" | ||
| case expiresIn = "expires_in" | ||
| case refreshExpiresIn = "refresh_expires_in" | ||
| } | ||
| } | ||
|
|
||
| struct ErrorResponse: Decodable { | ||
| let timestamp: String | ||
| let status: String | ||
| let message: String | ||
| let errors: [String]? | ||
| } | ||
|
|
||
| class AuthService { | ||
|
|
||
| private enum Constants { | ||
| static let serverUrl = "https://tdei-gateway-stage.azurewebsites.net/api/v1/authenticate" | ||
| } | ||
|
|
||
| func authenticate( | ||
| username: String, | ||
| password: String, | ||
| completion: @escaping (Result<AuthResponse, AuthError>) -> Void | ||
| ) { | ||
| guard let request = createRequest(username: username, password: password) else { | ||
| completion(.failure(.invalidURL)) | ||
| return | ||
| } | ||
|
|
||
| URLSession.shared.dataTask(with: request) { data, response, error in | ||
| if let error { | ||
| completion(.failure(.serverError(message: error.localizedDescription))) | ||
| return | ||
| } | ||
|
|
||
| guard let data else { | ||
| completion(.failure(.noData)) | ||
| return | ||
| } | ||
|
|
||
| guard let httpResponse = response as? HTTPURLResponse else { | ||
| completion(.failure(.invalidResponse)) | ||
| return | ||
| } | ||
|
|
||
| self.handleResponse(data: data, | ||
| httpResponse: httpResponse, | ||
| completion: completion) | ||
| }.resume() | ||
| } | ||
|
|
||
| private func createRequest(username: String, password: String) -> URLRequest? { | ||
| guard let url = URL(string: Constants.serverUrl) else { return nil } | ||
|
|
||
| var request = URLRequest(url: url) | ||
| request.httpMethod = "POST" | ||
| request.setValue("application/json", forHTTPHeaderField: "Content-Type") | ||
|
|
||
| let body = [ | ||
| "username": username, | ||
| "password": password | ||
| ] | ||
| request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) | ||
|
|
||
| return request | ||
| } | ||
|
|
||
| private func handleResponse( | ||
| data: Data, | ||
| httpResponse: HTTPURLResponse, | ||
| completion: @escaping (Result<AuthResponse, AuthError>) -> Void | ||
| ) { | ||
| if (200...299).contains(httpResponse.statusCode) { | ||
| decodeSuccessResponse(data: data, | ||
| completion: completion) | ||
| } else { | ||
| decodeErrorResponse(data: data, | ||
| statusCode: httpResponse.statusCode, | ||
| completion: completion) | ||
| } | ||
| } | ||
|
|
||
| private func decodeSuccessResponse( | ||
| data: Data, | ||
| completion: @escaping (Result<AuthResponse, AuthError>) -> Void | ||
| ) { | ||
| do { | ||
| let authResponse = try JSONDecoder().decode(AuthResponse.self, from: data) | ||
| completion(.success(authResponse)) | ||
| } catch { | ||
| completion(.failure(.decodingError)) | ||
| } | ||
| } | ||
|
|
||
| private func decodeErrorResponse( | ||
| data: Data, | ||
| statusCode: Int, | ||
| completion: @escaping (Result<AuthResponse, AuthError>) -> Void | ||
| ) { | ||
| do { | ||
| let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: data) | ||
| let errorMessage = errorResponse.message | ||
| .appending(": ") | ||
| .appending(errorResponse.errors?.joined(separator: "\n") ?? "") | ||
|
|
||
| completion(.failure(.serverError(message: errorMessage))) | ||
| } catch { | ||
| completion(.failure(.decodingError)) | ||
| } | ||
| } | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| // | ||
| // KeychainService.swift | ||
| // IOSAccessAssessment | ||
| // | ||
| // Created by Mariana Piz on 08.11.2024. | ||
| // | ||
|
|
||
| import Foundation | ||
| import Security | ||
|
|
||
| enum KeychainKey: String { | ||
| case accessToken | ||
| case expirationDate | ||
| } | ||
|
|
||
| final class KeychainService { | ||
|
|
||
| func setValue(_ value: String, for key: KeychainKey) { | ||
| guard let encodedValue = value.data(using: .utf8) else { return } | ||
|
|
||
| let query: [String: Any] = [ | ||
| kSecClass as String: kSecClassGenericPassword, | ||
| kSecAttrAccount as String: key.rawValue | ||
| ] | ||
|
|
||
| var status = SecItemCopyMatching(query as CFDictionary, nil) | ||
| if status == errSecSuccess { | ||
| let attributesToUpdate: [String: Any] = [ | ||
| kSecValueData as String: encodedValue | ||
| ] | ||
| status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) | ||
| } else if status == errSecItemNotFound { | ||
| var newItem = query | ||
| newItem[kSecValueData as String] = encodedValue | ||
| status = SecItemAdd(newItem as CFDictionary, nil) | ||
| } | ||
|
|
||
| if status != errSecSuccess { | ||
| print("Keychain setValue error: \(status)") | ||
| } | ||
| } | ||
|
|
||
| func getValue(for key: KeychainKey) -> String? { | ||
| let query: [String: Any] = [ | ||
| kSecClass as String: kSecClassGenericPassword, | ||
| kSecAttrAccount as String: key.rawValue, | ||
| kSecReturnData as String: kCFBooleanTrue!, | ||
| kSecMatchLimit as String: kSecMatchLimitOne | ||
| ] | ||
|
|
||
| var queryResult: AnyObject? | ||
| let status = withUnsafeMutablePointer(to: &queryResult) { | ||
| SecItemCopyMatching(query as CFDictionary, $0) | ||
| } | ||
|
|
||
| guard status == errSecSuccess else { | ||
| return nil | ||
| } | ||
|
|
||
| if let data = queryResult as? Data, let value = String(data: data, encoding: .utf8) { | ||
| return value | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func removeValue(for key: KeychainKey) { | ||
| let query: [String: Any] = [ | ||
| kSecClass as String: kSecClassGenericPassword, | ||
| kSecAttrAccount as String: key.rawValue | ||
| ] | ||
|
|
||
| let status = SecItemDelete(query as CFDictionary) | ||
| if status != errSecSuccess && status != errSecItemNotFound { | ||
| print("Keychain removeValue error: \(status)") | ||
| } | ||
| } | ||
|
|
||
| func setDate(_ date: Date, for key: KeychainKey) { | ||
| let dateString = dateToString(date) | ||
| setValue(dateString, for: key) | ||
| } | ||
|
|
||
| func getDate(for key: KeychainKey) -> Date? { | ||
| guard let dateString = getValue(for: key) else { return nil } | ||
| return stringToDate(dateString) | ||
| } | ||
|
|
||
| func isTokenValid() -> Bool { | ||
| guard let _ = getValue(for: .accessToken) else { | ||
| return false | ||
| } | ||
|
|
||
| if let expirationDate = getDate(for: .expirationDate), expirationDate > Date() { | ||
| return true | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| private func dateToString(_ date: Date) -> String { | ||
| let formatter = ISO8601DateFormatter() | ||
| return formatter.string(from: date) | ||
| } | ||
|
|
||
| private func stringToDate(_ dateString: String) -> Date? { | ||
| let formatter = ISO8601DateFormatter() | ||
| return formatter.date(from: dateString) | ||
| } | ||
|
|
||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So mostly, I do not see a problem with this kind of initialization for this context, despite its misgivings given in:
https://stackoverflow.com/questions/56691630/swiftui-state-var-initialization-issue
More specifically: https://stackoverflow.com/a/71247040
I wonder if there would come a situation where this initialization could cause problems. Most likely it shouldn't.
I'll just keep it in mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just as a note, we don't have a problem with this way to setting the State because the struct IOSAccessAssessment does not have any dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this article does mention that this is bad practice, since we are accommodating what may lead to potentially inconsistent behavior:
https://www.swiftcraft.io/articles/how-to-initialize-state-inside-the-views-init-