Search code examples
iosswiftsiesta-swift

Siesta REST login


How to translate my login user URLSession code into Siesta framework code? My current attempt isn't working.

I've looked at the example in the GithubBrowser but the API I have doesn't work like that.

The issue is that the user structure is kind of split by how the endpoint in the API I'm consuming works. The endpoint is http://server.com/api/key. Yes, it really is called key and not user or login. Its called that by the authors because you post a user/pass pair and get a key back. So it takes in (via post) a json struct like:

{"name": "bob", "password": "s3krit"}

and returns as a response:

{"token":"AEWasBDasd...AAsdga"}

I have a SessionUser struct:

struct SessionUser: Codable
{
    let name: String
    let password: String
    let token: String
}

...which encapsulates the state (the "S" in REST) for the user. The trouble is name & password get posted and token is the response.

When this state changes I do my:

service.invalidateConfiguration()  // So requests get config change
service.wipeResources()            // Scrub all unauthenticated data

An instance is stored in a singleton, which is picked up by the configure block so that the token from the API is put in the header for all other API requests:

    configure("**") {
        // This block ^ is run AGAIN when the configuration is invalidated
        // even if loadManifest is not called again.
        if let haveToken = SessionManager.shared.currentUser?.token
        {
            $0.headers["Authorization"] = haveToken
        }
    }

That token injection part is already working well, by the way. Yay, Siesta!

URLSession version

This is bloated compared to Siesta, and I'm now not using this but here is what it used to be:

func login(user: SessionUser, endpoint: URL)
{
    DDLogInfo("Logging in: \(user.name) with \(user.password)")

    let json: [String: Any] = ["name": user.name, "password": user.password]
    let jsonData = try? JSONSerialization.data(withJSONObject: json)
    var request = URLRequest(url: endpoint)
    request.httpMethod = "POST"
    request.httpBody = jsonData
    _currentStatus = .Unknown

    weak var welf = self
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data else {
            handleLogin(error: error, message: "No data from login attempt")
            return
        }
        let jsonData:Any
        do {
            jsonData = try JSONSerialization.jsonObject(with: data, options: [])
        }
        catch let jsonDecodeError {
            handleLogin(error: jsonDecodeError, message: "Could not get JSON from login response data")
            return
        }
        guard let jsonDecoded = jsonData as? [String: Any] else {
            handleLogin(error: error, message: "Could not decode JSON as dictionary")
            return
        }
        guard let token = jsonDecoded["token"] as? String else {
            handleLogin(error: error, message: "No auth token in login response")
            return
        }
        let newUser = SessionUser(name: user.name, password: "", token: token)
        welf?.currentUser = newUser
        welf?.saveCurrentSession()
        welf?._currentStatus = .LoggedIn
        DDLogInfo("User \(newUser.name) logged in")
        loginUpdate(user: newUser, status: .LoggedIn, message: nil, error: nil)
    }

    task.resume()
}

Siesta Version

Here is my attempt right now:

func login(user: String, pass: String, status: @escaping (String?) -> ())
{
    let json = [ "name": user, "password": pass]
    let req = ManifestCloud.shared.keys.request(.post, json: json)
    req.onSuccess { (tokenInfo) in
        if let token = tokenInfo.jsonDict["token"] as? String
        {
            let newUser = SessionUser(name: user, password: pass, token: token)
            self.currentUser = newUser
        }
        status("success")
    }
    req.onFailure { (error) in
        status(error.userMessage)
    }
    req.onCompletion { (response) in
        status(nil)
    }
}

Its sort of working, but the log in credentials are not saved by Siesta and I've had to rig up a new notification system for login state which I'd hoped Siesta would do for me.

I want to use Siesta's caching so that the SessionUser object is cached locally and I can use it to get a new token, if required, using the cached credentials. At the moment I have a jury-rigged system using UserDefaults.

Any help appreciated!


Solution

  • The basic problem here is that you are requesting but not loading the resource. Siesta draws a distinction between those two things: the first is essentially a fancied-up URLSession request; the second means that Siesta hangs on to some state and notifies observers about it.

    Funny thing, I just answered a different but related question about this a few minutes ago! You might find that answer a helpful starting point.

    In your case, the problem is here:

    let req = ManifestCloud.shared.keys.request(.post, json: json)
    

    That .request(…) means that only your request hooks (onSuccess etc.) receive a notification when your POST request finishes, and Siesta doesn’t keep the state around for others to observe.

    You would normally accomplish that by using .load(); however, that creates a GET request and you need a POST. You probably want to promote your POST to be a full-fledge load request like this:

    let keysResource = ManifestCloud.shared.keys
    let req = keysResource.load(using:
        keysResource.request(.post, json: json))
    

    This will take whatever that POST request returns and make it the (observable) latestData of ManifestCloud.shared.keys, which should give you the “notification system for login state” that you’re looking for.