snaplet-oauth 0.0.5 → 0.0.6
raw patch · 30 files changed
+1031/−119 lines, 30 filesdep ~data-lensdep ~hoauth2dep ~http-conduit
Dependency ranges changed: data-lens, hoauth2, http-conduit, snaplet-oauth, test-framework, test-framework-hunit
Files
- Makefile +68/−0
- README.md +36/−0
- example/Makefile +31/−0
- example/example.cabal +55/−0
- example/snaplets/heist/templates/_login.tpl +21/−0
- example/snaplets/heist/templates/_new_user.tpl +5/−0
- example/snaplets/heist/templates/base.tpl +13/−0
- example/snaplets/heist/templates/index.tpl +22/−0
- example/snaplets/heist/templates/login.tpl +3/−0
- example/snaplets/heist/templates/new_user.tpl +3/−0
- example/snaplets/heist/templates/userform.tpl +14/−0
- example/src/Application.hs +36/−0
- example/src/Keys.hs.default +34/−0
- example/src/Main.hs +102/−0
- example/src/OAuthHandlers.hs +117/−0
- example/src/Site.hs +104/−0
- example/src/Splices.hs +21/−0
- example/static/screen.css +26/−0
- snaplet-oauth.cabal +42/−21
- src/Snap/Snaplet/OAuth.hs +15/−11
- src/Snap/Snaplet/OAuth/Github.hs +59/−0
- src/Snap/Snaplet/OAuth/Github/Api.hs +56/−0
- src/Snap/Snaplet/OAuth/Google.hs +21/−18
- src/Snap/Snaplet/OAuth/Google/Api.hs +7/−5
- src/Snap/Snaplet/OAuth/Internal/Handlers.hs +5/−3
- src/Snap/Snaplet/OAuth/Internal/Types.hs +15/−11
- src/Snap/Snaplet/OAuth/Internal/Utils.hs +10/−16
- src/Snap/Snaplet/OAuth/Weibo.hs +25/−22
- src/Snap/Snaplet/OAuth/Weibo/Api.hs +12/−12
- tests/Weibo.hs +53/−0
+ Makefile view
@@ -0,0 +1,68 @@++HC=ghc+DIST=dist+CDEV=cabal-dev+STYLE=stylish-haskell+++.PHONY: clean test hlint++default: build++init:+ cabal update+ $(CDEV) install --only-dependencies++clean:+ rm -rf $(DIST)+ rm -rf ./cabal-dev/lib/snaplet-oauth*+ rm -rf ./cabal-dev/packages/snaplet-oauth*+ rm -f ./cabal-dev/packages-7.4.1.conf/snaplet-oauth-*++hlint:+ hlint src/ tests/ --report=$(DIST)/hlint.html+ find src tests example -name '*.hs' | xargs $(STYLE) -i++conf:+ $(CDEV) configure --enable-tests++build: conf+ $(CDEV) build++rebuild: clean build++install: build+ $(CDEV) install --enable-tests++reinstall: clean install++test: build+ $(CDEV) test++demo:+ cd example && make clean p++doc:+ $(CDEV) haddock++dist: clean build+ $(CDEV) sdist++### build before push in case compilation error++push: clean build+ git push --all++fun:+ cloc src+ cloc tests++local:+ rm -rf ./cabal-dev/lib/hoauth2*+ rm -rf ./cabal-dev/packages/hoauth2*+ rm -f ./cabal-dev/packages-7.4.1.conf/hoauth2-*+ $(CDEV) add-source ../hoauth2+ $(CDEV) install++##+## cd example/ && runghc -package-conf=../cabal-dev/packages-7.4.1.conf/ snap.hs -b 127.0.0.1 -p 9988
+ README.md view
@@ -0,0 +1,36 @@+++- master: [](http://travis-ci.org/HaskellCNOrg/snap-oauth)+- branch/0.0.6: [](https://travis-ci.org/HaskellCNOrg/snaplet-oauth)++snaplet-oauth+=============++oauth client snaplet++support oauth providers++ - [weibo](http://weib.com)+ - [google](https://developers.google.com/accounts/docs/OAuth2WebServer)+ - github++Build & Tests+=============+ + - `make init rebuild`+ - `make test`++Demo+=============++ 1. `cd example`+ 2. update `src/Keys.hs.default` and rename to `Keys.hs`+ 3. make clean prev+++snaplet+=============++- [what is snaplet]++[what is snaplet]: http://snapframework.com/docs/tutorials/snaplets-tutorial
+ example/Makefile view
@@ -0,0 +1,31 @@++DIST=dist+CDEV=cabal-dev --sandbox=../cabal-dev+TARGET=./dist/build/example/example++.PHONY: example++default: build++init:+ cabal update+ $(CDEV) install++clean:+ rm -rf $(DIST)++conf:+ $(CDEV) configure --flags="development"++build: conf+ $(CDEV) build++rebuild: clean build++install: build+ $(CDEV) install++p: build+ $(TARGET) -p 9988++# cd example/ && runghc -package-conf=../cabal-dev/packages-7.4.1.conf/ snap.hs -b 127.0.0.1 -p 9988
+ example/example.cabal view
@@ -0,0 +1,55 @@+Name: example+Version: 0.1+Synopsis: Project Synopsis Here+Description: Project Description Here+License: AllRightsReserved+Author: Author+Maintainer: maintainer@example.com+Stability: Experimental+Category: Web+Build-type: Simple+Cabal-version: >=1.2++Flag development+ Description: Whether to build the server in development (interpreted) mode+ Default: False++Executable example+ hs-source-dirs: src+ main-is: Main.hs++ Build-depends:+ base >= 4 && < 5,+ bytestring >= 0.9.1 && < 0.10,+ data-lens >= 2.0.1 && < 2.11,+ data-lens-template >= 2.1 && < 2.2,+ heist >= 0.8 && < 0.9,+ MonadCatchIO-transformers >= 0.2.1 && < 0.4,+ mtl >= 2 && < 3,+ snap == 0.9.*,+ snap-core == 0.9.*,+ snap-server == 0.9.*,+ snap-loader-static == 0.9.*,+ text >= 0.11 && < 0.12,+ time >= 1.1 && < 1.5,+ xmlhtml >= 0.1,+ snaplet-oauth >= 0.0.6 && < 0.0.7,+ unordered-containers >= 0.2.2 && < 0.3++ if flag(development)+ build-depends:+ snap-loader-dynamic == 0.9.*+ cpp-options: -DDEVELOPMENT+ -- In development mode, speed is already going to suffer, so skip+ -- the fancy optimization flags. Additionally, disable all+ -- warnings. The hint library doesn't give an option to execute+ -- compiled code when there were also warnings, so disabling+ -- warnings allows quicker workflow.+ ghc-options: -threaded -w+ else+ if impl(ghc >= 6.12.0)+ ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2+ -fno-warn-orphans -fno-warn-unused-do-bind+ else+ ghc-options: -threaded -Wall -fwarn-tabs -funbox-strict-fields -O2+ -fno-warn-orphans
+ example/snaplets/heist/templates/_login.tpl view
@@ -0,0 +1,21 @@+<h1>Snap Example App Login</h1>++<p><loginError/></p>++<bind tag="postAction">/login</bind>+<bind tag="submitText">Login</bind>+<apply template="userform"/>++<p>Don't have a login yet? <a href="/new_user">Create a new user</a></p>+<ul>+ <li>+ <a href="/oauth/weibo">Login with Weibo</a>+ </li>+ <li>+ <a href="/oauth/google">Login with Google</a>+ </li>+ <li>+ <a href="/oauth/github">Login with Github</a>+ </li>+</ul>+
+ example/snaplets/heist/templates/_new_user.tpl view
@@ -0,0 +1,5 @@+<h1>Register a new user</h1>++<bind tag="postAction">/new_user</bind>+<bind tag="submitText">Add User</bind>+<apply template="userform"/>
+ example/snaplets/heist/templates/base.tpl view
@@ -0,0 +1,13 @@+<html>+ <head>+ <title>Snap web server</title>+ <link rel="stylesheet" type="text/css" href="/screen.css"/>+ </head>+ <body>+ <div id="content">++ <content/>++ </div>+ </body>+</html>
+ example/snaplets/heist/templates/index.tpl view
@@ -0,0 +1,22 @@+<apply template="base">++ <ifLoggedIn>+ <p>+ This is a simple demo page served using+ <a href="http://snapframework.com/docs/tutorials/heist">Heist</a>+ and the <a href="http://snapframework.com/">Snap</a> web framework.+ </p>++ <p>Congrats! You're logged in as '<loggedInUser/>'</p>+ <p><rawResponseSplices /></p>++ <p><a href="/">Home</a></p>+ <p><a href="/logout">Logout</a></p>++ </ifLoggedIn>++ <ifLoggedOut>+ <apply template="_login"/>+ </ifLoggedOut>++</apply>
+ example/snaplets/heist/templates/login.tpl view
@@ -0,0 +1,3 @@+<apply template="base">+ <apply template="_login"/>+</apply>
+ example/snaplets/heist/templates/new_user.tpl view
@@ -0,0 +1,3 @@+<apply template="base">+ <apply template="_new_user" />+</apply>
+ example/snaplets/heist/templates/userform.tpl view
@@ -0,0 +1,14 @@+<form method="post" action="${postAction}">+ <table id="info">+ <tr>+ <td>Login:</td><td><input type="text" name="login" size="20" /></td>+ </tr>+ <tr>+ <td>Password:</td><td><input type="password" name="password" size="20" /></td>+ </tr>+ <tr>+ <td></td>+ <td><input type="submit" value="${submitText}" /></td>+ </tr>+ </table>+</form>
+ example/src/Application.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE TemplateHaskell #-}++------------------------------------------------------------------------------+-- | This module defines our application's state type and an alias for its+-- handler monad.+module Application where++------------------------------------------------------------------------------+import Data.Lens.Template+import Snap.Snaplet+import Snap.Snaplet.Auth+import Snap.Snaplet.Heist+import Snap.Snaplet.OAuth+import Snap.Snaplet.Session++------------------------------------------------------------------------------+data App = App+ { _heist :: Snaplet (Heist App)+ , _sess :: Snaplet SessionManager+ , _auth :: Snaplet (AuthManager App)+ , _oauth :: Snaplet OAuthSnaplet+ }++makeLens ''App++instance HasHeist App where+ heistLens = subSnaplet heist++instance HasOAuth App where+ oauthLens = oauth++------------------------------------------------------------------------------++type AppHandler = Handler App App++
+ example/src/Keys.hs.default view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}++module Keys where++import Snap.Snaplet.OAuth++----------------------------------------------------------------------++keys = [ (weibo, weiboKey)+ , (google, googleKey)+ , (github, githubKey)+ ]++----------------------------------------------------------------------++googleKey :: OAuth2+googleKey = OAuth2 { oauthClientId = ""+ , oauthClientSecret = ""+ , oauthCallback = Just "http://127.0.0.1:9988/googleCallback"+ , oauthOAuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth"+ , oauthAccessTokenEndpoint = "https://accounts.google.com/o/oauth2/token"+ , oauthAccessToken = Nothing+ }++weiboKey :: OAuth2+weiboKey = OAuth2 { oauthClientId = ""+ , oauthClientSecret = ""+ , oauthCallback = Just "http://127.0.0.1:9988/oauthCallback"+ , oauthOAuthorizeEndpoint = "https://api.weibo.com/oauth2/authorize"+ , oauthAccessTokenEndpoint = "https://api.weibo.com/oauth2/access_token"+ , oauthAccessToken = Nothing+ }+githubKey :: OAuth2+githubKey = undefined
+ example/src/Main.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}++module Main where++------------------------------------------------------------------------------+import Control.Exception (SomeException, try)+import qualified Data.Text as T+import Site+import Snap.Core+import Snap.Http.Server+import Snap.Snaplet+import Snap.Snaplet.Config+import System.IO++#ifdef DEVELOPMENT+import Snap.Loader.Dynamic+#else+import Snap.Loader.Static+#endif+++------------------------------------------------------------------------------+-- | This is the entry point for this web server application. It supports+-- easily switching between interpreting source and running statically compiled+-- code.+--+-- In either mode, the generated program should be run from the root of the+-- project tree. When it is run, it locates its templates, static content, and+-- source files in development mode, relative to the current working directory.+--+-- When compiled with the development flag, only changes to the libraries, your+-- cabal file, or this file should require a recompile to be picked up.+-- Everything else is interpreted at runtime. There are a few consequences of+-- this.+--+-- First, this is much slower. Running the interpreter takes a significant+-- chunk of time (a couple tenths of a second on the author's machine, at this+-- time), regardless of the simplicity of the loaded code. In order to+-- recompile and re-load server state as infrequently as possible, the source+-- directories are watched for updates, as are any extra directories specified+-- below.+--+-- Second, the generated server binary is MUCH larger, since it links in the+-- GHC API (via the hint library).+--+-- Third, and the reason you would ever want to actually compile with+-- development mode, is that it enables a faster development cycle. You can+-- simply edit a file, save your changes, and hit reload to see your changes+-- reflected immediately.+--+-- When this is compiled without the development flag, all the actions are+-- statically compiled in. This results in faster execution, a smaller binary+-- size, and having to recompile the server for any code change.+--+main :: IO ()+main = do+ -- Depending on the version of loadSnapTH in scope, this either enables+ -- dynamic reloading, or compiles it without. The last argument to+ -- loadSnapTH is a list of additional directories to watch for changes to+ -- trigger reloads in development mode. It doesn't need to include source+ -- directories, those are picked up automatically by the splice.+ (conf, site, cleanup) <- $(loadSnapTH [| getConf |]+ 'getActions+ ["snaplets/heist/templates"])++ _ <- try $ httpServe conf site :: IO (Either SomeException ())+ cleanup+++------------------------------------------------------------------------------+-- | This action loads the config used by this application. The loaded config+-- is returned as the first element of the tuple produced by the loadSnapTH+-- Splice. The type is not solidly fixed, though it must be an IO action that+-- produces the same type as 'getActions' takes. It also must be an instance of+-- Typeable. If the type of this is changed, a full recompile will be needed to+-- pick up the change, even in development mode.+--+-- This action is only run once, regardless of whether development or+-- production mode is in use.+getConf :: IO (Config Snap AppConfig)+getConf = commandLineAppConfig defaultConfig+++------------------------------------------------------------------------------+-- | This function generates the the site handler and cleanup action from the+-- configuration. In production mode, this action is only run once. In+-- development mode, this action is run whenever the application is reloaded.+--+-- Development mode also makes sure that the cleanup actions are run+-- appropriately before shutdown. The cleanup action returned from loadSnapTH+-- should still be used after the server has stopped handling requests, as the+-- cleanup actions are only automatically run when a reload is triggered.+--+-- This sample doesn't actually use the config passed in, but more+-- sophisticated code might.+getActions :: Config Snap AppConfig -> IO (Snap (), IO ())+getActions conf = do+ (msgs, site, cleanup) <- runSnaplet+ (appEnvironment =<< getOther conf) app+ hPutStrLn stderr $ T.unpack msgs+ return (site, cleanup)
+ example/src/OAuthHandlers.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE OverloadedStrings #-}++module OAuthHandlers+ ( routes ) where++------------------------------------------------------------------------------+import Control.Applicative+import Control.Monad+import Control.Monad.Trans+import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as BS+import Data.Maybe+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Snap.Core+import Snap.Snaplet+import Snap.Snaplet.Auth+import Snap.Snaplet.Auth.Backends.JsonFile+import Snap.Snaplet.Heist+import Snap.Snaplet.OAuth+import Text.Templating.Heist+++import qualified Snap.Snaplet.OAuth.Github as GH+import qualified Snap.Snaplet.OAuth.Google as G+import qualified Snap.Snaplet.OAuth.Weibo as W++import Application+import Splices++----------------------------------------------------------------------+-- Weibo+----------------------------------------------------------------------++-- | Logs out and redirects the user to the site index.+weiboOauthCallbackH :: AppHandler ()+weiboOauthCallbackH = W.weiboUserH+ >>= success+ where success Nothing = writeBS "No user info found"+ success (Just usr) = do+ with auth $ createOAuthUser weibo $ W.wUidStr usr+ --writeText $ T.pack $ show usr+ toHome usr+++----------------------------------------------------------------------+-- Google+----------------------------------------------------------------------++googleOauthCallbackH :: AppHandler ()+googleOauthCallbackH = G.googleUserH+ >>= googleUserId++googleUserId :: Maybe G.GoogleUser -> AppHandler ()+googleUserId Nothing = redirect "/"+googleUserId (Just user) = with auth (createOAuthUser google (G.gid user))+ >> toHome user++----------------------------------------------------------------------+-- Github+----------------------------------------------------------------------++githubOauthCallbackH :: AppHandler ()+githubOauthCallbackH = GH.githubUserH+ >>= githubUser++githubUser :: Maybe GH.GithubUser -> AppHandler ()+githubUser Nothing = redirect "/"+githubUser (Just user) = with auth (createOAuthUser github uid)+ >> toHome user+ where uid = intToText $ GH.gid user+ intToText = T.pack . show+++----------------------------------------------------------------------+-- Create User per oAuth response+----------------------------------------------------------------------++-- | Create new user for Weibo User to local+--+createOAuthUser :: OAuthKey+ -> T.Text -- ^ oauth user id+ -> Handler App (AuthManager App) ()+createOAuthUser key name = do+ let name' = textToBS name+ passwd = ClearText name'+ role = Role (BS.pack $ show key)+ exists <- usernameExists name+ unless exists (void (createUser name name'))+ res <- loginByUsername name' passwd False+ case res of+ Left l -> liftIO $ print l+ Right r -> do+ res2 <- saveUser (r {userRoles = [ role ]})+ return ()+ --either (liftIO . print) (const $ return ()) res2++----------------------------------------------------------------------+-- Routes+----------------------------------------------------------------------++-- | The application's routes.+routes :: [(ByteString, AppHandler ())]+routes = [ ("/oauthCallback", weiboOauthCallbackH)+ , ("/googleCallback", googleOauthCallbackH)+ , ("/githubCallback", githubOauthCallbackH)+ ]++-- | NOTE: when use such way to show callback result,+-- the url does not change, which can not be invoke twice.+-- This is quite awkful thing and only for testing purpose.+--+toHome a = heistLocal (bindRawResponseSplices a) $ render "index"++----------------------------------------------------------------------+--+----------------------------------------------------------------------
+ example/src/Site.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE OverloadedStrings #-}++------------------------------------------------------------------------------+-- | This module is where all the routes and handlers are defined for your+-- site. The 'app' function is the initializer that combines everything+-- together and is exported by this module.+module Site+ ( app+ ) where++------------------------------------------------------------------------------+import Control.Applicative+import Data.ByteString (ByteString)+import qualified Data.HashMap.Strict as M+import Data.Maybe+import qualified Data.Text as T+import Snap.Core+import Snap.Snaplet+import Snap.Snaplet.Auth+import Snap.Snaplet.Auth.Backends.JsonFile+import Snap.Snaplet.Heist+import Snap.Snaplet.OAuth+import Snap.Snaplet.Session.Backends.CookieSession+import Snap.Util.FileServe+import Text.Templating.Heist++------------------------------------------------------------------------------++import Application+import Keys+import qualified OAuthHandlers as OA++------------------------------------------------------------------------------+-- | Render login form+handleLogin :: Maybe T.Text -> Handler App (AuthManager App) ()+handleLogin authError = heistLocal (bindSplices errs) $ render "login"+ where+ errs = [("loginError", textSplice c) | c <- maybeToList authError]+++------------------------------------------------------------------------------+-- | Handle login submit+handleLoginSubmit :: Handler App (AuthManager App) ()+handleLoginSubmit =+ loginUser "login" "password" Nothing+ (\_ -> handleLogin err) (redirect "/")+ where+ err = Just "Unknown user or password"+++------------------------------------------------------------------------------+-- | Logs out and redirects the user to the site index.+handleLogout :: Handler App (AuthManager App) ()+handleLogout = logout >> redirect "/"+++------------------------------------------------------------------------------+-- | Handle new user form submit+handleNewUser :: Handler App (AuthManager App) ()+handleNewUser = method GET handleForm <|> method POST handleFormSubmit+ where+ handleForm = render "new_user"+ handleFormSubmit = registerUser "login" "password" >> redirect "/"+++------------------------------------------------------------------------------+-- | The application's routes.+routes :: [(ByteString, Handler App App ())]+routes = [ ("/login", with auth handleLoginSubmit)+ , ("/logout", with auth handleLogout)+ , ("/new_user", with auth handleNewUser)+ ]+ <|>+ OA.routes+ <|>+ [ ("", serveDirectory "static") ]+++------------------------------------------------------------------------------+-- | The application initializer.+app :: SnapletInit App App+app = makeSnaplet "app" "An snaplet example application." Nothing $ do+ h <- nestSnaplet "" heist $ heistInit "templates"+ s <- nestSnaplet "sess" sess $+ initCookieSessionManager "site_key.txt" "sess" (Just 3600)++ -- NOTE: We're using initJsonFileAuthManager here because it's easy and+ -- doesn't require any kind of database server to run. In practice,+ -- you'll probably want to change this to a more robust auth backend.+ a <- nestSnaplet "auth" auth $+ initJsonFileAuthManager defAuthSettings sess "users.json"++ --+ -- NOTE: if defined as:+ -- oa <- nestSnaplet "oauth" oauth $ initOauthSnaplet+ -- the url becomes /oauth/weibo+ --+ oa <- nestSnaplet "oauth" oauth $ initOauthSnaplet True oauthkeys+ addRoutes routes+ addAuthSplices auth+ return $ App h s a oa++oauthkeys :: OAuthMap+oauthkeys = OAuthMap $ M.fromList Keys.keys
+ example/src/Splices.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE OverloadedStrings #-}+++module Splices where++import Snap.Snaplet.OAuth+import Text.Templating.Heist++----------------------------------------------------------------------+-- Splices+----------------------------------------------------------------------+++rawResponseSplices :: (Monad m, Show a) => a -> Splice m+rawResponseSplices = textSplice . sToText++bindRawResponseSplices :: (Monad m, Show a)+ => a+ -> HeistState m+ -> HeistState m+bindRawResponseSplices value = bindSplice "rawResponseSplices" (rawResponseSplices value)
+ example/static/screen.css view
@@ -0,0 +1,26 @@+html {+ padding: 0;+ margin: 0;+ background-color: #ffffff;+ font-family: Verdana, Helvetica, sans-serif;+}+body {+ padding: 0;+ margin: 0;+}+a {+ text-decoration: underline;+}+a :hover {+ cursor: pointer;+ text-decoration: underline;+}+img {+ border: none;+}+#content {+ padding-left: 1em;+}+#info {+ font-size: 60%;+}
snaplet-oauth.cabal view
@@ -1,9 +1,9 @@ Name: snaplet-oauth-Version: 0.0.5+Version: 0.0.6 Synopsis: snaplet-oauth Description: This lib is in Alpha status and APIs are likely to be changed.-Homepage: freizl.github.com+Homepage: https://github.com/HaskellCNOrg/snaplet-oauth License: BSD3 License-file: LICENSE Author: Haisheng,Wu@@ -12,48 +12,69 @@ Category: Web Build-type: Simple stability: alpha+Cabal-version: >=1.10 + -- Extra files to be distributed with the package, such as examples or -- a README.--- Extra-source-files: +Extra-source-files: + README.md+ Makefile+ tests/Weibo.hs+ example/example.cabal+ example/Makefile+ example/snaplets/heist/templates/*.tpl+ example/src/Splices.hs+ example/src/Site.hs+ example/src/OAuthHandlers.hs+ example/src/Main.hs+ example/src/Application.hs+ example/src/Keys.hs.default+ example/static/*.css -Cabal-version: >=1.10+Source-Repository head+ Type: git+ Location: git://github.com/HaskellCNOrg/snaplet-oauth.git ++ Library default-language: Haskell2010 Hs-Source-Dirs: src Exposed-modules: Snap.Snaplet.OAuth- Snap.Snaplet.OAuth.Internal.Types- Snap.Snaplet.OAuth.Internal.Handlers- Snap.Snaplet.OAuth.Internal.Utils Snap.Snaplet.OAuth.Weibo Snap.Snaplet.OAuth.Google+ Snap.Snaplet.OAuth.Github Other-Modules:+ Snap.Snaplet.OAuth.Internal.Types+ Snap.Snaplet.OAuth.Internal.Handlers+ Snap.Snaplet.OAuth.Internal.Utils Snap.Snaplet.OAuth.Weibo.Api Snap.Snaplet.OAuth.Google.Api+ Snap.Snaplet.OAuth.Github.Api Build-Depends: + MonadCatchIO-mtl >= 0.3 && < 0.4,+ aeson >= 0.6 && < 0.7, base >= 4 && < 5, bytestring >= 0.9 && < 1.0,- data-lens >= 2.0.1 && < 2.11,- text >= 0.11 && < 0.12, bytestring-show >= 0.3.5 && < 0.4,- aeson >= 0.6 && < 0.7,- data-lens-template == 2.1.*,+ data-lens >= 2.0 && < 2.1,+ data-lens-template >= 2.1 && < 2.2, failure >= 0.1,+ hashable >= 1.1.2 && < 1.2,+ heist == 0.8.*,+ hoauth2 >= 0.2.6 && < 0.2.7,+ http-conduit >= 1.8.3 && < 1.8.5,+ http-types >= 0.7 && < 0.8, snap == 0.9.*, snap-core == 0.9.*, snap-loader-dynamic == 0.9.*, snap-loader-static == 0.9.*,- heist == 0.8.*,- MonadCatchIO-mtl >= 0.3 && < 0.4,- unordered-containers >= 0.2 && < 0.3,- hashable >= 1.1.2 && < 1.2,- http-types >= 0.7 && < 0.8,- http-conduit >= 1.6 && < 1.7,- hoauth2 == 0.2.4+ text >= 0.11 && < 0.12,+ unordered-containers >= 0.2 && < 0.3 if impl(ghc >= 6.12.0)@@ -76,7 +97,7 @@ base >= 4 && < 5, bytestring >= 0.9 && < 1.0, bytestring-show >= 0.3 && < 0.4,- test-framework >= 0.6 && < 0.7,- test-framework-hunit >= 0.2 && < 0.3,+ test-framework >= 0.6 && < 0.9,+ test-framework-hunit >= 0.2 && < 0.4, text >= 0.11 && < 0.12,- snaplet-oauth >= 0.0.5 && < 0.0.6+ snaplet-oauth >= 0.0.5 && < 0.0.7
src/Snap/Snaplet/OAuth.hs view
@@ -8,9 +8,11 @@ , module TY ) where import Data.ByteString (ByteString)+import Data.HashMap.Strict (member) import Snap -import Network.OAuth2.OAuth2 as OA+import Network.OAuth.OAuth2 as OA+import qualified Snap.Snaplet.OAuth.Github as GH import qualified Snap.Snaplet.OAuth.Google as G import Snap.Snaplet.OAuth.Internal.Handlers as HS import Snap.Snaplet.OAuth.Internal.Types as TY@@ -28,14 +30,16 @@ -- ^ Oauth Keys -> SnapletInit b OAuthSnaplet initOauthSnaplet rt oauths =- makeSnaplet "OAuthSnaplet" "" Nothing $ do- when rt (addRoutes routes)- return $ emptyOAuthSnaplet { oauthKeys = oauths }+ makeSnaplet "OAuth" "Snaplet - OAuth Client" Nothing $ do+ when rt (addDefaultRouters oauths)+ return $ emptyOAuthSnaplet { oauthKeys = oauths } --- | Snap Handlers--- ?? TODO: add routes per config [weibo, google, github]----routes :: HasOAuth b => [(ByteString, Handler b v ())]-routes = W.routes- <|>- G.routes+addDefaultRouters :: HasOAuth b => OAuthMap -> Initializer b v ()+addDefaultRouters (OAuthMap maps) = addRoutes $ concat+ [ r | (k, r) <- defaultRoutes, k `member` maps ]++defaultRoutes :: HasOAuth b => [(OAuthKey, [(ByteString, Handler b v ())])]+defaultRoutes = [ (weibo, W.routes)+ , (google, G.routes)+ , (github, GH.routes)+ ]
+ src/Snap/Snaplet/OAuth/Github.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}++module Snap.Snaplet.OAuth.Github+ (+ -- * Routes+ routes+ -- * Handlers+ , githubLoginH+ , githubCallbackH+ , githubUserH+ -- * Types and API+ , module Snap.Snaplet.OAuth.Github.Api+ ) where++------------------------------------------------------------------------------+import Control.Category+import Control.Monad+import Data.ByteString (ByteString)+import Data.Maybe+import Prelude hiding ((.))+import Snap++import Snap.Snaplet.OAuth.Github.Api+import Snap.Snaplet.OAuth.Internal.Handlers+import Snap.Snaplet.OAuth.Internal.Types++------------------------------------------------------------------------------+-- Github+------------------------------------------------------------------------------++githubLoginH :: HasOAuth b => Handler b v ()+githubLoginH = loginWithOauthH github Nothing+++-- | token access callback.+-- return a @OAuthValue@ having access token has been filled.+--+githubCallbackH :: HasOAuth b => Handler b v OAuthValue+githubCallbackH = oauthCallbackH github++-- | token access callback+-- return a @Maybe@ @GithubUser@ if everything goes well otherwise @Nothing@.+--+githubUserH :: HasOAuth b => Handler b v (Maybe GithubUser)+githubUserH = githubCallbackH >>= liftIO . apiUser+++------------------------------------------------------------------------------++-- | The application's routes.+--+routes :: HasOAuth b => [(ByteString, Handler b v ())]+routes = [ ("/github" , githubLoginH)+ ]+ -- where the callback handler is suppose to be added by user.+++----------------------------------------------------------------------------
+ src/Snap/Snaplet/OAuth/Github/Api.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}++{-+++-}++module Snap.Snaplet.OAuth.Github.Api where++import Control.Applicative+import Control.Monad (mzero)+import Data.Aeson+import qualified Data.ByteString as BS+import Data.Text (Text)++import Snap.Snaplet.OAuth.Internal.Types+import Snap.Snaplet.OAuth.Internal.Utils+++----------------------------------------------------------------------+-- APIs Impl+----------------------------------------------------------------------++-- | TODO: maybe need email infomation+--+data GithubUser = GithubUser { gid :: Integer+ , gname :: Text+ , glogin :: Text+ , glink :: Text+ } deriving (Show, Eq)++instance FromJSON GithubUser where+ parseJSON (Object o) = GithubUser+ <$> o .: "id"+ <*> o .: "name"+ <*> o .: "login"+ <*> o .: "html_url"+ parseJSON _ = mzero++----------------------------------------------------------------------+-- APIs Impl+----------------------------------------------------------------------+++apiUser :: OAuthValue -> IO (Maybe GithubUser)+apiUser = apiRequestOAuth apiUrlUser+++----------------------------------------------------------------------+-- APIs URI+----------------------------------------------------------------------++-- | Possible operations of UserInfo+--+apiUrlUser :: BS.ByteString+apiUrlUser = "https://api.github.com/user"
src/Snap/Snaplet/OAuth/Google.hs view
@@ -3,22 +3,26 @@ {-# LANGUAGE RankNTypes #-} module Snap.Snaplet.OAuth.Google- ( routes- , loginWithGoogleH+ (+ -- * Routes+ routes+ -- * Handlers+ , googleLoginH , googleCallbackH- , userInfoH+ , googleUserH+ -- * Types and API , module Snap.Snaplet.OAuth.Google.Api ) where ------------------------------------------------------------------------------ import Control.Category import Data.ByteString (ByteString)+import qualified Data.ByteString as BS import Data.Maybe import Network.HTTP.Types (renderSimpleQuery) import Prelude hiding ((.)) import Snap -import Network.OAuth2.OAuth2 import Snap.Snaplet.OAuth.Google.Api import Snap.Snaplet.OAuth.Internal.Handlers import Snap.Snaplet.OAuth.Internal.Types@@ -27,28 +31,27 @@ -- Google ------------------------------------------------------------------------------ --- | FIXME: How to support multiple scope??--- according to OAuth 2.0 playround, multiple scope is supposed.--- BS.intercalate "+" scopes)] **does not work**--- scopes = [googleScopeEmail, googleScopeUserInfo] **in order to get email**----loginWithGoogleH :: HasOAuth b => Handler b v ()-loginWithGoogleH = loginWithOauthH Google scopeParam- where scopeParam = Just $ renderSimpleQuery False scopeQuery- scopeQuery = [(googleScopeKey, googleScopeUserInfo)]+-- |+googleLoginH :: HasOAuth b => Handler b v ()+googleLoginH = loginWithOauthH google scopeParam+ where scopeParam = Just $ renderSimpleQuery False scopeQuery+ scopeQuery = [(googleScopeKey, BS.intercalate " " scopes)]+ -- | multiple scope in order to get email info+ scopes = [googleScopeEmail, googleScopeUserInfo] -googleCallbackH :: HasOAuth b => Handler b v OAuth2-googleCallbackH = oauthCallbackH Google+googleCallbackH :: HasOAuth b => Handler b v OAuthValue+googleCallbackH = oauthCallbackH google -userInfoH :: HasOAuth b => OAuth2 -> Handler b v (Maybe GoogleUser)-userInfoH = liftIO . userInfo+googleUserH :: HasOAuth b => Handler b v (Maybe GoogleUser)+googleUserH = googleCallbackH+ >>= liftIO . userInfo ------------------------------------------------------------------------------ -- | The application's routes. -- routes :: HasOAuth b => [(ByteString, Handler b v ())]-routes = [ ("/google", loginWithGoogleH)+routes = [ ("/google", googleLoginH) ]
src/Snap/Snaplet/OAuth/Google/Api.hs view
@@ -18,7 +18,7 @@ import qualified Data.ByteString as BS import Data.Text (Text) -import Network.OAuth2.OAuth2+import Snap.Snaplet.OAuth.Internal.Types import Snap.Snaplet.OAuth.Internal.Utils @@ -26,9 +26,10 @@ -- APIs Impl ---------------------------------------------------------------------- -data GoogleUser = GoogleUser { gid :: Text- , gname :: Text- , glink :: Text+data GoogleUser = GoogleUser { gid :: Text+ , gname :: Text+ , glink :: Text+ , gemail :: Text } deriving (Show, Eq) instance FromJSON GoogleUser where@@ -36,6 +37,7 @@ <$> o .: "id" <*> o .: "name" <*> o .: "link"+ <*> o .: "email" parseJSON _ = mzero ----------------------------------------------------------------------@@ -43,7 +45,7 @@ ---------------------------------------------------------------------- -userInfo :: OAuth2 -> IO (Maybe GoogleUser)+userInfo :: OAuthValue -> IO (Maybe GoogleUser) userInfo = apiRequestOAuth uriUserInfor
src/Snap/Snaplet/OAuth/Internal/Handlers.hs view
@@ -7,8 +7,8 @@ import Control.Monad.CatchIO (throw) import qualified Data.ByteString as BS import Data.Maybe-import Network.OAuth2.HTTP.HttpClient-import Network.OAuth2.OAuth2+import Network.OAuth.OAuth2+import Network.OAuth.OAuth2.HttpClient import Prelude hiding ((.)) import Snap @@ -35,7 +35,7 @@ -- oauthCallbackH :: HasOAuth b => OAuthKey- -> Handler b v OAuth2+ -> Handler b v OAuthValue oauthCallbackH key = withOAuthH key fn where fn oauth = do codeParam <- decodedParam accessTokenKey@@ -55,6 +55,8 @@ ---------------------------------------------------------------------- +-- | Perform an action with a full initilized @OAuth2@.+-- withOAuthH :: HasOAuth b => OAuthKey -> (OAuthValue -> Handler b v a)
src/Snap/Snaplet/OAuth/Internal/Types.hs view
@@ -1,15 +1,14 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-} module Snap.Snaplet.OAuth.Internal.Types where +import Data.Hashable (Hashable (..))+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as M import Data.Lens.Common-import Network.OAuth2.OAuth2---import Prelude hiding ((.))-import Data.Hashable (Hashable (..))-import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as M+import Network.OAuth.OAuth2 import Snap-import Snap.Snaplet.OAuth.Internal.Utils ---------------------------------------------------------------------- -- Snaplet@@ -54,10 +53,15 @@ newtype OAuthMap = OAuthMap (HashMap OAuthKey OAuthValue) +newtype OAuthKey = OAuthKey { unkey :: String }+ deriving (Hashable, Eq)+ type OAuthValue = OAuth2 -data OAuthKey = Google | Github | Twitter | Facebook | Weibo | QQ- deriving (Show, Eq, Enum)+google, github, weibo :: OAuthKey+google = OAuthKey "google"+github = OAuthKey "github"+weibo = OAuthKey "weibo" -instance Hashable OAuthKey where- hash = hash . sToBS . show+instance Show OAuthKey where+ show = unkey
src/Snap/Snaplet/OAuth/Internal/Utils.hs view
@@ -4,16 +4,15 @@ import Control.Applicative import Data.Aeson-import qualified Data.ByteString as BS-import qualified Data.ByteString.Char8 as BS8-import qualified Data.ByteString.Lazy as LBS-import Data.Maybe (fromMaybe)-import qualified Data.Text as T-import qualified Data.Text.Encoding as T-import Network.OAuth2.HTTP.HttpClient-import Network.OAuth2.OAuth2-import Snap hiding (Response)-import qualified Text.Show.ByteString as TSB+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as LBS+import Data.Maybe (fromMaybe)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.OAuth.OAuth2+import Network.OAuth.OAuth2.HttpClient+import Snap hiding (Response)+import qualified Text.Show.ByteString as TSB ---------------------------------------------------------------------- @@ -44,9 +43,4 @@ => BS.ByteString -- ^ API URL -> OAuth2 -- ^ For append access token -> IO (Maybe a)-apiRequestOAuth uri oa = apiRequest $ appendAccessToken uri oa--apiRequest :: FromJSON a- => BS.ByteString -- ^ Full API URL- -> IO (Maybe a)-apiRequest = doJSONGetRequest . BS8.unpack+apiRequestOAuth uri oa = doJSONGetRequest $ appendAccessToken uri oa
src/Snap/Snaplet/OAuth/Weibo.hs view
@@ -2,11 +2,14 @@ {-# LANGUAGE OverloadedStrings #-} module Snap.Snaplet.OAuth.Weibo- ( routes- , loginWithWeiboH+ ( -- * Routes+ routes+ -- * Handlers+ , weiboLoginH , weiboCallbackH- , userIdH- , accountShowH+ , weiboUserIdH+ , weiboUserH+ -- * Types and API , module Snap.Snaplet.OAuth.Weibo.Api ) where @@ -15,7 +18,6 @@ import Control.Monad import Data.ByteString (ByteString) import Data.Maybe-import Network.OAuth2.OAuth2 import Prelude hiding ((.)) import Snap @@ -27,38 +29,39 @@ -- Weibo ------------------------------------------------------------------------------ -loginWithWeiboH :: HasOAuth b => Handler b v ()-loginWithWeiboH = loginWithOauthH Weibo Nothing+weiboLoginH :: HasOAuth b => Handler b v ()+weiboLoginH = loginWithOauthH weibo Nothing -- | token access callback.--- return a @OAuth2@ having access token has been filled.+-- return a @OAuthValue@ having access token has been filled. ---weiboCallbackH :: HasOAuth b => Handler b v OAuth2-weiboCallbackH = oauthCallbackH Weibo+weiboCallbackH :: HasOAuth b => Handler b v OAuthValue+weiboCallbackH = oauthCallbackH weibo -- | userID is must for access other datas. ---userIdH :: HasOAuth b => OAuth2 -> Handler b v (Maybe WeiboUserId)-userIdH = liftIO . requestUid+weiboUserIdH :: HasOAuth b => Handler b v (OAuthValue, Maybe WeiboUserId)+weiboUserIdH = do+ oauth <- weiboCallbackH+ uid <- liftIO $ requestUid oauth+ return (oauth, uid) --- | Show Account detail info.++-- | fetch weibo user info. ---accountShowH :: HasOAuth b- => (Maybe WeiboUser -> Handler b v ())- -> OAuth2- -> Handler b v ()-accountShowH fn oauth =- userIdH oauth >>= maybe failure success- where success uid = liftIO (requestAccount oauth uid) >>= fn- failure = writeBS "Failed at getting UID."+weiboUserH :: HasOAuth b => Handler b v (Maybe WeiboUser)+weiboUserH = do+ (oauth, uid) <- weiboUserIdH+ maybe failure (liftIO . requestAccount oauth) uid+ where failure = return Nothing ------------------------------------------------------------------------------ -- | The application's routes. routes :: HasOAuth b => [(ByteString, Handler b v ())]-routes = [ ("/weibo" , loginWithWeiboH)+routes = [ ("/weibo" , weiboLoginH) ]
src/Snap/Snaplet/OAuth/Weibo/Api.hs view
@@ -7,9 +7,11 @@ import qualified Data.ByteString as BS import Data.Maybe (fromMaybe) import Data.Text (Text)-import qualified Network.HTTP.Types as HT-import Network.OAuth2.OAuth2+--import qualified Network.HTTP.Types as HT+import Network.OAuth.OAuth2+import Network.OAuth.OAuth2.HttpClient import Snap+import Snap.Snaplet.OAuth.Internal.Types import Snap.Snaplet.OAuth.Internal.Utils ----------------------------------------------------------------------@@ -20,10 +22,10 @@ -- FIXME: chinese name doest display correctly data WeiboUserId = WeiboUserId { weiboUserId :: Integer } deriving (Show, Eq) -data WeiboUser = WeiboUser { wUidStr :: Text- , wScreenNname :: Text- , wName :: Text- , wUrl :: Text+data WeiboUser = WeiboUser { wUidStr :: Text+ , wScreenName :: Text+ , wName :: Text+ , wUrl :: Text } deriving (Show) -- | Parse UID response --@@ -46,16 +48,15 @@ -- | User ID ---requestUid :: OAuth2 -> IO (Maybe WeiboUserId)+requestUid :: OAuthValue -> IO (Maybe WeiboUserId) requestUid = apiRequestOAuth accountUidUri -- | User Info ---requestAccount :: OAuth2 -> WeiboUserId -> IO (Maybe WeiboUser)-requestAccount oa uid = apiRequest uri- where uri = accountShowUri `BS.append` query- query = HT.renderSimpleQuery True params+requestAccount :: OAuthValue -> WeiboUserId -> IO (Maybe WeiboUser)+requestAccount oa uid = doJSONGetRequest uri+ where uri = appendQueryParam accountShowUri params params = accessTokenToParam token ++ uidToParam uid token = fromMaybe "" (oauthAccessToken oa) @@ -72,4 +73,3 @@ accountShowUri :: BS.ByteString accountShowUri = sToBS "https://api.weibo.com/2/users/show.json"-
+ tests/Weibo.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings #-}++module Weibo where++import Data.Aeson (decode)+import qualified Data.ByteString.Lazy.Char8 as BSL+import Data.Maybe (fromJust, isJust)+import Test.Framework (Test, defaultMain, testGroup)+import Test.Framework.Providers.HUnit (testCase)+import Test.HUnit ((@?), (@?=))++import Snap.Snaplet.OAuth.Weibo+++uidTests :: Test+uidTests = testGroup "uid test cases"+ [ testCase "uid shall be 12345" $ getUid @?= Just aOid+ , testCase "uid shall be 2709495807" $ getUid2 @?= Just aOid2+ , testCase "uid shall be any number" $ isJust getUid @? "uid any number test"+ , testCase "uid is not string" $ getInvalidUid @?= Nothing+ , testCase "uidstr is string" $ getIdStr @?= "1814581760"+ , testCase "uidstr is string" $ getNameStr @?= "HaishengWoo"+ ]++------------------------------------------------++invalidUidString :: BSL.ByteString+invalidUidString = "{\"uid\" : \"12345\" }"++getInvalidUid :: Maybe WeiboUserId+getInvalidUid = decode invalidUidString++------------------------------------------------++aOid :: WeiboUserId+aOid = WeiboUserId 12345++getUid :: Maybe WeiboUserId+getUid = decode ("{\"uid\" : 12345 }" :: BSL.ByteString)+++aOid2 = WeiboUserId 2709495807123+getUid2 = decode ("{\"uid\" : 2709495807123 }" :: BSL.ByteString)++------------------------------------------------+getIdStr = wUidStr $ fromJust userInfo+getNameStr = wName $ fromJust userInfo++userInfo :: Maybe WeiboUser+userInfo = decode userInfoStr++userInfoStr :: BSL.ByteString+userInfoStr = BSL.pack "{\"id\": 1814581760, \"idstr\": \"1814581760\", \"screen_name\": \"HaishengWoo\", \"name\": \"HaishengWoo\", \"url\": \"http://freizl.github.com/\" }"