A team captain cain invite other members

This commit is contained in:
Filip Gralinski 2021-03-13 11:16:54 +01:00
parent c23409526d
commit cb6aedd3ee
11 changed files with 173 additions and 26 deletions

View File

@ -28,6 +28,13 @@ postCreateResetLinkR = do
_ -> Nothing _ -> Nothing
doCreateResetLink mUserIdentifier mCourseId doCreateResetLink mUserIdentifier mCourseId
createLinkToken :: Handler (Text, UTCTime)
createLinkToken = do
key <- newToken
theNow <- liftIO getCurrentTime
let expirationMoment = addUTCTime (60*60*24) theNow
return (key, expirationMoment)
doCreateResetLink :: Maybe Text -> Maybe CourseId -> Handler Html doCreateResetLink :: Maybe Text -> Maybe CourseId -> Handler Html
doCreateResetLink (Just userIdentifier) mCourseId = do doCreateResetLink (Just userIdentifier) mCourseId = do
mUserEnt <- runDB $ getBy $ UniqueUser userIdentifier mUserEnt <- runDB $ getBy $ UniqueUser userIdentifier
@ -35,9 +42,7 @@ doCreateResetLink (Just userIdentifier) mCourseId = do
addParticipant userId mCourseId addParticipant userId mCourseId
key <- newToken (key, expirationMoment) <- createLinkToken
theNow <- liftIO getCurrentTime
let expirationMoment = addUTCTime (60*60*24) theNow
runDB $ update userId [UserVerificationKey =. Just key, UserKeyExpirationDate =. Just expirationMoment] runDB $ update userId [UserVerificationKey =. Just key, UserKeyExpirationDate =. Just expirationMoment]
defaultLayout $ do defaultLayout $ do

View File

