feat(users): allow users to set postal address and email encryption password

This commit is contained in:
Steffen Jost 2022-09-06 16:49:51 +02:00
parent 9f3cb4ffe4
commit 655fcf7564
9 changed files with 97 additions and 19 deletions

View File

@ -27,6 +27,16 @@ WarningDaysTip: Wie viele Tage im Voraus sollen Fristen von Prüfungen etc. auf
ShowSex: Geschlechter anderer Nutzer:innen anzeigen ShowSex: Geschlechter anderer Nutzer:innen anzeigen
ShowSexTip: Sollen in Kursteilnehmer:innen-Tabellen u.Ä. die Geschlechter der Nutzer:innen angezeigt werden? ShowSexTip: Sollen in Kursteilnehmer:innen-Tabellen u.Ä. die Geschlechter der Nutzer:innen angezeigt werden?
PDFPassword: Passwort zur Verschlüsselung von PDF Anhängen an Email Benachrichtigungen
PDFPasswordTip: Achtung, dieses Passwort ist für FRADrive Administratoren einsehbar und wird unverschlüsselt gespeichert!
PDFPasswordInvalid: Bitte ein nicht-triviales Passwort ohne Leerzeichen für PDF Email Anhänge eintragen!
PrefersPostal: Sollen Benachrichtigung möglichst per Post versendet werden anstatt per Email?
PostalTip: Postversand kann in Rechnung gestellt werden und ist derzeit nur für Benachrichtigungen über Erneuerung und Ablauf von Qualifikation, wie z.B. Führerscheine, verfügbar.
PostAddress: Postalische Adresse
PostAddressTip: Mindestens eine Zeile mit Straße und Hausnummer und eine Zeile mit Postleitzahl und Ort. Kein Empfängername, denn dieser wird später automatisch hinzugefügt.
UserPostalInvalid: Postadresse muss mindestens eine Zeile mit Straße und Hausnummer und eine separate Zeile mit Postleitzahl und Ort enthalten!
UserPrefersPostalInvalid: Entweder postalische Benachrichtigungen deaktivieren oder eine Postadresse angeben!
FormNotifications: Benachrichtigungen FormNotifications: Benachrichtigungen
UserSchools: Relevante Institute UserSchools: Relevante Institute

View File

@ -27,6 +27,16 @@ WarningDaysTip: How many days ahead should deadlines regarding exams etc. be dis
ShowSex: Show sex of other users ShowSex: Show sex of other users
ShowSexTip: Should users' sex be displayed in (among others) lists of course participants? ShowSexTip: Should users' sex be displayed in (among others) lists of course participants?
PDFPassword: Password to lock PDF email attachments
PDFPasswordTip: Please note that this password is displayed to FRADrive admins and is saved unencrypted
PDFPasswordInvalid: Please supply a sensible password for encrypting PDF email attachments!
PrefersPostal: Should notifications preferably send by post instead of email?
PostalTip: Mailing may incur a fee and is currently only avaulable for qualification expiry notifications, such as driving lincence renewal.
PostAddress: Postal address
PostAddressTip: Should contain at least one line with street and house number and another line featuring zip code and town. Omit a recipient name, since it will be added later.
UserPostalInvalid: Postal address must have at least one line with street and house number and another with zip code and town!
UserPrefersPostalInvalid: Either deactivate postal notification or supply a valid postal address!
FormNotifications: Notifications FormNotifications: Notifications
UserSchools: Relevant departments UserSchools: Relevant departments

View File

@ -15,6 +15,7 @@ AdminUserFDepartment: Abteilung
AdminUserPostAddress: Postalische Anschrift AdminUserPostAddress: Postalische Anschrift
AdminUserPrefersPostal: Briefe anstatt Email bevorzugt AdminUserPrefersPostal: Briefe anstatt Email bevorzugt
AdminUserPinPassword: Passwort zur Verschlüsselung von PDF Anhängen in Emails AdminUserPinPassword: Passwort zur Verschlüsselung von PDF Anhängen in Emails
AdminUserNoPassword: Kein Passwort gesetzt
AdminUserAssimilate: Benutzer assimilieren AdminUserAssimilate: Benutzer assimilieren
UserAdded: Benutzer erfolgreich angelegt UserAdded: Benutzer erfolgreich angelegt
UserCollision: Benutzer konnte wegen Eindeutigkeit nicht angelegt werden UserCollision: Benutzer konnte wegen Eindeutigkeit nicht angelegt werden

