diff --git a/.gitignore b/.gitignore index f744360b3..84a8fe8a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ dist* static/tmp/ static/combined/ -client_session_key.aes -cryptoid_key.bf *.hi *.o *.sqlite3 diff --git a/config/settings.yml b/config/settings.yml index f4602cd0e..658b10f55 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -26,6 +26,7 @@ job-stale-threshold: 300 notification-rate-limit: 3600 notification-collate-delay: 300 notification-expiration: 259201 +session-timeout: 7200 log-settings: log-detailed: "_env:DETAILED_LOGGING:false" @@ -41,10 +42,11 @@ auth-pw-hash: strength: 14 # Optional values with the following production defaults. -# In development, they default to true. +# In development, they default to the opposite. # reload-templates: false # mutable-static: false # skip-combining: false +# encrypt-errors: true database: user: "_env:PGUSER:uniworx" @@ -86,5 +88,4 @@ user-defaults: time-format: "%R" download-files: false -cryptoid-keyfile: "_env:CRYPTOID_KEYFILE:cryptoid_key.bf" -instance-idfile: "_env:INSTANCEID_FILE:instance" +instance-idfile: "_env:INSTANCE_ID:instance" diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg index 898b4385a..f27bdb02b 100644 --- a/messages/uniworx/de.msg +++ b/messages/uniworx/de.msg @@ -376,7 +376,7 @@ NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr abgeben NotificationTriggerSheetInactive: Abgabefrist eines meiner Übungsblätter ist abgelaufen -NotificationCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt +NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt CorrCreate: Abgaben erstellen UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}" @@ -440,4 +440,25 @@ MessageWarning: Warnung MessageInfo: Information MessageSuccess: Erfolg -InvalidLangFormat: Ungültiger Sprach-Code (RFC1766) \ No newline at end of file +InvalidLangFormat: Ungültiger Sprach-Code (RFC1766) + +ErrorResponseTitleNotFound: Ressource nicht gefunden +ErrorResponseTitleInternalError internalError@Text: Ein interner Fehler ist aufgetreten +ErrorResponseTitleInvalidArgs invalidArgs@Texts: Anfrage-Nachricht enthielt ungültige Argumente +ErrorResponseTitleNotAuthenticated: Anfrage benötigt Authentifizierung +ErrorResponseTitlePermissionDenied permissionDenied@Text: Mangelnde Authorisierung +ErrorResponseTitleBadMethod requestMethod@Method: HTTP-Methode nicht unterstützt + +UnknownErrorResponse: Ein nicht weiter klassifizierter Fehler ist aufgetreten: +ErrorResponseNotFound: Unter der von Ihrem Browser angefragten URL wurde keine Seite gefunden. +ErrorResponseNotAuthenticated: Um Zugriff auf einige Teile des Systems zu erhalten müssen Sie sich zunächst anmelden. +ErrorResponseBadMethod requestMethodText@Text: Ihr Browser kann auf mehrere verschiedene Arten versuchen mit den vom System angebotenen Ressourcen zu interagieren. Die aktuell versuchte Methode (#{requestMethodText}) wird nicht unterstützt. + +ErrorResponseEncrypted: Um keine sensiblen Daten preiszugeben wurden nähere Details verschlüsselt. Wenn Sie eine Anfrage an den Support schicken fügen Sie bitte die unten aufgeführten verschlüsselten Daten mit an. +ErrMsgCiphertext: Verschlüsselte Fehlermeldung +ErrMsgCiphertextTooShort: Verschlüsselte Daten zu kurz um valide zu sein +ErrMsgInvalidBase64 base64Err@String: Verschlüsselte Daten nicht korrekt base64url-kodiert: #{base64Err} +ErrMsgCouldNotDecodeNonce: Konnte secretbox-nonce nicht dekodieren +ErrMsgCouldNotOpenSecretbox: Konnte libsodium-secretbox nicht öffnen (Verschlüsselte Daten sind nicht authentisch) +ErrMsgCouldNotDecodePlaintext utf8Err@Text: Konnte Klartext nicht UTF8-dekodieren: #{utf8Err} +ErrMsgHeading: Fehlermeldung entschlüsseln \ No newline at end of file diff --git a/models b/models index 1398a65a5..88e88243f 100644 --- a/models +++ b/models @@ -255,4 +255,8 @@ SystemMessageTranslation language Lang content Html summary Html Maybe - UniqueSystemMessageTranslation message language \ No newline at end of file + UniqueSystemMessageTranslation message language +ClusterConfig + setting ClusterSettingsKey + value Value + Primary setting \ No newline at end of file diff --git a/package.yaml b/package.yaml index 4a48ee43d..246f6bcf3 100644 --- a/package.yaml +++ b/package.yaml @@ -48,6 +48,7 @@ dependencies: - wai - cryptonite - cryptonite-conduit +- saltine - base64-bytestring - memory - http-api-data @@ -67,6 +68,7 @@ dependencies: - cryptoids - cryptoids-class - binary +- cereal - mtl - sandi - esqueleto @@ -107,6 +109,55 @@ dependencies: - postgresql-simple - word24 - mmorph +- clientsession + +other-extensions: + - GeneralizedNewtypeDeriving + - IncoherentInstances + - OverloadedLists + - UndecidableInstances + +default-extensions: + - OverloadedStrings + - PartialTypeSignatures + - ScopedTypeVariables + - TemplateHaskell + - QuasiQuotes + - CPP + - TypeSynonymInstances + - KindSignatures + - ConstraintKinds + - ViewPatterns + - TypeOperators + - TupleSections + - TypeFamilies + - GADTs + - StandaloneDeriving + - RecordWildCards + - RankNTypes + - PatternGuards + - PatternSynonyms + - ParallelListComp + - NumDecimals + - MultiWayIf + - NamedFieldPuns + - NoImplicitPrelude + - LambdaCase + - MultiParamTypeClasses + - FlexibleContexts + - FlexibleInstances + - FunctionalDependencies + - EmptyDataDecls + - ExistentialQuantification + - DefaultSignatures + - DeriveDataTypeable + - DeriveGeneric + - DeriveLift + - DeriveFunctor + - DerivingStrategies + - DataKinds + - BinaryLiterals + - PolyKinds # The library contains all of our application code. The executable # defined below is just a thin wrapper. diff --git a/routes b/routes index 991318c6a..17a653125 100644 --- a/routes +++ b/routes @@ -36,6 +36,7 @@ /admin/test AdminTestR GET POST /admin/user/#CryptoUUIDUser AdminUserR GET /admin/user/#CryptoUUIDUser/hijack AdminHijackUserR POST +/admin/errMsg AdminErrMsgR GET POST /info VersionR GET !free /help HelpR GET POST !free diff --git a/src/Application.hs b/src/Application.hs index 9ffcf2106..3757d98f7 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -1,12 +1,5 @@ -{-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE ViewPatterns #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE FlexibleContexts #-} {-# OPTIONS_GHC -fno-warn-orphans #-} + module Application ( getApplicationDev, getAppDevSettings , appMain @@ -67,6 +60,12 @@ import qualified Yesod.Core.Types as Yesod (Logger(..)) import qualified Data.HashMap.Strict as HashMap import Control.Lens ((&)) + +import Data.Proxy + +import qualified Data.Aeson as Aeson + +import System.Exit (exitFailure) -- Import all relevant handler modules here. -- (HPack takes care to add new modules to our cabal file nowadays.) @@ -105,8 +104,7 @@ makeFoundation appSettings@(AppSettings{..}) = do return $ Yesod.Logger loggerSet tgetter appStatic <- liftIO $ bool static staticDevel appMutableStatic appStaticDir - appCryptoIDKey <- readKeyFile appCryptoIDKeyFile - appInstanceID <- liftIO $ maybe UUID.nextRandom readInstanceIDFile appInstanceIDFile + appInstanceID <- liftIO $ maybe UUID.nextRandom (either readInstanceIDFile return) appInitialInstanceID (appJobCtl, recvChans) <- fmap unzip . atomically . replicateM appJobWorkers $ do chan <- newBroadcastTMChan @@ -120,11 +118,16 @@ makeFoundation appSettings@(AppSettings{..}) = do -- logging function. To get out of this loop, we initially create a -- temporary foundation without a real connection pool, get a log function -- from there, and then create the real foundation. - let mkFoundation appConnPool appSmtpPool = UniWorX {..} + let mkFoundation appConnPool appSmtpPool appCryptoIDKey appSessionKey appErrorMsgKey = UniWorX {..} -- The UniWorX {..} syntax is an example of record wild cards. For more -- information, see: -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html - tempFoundation = mkFoundation (error "connPool forced in tempFoundation") (error "smtpPool forced in tempFoundation") + tempFoundation = mkFoundation + (error "connPool forced in tempFoundation") + (error "smtpPool forced in tempFoundation") + (error "cryptoIDKey forced in tempFoundation") + (error "sessionKey forced in tempFoundation") + (error "errorMsgKey forced in tempFoundation") logFunc = messageLoggerSource tempFoundation appLogger flip runLoggingT logFunc $ do @@ -140,12 +143,36 @@ makeFoundation appSettings@(AppSettings{..}) = do -- Perform database migration using our application's logging settings. migrateAll `runSqlPool` sqlPool + appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool + appSessionKey <- clusterSetting (Proxy :: Proxy 'ClusterClientSessionKey) `runSqlPool` sqlPool + appErrorMsgKey <- clusterSetting (Proxy :: Proxy 'ClusterErrorMessageKey) `runSqlPool` sqlPool - handleJobs recvChans $ mkFoundation sqlPool smtpPool + let foundation = mkFoundation sqlPool smtpPool appCryptoIDKey appSessionKey appErrorMsgKey + + handleJobs recvChans foundation -- Return the foundation - return $ mkFoundation sqlPool smtpPool + return foundation +clusterSetting :: forall key m p. + ( MonadIO m + , ClusterSetting key + , MonadLogger m + ) + => p (key :: ClusterSettingsKey) + -> ReaderT SqlBackend m (ClusterSettingValue key) +clusterSetting proxy@(knownClusterSetting -> key) = do + current' <- get (ClusterConfigKey key) + case Aeson.fromJSON . clusterConfigValue <$> current' of + Just (Aeson.Success c) -> return c + Just (Aeson.Error str) -> do + $logErrorS "clusterSetting" $ "Could not parse JSON-Value for " <> toPathPiece key <> ": " <> pack str + liftIO exitFailure + Nothing -> do + new <- initClusterSetting proxy + void . insert $ ClusterConfig key (Aeson.toJSON new) + return new + readInstanceIDFile :: MonadIO m => FilePath -> m UUID readInstanceIDFile idFile = liftIO . handle generateInstead $ LBS.readFile idFile >>= parseBS where diff --git a/src/Auth/Dummy.hs b/src/Auth/Dummy.hs index 809db8647..df4ab5e40 100644 --- a/src/Auth/Dummy.hs +++ b/src/Auth/Dummy.hs @@ -1,11 +1,3 @@ -{-# LANGUAGE NoImplicitPrelude - , RecordWildCards - , TemplateHaskell - , FlexibleContexts - , TypeFamilies - , OverloadedStrings - #-} - module Auth.Dummy ( dummyLogin , DummyMessage(..) diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs index 32c185519..2b053ce05 100644 --- a/src/Auth/LDAP.hs +++ b/src/Auth/LDAP.hs @@ -1,14 +1,3 @@ -{-# LANGUAGE RecordWildCards - , OverloadedStrings - , TemplateHaskell - , ViewPatterns - , TypeFamilies - , FlexibleContexts - , FlexibleInstances - , NoImplicitPrelude - , ScopedTypeVariables - #-} - module Auth.LDAP ( campusLogin , CampusUserException(..) diff --git a/src/Auth/PWHash.hs b/src/Auth/PWHash.hs index ba7198710..3efad0d32 100644 --- a/src/Auth/PWHash.hs +++ b/src/Auth/PWHash.hs @@ -1,13 +1,3 @@ -{-# LANGUAGE NoImplicitPrelude - , QuasiQuotes - , TemplateHaskell - , ViewPatterns - , RecordWildCards - , OverloadedStrings - , FlexibleContexts - , TypeFamilies - #-} - module Auth.PWHash ( hashLogin , PWHashMessage(..) diff --git a/src/Cron.hs b/src/Cron.hs index 2620aec12..cb2d9a338 100644 --- a/src/Cron.hs +++ b/src/Cron.hs @@ -1,12 +1,3 @@ -{-# LANGUAGE NoImplicitPrelude - , RecordWildCards - , PatternGuards - , ViewPatterns - , DeriveFunctor - , TemplateHaskell - , NamedFieldPuns - #-} - module Cron ( CronNextMatch(..) , nextCronMatch diff --git a/src/Cron/Types.hs b/src/Cron/Types.hs index fa95477f0..ab3e92972 100644 --- a/src/Cron/Types.hs +++ b/src/Cron/Types.hs @@ -1,8 +1,3 @@ -{-# LANGUAGE NoImplicitPrelude - , TemplateHaskell - , DuplicateRecordFields - #-} - module Cron.Types ( Cron(..), Crontab , CronMatch(..) diff --git a/src/CryptoID.hs b/src/CryptoID.hs index 58f68171e..6d4163982 100644 --- a/src/CryptoID.hs +++ b/src/CryptoID.hs @@ -1,11 +1,3 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-} -{-# LANGUAGE RecordWildCards, ViewPatterns, PatternGuards #-} -{-# LANGUAGE PatternSynonyms #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module CryptoID diff --git a/src/CryptoID/TH.hs b/src/CryptoID/TH.hs index 23122dadf..589c30637 100644 --- a/src/CryptoID/TH.hs +++ b/src/CryptoID/TH.hs @@ -1,7 +1,3 @@ -{-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE ViewPatterns #-} - module CryptoID.TH where import ClassyPrelude diff --git a/src/Data/CaseInsensitive/Instances.hs b/src/Data/CaseInsensitive/Instances.hs index ea5253f44..214283124 100644 --- a/src/Data/CaseInsensitive/Instances.hs +++ b/src/Data/CaseInsensitive/Instances.hs @@ -1,11 +1,8 @@ -{-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Data.CaseInsensitive.Instances - () where + ( + ) where import ClassyPrelude.Yesod diff --git a/src/Data/Universe/Instances/Reverse/Hashable.hs b/src/Data/Universe/Instances/Reverse/Hashable.hs index e7459f613..d264fa41f 100644 --- a/src/Data/Universe/Instances/Reverse/Hashable.hs +++ b/src/Data/Universe/Instances/Reverse/Hashable.hs @@ -1,6 +1,3 @@ -{-# LANGUAGE NoImplicitPrelude - , ScopedTypeVariables - #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Data.Universe.Instances.Reverse.Hashable diff --git a/src/Data/Universe/Instances/Reverse/JSON.hs b/src/Data/Universe/Instances/Reverse/JSON.hs index 60b7ba6ae..14c7d04fa 100644 --- a/src/Data/Universe/Instances/Reverse/JSON.hs +++ b/src/Data/Universe/Instances/Reverse/JSON.hs @@ -1,6 +1,3 @@ -{-# LANGUAGE NoImplicitPrelude - , ScopedTypeVariables - #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Data.Universe.Instances.Reverse.JSON diff --git a/src/Foundation.hs b/src/Foundation.hs index 06e945166..3f30e95b8 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1,17 +1,4 @@ -{-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell, QuasiQuotes #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE ViewPatterns #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE PatternSynonyms #-} -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE PatternGuards, MultiWayIf #-} -{-# LANGUAGE TupleSections #-} -{-# LANGUAGE FlexibleInstances, UndecidableInstances, FlexibleContexts #-} +{-# LANGUAGE UndecidableInstances #-} module Foundation where @@ -20,6 +7,8 @@ import Database.Persist.Sql (ConnectionPool, runSqlPool) import Text.Hamlet (hamletFile) import Text.Jasmine (minifym) +import qualified Web.ClientSession as ClientSession + import Yesod.Auth.Message import Yesod.Auth.Dummy import Auth.LDAP @@ -96,6 +85,9 @@ import qualified Yesod.Auth.Message as Auth import qualified Data.Conduit.List as C +import qualified Crypto.Saltine.Core.SecretBox as SecretBox +import qualified Crypto.Saltine.Class as Saltine + instance DisplayAble b => DisplayAble (E.CryptoID a b) where display = display . ciphertext @@ -127,6 +119,8 @@ data UniWorX = UniWorX , appCryptoIDKey :: CryptoIDKey , appInstanceID :: InstanceId , appJobCtl :: [TMChan JobCtl] + , appErrorMsgKey :: SecretBox.Key + , appSessionKey :: ClientSession.Key } type SMTPPool = Pool SMTPConnection @@ -197,14 +191,8 @@ instance RenderMessage UniWorX TermIdentifier where Winter -> renderMessage' $ MsgWinterTerm year where renderMessage' = renderMessage foundation ls -instance RenderMessage UniWorX StudyFieldType where - renderMessage foundation ls = renderMessage foundation ls . \case - FieldPrimary -> MsgFieldPrimary - FieldSecondary -> MsgFieldSecondary - newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier deriving (Eq, Ord, Read, Show) - instance RenderMessage UniWorX ShortTermIdentifier where renderMessage foundation ls (ShortTermIdentifier TermIdentifier{..}) = case season of Summer -> renderMessage' $ MsgSummerTermShort year @@ -214,33 +202,12 @@ instance RenderMessage UniWorX ShortTermIdentifier where instance RenderMessage UniWorX String where renderMessage f ls str = renderMessage f ls $ Text.pack str -instance RenderMessage UniWorX SheetFileType where - renderMessage foundation ls = renderMessage foundation ls . \case - SheetExercise -> MsgSheetExercise - SheetHint -> MsgSheetHint - SheetSolution -> MsgSheetSolution - SheetMarking -> MsgSheetMarking - -instance RenderMessage UniWorX CorrectorState where - renderMessage foundation ls = renderMessage foundation ls . \case - CorrectorNormal -> MsgCorrectorNormal - CorrectorMissing -> MsgCorrectorMissing - CorrectorExcused -> MsgCorrectorExcused - - instance RenderMessage UniWorX Load where renderMessage foundation ls = renderMessage foundation ls . \case (Load {byTutorial=Nothing , byProportion=p}) -> MsgCorByProportionOnly p (Load {byTutorial=Just True , byProportion=p}) -> MsgCorByProportionIncludingTutorial p (Load {byTutorial=Just False, byProportion=p}) -> MsgCorByProportionExcludingTutorial p -instance RenderMessage UniWorX SheetType where - renderMessage foundation ls = renderMessage foundation ls . \case - Bonus{..} -> MsgSheetTypeBonus' maxPoints - Normal{..} -> MsgSheetTypeNormal' maxPoints - Pass{..} -> MsgSheetTypePass' maxPoints passingPoints - NotGraded{} -> MsgSheetTypeNotGraded' - newtype MsgLanguage = MsgLanguage Lang deriving (Eq, Ord, Show, Read) instance RenderMessage UniWorX MsgLanguage where @@ -250,24 +217,18 @@ instance RenderMessage UniWorX MsgLanguage where where mr = renderMessage foundation ls -instance RenderMessage UniWorX NotificationTrigger where - renderMessage foundation ls = renderMessage foundation ls . \case - NTSubmissionRatedGraded -> MsgNotificationTriggerSubmissionRatedGraded - NTSubmissionRated -> MsgNotificationTriggerSubmissionRated - NTSheetActive -> MsgNotificationTriggerSheetActive - NTSheetSoonInactive -> MsgNotificationTriggerSheetSoonInactive - NTSheetInactive -> MsgNotificationTriggerSheetInactive - NTCorrectionsAssigned -> MsgNotificationCorrectionsAssigned - instance RenderMessage UniWorX (UnsupportedAuthPredicate (Route UniWorX)) where renderMessage f ls (UnsupportedAuthPredicate tag route) = renderMessage f ls $ MsgUnsupportedAuthPredicate tag (show route) -instance RenderMessage UniWorX MessageClass where - renderMessage f ls = renderMessage f ls . \case - Error -> MsgMessageError - Warning -> MsgMessageWarning - Info -> MsgMessageInfo - Success -> MsgMessageSuccess +embedRenderMessage ''UniWorX ''MessageClass ("Message" <>) +embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel +embedRenderMessage ''UniWorX ''SheetType $ \st -> "SheetType" <> st <> "'" +embedRenderMessage ''UniWorX ''StudyFieldType id +embedRenderMessage ''UniWorX ''SheetFileType id +embedRenderMessage ''UniWorX ''CorrectorState id + +newtype ErrorResponseTitle = ErrorResponseTitle ErrorResponse +embedRenderMessageVariant ''UniWorX ''ErrorResponseTitle ("ErrorResponseTitle" <>) data instance ButtonCssClass UniWorX = BCDefault | BCPrimary | BCSuccess | BCInfo | BCWarning | BCDanger | BCLink @@ -573,9 +534,9 @@ instance Yesod UniWorX where -- Store session data on the client in encrypted cookies, -- default session idle timeout is 120 minutes - makeSessionBackend _ = Just <$> defaultClientSessionBackend - 120 -- timeout in minutes - "client_session_key.aes" + makeSessionBackend UniWorX{appSessionKey,appSettings=AppSettings{appSessionTimeout}} = do + (getCachedDate, _) <- clientSessionDateCacher appSessionTimeout + return . Just $ clientSessionBackend appSessionKey getCachedDate maximumContentLength _ _ = Just $ 50 * 2^20 @@ -627,101 +588,52 @@ instance Yesod UniWorX where $logDebugS "normalizeRouteMiddleware" [st|Redirecting to #{tshow route'}|] redirectWith movedPermanently301 route' - defaultLayout widget = do - master <- getYesod - let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master + -- Since we implement `errorHandler` ourselves we don't need `defaultMessageWidget` + defaultMessageWidget _title _body = error "defaultMessageWidget: undefined" - applySystemMessages - mmsgs <- getMessages + errorHandler err = do + mr <- getMessageRender + let + encrypted :: ToJSON a => a -> Widget -> Widget + encrypted plaintextJson plaintext = do + canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True + shouldEncrypt <- getsYesod $ appEncryptErrors . appSettings + errKey <- getsYesod appErrorMsgKey + if + | shouldEncrypt + , not canDecrypt -> do + nonce <- liftIO SecretBox.newNonce + let ciphertext = SecretBox.secretbox errKey nonce . Lazy.ByteString.toStrict $ encode plaintextJson + encoded = decodeUtf8 . Base64.encode $ Saltine.encode nonce <> ciphertext + formatted = Text.intercalate "\n" $ Text.chunksOf 76 encoded + [whamlet| +
_{MsgErrorResponseEncrypted} +
+ #{formatted}
+ |]
+ | otherwise -> plaintext
+
+ errPage = case err of
+ NotFound -> [whamlet|_{MsgErrorResponseNotFound}|]
+ InternalError err -> encrypted err [whamlet|
#{err}|]
+ InvalidArgs errs -> [whamlet|
+
_{MsgErrorResponseNotAuthenticated}|] + PermissionDenied err -> [whamlet|
#{err}|] + BadMethod method -> [whamlet|
_{MsgErrorResponseBadMethod (decodeUtf8 method)}|] + fmap toTypedContent . siteLayout (Just . toHtml . mr $ ErrorResponseTitle err) $ do + toWidget + [cassius| + .errMsg + white-space: pre-wrap + font-family: monospace + |] + errPage - mcurrentRoute <- getCurrentRoute - - -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. - (title, parents) <- breadcrumbs - --- let isParent :: Route UniWorX -> Bool --- isParent r = r == (fst parents) - - let menu = defaultLinks ++ maybe [] pageActions mcurrentRoute - - menuTypes <- mapM (\x -> (x, ) <$> newIdent) =<< filterM (menuItemAccessCallback . menuItem) menu - - isAuth <- isJust <$> maybeAuthId - - -- Lookup Favourites & Theme if possible -- TODO: cache this info in a cookie?! - (favourites', currentTheme) <- do - muid <- maybeAuthPair - case muid of - Nothing -> return ([],userDefaultTheme) - (Just (uid,user)) -> do - favs <- runDB $ E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do - E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse) - E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid) - E.orderBy [ E.asc $ course E.^. CourseShorthand ] - return course - return (favs, userTheme user) - favourites <- forM favourites' $ \(Entity _ c@Course{..}) - -> let - courseRoute = CourseR courseTerm courseSchool courseShorthand CShowR - in (c, courseRoute, ) <$> filterM (menuItemAccessCallback . menuItem) (pageActions courseRoute) - - let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority - highlight = let crumbs = mcons mcurrentRoute $ fst <$> reverse parents - navItems = map snd3 favourites ++ map (menuItemRoute . menuItem . fst) menuTypes - highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map snd3 favourites) crumbs - in \r -> Just r == highR - favouriteTerms :: [TermIdentifier] - favouriteTerms = Set.toDescList $ foldMap (\(Course{..}, _, _) -> Set.singleton $ unTermKey courseTerm) favourites - favouriteTerm :: TermIdentifier -> [(Course, Route UniWorX, [MenuTypes])] - favouriteTerm tid = filter (\(Course{..}, _, _) -> unTermKey courseTerm == tid) favourites - - -- We break up the default layout into two components: - -- default-layout is the contents of the body tag, and - -- default-layout-wrapper is the entire page. Since the final - -- value passed to hamletToRepHtml cannot be a widget, this allows - -- you to use normal widget features in default-layout. - - - let - navbar :: Widget - navbar = $(widgetFile "widgets/navbar") - asidenav :: Widget - asidenav = $(widgetFile "widgets/asidenav") - contentHeadline :: Maybe Widget - contentHeadline = pageHeading =<< mcurrentRoute - breadcrumbs :: Widget - breadcrumbs = $(widgetFile "widgets/breadcrumbs") - pageactionprime :: Widget - pageactionprime = $(widgetFile "widgets/pageactionprime") -- TODO: rename, since it also shows secondary pageActions now - -- functions to determine if there are page-actions (primary or secondary) - isPageActionPrime :: MenuTypes -> Bool - isPageActionPrime (PageActionPrime _) = True - isPageActionPrime (PageActionSecondary _) = True - isPageActionPrime _ = False - hasPageActions :: Bool - hasPageActions = any (isPageActionPrime . fst) menuTypes - - pc <- widgetToPageContent $ do - addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900|Roboto:300,400,600" - addScript $ StaticR js_zepto_js - addScript $ StaticR js_fetchPolyfill_js - addScript $ StaticR js_urlPolyfill_js - addScript $ StaticR js_featureChecker_js - addScript $ StaticR js_flatpickr_js - addScript $ StaticR js_tabber_js - addStylesheet $ StaticR css_flatpickr_css - addStylesheet $ StaticR css_tabber_css - addStylesheet $ StaticR css_fonts_css - addStylesheet $ StaticR css_fontawesome_css - $(widgetFile "default-layout") - $(widgetFile "standalone/modal") - $(widgetFile "standalone/showHide") - $(widgetFile "standalone/inputs") - $(widgetFile "standalone/tooltip") - $(widgetFile "standalone/tabber") - $(widgetFile "standalone/alerts") - $(widgetFile "standalone/datepicker") - withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") + defaultLayout = siteLayout Nothing -- The page to be redirected to when authentication is required. authRoute _ = Just $ AuthR LoginR @@ -768,6 +680,105 @@ instance Yesod UniWorX where makeLogger = return . appLogger +siteLayout :: Maybe Html -- ^ Optionally override `pageHeading` + -> Widget -> Handler Html +siteLayout headingOverride widget = do + master <- getYesod + let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master + + applySystemMessages + mmsgs <- getMessages + + mcurrentRoute <- getCurrentRoute + + -- Get the breadcrumbs, as defined in the YesodBreadcrumbs instance. + (title, parents) <- breadcrumbs + + -- let isParent :: Route UniWorX -> Bool + -- isParent r = r == (fst parents) + + let menu = defaultLinks ++ maybe [] pageActions mcurrentRoute + + menuTypes <- mapM (\x -> (x, ) <$> newIdent) =<< filterM (menuItemAccessCallback . menuItem) menu + + isAuth <- isJust <$> maybeAuthId + + -- Lookup Favourites & Theme if possible -- TODO: cache this info in a cookie?! + (favourites', currentTheme) <- do + muid <- maybeAuthPair + case muid of + Nothing -> return ([],userDefaultTheme) + (Just (uid,user)) -> do + favs <- runDB $ E.select . E.from $ \(course `E.InnerJoin` courseFavourite) -> do + E.on (course E.^. CourseId E.==. courseFavourite E.^. CourseFavouriteCourse) + E.where_ (courseFavourite E.^. CourseFavouriteUser E.==. E.val uid) + E.orderBy [ E.asc $ course E.^. CourseShorthand ] + return course + return (favs, userTheme user) + favourites <- forM favourites' $ \(Entity _ c@Course{..}) + -> let + courseRoute = CourseR courseTerm courseSchool courseShorthand CShowR + in (c, courseRoute, ) <$> filterM (menuItemAccessCallback . menuItem) (pageActions courseRoute) + + let highlight :: Route UniWorX -> Bool -- highlight last route in breadcrumbs, favorites taking priority + highlight = let crumbs = mcons mcurrentRoute $ fst <$> reverse parents + navItems = map snd3 favourites ++ map (menuItemRoute . menuItem . fst) menuTypes + highR = find (`elem` navItems) . uncurry (++) $ partition (`elem` map snd3 favourites) crumbs + in \r -> Just r == highR + favouriteTerms :: [TermIdentifier] + favouriteTerms = Set.toDescList $ foldMap (\(Course{..}, _, _) -> Set.singleton $ unTermKey courseTerm) favourites + favouriteTerm :: TermIdentifier -> [(Course, Route UniWorX, [MenuTypes])] + favouriteTerm tid = filter (\(Course{..}, _, _) -> unTermKey courseTerm == tid) favourites + + -- We break up the default layout into two components: + -- default-layout is the contents of the body tag, and + -- default-layout-wrapper is the entire page. Since the final + -- value passed to hamletToRepHtml cannot be a widget, this allows + -- you to use normal widget features in default-layout. + + + let + navbar :: Widget + navbar = $(widgetFile "widgets/navbar") + asidenav :: Widget + asidenav = $(widgetFile "widgets/asidenav") + contentHeadline :: Maybe Widget + contentHeadline = (toWidget <$> headingOverride) <|> (pageHeading =<< mcurrentRoute) + breadcrumbs :: Widget + breadcrumbs = $(widgetFile "widgets/breadcrumbs") + pageactionprime :: Widget + pageactionprime = $(widgetFile "widgets/pageactionprime") -- TODO: rename, since it also shows secondary pageActions now + -- functions to determine if there are page-actions (primary or secondary) + isPageActionPrime :: MenuTypes -> Bool + isPageActionPrime (PageActionPrime _) = True + isPageActionPrime (PageActionSecondary _) = True + isPageActionPrime _ = False + hasPageActions :: Bool + hasPageActions = any (isPageActionPrime . fst) menuTypes + + pc <- widgetToPageContent $ do + addStylesheetRemote "https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,800,900|Roboto:300,400,600" + addScript $ StaticR js_zepto_js + addScript $ StaticR js_fetchPolyfill_js + addScript $ StaticR js_urlPolyfill_js + addScript $ StaticR js_featureChecker_js + addScript $ StaticR js_flatpickr_js + addScript $ StaticR js_tabber_js + addStylesheet $ StaticR css_flatpickr_css + addStylesheet $ StaticR css_tabber_css + addStylesheet $ StaticR css_fonts_css + addStylesheet $ StaticR css_fontawesome_css + $(widgetFile "default-layout") + $(widgetFile "standalone/modal") + $(widgetFile "standalone/showHide") + $(widgetFile "standalone/inputs") + $(widgetFile "standalone/tooltip") + $(widgetFile "standalone/tabber") + $(widgetFile "standalone/alerts") + $(widgetFile "standalone/datepicker") + withUrlRenderer $(hamletFile "templates/default-layout-wrapper.hamlet") + + applySystemMessages :: (MonadHandler m, HandlerSite m ~ UniWorX) => m () applySystemMessages = liftHandlerT . runDB . runConduit $ selectSource [] [] .| C.mapM_ applyMessage where @@ -949,8 +960,8 @@ pageActions (HomeR) = -- , menuItemAccessCallback' = return True -- } -- , - NavbarAside $ MenuItem - { menuItemLabel = "AdminDemo" + PageActionPrime $ MenuItem + { menuItemLabel = "Admin-Demo" , menuItemIcon = Just "screwdriver" , menuItemRoute = AdminTestR , menuItemModal = False @@ -963,6 +974,13 @@ pageActions (HomeR) = , menuItemModal = False , menuItemAccessCallback' = return True } + , PageActionPrime $ MenuItem + { menuItemLabel = "Fehlermeldung entschlüsseln" + , menuItemIcon = Nothing + , menuItemRoute = AdminErrMsgR + , menuItemModal = False + , menuItemAccessCallback' = return True + } ] pageActions (ProfileR) = [ PageActionPrime $ MenuItem @@ -1210,6 +1228,8 @@ pageHeading (AdminTestR) = Just $ [whamlet|Internal Code Demonstration Page|] pageHeading (AdminUserR _) = Just $ [whamlet|User Display for Admin|] +pageHeading (AdminErrMsgR) + = Just $ i18nHeading MsgErrMsgHeading pageHeading (VersionR) = Just $ i18nHeading MsgImpressumHeading pageHeading (HelpR) diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index 1b5c3ae9d..50b1963e7 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -1,20 +1,22 @@ -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ViewPatterns #-} -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE QuasiQuotes #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE LambdaCase #-} - module Handler.Admin where import Import import Handler.Utils import Jobs +import qualified Data.ByteString as BS + +import qualified Crypto.Saltine.Internal.ByteSizes as Saltine +import qualified Data.ByteString.Base64.URL as Base64 +import Crypto.Saltine.Core.SecretBox (secretboxOpen) +import qualified Crypto.Saltine.Class as Saltine + +import qualified Data.Text as Text +import qualified Data.Text.Encoding as Text +import Data.Char (isSpace) + +import Control.Monad.Trans.Except + -- import Data.Time -- import qualified Data.Text as T -- import Data.Function ((&)) @@ -105,3 +107,35 @@ getAdminUserR uuid = do
+ #{t}
+
+