Skip to content

Commit 362a1dc

Browse files
committed
adding delete event form, regenerateSessionId before login and csrf protection
1 parent 965d3a7 commit 362a1dc

File tree

11 files changed

+136
-54
lines changed

11 files changed

+136
-54
lines changed

src/Web/Gathering/Actions/Auth.hs

+11-8
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,19 @@ adminHook = do
101101
signInAction :: (ListContains n IsGuest xs, NotInList User xs ~ 'True) => Action (HVect xs) ()
102102
signInAction = do
103103
title <- cfgTitle . appConfig <$> getState
104+
csrfToken <- getCsrfToken
104105
let
105106
-- | Display the form to the user
106107
formView mErr view = do
107108
formViewer title "Sign-in" FS.signinFormView mErr view
108109

109110
-- Run the form
110-
form <- runForm "loginForm" FS.signinForm
111+
form <- runForm "" (FS.signinForm csrfToken)
111112
-- validate the form.
112113
-- Nothing means failure. will display the form view back to the user when validation fails.
113114
case form of
114115
(view, Nothing) ->
115-
lucid $ formView Nothing view
116+
formView Nothing view
116117
-- If basic validation of fields succeeds, continue to check validation against db
117118

118119
(view, Just FS.Signin{sinLogin, sinPassword}) -> do
@@ -125,15 +126,15 @@ signInAction = do
125126
text $ T.pack (show err)
126127

127128
Right Nothing ->
128-
lucid $ formView (pure $ p_ "Invalid user name/email.") view
129+
formView (pure $ p_ "Invalid user name/email.") view
129130

130131
Right (Just (user, pass)) -> do
131132
if verifyPassword (T.encodeUtf8 sinPassword) pass
132133
then do -- success - create the session for the user
133134
makeSession (userId user) $
134135
redirect "/"
135136
else
136-
lucid $ formView (pure $ p_ "Invalid password.") view
137+
formView (pure $ p_ "Invalid password.") view
137138

138139
-- | Describe the action to do when a user wants to sign up for the system:
139140
--
@@ -144,18 +145,19 @@ signInAction = do
144145
signUpAction :: (ListContains n IsGuest xs, NotInList User xs ~ 'True) => Action (HVect xs) ()
145146
signUpAction = do
146147
title <- cfgTitle . appConfig <$> getState
148+
csrfToken <- getCsrfToken
147149
let
148150
-- | Display the form to the user
149151
formView mErr view = do
150152
formViewer title "Sign-up" FS.signupFormView mErr view
151153

152154
-- Run the form
153-
form <- runForm "registerForm" FS.signupForm
155+
form <- runForm "" (FS.signupForm csrfToken)
154156
-- validate the form.
155157
-- Nothing means failure. will display the form view back to the user when validation fails.
156158
case form of
157159
(view, Nothing) ->
158-
lucid $ formView Nothing view
160+
formView Nothing view
159161

160162
-- Case for bots
161163
(_, Just (FS.Signup { supUsername, supSpamHoneyPot }))
@@ -173,11 +175,11 @@ signUpAction = do
173175
text $ T.pack (show err)
174176

175177
Right (Just _) -> do
176-
lucid $ formView (pure $ p_ "Username or email already exists.") view
178+
formView (pure $ p_ "Username or email already exists.") view
177179

178180
Right Nothing
179181
| pass /= passConfirm ->
180-
lucid $ formView (pure $ p_ "Passwords do not match.") view
182+
formView (pure $ p_ "Passwords do not match.") view
181183

182184
-- User does not exists and passwords match. try to create a new user
183185
Right Nothing -> do
@@ -240,6 +242,7 @@ verificationAction key email = do
240242
makeSession :: (ListContains n IsGuest xs, NotInList User xs ~ 'True)
241243
=> UserId -> Action (HVect xs) () -> Action (HVect xs) ()
242244
makeSession uid act = do
245+
sessionRegenerateId
243246
sessRes <- runQuery $ Sql.run $ upsertUserSession uid
244247
case sessRes of
245248
Left err -> do

src/Web/Gathering/Actions/Events.hs

+37-14
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,19 @@ displayEvents getEventsQuery mUser = do
5555
newEventAction :: (ListContains n User xs, ListContains m IsAdmin xs) => Action (HVect xs) ()
5656
newEventAction = do
5757
title <- cfgTitle . appConfig <$> getState
58+
csrfToken <- getCsrfToken
5859
let
5960
-- | Display the form to the user
6061
formView mErr view = do
6162
formViewer title "Sign-up" (editEventFormView path "Create") mErr view
6263

6364
-- Run the form
64-
form <- runForm path (editEventForm Nothing)
65+
form <- runForm "" (editEventForm csrfToken Nothing)
6566
-- validate the form.
6667
-- Nothing means failure. will display the form view back to the user when validation fails.
6768
case form of
6869
(view, Nothing) ->
69-
lucid $ formView Nothing view
70+
formView Nothing view
7071
-- If basic validation of fields succeeds, continue to check validation against db
7172

7273
(_, Just (EditEvent name desc loc mWhen mDur))
@@ -96,6 +97,7 @@ newEventAction = do
9697
editEventAction :: (ListContains n User xs, ListContains m IsAdmin xs) => EventId -> Action (HVect xs) ()
9798
editEventAction eid = do
9899
title <- cfgTitle . appConfig <$> getState
100+
csrfToken <- getCsrfToken
99101
let
100102
-- | Display the form to the user
101103
formView mErr view = do
@@ -113,12 +115,12 @@ editEventAction eid = do
113115
Right (Just editedEvent) -> do
114116

115117
-- Run the form
116-
form <- runForm path (editEventForm $ Just $ eventToEditEvent editedEvent)
118+
form <- runForm "" (editEventForm csrfToken $ Just $ eventToEditEvent editedEvent)
117119
-- validate the form.
118120
-- Nothing means failure. will display the form view back to the user when validation fails.
119121
case form of
120122
(view, Nothing) ->
121-
lucid $ formView Nothing view
123+
formView Nothing view
122124
-- If basic validation of fields succeeds, continue to check validation against db
123125

124126
(_, Just (EditEvent name desc loc mWhen mDur))
@@ -143,9 +145,11 @@ editEventAction eid = do
143145

144146
-- | Describe the action to do when a user wants to delete an existing event
145147
--
146-
removeEventAction :: (ListContains n User xs, ListContains m IsAdmin xs) => EventId -> Action (HVect xs) ()
147-
removeEventAction eid = do
148+
deleteEventAction :: (ListContains n User xs, ListContains m IsAdmin xs) => EventId -> Action (HVect xs) ()
149+
deleteEventAction eid = do
148150
mEvent <- runQuery $ Sql.run (getEventById eid)
151+
csrfToken <- getCsrfToken
152+
149153
case mEvent of
150154
-- @TODO this is an internal error that we should take care of internally
151155
Left err ->
@@ -155,14 +159,33 @@ removeEventAction eid = do
155159
text "Event does not exist"
156160

157161
Right (Just event) -> do
158-
result <- runQuery $ Sql.run (removeEvent event)
159-
case result of
160-
-- @TODO this is an internal error that we should take care of internally
161-
Left err -> do
162-
text $ T.pack (show err)
163-
164-
Right _ ->
165-
redirect "/"
162+
163+
let
164+
path = "/event/" <> T.pack (show eid) <> "/delete"
165+
-- | Display the form to the user
166+
formView mErr view = do
167+
formViewer "Delete Event" "Delete" (deleteEventFormView path $ eventName event) mErr view
168+
169+
-- Run the form
170+
form <- runForm "" (deleteEventForm csrfToken)
171+
-- validate the form.
172+
-- Nothing means failure. will display the form view back to the user when validation fails.
173+
case form of
174+
(view, Nothing) ->
175+
formView Nothing view
176+
177+
(_, Just (DeleteEvent False)) -> do
178+
redirect $ "/event/" <> T.pack (show $ eventId event)
179+
180+
(_, Just (DeleteEvent True)) -> do
181+
result <- runQuery $ Sql.run (removeEvent event)
182+
case result of
183+
-- @TODO this is an internal error that we should take care of internally
184+
Left err -> do
185+
text $ T.pack (show err)
186+
187+
Right _ ->
188+
redirect "/"
166189

167190

168191
reportEventParsingError :: EditEvent -> Action (HVect xs) ()

src/Web/Gathering/Actions/Utils.hs

+12-8
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ module Web.Gathering.Actions.Utils where
77
import Data.Text
88
import Data.Monoid
99
import Web.Gathering.Html
10+
import Web.Gathering.Types
11+
import Web.Spock.Lucid (lucid)
12+
import Web.Spock
1013

1114
-- | Display the form to the user
12-
formViewer :: Text -> Text -> (t -> Html) -> Maybe Html -> t -> Html
15+
formViewer :: Text -> Text -> (t -> Html) -> Maybe Html -> t -> Action v ()
1316
formViewer title actionName form mErr view = do
14-
template
15-
(title <> " - " <> actionName)
16-
title
17-
(pure ())
18-
$ do
19-
maybe (pure ()) id mErr
20-
form view
17+
lucid $ do
18+
template
19+
(title <> " - " <> actionName)
20+
title
21+
(pure ())
22+
$ do
23+
maybe (pure ()) id mErr
24+
form view

src/Web/Gathering/Database.hs

+1-2
Original file line numberDiff line numberDiff line change
@@ -299,15 +299,14 @@ updateEvent event = do
299299
removeEvent :: Event -> Sql.Session ()
300300
removeEvent event = do
301301
removeAttendants event
302+
removeNewEvent event
302303
Sql.query (eventId event) $
303304
Sql.statement
304305
"delete from events where event_id = $1"
305306
(SqlE.value SqlE.int4)
306307
SqlD.unit
307308
True
308309

309-
removeNewEvent event
310-
311310
removeNewEvent :: Event -> Sql.Session ()
312311
removeNewEvent event =
313312
Sql.query (eventId event) $

src/Web/Gathering/Forms/EditEvent.hs

+36-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Web.Gathering.Model
1515
import Web.Gathering.Forms.Utils
1616
import Web.Gathering.Html (Html)
1717

18+
import Data.Monoid
1819
import Data.Maybe (isNothing)
1920
import Text.Digestive ((.:))
2021
import qualified Text.Digestive as D
@@ -38,9 +39,10 @@ data EditEvent
3839
deriving (Show)
3940

4041
-- | Definition of a form and it's validation
41-
editEventForm :: Monad m => Maybe EditEvent -> D.Form Html m EditEvent
42-
editEventForm mEvent = EditEvent
43-
<$> "name" .: D.check "Cannot be empty" (not . T.null) (fmap (fmap trim) D.text (eEventName <$> mEvent))
42+
editEventForm :: Monad m => T.Text -> Maybe EditEvent -> D.Form Html m EditEvent
43+
editEventForm csrfToken mEvent = const EditEvent
44+
<$> "__csrf_token" .: D.text (Just csrfToken)
45+
<*> "name" .: D.check "Cannot be empty" (not . T.null) (fmap (fmap trim) D.text (eEventName <$> mEvent))
4446
<*> "desc" .: D.check "Cannot be empty" (not . T.null) (fmap (fmap trim) D.text (eEventDesc <$> mEvent))
4547
<*> "location" .: D.check "Cannot be empty" (not . T.null) (fmap (fmap trim) D.text (eEventLocation <$> mEvent))
4648
<*> "datetime" .: D.validateM validateDateTime (fmap (fmap trim) D.text (eEventDateTime <$> mEvent))
@@ -92,6 +94,8 @@ editEventFormView formName submitText view =
9294
D.inputTextArea
9395
(Just 60) (Just 80) "desc" view
9496

97+
D.inputHidden "__csrf_token" view
98+
9599
D.inputSubmit submitText
96100

97101

@@ -104,3 +108,32 @@ eventToEditEvent Event { eventName, eventDesc, eventLocation, eventDateTime, eve
104108
(formatDateTime eventDateTime)
105109
(formatDiffTime eventDuration)
106110

111+
112+
-- Delete event --
113+
114+
-- | Definition of a delete event data type
115+
data DeleteEvent
116+
= DeleteEvent
117+
{ imSure :: Bool
118+
}
119+
deriving (Show)
120+
121+
-- | Definition of a form and it's validation
122+
deleteEventForm :: Monad m => T.Text -> D.Form Html m DeleteEvent
123+
deleteEventForm csrfToken = const DeleteEvent
124+
<$> "__csrf_token" .: D.text (Just csrfToken)
125+
<*> "imsure" .: D.bool Nothing
126+
127+
128+
-- | Defining the view for the delete event form
129+
deleteEventFormView :: T.Text -> T.Text -> D.View Html -> Html
130+
deleteEventFormView formName eName view =
131+
D.form view formName $ do
132+
H.div_ $ do
133+
D.inputCheckbox "imsure" view
134+
D.label "imsure" view . H.toHtml $
135+
"I'm sure I want to delete the event '" <> eName <> "'."
136+
137+
D.inputHidden "__csrf_token" view
138+
139+
D.inputSubmit "Delete Event"

src/Web/Gathering/Forms/Sign.hs

+12-6
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ data Signup
5656
} deriving (Show)
5757

5858
-- | Definition of a form and it's validation
59-
signupForm :: Monad m => D.Form Html m Signup
60-
signupForm = Signup
61-
<$> "name" .: D.validateM validateName (fmap (fmap trim) D.text Nothing)
59+
signupForm :: Monad m => T.Text -> D.Form Html m Signup
60+
signupForm csrfToken = const Signup
61+
<$> "__csrf_token" .: D.text (Just csrfToken)
62+
<*> "name" .: D.validateM validateName (fmap (fmap trim) D.text Nothing)
6263
<*> "email" .: D.validateM validateMail (fmap (fmap trim) D.text Nothing)
6364
<*> "password1" .: D.validateM validatePass (fmap (fmap trim) D.text Nothing)
6465
<*> "password2" .: D.text Nothing
@@ -121,6 +122,8 @@ signupFormView view =
121122
H.div_ $ do
122123
D.inputHidden "shp" view
123124

125+
D.inputHidden "__csrf_token" view
126+
124127
D.inputSubmit "Sign-up"
125128

126129
-------------
@@ -135,9 +138,10 @@ data Signin
135138
} deriving (Show)
136139

137140
-- | Defining the form
138-
signinForm :: Monad m => D.Form Html m Signin
139-
signinForm = Signin
140-
<$> "login" .: fmap (fmap trim) D.text Nothing
141+
signinForm :: Monad m => T.Text -> D.Form Html m Signin
142+
signinForm csrfToken = const Signin
143+
<$> "__csrf_token" .: D.text (Just csrfToken)
144+
<*> "login" .: fmap (fmap trim) D.text Nothing
141145
<*> "password" .: fmap (fmap trim) D.text Nothing
142146

143147
-- | Defining the view for the signin form
@@ -155,5 +159,7 @@ signinFormView view =
155159
D.label "password" view "Password: "
156160
D.inputPassword "password" view
157161

162+
D.inputHidden "__csrf_token" view
163+
158164
D.inputSubmit "Sign-in"
159165

src/Web/Gathering/Router.hs

+2-2
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ appRouter = prehook baseHook $ do
103103
getpost ("event" <//> var <//> "edit") $ \(eid :: EventId) ->
104104
editEventAction eid
105105

106-
get ("event" <//> var <//> "delete") $ \(eid :: EventId) ->
107-
removeEventAction eid
106+
getpost ("event" <//> var <//> "delete") $ \(eid :: EventId) ->
107+
deleteEventAction eid
108108

109109
-----------
110110
-- Hooks --

src/Web/Gathering/Run.hs

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ run = do
4040
(uncurry AppState -> state) <- parseArgs
4141
print state
4242
let connstr = cfgDbConnStr (appConfig state)
43-
spockCfg <- defaultSpockCfg EmptySession (PCConn $ hasqlPool connstr) state
43+
spockCfg <- (\cfg -> cfg { spc_csrfProtection = True, spc_csrfPostName = ".__csrf_token" })
44+
<$> defaultSpockCfg EmptySession (PCConn $ hasqlPool connstr) state
4445

4546
-- Background workers
4647
void $ forkIO $ newEventsWorker state

src/Web/Gathering/Utils.hs

+12
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
module Web.Gathering.Utils where
55

6+
import Turtle (err)
67
import Data.Time
8+
import Data.Monoid
79
import qualified Data.Text as T
10+
import qualified Data.Text.IO as T
811

912
fst3 :: (a, b, c) -> a
1013
fst3 (a, _, _) = a
@@ -59,3 +62,12 @@ parseDiffTime (trim -> T.unpack -> duration) =
5962
where
6063
isDigit = (`elem` ['0'..'9'])
6164

65+
putStrTime :: T.Text -> IO ()
66+
putStrTime txt = do
67+
t <- getCurrentTime
68+
T.putStrLn ("[" <> formatDateTime t <> "] - " <> txt)
69+
70+
errTime :: T.Text -> IO ()
71+
errTime txt = do
72+
t <- getCurrentTime
73+
err ("<<" <> formatDateTime t <> ">> - " <> txt)

0 commit comments

Comments
 (0)