View File

@ -14,7 +14,8 @@ AdminUserFPersonalNumber: Personalnumber (Fraport AG only)
AdminUserFDepartment: Department AdminUserFDepartment: Department
AdminUserPostAddress: Postal Address AdminUserPostAddress: Postal Address
AdminUserPrefersPostal: Prefers postal letters over email AdminUserPrefersPostal: Prefers postal letters over email
AdminUserPinPassword: Password used for all PDF attachments to emails AdminUserPinPassword: Password used for PDF attachments to emails
AdminUserNoPassword: No password set
AdminUserAssimilate: Assimilate user AdminUserAssimilate: Assimilate user
UserAdded: Successfully added user UserAdded: Successfully added user
UserCollision: Could not create user due to uniqueness constraint UserCollision: Could not create user due to uniqueness constraint

View File

@ -13,6 +13,8 @@ import Import
import Handler.Utils import Handler.Utils
import Handler.Utils.Profile import Handler.Utils.Profile
import Utils.Print (validCmdArgument)
-- import Colonnade hiding (fromMaybe, singleton) -- import Colonnade hiding (fromMaybe, singleton)
-- import Yesod.Colonnade -- import Yesod.Colonnade
import Data.Map ((!)) import Data.Map ((!))
@ -57,6 +59,11 @@ data SettingsForm = SettingsForm
, stgDownloadFiles :: Bool , stgDownloadFiles :: Bool
, stgWarningDays :: NominalDiffTime , stgWarningDays :: NominalDiffTime
, stgShowSex :: Bool , stgShowSex :: Bool
, stgPinPassword :: Text
, stgPrefersPostal :: Bool
, stgPostAddress :: Maybe StoredMarkup
, stgExamOfficeSettings :: ExamOfficeSettings , stgExamOfficeSettings :: ExamOfficeSettings
, stgSchools :: Set SchoolId , stgSchools :: Set SchoolId
, stgNotificationSettings :: NotificationSettings , stgNotificationSettings :: NotificationSettings
@ -130,8 +137,13 @@ makeSettingForm template html = do
& setTooltip MsgWarningDaysTip & setTooltip MsgWarningDaysTip
) (stgWarningDays <$> template) ) (stgWarningDays <$> template)
<*> apopt checkBoxField (fslI MsgShowSex & setTooltip MsgShowSexTip) (stgShowSex <$> template) <*> apopt checkBoxField (fslI MsgShowSex & setTooltip MsgShowSexTip) (stgShowSex <$> template)
<*> examOfficeForm (stgExamOfficeSettings <$> template)
<* aformSection MsgFormNotifications <* aformSection MsgFormNotifications
<*> areq (textField & cfStrip) (fslI MsgPDFPassword & setTooltip MsgPDFPasswordTip) (stgPinPassword <$> template)
<*> apopt checkBoxField (fslI MsgPrefersPostal & setTooltip MsgPostalTip) (stgPrefersPostal <$> template)
<*> aopt htmlField (fslI MsgPostAddress & setTooltip MsgPostAddressTip) (stgPostAddress <$> template)
<*> examOfficeForm (stgExamOfficeSettings <$> template)
<*> schoolsForm (stgSchools <$> template) <*> schoolsForm (stgSchools <$> template)
<*> notificationForm (stgNotificationSettings <$> template) <*> notificationForm (stgNotificationSettings <$> template)
<*> allocationNotificationForm (stgAllocationNotificationSettings <$> template) <*> allocationNotificationForm (stgAllocationNotificationSettings <$> template)
@ -426,10 +438,23 @@ examOfficeForm template = wFormToAForm $ do
validateSettings :: User -> FormValidator SettingsForm Handler () validateSettings :: User -> FormValidator SettingsForm Handler ()
validateSettings User{..} = do validateSettings User{..} = do
userDisplayName' <- use _stgDisplayName userDisplayName' <- use _stgDisplayName
guardValidation MsgUserDisplayNameInvalid $ guardValidation MsgUserDisplayNameInvalid $
validDisplayName userTitle userFirstName userSurname userDisplayName' validDisplayName userTitle userFirstName userSurname userDisplayName'
userPinPassword' <- use _stgPinPassword
guardValidation MsgPDFPasswordInvalid $
validCmdArgument userPinPassword' -- used as CMD argument for pdftk
userPostAddress' <- use _stgPostAddress
let postalNotSet = isNothing userPostAddress'
postalIsValid = validPostAddress userPostAddress'
guardValidation MsgUserPostalInvalid $
postalNotSet || postalIsValid
userPrefersPostal' <- use _stgPrefersPostal
guardValidation MsgUserPrefersPostalInvalid $
not $ userPrefersPostal' && postalNotSet
data ButtonResetTokens = BtnResetTokens data ButtonResetTokens = BtnResetTokens
deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable) deriving (Enum, Eq, Ord, Bounded, Read, Show, Generic, Typeable)
@ -478,6 +503,9 @@ postProfileR = do
, stgNotificationSettings = userNotificationSettings , stgNotificationSettings = userNotificationSettings
, stgWarningDays = userWarningDays , stgWarningDays = userWarningDays
, stgShowSex = userShowSex , stgShowSex = userShowSex
, stgPinPassword = fromMaybe "" userPinPassword
, stgPostAddress = userPostAddress
, stgPrefersPostal = userPrefersPostal
, stgExamOfficeSettings = ExamOfficeSettings , stgExamOfficeSettings = ExamOfficeSettings
{ eosettingsGetSynced = userExamOfficeGetSynced { eosettingsGetSynced = userExamOfficeGetSynced
, eosettingsGetLabels = userExamOfficeGetLabels , eosettingsGetLabels = userExamOfficeGetLabels
@ -501,6 +529,9 @@ postProfileR = do
, UserWarningDays =. stgWarningDays , UserWarningDays =. stgWarningDays
, UserNotificationSettings =. stgNotificationSettings , UserNotificationSettings =. stgNotificationSettings
, UserShowSex =. stgShowSex , UserShowSex =. stgShowSex
, UserPinPassword =. Just stgPinPassword
, UserPostAddress =. stgPostAddress
, UserPrefersPostal =. stgPrefersPostal
, UserExamOfficeGetSynced =. (stgExamOfficeSettings & eosettingsGetSynced) , UserExamOfficeGetSynced =. (stgExamOfficeSettings & eosettingsGetSynced)
, UserExamOfficeGetLabels =. (stgExamOfficeSettings & eosettingsGetLabels) , UserExamOfficeGetLabels =. (stgExamOfficeSettings & eosettingsGetLabels)
] ++ [ UserDisplayEmail =. stgDisplayEmail | userDisplayEmail == stgDisplayEmail ] ] ++ [ UserDisplayEmail =. stgDisplayEmail | userDisplayEmail == stgDisplayEmail ]

