packages feed

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

module DarcsDen.State.User where

import Control.Monad.Trans
import Control.Monad (replicateM)
import Data.Digest.Pure.SHA (sha512, bytestringDigest)
import Data.Maybe (catMaybes, isJust)
import Data.Time (UTCTime, formatTime)
import Data.Word (Word8)
import Database.CouchDB
import System.Directory (createDirectoryIfMissing)
import System.Locale (defaultTimeLocale)
import System.Random
import Text.JSON
import qualified Data.ByteString.Lazy as LBS

import DarcsDen.State.Repository
import DarcsDen.State.Util
import DarcsDen.Settings


data User =
    User
        { uID :: Maybe Doc
        , uRev :: Maybe Rev
        , uName :: String
        , uPaS :: Maybe ([Word8], [Word8]) -- uPassword, and uSalt
        , uOAuthIDs :: [OAuthID]
        , uFullName :: String
        , uWebsite :: String
        , uEmail :: String
        , uKeys :: [String]
        , uJoined :: UTCTime
        }
    deriving (Eq, Show)

data OAuthID = Github String -- Github login name
             | Google String -- Google sub
    deriving (Eq, Show, Read)

uPassword :: User -> Maybe [Word8]
uPassword u = fmap fst (uPaS u)

uSalt :: User -> Maybe [Word8]
uSalt u = fmap snd (uPaS u)

uGithubLoginName :: User -> Maybe String
uGithubLoginName u = foldl f Nothing (uOAuthIDs u)
   where f :: Maybe String -> OAuthID -> Maybe String
         f _ (Github xs) = Just xs
         f a _ = a

uGoogleSub :: User -> Maybe String
uGoogleSub u = foldl f Nothing (uOAuthIDs u)
   where f :: Maybe String -> OAuthID -> Maybe String
         f _ (Google xs) = Just xs
         f a _ = a

instance JSON User where
    readJSON o = do
        id' <- getID o
        rev' <- getRev o
        name <- getAttr o "name"
        pas <- gPas
        gid <- fmap (fmap Github) (getAttrOr o "gid" Nothing)
        gsub <- fmap (fmap Google) (getAttrOr o "gsub" Nothing)
        fullName <- getAttrOr o "full_name" ""
        website <- getAttrOr o "website" ""
        email <- getAttrOr o "email" ""
        keys <- getAttrOr o "keys" []
        joined <- getTime o "joined"
        return User
            { uID = Just id'
            , uRev = Just rev'
            , uName = name
            , uPaS = pas
            , uOAuthIDs = catMaybes [gid, gsub]
            , uFullName = fullName
            , uWebsite = website
            , uEmail = email
            , uKeys = keys
            , uJoined = joined
            }
      where
        gPas = do
           v <- getAttrOr o "pas" Nothing
           if isJust v
            then return v
            else do
              p <- getAttrOr o "password" []
              s <- getAttrOr o "salt" []
              if null p || null s then return Nothing else return (Just (p,s))

    showJSON u = JSObject . toJSObject $
        [ ("name", showJSON (uName u))
        , ("pas", showJSON (uPaS u))
        , ("gid", showJSON (uGithubLoginName u))
        , ("gsub", showJSON (uGoogleSub u))
        , ("full_name", showJSON (uFullName u))
        , ("website", showJSON (uWebsite u))
        , ("email", showJSON (uEmail u))
        , ("keys", showJSON (uKeys u))
        , ("joined", showJSON (formatTime defaultTimeLocale "%F %T" (uJoined u)))
        ] ++ id' ++ rev'
      where
        id' =
            case uID u of
                Just id'' -> [("_id", showJSON (show id''))]
                Nothing -> []
        rev' =
            case uRev u of
                Just rev'' -> [("_rev", showJSON (show rev''))]
                Nothing -> []


userURL :: User -> String
userURL = (baseUrl ++) . uName

