packages feed

darcsden-1.1: src/DarcsDen/Handler/User.hs

{-# LANGUAGE OverloadedStrings #-}
module DarcsDen.Handler.User where

import Control.Monad (when, replicateM)
import Control.Monad.Trans
import qualified Data.ByteString.Char8 as BS
import Data.Char (toLower)
import Data.Function (on)
import Data.Time (getCurrentTime)
import Data.Map ((!))
import Data.Maybe (fromJust, isJust)
import Data.List (intercalate, nub, nubBy)
import Data.Text (pack)
import qualified Data.Text.Lazy as DTL
import System.Random (randomRIO)
import Snap.Core
import Network.HTTP.Conduit (queryString)
import Network.Mail.Mime(simpleMail, Address(..), renderSendMail)

import DarcsDen.Settings
import DarcsDen.State.ForgotPassword
import DarcsDen.State.Repository
import DarcsDen.State.Session
import DarcsDen.State.User
import DarcsDen.State.Util
import DarcsDen.Util (fromBS, toUrl)
import DarcsDen.Validate
import DarcsDen.WebUtils
import DarcsDen.Github.Paths
import DarcsDen.Github.Util
import DarcsDen.Github.Handler
import DarcsDen.Google.Paths
import DarcsDen.Google.Util
import DarcsDen.Google.Handler
import qualified DarcsDen.Pages.User as Page


user :: Page
user s = do
    mname <- getParam "user"
    when (mname == Nothing) (errorPage "Username not specified.")

    muser <- getUser . fromBS . fromJust $ mname
    case muser of
        Nothing -> notFound
        Just u -> do
            let getrepos | sUser s == Just (uName u) = getOwnerRepositories
                         | otherwise                 = getUserRepositories
            rs <- getrepos (uName u)
            doPage (Page.user u rs) s

register :: Page
register s = doPage (Page.register []) s

githubLogin :: Page
githubLogin s = do
    cid <- getGId
    random <- liftIO $ replicateM 50 (randomRIO ('a', 'z'))
    let options = [("client_id", cid),
                   ("redirect_uri", baseUrl++"login/github/response"),
                   ("scope", "user,user:email"),
                   ("state", random)]
    setOAuthReg (Just (Left random)) s
    redirectTo (BS.unpack $ toUrl $ githubAccessRequest { queryString = BS.pack $ intercalate "&" (map (\(x,y) -> x ++ "=" ++ y) options) } )

githubLoginResponse :: Page
githubLoginResponse s = validate
    [ predicate "state" ((sOAuthReg s ==). Just . Left) "state did not match!"
    ]
    (\(OK _) -> do
         access_token <- getAccessToken
         login' <- getLoginID access_token
         u <- getUserByGithub login'
         case u of
             Nothing -> do
                email <- getEmailID access_token
                keys <- getKeys access_token
                setOAuthReg (Just (Right (Github login'))) s
                message "There is no account with this github credentials, creating new account" s
                Just s' <- getSession (sID s)
                doPage (Page.register [("name", login'), ("email", email), ("keys", unlines keys), ("oauth", "yes")]) s'
             Just r -> do
                setUser (Just $ uName r) s
                success "Logged in!" s
                redirectTo (baseUrl ++ uName r))
    (\(Invalid failed) -> 
        notify Warning s failed
            >>= doPage (Page.login []))

googleLogin :: Page
googleLogin s = do
   cid <- getGoogleClientId
   random <- liftIO $ replicateM 50 (randomRIO ('a', 'z'))
   let options = [ ("client_id", cid)
                 , ("response_type", "code")
                 , ("scope", "openid email")
                 , ("redirect_uri", baseUrl++"login/google/response")
                 , ("state", random)
                 ]
   setOAuthReg (Just (Left random)) s
   redirectTo (BS.unpack $ toUrl $ googleAccessRequest { queryString = BS.pack $ intercalate "&" (map (\(x,y) -> x ++ "=" ++ y) options) } )

googleLoginResponse :: Page
googleLoginResponse s = validate
   [ predicate "state" ((sOAuthReg s ==). Just . Left) "state did not match!"
   ]
   (\(OK _) -> do
      (sub, email) <- getSubAndEmail "login"
      u <- getUserByGoogle sub
      case u of
         Nothing -> do
            setOAuthReg (Just (Right (Google sub))) s
            message "There is no account with this google credentials, creating new account" s
            Just s' <- getSession (sID s)
            doPage (Page.register [("email", email), ("oauth", "yes")]) s'
         Just r -> do
            setUser (Just $ uName r) s
            success "Logged in!" s
            redirectTo (baseUrl ++ uName r))
    (\(Invalid failed) -> 
        notify Warning s failed
            >>= doPage (Page.login []))


githubRegister :: Page
githubRegister s = do
      cid <- getGId
      random <- liftIO $ replicateM 50 (randomRIO ('a', 'z'))
      let options = [("client_id", cid),
                     ("redirect_uri", baseUrl++"register/github/response"),
                     ("scope", "user,user:email"),
                     ("state", random)]
      setOAuthReg (Just (Left random)) s
      redirectTo (BS.unpack $ toUrl $ githubAccessRequest { queryString = BS.pack $ intercalate "&" (map (\(x,y) -> x ++ "=" ++ y) options) } )

githubRegisterResponse :: Page
githubRegisterResponse s = validate
    [ predicate "state" ((sOAuthReg s ==). Just. Left) "state did not match!"
    ]
    (\(OK _) -> do
         access_token <- getAccessToken
         login' <- getLoginID access_token
         u <- getUserByGithub login'
         case u of
            Nothing -> do
               email <- getEmailID access_token
               keys <- getKeys access_token
               setOAuthReg (Just (Right (Github login'))) s
               doPage (Page.register [("name", login'), ("email", email), ("keys", unlines keys), ("oauth", "yes")]) s
            Just _ -> do
               warn "This github account is already matched with an account!" s
               redirectTo baseUrl)
    (\(Invalid failed) ->
        notify Warning s failed
            >> redirectTo baseUrl)

googleRegister :: Page
googleRegister s = do
   cid <- getGoogleClientId
   random <- liftIO $ replicateM 50 (randomRIO ('a', 'z'))
   let options = [ ("client_id", cid)
                 , ("response_type", "code")
                 , ("scope", "openid email")
                 , ("redirect_uri", baseUrl++"register/google/response")
                 , ("state", random)
                 ]
   setOAuthReg (Just (Left random)) s
   redirectTo (BS.unpack $ toUrl $ googleAccessRequest { queryString = BS.pack $ intercalate "&" (map (\(x,y) -> x ++ "=" ++ y) options) } )

googleRegisterResponse :: Page
googleRegisterResponse s = validate
    [ predicate "state" ((sOAuthReg s ==). Just. Left) "state did not match!"
    ]
    (\(OK _) -> do
         (sub, email) <- getSubAndEmail "register"
         u <- getUserByGoogle sub
         case u of
            Nothing -> do
               setOAuthReg (Just (Right (Google sub))) s
               doPage (Page.register [("email", email), ("oauth", "yes")]) s
            Just _ -> do
               warn "This google account is already matched with an account!" s
               redirectTo baseUrl)
    (\(Invalid failed) ->
        notify Warning s failed
            >> redirectTo baseUrl)

doRegister :: Page
doRegister s = validate
    [ iff (nonEmpty "name")
          (\(OK r) -> io "name is already in use" $ do
              u <- getUser (r ! "name")
              return (u == Nothing))
    , predicate "name" isSane
        "contain only alphanumeric characters, underscores, and hyphens"
    , nonEmpty "email"
    , iff (nonEmpty "password1" `And` nonEmpty "password2")
          (const $ equal "password1" "password2") `Or` predicate "password1" (const usingOAuth) ""
    , predicate "email" (const True) "be a valid email"
    , Predicate "security_question" ((=="darcs").map toLower) "be answered correctly" `Or` predicate "security_question" (const usingOAuth) ""
    ]
    (\(OK r) -> do
       now <- liftIO getCurrentTime
       slt <- liftIO (salt 32)
       keys <- input "keys" ""
       new <- newUser
                  User { uID = Nothing
                       , uRev = Nothing
                       , uName = r ! "name"
                       , uPaS = fmap (\p -> (hashPassword p slt, slt)) (let v = r ! "password1" in if null v then Nothing else Just v)
                       , uOAuthIDs = oid
                       , uFullName = ""
                       , uWebsite = ""
                       , uEmail = r ! "email"
                       , uKeys = lines keys
                       , uJoined = now
                       }

       setUser (Just (uName new)) s
       success "You have been successfully registered and logged in." s
       redirectTo (baseUrl ++ (r ! "name")))
    (\(Invalid failed) -> do
        is <- getInputs
        notify Warning s failed
            >>= doPage (Page.register is))
    where sor = sOAuthReg s
          (oid, usingOAuth) = case sor of
                   Just (Right oid') -> ([oid'], True)
                   _ -> ([], False)

login :: Page
login s = doPage (Page.login []) s

doLogin :: Page
doLogin s = validate
    [ iff (nonEmpty "name" `And` nonEmpty "password") $ \(OK r) ->
          io "invalid username or password" $ do
          c <- getUser (r ! "name")
          case c of
              Nothing -> return False
              Just u -> return (checkPassword (r ! "password") u)
    ]
    (\(OK r) -> do
        setUser (Just $ r ! "name") s
        success "Logged in!" s
        redirectTo (baseUrl ++ (r ! "name")))
    (\(Invalid failed) -> do
        is <- getInputs
        notify Warning s failed
            >>= doPage (Page.login is))

logout :: Page
logout s = do
    setUser Nothing s
    success "Logged out." s
    redirectTo baseUrl

settings :: Page
settings s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to change your settings." s
    redirectTo (baseUrl ++ "login")
settings s@(Session { sUser = Just n }) = validate
    [ io "you do not exist" $ fmap (/= Nothing) (getUser n)
    ]
    (\(OK _) -> do
       Just u <- getUser n
       doPage (Page.settings u) s)
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

doSettings :: Page
doSettings s@(Session { sUser = Nothing }) = do
    warn "You must be logged in to change your settings." s
    redirectTo (baseUrl ++ "login")
doSettings s@(Session { sUser = Just n }) = validate
    [ io "you do not exist" $ fmap (/= Nothing) (getUser n)
    , onlyIf (nonEmpty "password1") $
        And (nonEmpty "password2" `And` equal "password1" "password2") $
            onlyIf (io "" $ do
                      Just u <- getUser n
                      return (isJust.uPaS $ u)) $
                iff (nonEmpty "password") $ \(OK is) -> io "password incorrect" $ do
                    Just u <- getUser n
                    return (checkPassword (is ! "password") u)
    ]
    (\(OK _) -> do
        Just u <- getUser n

        fullName <- input "full_name" (uFullName u)
        website <- input "website" (uWebsite u)
        keys <- input "keys" (unlines (uKeys u))

        oldp <- input "password" ""
        npass1 <- input "password1" ""
        npass2 <- input "password2" ""
        slt <- liftIO (salt 32)
        let npass
                | not ((null oldp && isJust (uPaS u)) || null npass1 || null npass2) && npass1 == npass2 =
                    Just (hashPassword npass1 slt, slt)
                | otherwise =
                    uPaS u

        updateUser u
            { uFullName = fullName
            , uWebsite = website
            , uKeys = lines keys
            , uPaS = npass
            }

        success "Settings updated." s

        redirectTo (baseUrl ++ "settings"))
    (\(Invalid f) -> notify Warning s f >> redirectTo (baseUrl ++ "settings"))

syncWithGithub :: Page
syncWithGithub s@(Session { sUser = Nothing }) = do
   warn "You must be logged in." s
   redirectTo (baseUrl ++ "login")
syncWithGithub s@(Session { sUser = Just n }) = validate
    [ io "you do not exist" $ fmap (/= Nothing) (getUser n)
    ]
    (\(OK _) -> do
        cid <- getGId
        random <- liftIO $ replicateM 50 (randomRIO ('a', 'z'))
        let options = [("client_id", cid),
                       ("redirect_uri", baseUrl++"sync/github/response"),
                       ("scope", "user"),
                       ("state", random)]
        setOAuthReg (Just (Left random)) s
        redirectTo (BS.unpack $ toUrl $ githubAccessRequest { queryString = BS.pack $ intercalate "&" (map (\(x,y) -> x ++ "=" ++ y) options) } ))
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

syncWithGithubResponse :: Page
syncWithGithubResponse s@(Session { sUser = Nothing }) = do
   warn "You must be logged in." s
   redirectTo (baseUrl ++ "login")
syncWithGithubResponse s@(Session { sUser = Just n }) = validate
    [ io "you do not exist" $ fmap (/= Nothing) (getUser n)
    , predicate "state" ((sOAuthReg s ==). Just . Left) "state did not match!"
    ]
    (\(OK _) -> do
         Just u' <- getUser n
         access_token <- getAccessToken
         login' <- getLoginID access_token
         u <- getUserByGithub login'
         case u of
            Just x | uName x /= uName u' -> do
               warn "This github account is already linked to another account!" s
               setOAuthReg Nothing s
               redirectTo (baseUrl ++ "settings")
            _ -> do
               keys <- getKeys access_token
               updateUser u'
                  { uOAuthIDs = nub (Github login':uOAuthIDs u')
                  , uKeys = nubBy (on (==) cp) (keys ++ uKeys u')
                  }
               success "Account Synced." s
               setOAuthReg Nothing s
               redirectTo (baseUrl ++ "settings"))
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)
        where cp = take 2 . words

syncWithGoogle :: Page
syncWithGoogle s@(Session { sUser = Nothing }) = do
   warn "You must be logged in." s
   redirectTo (baseUrl ++ "login")
syncWithGoogle s@(Session { sUser = Just n }) = validate
   [ io "you do not exist" $ fmap (/= Nothing) (getUser n)
   ]
   (\(OK _) -> do
      cid <- getGoogleClientId
      random <- liftIO $ replicateM 50 (randomRIO ('a', 'z'))
      let options = [ ("client_id", cid)
                    , ("response_type", "code")
                    , ("scope", "openid email")
                    , ("redirect_uri", baseUrl++"sync/google/response")
                    , ("state", random)
                    ]
      setOAuthReg (Just (Left random)) s
      redirectTo (BS.unpack $ toUrl $ googleAccessRequest { queryString = BS.pack $ intercalate "&" (map (\(x,y) -> x ++ "=" ++ y) options) } ))
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

syncWithGoogleResponse :: Page
syncWithGoogleResponse s@(Session { sUser = Nothing }) = do
   warn "You must be logged in." s
   redirectTo (baseUrl ++ "login")
syncWithGoogleResponse s@(Session { sUser = Just n }) = validate
    [ io "you do not exist" $ fmap (/= Nothing) (getUser n)
    , predicate "state" ((sOAuthReg s ==). Just . Left) "state did not match!"
    ]
    (\(OK _) -> do
         Just u' <- getUser n
         (sub, _) <- getSubAndEmail "sync"
         u <- getUserByGoogle sub
         case u of
            Just x | uName x /= uName u' -> do
               warn "This google account is already linked to another account!" s
               setOAuthReg Nothing s
               redirectTo (baseUrl ++ "settings")
            _ -> do
               updateUser u'
                  { uOAuthIDs = nub (Google sub:uOAuthIDs u')
                  }
               success "Account Synced." s
               setOAuthReg Nothing s
               redirectTo (baseUrl ++ "settings"))
    (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

forgotPassword :: Page
forgotPassword s = doPage (Page.forgotPassword []) s

forgotPasswordResponse :: Page
forgotPasswordResponse s = validate
   [ iff (nonEmpty "username") (\(OK r) -> io "User does not exist." (fmap isJust (getUser (r ! "username")))) ]
   (\(OK r) -> do
      Just u <- getUser (r ! "username")
      str <- createFpForUser u
      let v = baseUrl ++ "recovery?fpid=" ++ str
      liftIO $ simpleMail
         (Address (Just . pack $ uName u) (pack $ uEmail u))
         (Address (Just . pack $ sendName) (pack $ sendEmail))
         (pack $ "Reset Password")
         DTL.empty
         (DTL.pack $ "<a href="++v++">reset password</a>")
         []
       >>= renderSendMail
      success "Mail sent to user's email account." s
      redirectTo baseUrl)
   (\(Invalid f) -> notify Warning s f >> redirectTo (baseUrl ++ "forgotpassword"))

resetPassword :: Page
resetPassword s = validate
   [ iff (nonEmpty "fpid") (\(OK r) -> io "invalid or outdated fpid" (fmap isJust (getUserFromFp (r ! "fpid"))))
   ]
   (\(OK r) -> do
      Just u <- getUserFromFp (r ! "fpid")
      s' <- setUser (Just $ uName u) s
      deleteFp (r!"fpid")
      doPage (Page.resetPassword []) s'
   )
   (\(Invalid f) -> notify Warning s f >> redirectTo baseUrl)

resetPasswordResponse :: Page
resetPasswordResponse s@(Session { sUser = Nothing }) = do
   warn "You must be logged in." s
   redirectTo baseUrl
resetPasswordResponse s@(Session { sUser = Just n }) = validate
   [  nonEmpty "password1" 
   ,  equal "password1" "password2"
   ]
   (\(OK r) -> do
     Just u <- getUser n
     slt <- liftIO (salt 32)
     let npass = Just (hashPassword (r!"password1") slt, slt)
     updateUser u
        { uPaS = npass
        }
     success "Password Changed." s
     redirectTo baseUrl)
    (\(Invalid failed) -> do
        notify Warning s failed
            >>= doPage (Page.resetPassword []))