View File

@ -1,12 +1,16 @@
module Handler.Utils.Profile module Handler.Utils.Profile
( checkDisplayName ( checkDisplayName
, validDisplayName , validDisplayName
, fixDisplayName , fixDisplayName
, validPostAddress
) where ) where
import Import.NoFoundation import Import.NoFoundation
import Data.Char
import qualified Data.Text as Text import qualified Data.Text as Text
import qualified Data.Text.Lazy as LT
import qualified Data.MultiSet as MultiSet import qualified Data.MultiSet as MultiSet
import qualified Data.Set as Set import qualified Data.Set as Set
@ -49,3 +53,13 @@ validDisplayName (fmap Text.strip -> mTitle) (Text.strip -> fName) (Text.strip -
isAdd = (`Set.member` addLetters) isAdd = (`Set.member` addLetters)
splitAdd = Text.split isAdd splitAdd = Text.split isAdd
makeMultiSet = MultiSet.fromList . filter (not . Text.null) . splitAdd makeMultiSet = MultiSet.fromList . filter (not . Text.null) . splitAdd
-- | Primitive postal address requires at least one alphabetic character, one digit and a line break
validPostAddress :: Maybe StoredMarkup -> Bool
validPostAddress (Just StoredMarkup {markupInput = addr})
| Just _ <- LT.find isLetter addr
, Just _ <- LT.find isNumber addr
-- , Just _ <- LT.find ((LineSeparator ==) . generalCategory) addr -- THIS DID NOT WORK
, 1 < length (LT.lines addr)
= True
validPostAddress _ = False

View File

@ -106,6 +106,7 @@ data Icon
| IconReset | IconReset
| IconBlocked | IconBlocked
| IconPrintCenter | IconPrintCenter
| IconAt
deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable) deriving (Eq, Ord, Enum, Bounded, Show, Read, Generic, Typeable)
deriving anyclass (Universe, Finite, NFData) deriving anyclass (Universe, Finite, NFData)
@ -144,7 +145,7 @@ iconText = \case
IconSFTHint -> "life-ring" -- for SheetFileType only IconSFTHint -> "life-ring" -- for SheetFileType only
IconSFTSolution -> "exclamation-circle" -- for SheetFileType only IconSFTSolution -> "exclamation-circle" -- for SheetFileType only
IconSFTMarking -> "check-circle" -- for SheetFileType only IconSFTMarking -> "check-circle" -- for SheetFileType only
IconEmail -> "envelope" IconEmail -> "envelope" -- envelope is no longer unamibuous
IconRegisterTemplate -> "file-alt" IconRegisterTemplate -> "file-alt"
IconApplyTrue -> "file-alt" IconApplyTrue -> "file-alt"
IconApplyFalse -> "trash" IconApplyFalse -> "trash"
@ -195,6 +196,7 @@ iconText = \case
IconReset -> "undo" -- From fontawesome v6 onwards: "arrow-rotate-left" IconReset -> "undo" -- From fontawesome v6 onwards: "arrow-rotate-left"
IconBlocked -> "ban" IconBlocked -> "ban"
IconPrintCenter -> "mail-bulk" -- From fontawesome v6 onwards: "envelope-bulk" IconPrintCenter -> "mail-bulk" -- From fontawesome v6 onwards: "envelope-bulk"
IconAt -> "at"
nullaryPathPiece ''Icon $ camelToPathPiece' 1 nullaryPathPiece ''Icon $ camelToPathPiece' 1
deriveLift ''Icon deriveLift ''Icon

