To receive notifications about scheduled maintenance, please subscribe to the mailing-list gitlab-operations@sympa.ethz.ch. You can subscribe to the mailing-list at https://sympa.ethz.ch

Commit 67347965 authored by domenicw's avatar domenicw
Browse files

Improvements to login and logout

Job offers are now sorted by company
parent 27e6a53a
......@@ -81,6 +81,8 @@
B0845924215B78C700479D27 /* AmivMicroAppCheckin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0845923215B78C700479D27 /* AmivMicroAppCheckin.swift */; };
B0845926215B797200479D27 /* AmivMicroApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0845925215B797200479D27 /* AmivMicroApp.swift */; };
B0845928215B7AF200479D27 /* AmivMicroAppBarcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0845927215B7AF200479D27 /* AmivMicroAppBarcode.swift */; };
B097D4332173573B00B9D791 /* JobsViewModelSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B097D4322173573B00B9D791 /* JobsViewModelSection.swift */; };
B097D4352173578100B9D791 /* JobsViewModelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B097D4342173578100B9D791 /* JobsViewModelCell.swift */; };
B0AF91292157B0A3008F3B80 /* EndPointType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AF91282157B0A3008F3B80 /* EndPointType.swift */; };
B0AF912E2157B19A008F3B80 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AF912D2157B19A008F3B80 /* HTTPMethod.swift */; };
B0AF91302157B26C008F3B80 /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AF912F2157B26C008F3B80 /* HTTPTask.swift */; };
......@@ -227,6 +229,8 @@
B0845925215B797200479D27 /* AmivMicroApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmivMicroApp.swift; sourceTree = "<group>"; };
B0845927215B7AF200479D27 /* AmivMicroAppBarcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmivMicroAppBarcode.swift; sourceTree = "<group>"; };
B0845929215B81DE00479D27 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
B097D4322173573B00B9D791 /* JobsViewModelSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobsViewModelSection.swift; sourceTree = "<group>"; };
B097D4342173578100B9D791 /* JobsViewModelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobsViewModelCell.swift; sourceTree = "<group>"; };
B0A2F35921579FD0002C340F /* Amiv.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Amiv.entitlements; sourceTree = "<group>"; };
B0AF91282157B0A3008F3B80 /* EndPointType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndPointType.swift; sourceTree = "<group>"; };
B0AF912D2157B19A008F3B80 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = "<group>"; };
......@@ -710,6 +714,7 @@
B0AF912B2157B151008F3B80 /* Manager */ = {
isa = PBXGroup;
children = (
B0F5B9642172132B005E4591 /* SessionManager.swift */,
B0AF913D2157CE2D008F3B80 /* NetworkManager.swift */,
);
path = Manager;
......@@ -773,6 +778,8 @@
isa = PBXGroup;
children = (
B0F5B9572171480B005E4591 /* JobsViewModel.swift */,
B097D4322173573B00B9D791 /* JobsViewModelSection.swift */,
B097D4342173578100B9D791 /* JobsViewModelCell.swift */,
);
path = Jobs;
sourceTree = "<group>";
......@@ -850,7 +857,6 @@
B0FE2F1C21552AC800F3D073 /* KeychainSwiftAccessOptions.swift */,
B0FE2F1E21552AF700F3D073 /* KeychainSwiftConstants.swift */,
B0AF91412157D192008F3B80 /* KeychainKey.swift */,
B0F5B9642172132B005E4591 /* SessionManager.swift */,
);
path = Keychain;
sourceTree = "<group>";
......@@ -1013,6 +1019,7 @@
92F913012172015F00B883BA /* RSFocusMarkLayer.swift in Sources */,
B050E16B215177820090CB79 /* JobsNavigator.swift in Sources */,
B050E14C21516A590090CB79 /* AmivRootNavigator.swift in Sources */,
B097D4352173578100B9D791 /* JobsViewModelCell.swift in Sources */,
92F912F92172015F00B883BA /* RSCode128Generator.swift in Sources */,
B005A33A2172B3F600D0BA31 /* EventViewModelCell.swift in Sources */,
B005A3322172895F00D0BA31 /* JSONEncoder+Extension.swift in Sources */,
......@@ -1062,6 +1069,7 @@
B0F5B96B217225EF005E4591 /* Gender.swift in Sources */,
B050E168215176D50090CB79 /* AmivMicroAppDelegate.swift in Sources */,
B0AF91292157B0A3008F3B80 /* EndPointType.swift in Sources */,
B097D4332173573B00B9D791 /* JobsViewModelSection.swift in Sources */,
B050E144215169950090CB79 /* Navigator.swift in Sources */,
B050E17A215180D20090CB79 /* AmivMicroAppCell.swift in Sources */,
B0F5B96D217226CD005E4591 /* AMIVMemebership.swift in Sources */,
......
......@@ -10,10 +10,10 @@ import Foundation
public enum KeychainKey: String {
case username
case password
case authToken
case userID
case sessionID
case etag
}
......
......@@ -14,16 +14,16 @@ public struct JobsViewModel {
var viewTitle: String
var jobOffers: [JobOffer]
var sections: [JobsViewModelSection]
// MARK: - Initializers
public init(viewTitle: String, jobOffers: [JobOffer]) {
public init(viewTitle: String, jobOffers: [(String, [JobOffer])]) {
self.viewTitle = viewTitle
self.jobOffers = jobOffers
self.sections = jobOffers.map({JobsViewModelSection(title: $0.0, jobs: $0.1)})
}
public init(jobOffers: [JobOffer]) {
public init(jobOffers: [(String, [JobOffer])]) {
self.init(viewTitle: "Jobs", jobOffers: jobOffers)
}
......
//
// JobsViewModelCell.swift
// Amiv
//
// Created by Domenic Wüthrich on 14.10.18.
// Copyright © 2018 Amiv an der ETH. All rights reserved.
//
import Foundation
public struct JobsViewModelCell {
// MARK: - Variables
public var title: String
public var jobOffer: JobOffer
// MARK: - Initializers
public init(job: JobOffer) {
self.title = job.title
self.jobOffer = job
}
}
//
// JobsViewModelSection.swift
// Amiv
//
// Created by Domenic Wüthrich on 14.10.18.
// Copyright © 2018 Amiv an der ETH. All rights reserved.
//
import Foundation
public struct JobsViewModelSection {
// MARK: - Variables
public var title: String
public var cells: [JobsViewModelCell]
// MARK: - Initializers
public init(title: String, jobs: [JobOffer]) {
self.title = title
self.cells = jobs.map({JobsViewModelCell(job: $0)})
}
}
......@@ -38,7 +38,7 @@ public class JobsNavigator: Navigator {
return
}
let model = JobsViewModel(jobOffers: offers)
let model = JobsViewModel(jobOffers: offers.sortJobs())
jobs.model = model
}
}
......@@ -80,7 +80,7 @@ extension JobsNavigator: JobsViewControllerDelegate {
return
}
let model = JobsViewModel(jobOffers: offers)
let model = JobsViewModel(jobOffers: offers.sortJobs())
viewController.model = model
}
}
......
......@@ -56,31 +56,13 @@ extension OnboardingNavigator: InfoViewControllerDelegate {
extension OnboardingNavigator: LoginViewControllerDelegate {
public func login(_ viewController: LoginViewController, username: String, password: String) {
// TODO: - Check for valid credentials
debugPrint("Logging in with username: \(username) and password: \(password)")
self.networkManager.authenticate(username: username, password: password) { (response, error) in
guard let response = response else {
DispatchQueue.main.async {
SessionManager.login(username: username, password: password) { (success, error) in
DispatchQueue.main.async {
if success {
self.delegate?.onboardingFinished()
} else {
viewController.loginFailed(with: "Incorrect Username or Password.\nPlease try again.")
}
return
}
// Save token into secure and encrypted keychain
SessionManager.save(response)
let userManager = NetworkManager<AMIVApiUser>()
userManager.getUserInfo({ (user, error) in
guard error == nil, let user = user else {
return
}
let _ = user.saveLocal()
})
DispatchQueue.main.async {
self.delegate?.onboardingFinished()
}
}
}
......
......@@ -11,7 +11,7 @@ import Foundation
public enum AMIVApiSession: EndPointType {
case authenticate(username: String, password: String)
case logout
case logout(_ sessionID: String, _ etag: String)
}
......@@ -21,11 +21,7 @@ extension AMIVApiSession {
switch self {
case .authenticate:
return "/sessions"
case .logout:
let keychain = KeychainSwift()
guard let id = keychain.get(KeychainKey.userID.rawValue) else {
return "/sessions"
}
case .logout(let id, _):
return "/sessions/\(id)"
}
}
......@@ -51,8 +47,10 @@ extension AMIVApiSession {
public var headers: HTTPHeaders? {
switch self {
case .authenticate, .logout:
case .authenticate:
return nil
case .logout(_, let etag):
return ["If-Match": etag]
}
}
......
......@@ -10,7 +10,7 @@ import Foundation
public enum AMIVApiUser {
case userInfo
case userInfo(_ id: String)
case allUsers
}
......@@ -19,11 +19,8 @@ extension AMIVApiUser: EndPointType {
public var path: String {
switch self {
case .userInfo:
if let id = SessionManager.userID {
return "/users/\(id)"
}
return "/users/0"
case .userInfo(let id):
return "/users/\(id)"
case .allUsers:
return "/users"
}
......
......@@ -14,12 +14,13 @@ public struct NetworkManager<EndPoint: EndPointType> {
public enum NetworkResponse: String {
case success
case authenticationError = "You are not logged in."
case authenticationError = "You are not logged in"
case badRequest = "Bad request"
case failed = "Request failed"
case noData = "Request was without data to decode."
case unableToDecode = "Unable to decode data."
case serverError = "Something went wrong at the AMIV server."
case alreadyExists = "Entry already exists."
}
public enum RequestResult<String> {
......@@ -29,7 +30,7 @@ public struct NetworkManager<EndPoint: EndPointType> {
fileprivate func handleNetworkRequest(_ response: HTTPURLResponse) -> RequestResult<String> {
switch response.statusCode {
case 200...299:
case 200...299, 100:
return .success
case 400:
return .failure(NetworkResponse.badRequest.rawValue)
......@@ -37,6 +38,8 @@ public struct NetworkManager<EndPoint: EndPointType> {
return .failure(NetworkResponse.authenticationError.rawValue)
case 404:
return .failure(NetworkResponse.noData.rawValue)
case 422:
return .failure(NetworkResponse.alreadyExists.rawValue)
case 500:
return .failure(NetworkResponse.serverError.rawValue)
default:
......@@ -131,7 +134,7 @@ extension NetworkManager where EndPoint == AMIVApiEvents {
}
public func signupTo(_ event: String, _ completion: @escaping (_ signup: EventSignup?, _ error: String?) -> Void) {
guard let user = User.loadLocal()?.id else {
guard let user = SessionManager.userID else {
completion(nil, "Missing user id")
return
}
......@@ -141,10 +144,6 @@ extension NetworkManager where EndPoint == AMIVApiEvents {
return
}
if let data = data {
debugPrint(String(data: data, encoding: .utf8))
}
guard let response = response as? HTTPURLResponse else {
debugPrint("fail")
return
......@@ -173,7 +172,7 @@ extension NetworkManager where EndPoint == AMIVApiEvents {
extension NetworkManager where EndPoint == AMIVApiJobs {
public func getJobOffers(_ completion: @escaping (_ response: [JobOffer]?, _ error: String?) -> Void) {
public func getJobOffers(_ completion: @escaping (_ response: JobOffersResponse?, _ error: String?) -> Void) {
router.request(.jobs) { (data, response, error) in
guard error == nil else {
completion(nil, error?.localizedDescription)
......@@ -192,7 +191,7 @@ extension NetworkManager where EndPoint == AMIVApiJobs {
}
do {
let apiResponse = try JSONDecoder().decode(JobOffersResponse.self, from: responseData)
completion(apiResponse.jobs, nil)
completion(apiResponse, nil)
} catch {
completion(nil, NetworkResponse.unableToDecode.rawValue)
}
......@@ -317,9 +316,13 @@ extension NetworkManager where EndPoint == AMIVApiSession {
}
public func logout(_ completion: @escaping (_ success: Bool, _ error: String?) -> Void) {
router.request(.logout) { (data, response, error) in
guard let sessionID = SessionManager.sessionID, let etag = SessionManager.etag else {
completion(false, "Missing sessionID or etag")
return
}
router.request(.logout(sessionID, etag)) { (data, response, error) in
guard error == nil else {
completion(false, "Please check your network connection.")
completion(true, nil)
return
}
......@@ -367,8 +370,8 @@ extension NetworkManager where EndPoint == AMIVApiUser {
}
}
public func getUserInfo(_ completion: @escaping (_ response: User?, _ error: String?) -> Void) {
router.request(.userInfo) { (data, response, error) in
public func getUserInfo(userID: String, _ completion: @escaping (_ response: User?, _ error: String?) -> Void) {
router.request(.userInfo(userID)) { (data, response, error) in
guard error == nil else {
completion(nil, "Please check your network connection.")
return
......
......@@ -10,6 +10,8 @@ import Foundation
public class SessionManager {
private static var networkManager = NetworkManager<AMIVApiSession>()
/// Authentication Token for current session
public static var authToken: String? {
let keychain = KeychainSwift()
......@@ -21,22 +23,72 @@ public class SessionManager {
return keychain.get(KeychainKey.userID.rawValue)
}
public static var sessionID: String? {
let keychain = KeychainSwift()
return keychain.get(KeychainKey.sessionID.rawValue)
}
public static var etag: String? {
let keychain = KeychainSwift()
return keychain.get(KeychainKey.etag.rawValue)
}
/// Bool indicating if user is currently logged in
public static var isLoggedIn: Bool {
return SessionManager.authToken != nil
}
/// Destroys authentication token and session id from keychain store
public static func logout() {
let keychain = KeychainSwift()
keychain.delete(KeychainKey.authToken.rawValue)
keychain.delete(KeychainKey.userID.rawValue)
public static func logout(_ completion: @escaping (_ success: Bool, _ error: String?) -> Void) {
SessionManager.networkManager.logout { (success, error) in
if success {
SessionManager.delete()
completion(success, error)
} else {
completion(success, error)
}
}
}
public static func login(username: String, password: String, _ completion: @escaping (_ success: Bool, _ error: String?) -> Void) {
SessionManager.networkManager.authenticate(username: username, password: password) { (response, error) in
guard error == nil, let response = response else {
completion(false, error)
return
}
SessionManager.save(response)
let userManager = NetworkManager<AMIVApiUser>()
userManager.getUserInfo(userID: response.userID, { (user, error) in
guard error == nil, let user = user else {
completion(false, "Error getting user info.")
return
}
if user.save {
completion(true, nil)
} else {
completion(false, "Error storing user locally.")
}
})
}
}
public static func save(_ session: AuthenticationResponse) {
let keychain = KeychainSwift()
keychain.set(session.token, forKey: KeychainKey.authToken.rawValue)
keychain.set(session.userID, forKey: KeychainKey.userID.rawValue)
keychain.set(session.sessionId, forKey: KeychainKey.sessionID.rawValue)
keychain.set(session.etag, forKey: KeychainKey.etag.rawValue)
keychain.synchronizable = true
}
public static func delete() {
let keychain = KeychainSwift()
keychain.delete(KeychainKey.authToken.rawValue)
keychain.delete(KeychainKey.userID.rawValue)
keychain.delete(KeychainKey.sessionID.rawValue)
keychain.delete(KeychainKey.etag.rawValue)
keychain.synchronizable = true
}
......
......@@ -27,3 +27,25 @@ extension JobOffersResponse: Decodable {
}
}
extension JobOffersResponse {
public func sortJobs() -> [(String, [JobOffer])] {
let sorted = self.jobs.sorted(by: {$0.company.lowercased() <= $1.company.lowercased()})
guard let first = sorted.first else {
return []
}
var together = [[first]]
sorted.dropFirst().forEach { (offer) in
if offer.company == together.last?.first?.company {
together[together.count-1].append(offer)
} else {
together.append([offer])
}
print(together)
}
return together.map({($0.first!.company, $0)})
}
}
......@@ -86,7 +86,7 @@ extension User: Codable {
extension User {
public static func loadLocal() -> User? {
public static var current: User? {
let fileManager = FileManager.default
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
......@@ -104,11 +104,11 @@ extension User {
}
}
public func saveLocal() -> Bool {
return User.saveLocal(self)
public var save: Bool {
return User.save(self)
}
public static func saveLocal(_ user: User) -> Bool {
public static func save(_ user: User) -> Bool {
let fileManager = FileManager.default
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
......
......@@ -78,32 +78,31 @@ public class JobsViewController: UITableViewController {
extension JobsViewController {
public override func numberOfSections(in tableView: UITableView) -> Int {
return 1
return self.model.sections.count
}
public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.model.jobOffers.count
return self.model.sections[section].cells.count
}
public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = self.model.jobOffers[indexPath.row]
let model = self.model.sections[indexPath.section].cells[indexPath.row]
let cell: UITableViewCell = {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "JobCell") else {
return UITableViewCell(style: .subtitle, reuseIdentifier: "JobCell")
return UITableViewCell(style: .default, reuseIdentifier: "JobCell")
}
return cell
}()
cell.textLabel?.text = model.title
cell.textLabel?.numberOfLines = 0
cell.detailTextLabel?.text = model.company
return cell
}
public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return "Jobs"
return self.model.sections[section].title
}
}
......@@ -113,7 +112,7 @@ extension JobsViewController {
extension JobsViewController {
public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.delegate?.didSelectJob(self, job: self.model.jobOffers[indexPath.row])
self.delegate?.didSelectJob(self, job: self.model.sections[indexPath.section].cells[indexPath.row].jobOffer)
tableView.deselectRow(at: indexPath, animated: true)
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment