@@ -1,13 +1,15 @@
11{-# LANGUAGE
2− DataKinds ,
3− DeriveGeneric ,
2+ DataKinds,
3+ DeriveGeneric,
44 OverloadedLabels,
5− OverloadedStrings ,
6− TypeApplications ,
5+ OverloadedStrings,
6+ TypeApplications,
77 TypeOperators,
88 PartialTypeSignatures,
99 FlexibleInstances,
10− KindSignatures #-}
10+ KindSignatures,
11+ DeriveAnyClass,
12+ RecordWildCards #-}
1113
1214{-# OPTIONS_GHC -Wno-partial-type-signatures #-}
1315
@@ -16,7 +18,7 @@ module Data.Query (
1618 Schema,
1719 createTables,
1820 getPosts,
19− getUser,
21+ -- getUser,
2022 getUsers,
2123 getAllUsers,
2224 getLastPosts,
@@ -43,8 +45,16 @@ module Data.Query (
4345 getGroupChatForUser,
4446 getFaculty,
4547 updateFaculty,
46− getFacultyFromQuery
47−) where
48+ getFacultyFromQuery,
49+ getUserWithEmail,
50+ createToken,
51+ validateToken,
52+ invalidateToken,
53+ tryVerifyToken,
54+ getVerifiedToken,
55+ UserCreation(..),
56+ createUser
57+ ) where
4858
4959import Squeal.PostgreSQL
5060import Data.User
@@ -83,6 +93,9 @@ import Data.Faculty
8393import Data.ByteString (ByteString)
8494import Data.Foldable (fold)
8595import Data.Char
96+import qualified Data.Vector as V
97+import Server.Auth.Token
98+import Data.Int
8699
87100-- MARK: Documentation
88101
@@ -145,26 +158,68 @@ getPostQ oTe te = select (
145158-- let rows = M.fromList $ map (tupleBy $ quoteRowId . head) . groupBy ((==) `on` quoteRowId) . sortBy (compare `on` quoteRowId) $ rows'
146159-- return $ M.lookup qId rows >>= rowsToQuote rows
147160
148−getUserQ :: Query Schema (TuplePG (Only UserId)) (RowPG User)
149−getUserQ = selectStar $ from (table #users) & where_ (#userId .== param @1)
161+getUserWithEmailQ :: Query Schema (TuplePG (Only UserEmail)) (RowPG (User FacultyUrl))
162+getUserWithEmailQ = select (
163+ #userRowId `as` #userId :*
164+ #userRowFirstName `as` #firstName :*
165+ #userRowMiddleName `as` #middleName :*
166+ #userRowLastName `as` #lastName :*
167+ #userRowFacultyUrl `as` #userFaculty :*
168+ #userRowEmail `as` #userEmail
169+ ) (from (table #users) &
170+ where_ (#userRowEmail .== param @1) &
171+ where_ (notNull #userRowIsValidated))
150172
151173verifiedUserIdsQ :: [UserId] -> Maybe (Query Schema '[] (RowPG (Only UserId)))
152174verifiedUserIdsQ uIds = case idsToColumn uIds of
153− Just idQ -> Just $ select (#users ! #userId `as` #fromOnly) $ from (subquery (idQ `As` #ids) &
175+ Just idQ -> Just $ select (#users ! #userRowId `as` #fromOnly) $ from (subquery (idQ `As` #ids) &
154176 innerJoin (table #users)
155− (#ids ! #id .== #users ! #userId))
177+ (#ids ! #id .== #users ! #userRowId))
156178 Nothing -> Nothing
157179
158−
159−getUsersQ :: [UserId] -> Maybe (Query Schema '[] (RowPG User))
180+getUsersQ :: [UserId] -> Maybe (Query Schema '[] (RowPG UserRow))
160181getUsersQ uIds = case idsToColumn uIds of
161− Just idQ -> Just $ selectDotStar #users $ from (subquery (idQ `As` #ids) &
182+ Just idQ -> Just $ select (
183+ #users ! #userRowId :*
184+ #users ! #userRowFirstName :*
185+ #users ! #userRowMiddleName :*
186+ #users ! #userRowLastName :*
187+ #users ! #userRowFacultyUrl :*
188+ #users ! #userRowEmail :*
189+ #users ! #userRowIsValidated :*
190+ #faculties ! #facultyName `as` #userRowFacultyName :*
191+ #faculties ! #facultyUrl `as` #userRowFacultyURL :*
192+ #faculties ! #facultyPath `as` #userRowFacultyPath :*
193+ #faculties ! #facultyCampusName `as` #userRowFacultyCampusName :*
194+ #faculties ! #facultyCampusCode `as` #userRowFacultyCampusCode :*
195+ #faculties ! #facultyTags `as` #userRowFacultyTags
196+ ) (from (subquery (idQ `As` #ids) &
162197 innerJoin (table #users)
163− (#ids ! #id .== #users ! #userId))
198+ (#ids ! #id .== #users ! #userRowId) &
199+ innerJoin (table #faculties)
200+ (#users ! #userRowFacultyUrl .== #faculties ! #facultyUrl)) &
201+ where_ (notNull $ #users ! #userRowIsValidated))
164202 Nothing -> Nothing
165203
166−getAllUsersQ :: Query Schema '[] (RowPG User)
167−getAllUsersQ = selectStar $ from $ table #users
204+getAllUsersQ :: Query Schema '[] (RowPG UserRow)
205+getAllUsersQ = select (
206+ #users ! #userRowId :*
207+ #users ! #userRowFirstName :*
208+ #users ! #userRowMiddleName :*
209+ #users ! #userRowLastName :*
210+ #users ! #userRowFacultyUrl :*
211+ #users ! #userRowEmail :*
212+ #users ! #userRowIsValidated :*
213+ #faculties ! #facultyName `as` #userRowFacultyName :*
214+ #faculties ! #facultyUrl `as` #userRowFacultyURL :*
215+ #faculties ! #facultyPath `as` #userRowFacultyPath :*
216+ #faculties ! #facultyCampusName `as` #userRowFacultyCampusName :*
217+ #faculties ! #facultyCampusCode `as` #userRowFacultyCampusCode :*
218+ #faculties ! #facultyTags `as` #userRowFacultyTags
219+ ) (from ((table #users) &
220+ innerJoin (table #faculties)
221+ (#users ! #userRowFacultyUrl .== #faculties ! #facultyUrl))&
222+ where_ (notNull $ #users ! #userRowIsValidated))
168223
169224getPostsByIdQ :: [PostId] -> Maybe (Query Schema '[] (RowPG PostRowResponse))
170225getPostsByIdQ pIds = case idsToColumn pIds of
@@ -236,7 +291,7 @@ updateFacultyQ :: Manipulation Schema (TuplePG Faculty) '[]
236291updateFacultyQ = insertRow #faculties
237292 (
238293 Set (param @1) `as` #facultyName :*
239− Set (param @2) `as` #facultyURL :*
294+ Set (param @2) `as` #facultyUrl :*
240295 Set (param @3) `as` #facultyPath :*
241296 Set (param @4) `as` #facultyCampusName :*
242297 Set (param @5) `as` #facultyCampusCode :*
@@ -247,7 +302,7 @@ updateFacultyQ = insertRow #faculties
247302
248303getFacultyQ :: Query Schema (TuplePG (Only Text)) (RowPG Faculty)
249304getFacultyQ = selectStar (from (table #faculties) &
250− where_ (#facultyURL .== param @1))
305+ where_ (#facultyUrl .== param @1))
251306
252307createNamedChannelQ :: Manipulation Schema (TuplePG NamedChannelCreation) (RowPG (Only NamedChannelId))
253308createNamedChannelQ = insertRow #channels
@@ -503,17 +558,97 @@ textArrayToText
503558 -> Expression schema from grouping params ('NotNull 'PGtext)
504559textArrayToText = fold . L.intersperse space
505560
561+createUserQ :: Manipulation Schema (TuplePG UserCreation) (RowPG (Only UserId))
562+createUserQ = insertRow #users (
563+ Default `as` #userRowId :*
564+ Set (param @1) `as` #userRowFirstName :*
565+ Set (param @2) `as` #userRowMiddleName :*
566+ Set (param @3) `as` #userRowLastName :*
567+ Set (param @4) `as` #userRowFacultyUrl :*
568+ Set (param @5) `as` #userRowEmail :*
569+ Set false `as` #userRowIsValidated)
570+ OnConflictDoRaise
571+ (Returning (#userRowId `as` #fromOnly))
572+
573+removeUnverifiedUserQ :: Manipulation Schema (TuplePG (Only UserEmail)) '[]
574+removeUnverifiedUserQ = deleteFrom_ #users
575+ (#userRowEmail .== param @1 .&&
576+ (not_ . notNull $ #userRowIsValidated))
577+
578+
579+createTokenQ :: Manipulation Schema (TuplePG Token) '[]
580+createTokenQ = insertRow_ #tokens (
581+ Set (param @1) `as` #tokenUserId :*
582+ Set (param @2) `as` #tokenValue :*
583+ Set (param @3) `as` #tokenExpiryDate :*
584+ Set (param @4) `as` #tokenVerificationCode :*
585+ Set (param @5) `as` #tokenActivationTriesLeft
586+ )
587+
588+updateTokenQ :: Manipulation Schema (TuplePG Token) '[]
589+updateTokenQ = update_ #tokens (
590+ Same `as` #tokenUserId :*
591+ Same `as` #tokenValue :*
592+ Same `as` #tokenExpiryDate :*
593+ Set (param @4) `as` #tokenVerificationCode :*
594+ Set (param @5) `as` #tokenActivationTriesLeft)
595+ (#tokenUserId .== param @1 .&& #tokenValue .== param @2)
596+
597+deleteTokenQ :: Manipulation Schema (TuplePG (UserId, ByteString)) '[]
598+deleteTokenQ = deleteFrom_ #tokens
599+ (#tokenUserId .== param @1 .&& #tokenValue .== param @2)
600+
601+validateUserQ :: Manipulation Schema (TuplePG (Only UserId)) '[]
602+validateUserQ = update_ #users (
603+ Same `as` #userRowId :*
604+ Same `as` #userRowFirstName :*
605+ Same `as` #userRowMiddleName :*
606+ Same `as` #userRowLastName :*
607+ Same `as` #userRowFacultyUrl :*
608+ Same `as` #userRowEmail :*
609+ Set true `as` #userRowIsValidated)
610+ (#userRowId .== param @1)
611+
612+getTokenQ :: Query Schema (TuplePG (UserId, ByteString)) (RowPG Token)
613+getTokenQ = selectStar (from (table #tokens) &
614+ where_ (#tokenUserId .== param @1 .&&
615+ #tokenValue .== param @2))
616+
617+getVerifiedTokenQ :: Query Schema (TuplePG (UserId, ByteString)) (RowPG Token)
618+getVerifiedTokenQ = selectStar (from (table #tokens) &
619+ where_ (#tokenUserId .== param @1 .&&
620+ #tokenValue .== param @2 .&&
621+ isNull #tokenVerificationCode))
622+
506623-- MARK: FrontEnd Data Structures
507624
625+data UserCreation = UserCreation {
626+ userCreationFirstName :: Text,
627+ userCreationMiddleName :: Text,
628+ userCreationLastName :: Text,
629+ userCreationUserFaculty :: FacultyUrl,
630+ userCreationUserEmail :: Text
631+} deriving (GHC.Generic, SOP.Generic, SOP.HasDatatypeInfo)
632+
633+instance ToJSON UserCreation where
634+ toJSON (UserCreation fName mName lName faculty email) = object [
635+ "firstName" .= fName,
636+ "middleName" .= mName,
637+ "lastName" .= lName,
638+ "faculty" .= faculty,
639+ "email" .= email
640+ ]
641+
642+instance FromJSON UserCreation where
643+ parseJSON = withObject "named channel" $ \e ->
644+ UserCreation <$> e .: "firstName" <*> e .: "middleName" <*> e .: "lastName" <*> e .: "faculty" <*> e .: "email"
645+
508646data NamedChannelCreation = NamedChannelCreation {
509647 namedChannelCreationOwner :: UserId,
510648 namedChannelCreationName :: Text,
511649 namedChannelCreationTags :: Vector Text,
512650 namedChannelCreationPeopleIds :: Vector UserId
513−} deriving GHC.Generic
514−
515−instance SOP.Generic NamedChannelCreation
516−instance SOP.HasDatatypeInfo NamedChannelCreation
651+} deriving (GHC.Generic, SOP.Generic, SOP.HasDatatypeInfo)
517652
518653addUserToChannelCreate :: UserId -> NamedChannelCreationRequest -> NamedChannelCreation
519654addUserToChannelCreate uId (NamedChannelCreationRequest cName cTags cPeople) = NamedChannelCreation uId cName cTags cPeople
@@ -562,7 +697,12 @@ deleteGroupChat gId = do
562697 deleteGroupChatMessagesQ = deleteFrom_ #messages
563698 (#messageStorageDestinationGroupId .== param @1)
564699
565−
700+-- NOTE: Not transactional
701+createUser :: UserCreation -> StaticPQ UserId
702+createUser creation = do
703+ _ <- manipulateParams removeUnverifiedUserQ (Only $ userCreationUserEmail creation)
704+ rowThing <- manipulateParams createUserQ creation >>= firstRow >>= fromMaybe500
705+ return (fromOnly rowThing)
566706
567707-- MARK: FrontEnd functions
568708
@@ -594,14 +734,14 @@ getLastPosts pId lim dir = do
594734 postRows <- runQueryParams (getLastPostsQ id lim dir) (Only pId) >>= getRows
595735 rowsToPosts postRows `liftMaybe` wrapIntoResponse
596736
597−getUser :: UserId -> StaticPQ User
598−getUser uId = runQueryParams getUserQ (Only uId) >>= getRow 0
737+getUserWithEmail :: UserEmail -> StaticPQ (Maybe (User FacultyUrl))
738+getUserWithEmail email = runQueryParams getUserWithEmailQ (Only email) >>= firstRow
599739
600−getUsers :: [UserId] -> StaticPQ (Maybe [User])
601−getUsers ids = getUsersQ ids `liftMaybe` (fmap Just . (runQuery >=> getRows))
740+getUsers :: [UserId] -> StaticPQ (Maybe [User Faculty])
741+getUsers ids = getUsersQ ids `liftMaybe` (fmap (Just . map rowToUser) . (runQuery >=> getRows))
602742
603−getAllUsers :: StaticPQ [User]
604−getAllUsers = runQuery getAllUsersQ >>= getRows
743+getAllUsers :: StaticPQ [User Faculty]
744+getAllUsers = fmap rowToUser <$> (runQuery getAllUsersQ >>= getRows)
605745
606746publishPost :: UserId -> PostCreation -> StaticPQ PostId
607747publishPost uId pc = commitedTransactionallyUpdate $ do
@@ -753,8 +893,72 @@ getFacultyFromQuery :: T.Text -> StaticPQ [Faculty]
753893getFacultyFromQuery query =
754894 runQueryParams getFacultySearchResultQ (Only . queryFromText $ query) >>= getRows
755895
896+createToken :: Token -> StaticPQ ()
897+createToken token = void $ manipulateParams createTokenQ token
898+
899+validateToken :: Token -> StaticPQ ()
900+validateToken token = commitedTransactionallyUpdate $ do
901+ void $ manipulateParams updateTokenQ token{tokenVerificationCode = Nothing}
902+ void $ manipulateParams validateUserQ $ Only $ tokenUserId token
903+
904+invalidateToken :: UserId -> ByteString -> StaticPQ ()
905+invalidateToken uId bs = void . manipulateParams deleteTokenQ $ (uId, bs)
906+
907+tryVerifyToken :: UserId -> ByteString -> Int32 -> StaticPQ (Maybe Token)
908+tryVerifyToken uId tokenData code = commitedTransactionallyUpdate $ do
909+ token' <- runQueryParams getTokenQ (uId, tokenData) >>= firstRow
910+ case token' of
911+ Nothing -> return Nothing
912+ Just token -> do
913+ if (tokenVerificationCode token == Just code)
914+ then validateToken token >> (return $ Just token)
915+ else do
916+ let triesLeft = tokenActivationTriesLeft token - 1
917+ if triesLeft <= 0
918+ then invalidateToken (tokenUserId token) (tokenValue token) >> return Nothing
919+ else do
920+ let newToken = token{tokenActivationTriesLeft = triesLeft}
921+ _ <- manipulateParams updateTokenQ newToken
922+ return Nothing
923+
924+getVerifiedToken :: UserId -> ByteString -> StaticPQ (Maybe Token)
925+getVerifiedToken uId tokenData = runQueryParams getVerifiedTokenQ (uId, tokenData) >>= firstRow
926+
756927-- MARK: Utils
757928
929+data UserRow = UserRow {
930+ userRowId :: UserId,
931+ userRowFirstName :: Text,
932+ userRowMiddleName :: Text,
933+ userRowLastName :: Text,
934+ userRowFacultyUrl :: FacultyUrl,
935+ userRowEmail :: Text,
936+ userRowIsValidated :: Bool,
937+ userRowFacultyName :: T.Text,
938+ userRowFacultyURL :: FacultyUrl,
939+ userRowFacultyPath :: T.Text,
940+ userRowFacultyCampusName :: T.Text,
941+ userRowFacultyCampusCode :: T.Text,
942+ userRowFacultyTags :: V.Vector T.Text
943+} deriving (GHC.Generic, SOP.Generic, SOP.HasDatatypeInfo)
944+
945+rowToUser :: UserRow -> User Faculty
946+rowToUser UserRow{..} = User {
947+ userId = userRowId,
948+ firstName = userRowFirstName,
949+ middleName = userRowMiddleName,
950+ lastName = userRowLastName,
951+ userFaculty = Faculty {
952+ facultyName = userRowFacultyName,
953+ facultyUrl = userRowFacultyURL,
954+ facultyPath = userRowFacultyPath,
955+ facultyCampusName = userRowFacultyCampusName,
956+ facultyCampusCode = userRowFacultyCampusCode,
957+ facultyTags = userRowFacultyTags
958+ },
959+ userEmail = userRowEmail
960+ }
961+
758962queryFromText :: Text -> Text
759963queryFromText = T.unwords . L.intersperse "&" . map (`T.append` ":*") . filter (not . T.null) . T.split (not . isAlphaNum)
760964
@@ -788,7 +992,7 @@ unsafeIntToExpr = UnsafeExpression . pack . show
788992
789993data ResponseWithUsers r = ResponseWithUsers {
790994 response :: r,
791− users :: M.Map UserId User
995+ users :: M.Map UserId (User Faculty)
792996} deriving (Generic)
793997
794998instance (ToJSON r) => ToJSON (ResponseWithUsers r)