View File

@ -4,6 +4,7 @@ module Utils.Print
( pdfRenewal ( pdfRenewal
, sendLetter , sendLetter
, encryptPDF , encryptPDF
, sanitizeCmdArg, validCmdArgument
, templateDIN5008 , templateDIN5008
, templateRenewal , templateRenewal
-- , compileTemplate, makePDF -- , compileTemplate, makePDF
@ -271,8 +272,8 @@ sendLetter printJobName pdf printJobRecipient printJobSender printJobCourse prin
nameCourse = CI.original . courseShorthand <$> course nameCourse = CI.original . courseShorthand <$> course
nameQuali = CI.original . qualificationShorthand <$> quali nameQuali = CI.original . qualificationShorthand <$> quali
let printJobAcknowledged = Nothing let printJobAcknowledged = Nothing
jobFullName = unpack $ T.replace " " "-" (T.intercalate "_" . catMaybes $ [Just printJobName, nameQuali, nameCourse, nameSender, nameRecipient]) jobFullName = T.replace " " "-" (T.intercalate "_" . catMaybes $ [Just printJobName, nameQuali, nameCourse, nameSender, nameRecipient])
printJobFilename = jobFullName <> ".pdf" printJobFilename = T.unpack $ jobFullName <> ".pdf"
-- printJobFile <- sinkFileDB True $ yield $ LBS.toStrict pdf -- for PrintJobFile :: FileContentReference use this code -- printJobFile <- sinkFileDB True $ yield $ LBS.toStrict pdf -- for PrintJobFile :: FileContentReference use this code
printJobFile = LBS.toStrict pdf printJobFile = LBS.toStrict pdf
lprPDF jobFullName pdf >>= \case lprPDF jobFullName pdf >>= \case
@ -328,6 +329,12 @@ readProcess' pc = do
return (ec, st_err, st_out) return (ec, st_err, st_out)
sanitizeCmdArg :: Text -> Text
sanitizeCmdArg t =
T.snoc (T.cons '"' $ T.filter (\c -> '"' /= c && '\\' /= c) t) '"'
-- | Pin Password is used as a commandline argument in Utils.Print.encryptPDF and hence poses a security risk
validCmdArgument :: Text -> Bool
validCmdArgument t = not (T.null t) && (T.cons '"' (T.snoc t '"') == sanitizeCmdArg t)
----------- -----------
-- pdftk -- -- pdftk --
@ -342,10 +349,11 @@ readProcess' pc = do
encryptPDF :: MonadIO m => Text -> LBS.ByteString -> m (Either Text LBS.ByteString) encryptPDF :: MonadIO m => Text -> LBS.ByteString -> m (Either Text LBS.ByteString)
encryptPDF pw bs = over _Left (decodeUtf8 . LBS.toStrict) . exit2either <$> readProcess pc encryptPDF pw bs = over _Left (decodeUtf8 . LBS.toStrict) . exit2either <$> readProcess pc
where where
pc = setStdin (byteStringInput bs) $ pw' = sanitizeCmdArg pw
pc = setStdin (byteStringInput bs) $
proc "pdftk" [ "-" -- read from stdin proc "pdftk" [ "-" -- read from stdin
, "output", "-" -- write to stdout , "output", "-" -- write to stdout
, "user_pw", T.unpack pw -- encrypt pdf content , "user_pw", T.unpack pw' -- encrypt pdf content
, "dont_ask" -- no interaction , "dont_ask" -- no interaction
, "allow", "Printing" -- allow printing despite encryption , "allow", "Printing" -- allow printing despite encryption
] ]
@ -366,7 +374,7 @@ encryptPDF pw bs = over _Left (decodeUtf8 . LBS.toStrict) . exit2either <$> read
-- > lpr -P fradrive -H fravm017173.fra.fraport.de:515 -T printJobName - -- > lpr -P fradrive -H fravm017173.fra.fraport.de:515 -T printJobName -
-- | Internal only, use `sendLetter` instead -- | Internal only, use `sendLetter` instead
lprPDF :: (MonadHandler m, HasAppSettings (HandlerSite m)) => String -> LBS.ByteString -> m (Either Text Text) lprPDF :: (MonadHandler m, HasAppSettings (HandlerSite m)) => Text -> LBS.ByteString -> m (Either Text Text)
lprPDF jb bs = do lprPDF jb bs = do
lprServerArg <- $cachedHereBinary ("lprServer"::Text) getLprServerArg lprServerArg <- $cachedHereBinary ("lprServer"::Text) getLprServerArg
let pc = setStdin (byteStringInput bs) $ let pc = setStdin (byteStringInput bs) $
@ -376,7 +384,8 @@ lprPDF jb bs = do
, "-" -- read from stdin , "-" -- read from stdin
] ]
jobname | null jb = [] jobname | null jb = []
| otherwise = ["-J " <> jb] | otherwise = ["-J " <> jb']
jb' = T.unpack $ sanitizeCmdArg jb
exit2either <$> readProcess' pc exit2either <$> readProcess' pc
where where
getLprServerArg = do getLprServerArg = do
@ -397,4 +406,4 @@ lprPDF' jb bs = do
jobname | null jb = [] jobname | null jb = []
| otherwise = ["-J " <> jb] | otherwise = ["-J " <> jb]
exit2either <$> readProcess' pc exit2either <$> readProcess' pc
-} -}

