@@ -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,
@@ -40,8 +42,19 @@ module Data.Query (
4042 createGroupChat,
4143 updateGroupChat,
4244 leaveGroupChat,
43− getGroupChatForUser
44−) where
45+ getGroupChatForUser,
46+ getFaculty,
47+ updateFaculty,
48+ getFacultyFromQuery,
49+ getUserWithEmail,
50+ createToken,
51+ validateToken,
52+ invalidateToken,
53+ tryVerifyToken,
54+ getVerifiedToken,
55+ UserCreation(..),
56+ createUser
57+ ) where
4558
4659import Squeal.PostgreSQL
4760import Data.User
@@ -67,6 +80,7 @@ import Servant.Docs
6780import Data.Proxy
6881import Data.Vector (Vector, toList, fromList)
6982import Data.Text (Text)
83+import qualified Data.Text as T
7084import Data.Channel.NamedChannel
7185import Data.Channel.AnonymousChannel
7286import Data.Maybe (fromMaybe)
@@ -75,6 +89,13 @@ import Data.GroupChat
7589import Data.Message
7690import Server.Query.Dialog
7791import qualified Server.Query.Pagination as P
92+import Data.Faculty
93+import Data.ByteString (ByteString)
94+import Data.Foldable (fold)
95+import Data.Char
96+import qualified Data.Vector as V
97+import Server.Auth.Token
98+import Data.Int
7899
79100-- MARK: Documentation
80101
@@ -137,26 +158,68 @@ getPostQ oTe te = select (
137158-- let rows = M.fromList $ map (tupleBy $ quoteRowId . head) . groupBy ((==) `on` quoteRowId) . sortBy (compare `on` quoteRowId) $ rows'
138159-- return $ M.lookup qId rows >>= rowsToQuote rows
139160
140−getUserQ :: Query Schema (TuplePG (Only UserId)) (RowPG User)
141−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))
142172
143173verifiedUserIdsQ :: [UserId] -> Maybe (Query Schema '[] (RowPG (Only UserId)))
144174verifiedUserIdsQ uIds = case idsToColumn uIds of
145− 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) &
146176 innerJoin (table #users)
147− (#ids ! #id .== #users ! #userId))
177+ (#ids ! #id .== #users ! #userRowId))
148178 Nothing -> Nothing
149179
150−
151−getUsersQ :: [UserId] -> Maybe (Query Schema '[] (RowPG User))
180+getUsersQ :: [UserId] -> Maybe (Query Schema '[] (RowPG UserRow))
152181getUsersQ uIds = case idsToColumn uIds of
153− 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) &
154197 innerJoin (table #users)
155− (#ids ! #id .== #users ! #userId))
198+ (#ids ! #id .== #users ! #userRowId) &
199+ innerJoin (table #faculties)
200+ (#users ! #userRowFacultyUrl .== #faculties ! #facultyUrl)) &
201+ where_ (notNull $ #users ! #userRowIsValidated))
156202 Nothing -> Nothing
157203
158−getAllUsersQ :: Query Schema '[] (RowPG User)
159−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))
160223
161224getPostsByIdQ :: [PostId] -> Maybe (Query Schema '[] (RowPG PostRowResponse))
162225getPostsByIdQ pIds = case idsToColumn pIds of
@@ -224,6 +287,23 @@ getAllChannelsForUserQ = selectStar $
224287 where_ (#namedChannelFullOwner .== param @1) &
225288 orderBy [#namedChannelFullId & Desc]
226289
290+updateFacultyQ :: Manipulation Schema (TuplePG Faculty) '[]
291+updateFacultyQ = insertRow #faculties
292+ (
293+ Set (param @1) `as` #facultyName :*
294+ Set (param @2) `as` #facultyUrl :*
295+ Set (param @3) `as` #facultyPath :*
296+ Set (param @4) `as` #facultyCampusName :*
297+ Set (param @5) `as` #facultyCampusCode :*
298+ Set (param @6) `as` #facultyTags
299+ )
300+ (OnConflictDoNothing)
301+ (Returning Nil)
302+
303+getFacultyQ :: Query Schema (TuplePG (Only Text)) (RowPG Faculty)
304+getFacultyQ = selectStar (from (table #faculties) &
305+ where_ (#facultyUrl .== param @1))
306+
227307createNamedChannelQ :: Manipulation Schema (TuplePG NamedChannelCreation) (RowPG (Only NamedChannelId))
228308createNamedChannelQ = insertRow #channels
229309 (
@@ -427,18 +507,148 @@ getGroupChatForUserQ = selectStar $
427507 where_ (#groupChatId .== param @2) &
428508 where_ (#groupChatUsers .? param @1)
429509
510+getFacultySearchResultQ :: Query Schema (TuplePG (Only Text)) (RowPG Faculty)
511+getFacultySearchResultQ = selectStar (from (table #faculties) &
512+ where_ (vector @@ query) &
513+ orderBy [tsRankCd vector query & Desc])
514+ where
515+ vector = tsVector "russian" (textArrayToText [
516+ #facultyName,
517+ #facultyPath,
518+ #facultyCampusName,
519+ (arrayToText #facultyTags)])
520+ query = tsQuery (param @1)
521+
522+tsRankCd
523+ :: Expression schema from grouping params ('NotNull 'PGtext)
524+ -> Expression schema from grouping params ('NotNull 'PGtext)
525+ -> Expression schema from grouping params ('NotNull 'PGfloat4)
526+tsRankCd vector query = UnsafeExpression $
527+ "ts_rank_cd" <> parenthesized (commaSeparated . map renderExpression $ [vector, query])
528+
529+tsVector
530+ :: ByteString -- ^ Language
531+ -> Expression schema from grouping params ('NotNull 'PGtext)
532+ -> Expression schema from grouping params ('NotNull 'PGtext)
533+tsVector lang x = UnsafeExpression $
534+ "to_tsvector" <> parenthesized (commaSeparated [singleQuotedUtf8 lang, renderExpression x])
535+
536+tsQuery
537+ :: Expression schema from grouping params ('NotNull 'PGtext)
538+ -> Expression schema from grouping params ('NotNull 'PGtext)
539+tsQuery = unsafeFunction "to_tsquery"
540+
541+arrayToText
542+ :: Expression schema from grouping params ('NotNull ('PGvararray ('NotNull 'PGtext)))
543+ -> Expression schema from grouping params ('NotNull 'PGtext)
544+arrayToText x = UnsafeExpression $
545+ "array_to_string" <> parenthesized (commaSeparated [renderExpression x, "' '"])
546+
547+(@@)
548+ :: Expression schema from grouping params (nullity0 'PGtext)
549+ -> Expression schema from grouping params (nullity1 'PGtext)
550+ -> Expression schema from grouping params (nullity2 'PGbool)
551+(@@) = unsafeBinaryOp "@@"
552+
553+space :: Expression schema from grouping params ('NotNull 'PGtext)
554+space = UnsafeExpression "' '"
555+
556+textArrayToText
557+ :: [Expression schema from grouping params ('NotNull 'PGtext)]
558+ -> Expression schema from grouping params ('NotNull 'PGtext)
559+textArrayToText = fold . L.intersperse space
560+
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))
430622
431623-- MARK: FrontEnd Data Structures
432624
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+
433646data NamedChannelCreation = NamedChannelCreation {
434647 namedChannelCreationOwner :: UserId,
435648 namedChannelCreationName :: Text,
436649 namedChannelCreationTags :: Vector Text,
437650 namedChannelCreationPeopleIds :: Vector UserId
438−} deriving GHC.Generic
439−
440−instance SOP.Generic NamedChannelCreation
441−instance SOP.HasDatatypeInfo NamedChannelCreation
651+} deriving (GHC.Generic, SOP.Generic, SOP.HasDatatypeInfo)
442652
443653addUserToChannelCreate :: UserId -> NamedChannelCreationRequest -> NamedChannelCreation
444654addUserToChannelCreate uId (NamedChannelCreationRequest cName cTags cPeople) = NamedChannelCreation uId cName cTags cPeople
@@ -487,7 +697,12 @@ deleteGroupChat gId = do
487697 deleteGroupChatMessagesQ = deleteFrom_ #messages
488698 (#messageStorageDestinationGroupId .== param @1)
489699
490−
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)
491706
492707-- MARK: FrontEnd functions
493708
@@ -519,14 +734,14 @@ getLastPosts pId lim dir = do
519734 postRows <- runQueryParams (getLastPostsQ id lim dir) (Only pId) >>= getRows
520735 rowsToPosts postRows `liftMaybe` wrapIntoResponse
521736
522−getUser :: UserId -> StaticPQ User
523−getUser uId = runQueryParams getUserQ (Only uId) >>= getRow 0
737+getUserWithEmail :: UserEmail -> StaticPQ (Maybe (User FacultyUrl))
738+getUserWithEmail email = runQueryParams getUserWithEmailQ (Only email) >>= firstRow
524739
525−getUsers :: [UserId] -> StaticPQ (Maybe [User])
526−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))
527742
528−getAllUsers :: StaticPQ [User]
529−getAllUsers = runQuery getAllUsersQ >>= getRows
743+getAllUsers :: StaticPQ [User Faculty]
744+getAllUsers = fmap rowToUser <$> (runQuery getAllUsersQ >>= getRows)
530745
531746publishPost :: UserId -> PostCreation -> StaticPQ PostId
532747publishPost uId pc = commitedTransactionallyUpdate $ do
@@ -550,6 +765,12 @@ updateChannel uId req = commitedTransactionallyUpdate $ do
550765 _ <- manipulateParams updateNamedChannelQ $ addUserToChannelUpdate uId (req {namedChannelPeopleIds = uIds})
551766 return ()
552767
768+getFaculty :: Text -> StaticPQ Faculty
769+getFaculty t = runQueryParams getFacultyQ (Only t) >>= firstRow >>= fromMaybe404
770+
771+updateFaculty :: Faculty -> StaticPQ ()
772+updateFaculty fac = void $ manipulateParams updateFacultyQ fac
773+
553774getChannelPosts
554775 :: UserId
555776 -> Maybe PostId
@@ -668,8 +889,79 @@ getGroupChatForUser uId gId = do
668889 (Just c) -> return c
669890 Nothing -> lift $ S.throwError S.err404
670891
892+getFacultyFromQuery :: T.Text -> StaticPQ [Faculty]
893+getFacultyFromQuery query =
894+ runQueryParams getFacultySearchResultQ (Only . queryFromText $ query) >>= getRows
895+
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+
671927-- MARK: Utils
672928
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+
962+queryFromText :: Text -> Text
963+queryFromText = T.unwords . L.intersperse "&" . map (`T.append` ":*") . filter (not . T.null) . T.split (not . isAlphaNum)
964+
673965directionToOperator
674966 :: P.PaginationDirection
675967 -> (ConditionOp schema0 from0 grouping0 params0 nullity0 nullity1 (ty :: PGType),
@@ -700,7 +992,7 @@ unsafeIntToExpr = UnsafeExpression . pack . show
700992
701993data ResponseWithUsers r = ResponseWithUsers {
702994 response :: r,
703− users :: M.Map UserId User
995+ users :: M.Map UserId (User Faculty)
704996} deriving (Generic)
705997
706998instance (ToJSON r) => ToJSON (ResponseWithUsers r)
@@ -742,5 +1034,9 @@ liftMaybe Nothing _ = pure Nothing
7421034liftMaybe (Just a) f = f a
7431035
7441036fromMaybe500 :: Maybe a -> StaticPQ a
745−fromMaybe500 Nothing = lift $ S.throwError S.err404
746−fromMaybe500 (Just a) = return a
1037+fromMaybe500 Nothing = lift $ S.throwError S.err500
1038+fromMaybe500 (Just a) = return a
1039+
1040+fromMaybe404 :: Maybe a -> StaticPQ a
1041+fromMaybe404 Nothing = lift $ S.throwError S.err404
1042+fromMaybe404 (Just a) = return a