getUsers :: MonadIO m => m [User]
getUsers = do
    rs <- liftIO $ runDB $ queryView (db "users") (doc "users") (doc "by_name") []
    return $ map snd rs

-- getUsersWithRepos :: MonadIO m => m [User]
-- getUsersWithRepos = do
--     ids <- liftIO $ runDB (getAllDocIds (db "users"))
--     users <- mapM getUserByID ids
--     repos <- getRepositories
--     let userRepoCount u = \u -> (length $ filter ((u==).rOwner) repos)
--         userswithrepos = filter (\u -> userRepoCount u > 0) $ catMaybes users
--     return userswithrepos

getUser :: MonadIO m => String -> m (Maybe User)
getUser un = liftIO . runDB $
    getDocByView (db "users") (doc "users") (doc "by_name") un

getUserByID :: MonadIO m => Doc -> m (Maybe User)
getUserByID key = do
    res <- liftIO $ runDB (getDoc (db "users") key)
    case res of
         Just (_, _, r) -> return (Just r)
         Nothing -> return Nothing

getUserByEmail :: MonadIO m => String -> m (Maybe User)
getUserByEmail email = liftIO . runDB $
    getDocByView (db "users") (doc "users") (doc "by_email") email

getUserByGithub :: MonadIO m => String -> m (Maybe User)
getUserByGithub gid = liftIO . runDB $
   getDocByView (db "users") (doc "users") (doc "by_gid") (Just gid)

getUserByGoogle :: MonadIO m => String -> m (Maybe User)
getUserByGoogle gsub = liftIO . runDB $
   getDocByView (db "users") (doc "users") (doc "by_gsub") (Just gsub)

addUser :: MonadIO m => User -> m User
addUser u = do
    (id', rev') <- liftIO $ runDB (newDoc (db "users") u)
    return (u { uID = Just id', uRev = Just rev' })

updateUser :: MonadIO m => User -> m (Maybe User)
updateUser u =
    case (uID u, uRev u) of
        (Just id', Just rev') -> do
            update <- liftIO $ runDB (updateDoc (db "users") (id', rev') (u { uID = Nothing }))
            case update of
                Just (id'', rev'') ->
                    return $ Just u
                        { uID = Just id''
                        , uRev = Just rev''
                        }
                _ -> return Nothing
        _ -> return Nothing

deleteUser :: MonadIO m => User -> m Bool
deleteUser u =
    case (uID u, uRev u) of
        (Just id', Just rev') ->
            liftIO $ runDB (deleteDoc (db "users") (id', rev'))
        _ -> return False

salt :: Int -> IO [Word8]
salt num = do
    r <- replicateM num (randomRIO (0 :: Int, 255))
    return (map (\x -> fromIntegral x :: Word8) r)

hashPassword :: String -> [Word8] -> [Word8]
hashPassword p s
    = LBS.unpack
    . bytestringDigest
    . sha512
    . LBS.pack
    $ merge (map (fromIntegral . fromEnum) p) s
  where
    merge a b =
        concat (zipWith (\ x y -> [x, y]) a b) ++ remaining a b

    remaining a b
        | length a < length b = drop (length a) b
        | otherwise = drop (length b) a

checkPassword :: String -> User -> Bool
checkPassword p u = fmap (hashPassword p) (uSalt u) == uPassword u && isJust (uPaS u)

newUser :: MonadIO m => User -> m User
newUser u = do
    liftIO $ createDirectoryIfMissing True (userDir (uName u))
    addUser u

destroyUser :: MonadIO m => User -> m Bool
destroyUser u = do
    repos <- getUserRepositories (uName u)
    mapM_ destroyRepository repos
    deleteUser u

renameUser :: MonadIO m => String -> User -> m User
renameUser n u = do
    new <- newUser (u { uName = n })
    repos <- getUserRepositories n
    mapM_ (\r -> moveRepository (n, rName r) r) repos
    destroyUser u
    return new