View File

@ -19,14 +19,14 @@ $newline never
<dd .deflist__dd> <dd .deflist__dd>
#{matnr} #{matnr}
$maybe addr <- userPostAddress $maybe addr <- userPostAddress
<dt .deflist__dt>
_{MsgPrefersPostal}
<dd .deflist__dd>
#{icon (bool IconAt IconPrintCenter userPrefersPostal)}
<dt .deflist__dt> <dt .deflist__dt>
_{MsgAdminUserPostAddress} _{MsgAdminUserPostAddress}
<dd .deflist__dd> <dd .deflist__dd>
#{addr} #{addr}
<dt .deflist__dt>
_{MsgAdminUserPrefersPostal}
<dd .deflist__dd>
#{show userPrefersPostal}
<dt .deflist__dt> <dt .deflist__dt>
_{MsgTableEmail} _{MsgTableEmail}
<dd .deflist__dd> <dd .deflist__dd>
@ -39,10 +39,10 @@ $newline never
<dt .deflist__dt> <dt .deflist__dt>
_{MsgAdminUserPinPassword} _{MsgAdminUserPinPassword}
<dd .deflist__dd> <dd .deflist__dd>
$maybe _pass <- userPinPassword $maybe pass <- userPinPassword
OK #{pass}
$nothing $nothing
NO _{MsgAdminUserNoPassword}
$maybe telephonenr <- userTelephone $maybe telephonenr <- userTelephone
<dt .deflist__dt> <dt .deflist__dt>
_{MsgUserTelephone} _{MsgUserTelephone}