@ -8,6 +8,7 @@ import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3, bfs)
import Handler.Shared (fieldWithTooltip) import Handler.Shared (fieldWithTooltip)
import Handler.JWT import Handler.JWT
import Handler.AccountReset
import PersistTeamActionType import PersistTeamActionType
@ -24,6 +25,8 @@ import Data.Proxy as DPR
import Control.Lens hiding ((.=), (^.)) import Control.Lens hiding ((.=), (^.))
import Data.HashMap.Strict.InsOrd (fromList) import Data.HashMap.Strict.InsOrd (fromList)
import qualified Data.Text as T
getMyTeamsR :: Handler Html getMyTeamsR :: Handler Html
getMyTeamsR = do getMyTeamsR = do
_ <- requireAuth _ <- requireAuth
@ -60,21 +63,7 @@ createTeam userId teamCreationData = do
teamIdent = theIdent, teamIdent = theIdent,
teamAvatar = avatarBytes } teamAvatar = avatarBytes }
_ <- insert TeamMember { addMemberToTeam userId newTeamId True
teamMemberUser = userId,
teamMemberTeam = newTeamId,
teamMemberIsCaptain = True
}
theNow <- liftIO getCurrentTime
_ <- insert TeamLog {
teamLogStamp = theNow,
teamLogActionType = TeamCreation,
teamLogAgens = userId,
teamLogPatiens = Nothing,
teamLogVerificationKey = Nothing
}
return () return ()
@ -102,6 +91,7 @@ instance ToSchema TeamMemberView where
& required .~ [ "name", "isCaptain" ] & required .~ [ "name", "isCaptain" ]
data TeamView = TeamView { data TeamView = TeamView {
teamViewId :: TeamId,
teamViewIdent :: Text, teamViewIdent :: Text,
teamViewMembers :: [TeamMemberView] teamViewMembers :: [TeamMemberView]
} deriving (Eq, Show) } deriving (Eq, Show)
@ -124,9 +114,15 @@ instance ToSchema TeamView where
] ]
& required .~ [ "ident", "members" ] & required .~ [ "ident", "members" ]
invitation :: TeamId -> WidgetFor App ()
invitation teamId = do
(formWidget, formEnctype) <- handlerToWidget $ generateFormPost $ teamInvitationForm $ Just teamId
$(widgetFile "team-invitation-form")
doMyTeams :: Handler Html doMyTeams :: Handler Html
doMyTeams = do doMyTeams = do
(formWidget, formEnctype) <- generateFormPost createTeamForm (formWidget, formEnctype) <- generateFormPost createTeamForm
teams <- fetchMyTeams teams <- fetchMyTeams
defaultLayout $ do defaultLayout $ do
setTitle "Teams" setTitle "Teams"
@ -160,9 +156,9 @@ fetchMyTeams :: Handler [TeamView]
fetchMyTeams = do fetchMyTeams = do
Entity userId _ <- requireAuthPossiblyByToken Entity userId _ <- requireAuthPossiblyByToken
myTeams <- runDB $ E.select $ E.from $ \(team, member) -> do myTeams <- runDB $ E.select $ E.from $ \(team, tmember) -> do
E.where_ (member ^. TeamMemberTeam E.==. team ^. TeamId E.where_ (tmember ^. TeamMemberTeam E.==. team ^. TeamId
E.&&. member ^. TeamMemberUser E.==. E.val userId) E.&&. tmember ^. TeamMemberUser E.==. E.val userId)
E.orderBy [E.asc (team ^. TeamIdent)] E.orderBy [E.asc (team ^. TeamIdent)]
return team return team
@ -173,13 +169,14 @@ fetchTeamInfo :: (YesodPersist site,
PersistQueryRead (YesodPersistBackend site), PersistUniqueRead (YesodPersistBackend site)) PersistQueryRead (YesodPersistBackend site), PersistUniqueRead (YesodPersistBackend site))
=> Entity Team -> HandlerFor site TeamView => Entity Team -> HandlerFor site TeamView
fetchTeamInfo (Entity teamId team) = do fetchTeamInfo (Entity teamId team) = do
members <- runDB $ E.select $ E.from $ \(user, member) -> do members <- runDB $ E.select $ E.from $ \(user, tmember) -> do
E.where_ (member ^. TeamMemberTeam E.==. E.val teamId E.where_ (tmember ^. TeamMemberTeam E.==. E.val teamId
E.&&. member ^. TeamMemberUser E.==. user ^. UserId) E.&&. tmember ^. TeamMemberUser E.==. user ^. UserId)
E.orderBy [E.asc (user ^. UserIdent)] E.orderBy [E.asc (user ^. UserIdent)]
return (user, member) return (user, tmember)
return $ TeamView { return $ TeamView {
teamViewId = teamId,
teamViewIdent = teamIdent team, teamViewIdent = teamIdent team,
teamViewMembers = map (\(u, m) -> TeamMemberView { teamViewMembers = map (\(u, m) -> TeamMemberView {
teamMemberViewName = userIdent $ entityVal u, teamMemberViewName = userIdent $ entityVal u,
@ -191,3 +188,124 @@ createTeamForm :: Form TeamCreationData
createTeamForm = renderBootstrap3 BootstrapBasicForm $ TeamCreationData createTeamForm = renderBootstrap3 BootstrapBasicForm $ TeamCreationData
<$> areq textField (fieldWithTooltip MsgTeamIdent MsgTeamIdentTooltip) Nothing <$> areq textField (fieldWithTooltip MsgTeamIdent MsgTeamIdentTooltip) Nothing
<*> fileAFormOpt (bfs MsgAvatar) <*> fileAFormOpt (bfs MsgAvatar)
teamInvitationForm :: Maybe TeamId -> Form (Text, TeamId)
teamInvitationForm teamId = renderBootstrap3 BootstrapBasicForm $ (,)
<$> areq textField (bfs MsgInviteToTeam) Nothing
<*> areq hiddenField "" teamId
createTeamInvitationLink :: Key User -> Text -> Key Team -> HandlerFor App (Maybe Text)
createTeamInvitationLink userId ident teamId = do
result <- runDB $ selectList [TeamMemberUser ==. userId, TeamMemberIsCaptain ==. True, TeamMemberTeam ==. teamId] []
case result of
[] -> return Nothing
_ -> do
(key, expirationMoment) <- createLinkToken
theNow <- liftIO getCurrentTime
mInvitee <- runDB $ getBy $ UniqueUser ident
case mInvitee of
Nothing -> do
-- we do this quietly not to leak username IDs
return ()
Just (Entity inviteeId _) -> do
_ <- runDB $ insert $ TeamLog {
teamLogStamp = theNow,
teamLogActionType = TeamInvitation,
teamLogAgens = userId,
teamLogPatiens = Just inviteeId,
teamLogTeam = Just teamId,
teamLogVerificationKey = Just key,
teamLogKeyExpirationDate = Just expirationMoment }
return ()
return $ Just key
postCreateTeamInvitationLinkR :: Handler Html
postCreateTeamInvitationLinkR = do
Entity userId _ <- requireAuthPossiblyByToken
((result, _), _) <- runFormPost $ teamInvitationForm Nothing
let FormSuccess (ident', teamId) = result
let ident = T.strip ident'
mToken <- createTeamInvitationLink userId ident teamId
case mToken of
Just token -> do
defaultLayout $ do
setTitle "Invitation link"
$(widgetFile "invitation-link-created")
Nothing -> do
setMessage $ toHtml ("You must be a team captain to invite other people" :: Text)
doMyTeams
checkTeamInvitationKey :: UserId -> Text -> Handler (Maybe (Entity Team, Entity User))
checkTeamInvitationKey userId key = do
theNow <- liftIO getCurrentTime
teamLogEntry <- runDB $ selectList [TeamLogVerificationKey ==. Just key,
TeamLogPatiens ==. Just userId,
TeamLogKeyExpirationDate >. Just theNow] []
case teamLogEntry of
[Entity _ entry] -> do
let inviterId = teamLogAgens entry
inviter <- runDB $ get404 inviterId
let (Just teamId) = teamLogTeam entry
team <- runDB $ get404 teamId
return $ Just ((Entity teamId team), (Entity inviterId inviter))
_ -> return Nothing
addMemberToTeam :: (MonadIO m, PersistStoreWrite backend, BaseBackend backend ~ SqlBackend)
=> Key User -> Key Team -> Bool -> ReaderT backend m ()
addMemberToTeam userId teamId isCaptain = do
_ <- insert TeamMember {
teamMemberUser = userId,
teamMemberTeam = teamId,
teamMemberIsCaptain = isCaptain
}
theNow <- liftIO getCurrentTime
_ <- insert TeamLog {
teamLogStamp = theNow,
teamLogActionType = TeamCreation,
teamLogAgens = userId,
teamLogPatiens = Nothing,
teamLogTeam = Just teamId,
teamLogVerificationKey = Nothing,
teamLogKeyExpirationDate = Nothing
}
return ()
getTeamInvitationLinkR :: Text -> Handler Html
getTeamInvitationLinkR key = do
Entity userId _ <- requireAuthPossiblyByToken
result <- checkTeamInvitationKey userId key
case result of
Just (team, inviter) -> do
defaultLayout $ do
setTitle "Invitation link"
$(widgetFile "receive-invitation-link")
Nothing -> do
setMessage $ toHtml ("There is something wrong with this invitation link" :: Text)
doMyTeams
postTeamInvitationLinkR :: Text -> Handler Html
postTeamInvitationLinkR key = do
Entity userId _ <- requireAuthPossiblyByToken
result <- checkTeamInvitationKey userId key
case result of
Just (team, _) -> do
runDB $ addMemberToTeam userId (entityKey team) False
setMessage $ toHtml ("You joined " <> (teamIdent $ entityVal team))
Nothing -> do
setMessage $ toHtml ("There is something wrong with this invitation link" :: Text)
doMyTeams

View File

@ -5,7 +5,7 @@ import Database.Persist.Sql
import qualified Data.Text as T import qualified Data.Text as T
data TeamActionType = TeamCreation | TeamJoining | TeamLeaving data TeamActionType = TeamCreation | TeamJoining | TeamLeaving | TeamInvitation
deriving (Eq, Show, Read) deriving (Eq, Show, Read)
instance PersistField TeamActionType where instance PersistField TeamActionType where

View File

@ -25,7 +25,9 @@ TeamLog
actionType TeamActionType actionType TeamActionType
agens UserId agens UserId
patiens UserId Maybe patiens UserId Maybe
team TeamId Maybe
verificationKey Text Maybe verificationKey Text Maybe
keyExpirationDate UTCTime Maybe
PublicKey PublicKey
user UserId user UserId
pubkey Text pubkey Text

View File

@ -31,6 +31,8 @@
/my-teams MyTeamsR GET /my-teams MyTeamsR GET
/create-team CreateTeamR POST /create-team CreateTeamR POST
/create-team-invitation-link CreateTeamInvitationLinkR POST
/invitation-link/#Text TeamInvitationLinkR GET POST
/challenge/#Text ShowChallengeR GET /challenge/#Text ShowChallengeR GET
/challenge-readme/#Text ChallengeReadmeR GET /challenge-readme/#Text ChallengeReadmeR GET

View File

@ -103,3 +103,5 @@ TeamIdent: Team name
TeamIdentTooltip: Note that it cannot be changed later TeamIdentTooltip: Note that it cannot be changed later
YourTeams: your teams YourTeams: your teams
AsTeam: As team AsTeam: As team
InviteToTeam: Invite to team (give the identifier/login of a user)
Join: Join

View File

@ -0,0 +1,4 @@
<p>Send this link to #{ident}
<p>
<a href=@{TeamInvitationLinkR token}>@{TeamInvitationLinkR token}

View File

@ -7,6 +7,8 @@ $forall team <- teams
<ul> <ul>
<li>#{teamMemberViewName member} <li>#{teamMemberViewName member}
^{invitation $ teamViewId team}
<hr> <hr>
<h2>Create a new team <h2>Create a new team

View File

@ -0,0 +1,4 @@
<p>You are invited by #{userIdent $ entityVal inviter} to join the team #{teamIdent $ entityVal team}.
<form method=post action=@{TeamInvitationLinkR key}#form enctype="text/plain">
<button>_{MsgJoin}

View File

@ -0,0 +1,4 @@
<form method=post action=@{CreateTeamInvitationLinkR}#form enctype=#{formEnctype}>
^{formWidget}
<button .btn .btn-primary type="submit">
_{MsgSubmit} <span class="glyphicon glyphicon-upload"></span>

View File

@ -0,0 +1,4 @@
<form method=post action=@{CreateTeamInvitationLinkR}#form enctype=#{invitationFormEnctype}>
^{invitationFormWidget}
<button .btn .btn-primary type="submit">
_{MsgSubmit} <span class="glyphicon glyphicon-upload"></span>