haskell-servant / servant

Servat is a Haskell DSL for describing, serving, querying, mocking, documenting web applications and more!
https://docs.servant.dev/
1.82k stars 412 forks source link

JS code generation fails when Api contains BasicAuth #672

Open kseo opened 7 years ago

kseo commented 7 years ago

writeJSForAPI fails to type check after I add BasicAuth to my api.

/Users/kseo/KodeBoxProjects/kodebox-server/jsgen/Main.hs: 21, 15
• No instance for (servant-foreign-0.8.1:Servant.Foreign.Internal.HasForeign
                     NoTypes
                     Servant.API.ContentTypes.NoContent
                     (Servant.API.BasicAuth.BasicAuth
                        "kodebox-realm" Kodebox.Server.Types.Session
                      Servant.API.Sub.:> Kodebox.Server.Api.PrivateApi))
    arising from a use of ‘writeJSForAPI’
• In the expression:
    writeJSForAPI
      restApi vanillaJS (joinPath [outputDir, "Kodebox.js"])
  In an equation for ‘writeJSCode’:
      writeJSCode
        = writeJSForAPI
            restApi vanillaJS (joinPath [outputDir, "Kodebox.js"])
arianvp commented 7 years ago

Hmm I remember I implemented this at some point, but perhaps it never ended up in master.

phadej commented 7 years ago

Related/Duplicates: https://github.com/haskell-servant/servant/issues/463 https://github.com/haskell-servant/servant/issues/672 https://github.com/haskell-servant/servant/issues/465

@arianvp you have an opportunity to close 3 issues with one PR! See also #682

arianvp commented 7 years ago

@phadej Due to exams wasn't able to finish this week. however I don't think this should block the 0.10 release. We can do a 0.10.1 release as the changes are non-breaking

phadej commented 7 years ago

@arianvp ack.

joaomilho commented 7 years ago

Just had the same issue. How can I help?

arianvp commented 7 years ago

A HasForeign instance has to be written for the BasicAuth stuff. Which generates enough information such that servant-js can write auth client code.

Which should be the trivial part: https://stackoverflow.com/questions/5507234/how-to-use-basic-auth-with-jquery-and-ajax

I am a bit busy, so I haven't gotten around doing it yet.

fieldstrength commented 7 years ago

I've just been bitten by this one as well. I'm also willing to help...

Has it just been forgotten for a while? :) It sounded like it was close to being solved.

arianvp commented 7 years ago

Yes I haven't had the time to implement it yet. It is not a hard fix I think. If you want to take a shot at it, i'm willing to help! It shouldn't be more than 20 lines of code I assume. We just need to add that extra HasForeign instance that adds the neccesery javascript code to make a basicauth call.

Which in jQuery is:

beforeSend: function (xhr) {
    xhr.setRequestHeader ("Authorization", "Basic " + btoa(username + ":" + password));
},

but finding an equivalent for axios and angular and plain XHR should not be hard. we just need to set the request header. We already do this for the Header HasForeign instance which you can look at for inspiration

mcgizzle commented 6 years ago

Hey just wondering what the status on the issue is? I am using Generalized Authentication provided by servant-auth-server and I am receiving the following error:

 • No instance for (servant-foreign-0.11.1:Servant.Foreign.Internal.HasForeign
                         servant-foreign-0.11.1:Servant.Foreign.Internal.NoTypes
                         NoContent
                         (Auth auths0 AuthUser :> AdminProtected))
        arising from a use of ‘writeJSForAPI’
    • In the expression:
        writeJSForAPI
          (Proxy :: Proxy (UserAPI auths)) vanillaJS "./assets/api.js"
      In an equation for ‘generateJavaScript’:
          generateJavaScript
            = writeJSForAPI
                (Proxy :: Proxy (UserAPI auths)) vanillaJS "./assets/api.js"

Ideally I would like to use Axios instead of vanilla.

mcgizzle commented 6 years ago

After some searching and hacking I appear to have found a solution. Creating the following instance for Auth does the trick.

instance forall lang ftype api auths v. ( HasForeign lang ftype api
                                        , HasForeignType lang ftype T.Text
         ) =>
         HasForeign lang ftype (Auth auths v
                                :> api) where
  type Foreign ftype (Auth auths v
                      :> api) = Foreign ftype api
  foreignFor lang Proxy Proxy subR =
    foreignFor lang Proxy (Proxy :: Proxy api) req
    where
      req = subR {_reqHeaders = HeaderArg arg : _reqHeaders subR}
      arg =
        Arg
          { _argName = PathSegment "Authorization"
          , _argType =
              typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy T.Text)
          }

Now I'm pretty new to type level programming so a lot of this is going over my head, I just found a very similar solution online and fought with compiler until the types matched up.

The only issue with this solution is the fact that it produces:

Authorization: <key> instead of the desired Authorization: Bearer <key>

I cannot figure out what to change to achieve my desired result. My current solution is to just pass Bearer <> key to the javascript functions, but this doesn't feel right. Any help toward a solution to this/ helping my understanding of the type level stuff would be greatly appreciated.

Also, should the above instance be included by default?

alpmestan commented 6 years ago

@domenkozar Is it always the case that the Authorization header is used? Do we always want Bearer too? And more generally, is the above code a good start for supporting the new auth stuffs in -foreign?

mcgizzle commented 6 years ago

For reference for anyone attempting this in the future the below change is all that is needed to add Bearer to the generated code.


      req = subR {_reqHeaders = ReplaceHeaderArg arg "Bearer {Authorization}" : _reqHeaders subR}

In relation to supporting new auth stuffs in foreign:

Would it be possible to add another (optional) piece of information to the Auth data type that would resemble the scheme of the authorization?

For example: data Auth authScheme (auths :: [*]) val

Could this authScheme value be used to pattern match for the instances and add different behaviour? Or conversely be used a function to determine the behaviour?

I might be way off here but this would be something I would be very interested in working on if I could receive a few pointers along the way.

odanoburu commented 4 years ago

for basic authentication I managed to get away with this orphan instance:

instance (HasForeign lang ftype api,  HasForeignType lang ftype Text) => HasForeign lang ftype (BasicAuth a b :> api) where
  type Foreign ftype (BasicAuth a b :> api) = Foreign ftype api
  foreignFor lang proxy1 Proxy subR = foreignFor lang proxy1 (Proxy :: Proxy api) req
    where
      req = subR {_reqHeaders = HeaderArg arg : _reqHeaders subR}
      arg =
        Arg
          { _argName = PathSegment "Authorization"
          , _argType =
              typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy Text)
          }

(I've never done type-level programming with Haskell, this is largely an adaptation of the code by @mcgizzle and from https://github.com/sordina/servant-options/issues/3)