diff --git a/ChangeLog.md b/ChangeLog.md index 923e51045..401601e10 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,6 +9,8 @@ Unterstützung von Tabellenzusammenfassungen, z.B. Punktsummen Intelligente Verteilung von Abgaben auf Korrektoren (z.B. bei Krankheit) + + Übungsblätter können Abgabe von Dateien verbieten und angeben ob ZIP-Archive entpackt werden sollen * Version 06.08.2018 diff --git a/config/settings.yml b/config/settings.yml index 72965a276..75d5af052 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -13,9 +13,12 @@ detailed-logging: "_env:DETAILED_LOGGING:false" should-log-all: "_env:LOG_ALL:false" minimum-log-level: "_env:LOGLEVEL:warn" auth-dummy-login: "_env:DUMMY_LOGIN:false" -auth-pwfile: "_env:PWFILE:" allow-deprecated: "_env:ALLOW_DEPRECATED:false" +auth-pw-hash: + algorithm: "pbkdf2" + strength: 14 + # Optional values with the following production defaults. # In development, they default to true. # reload-templates: false @@ -42,7 +45,7 @@ ldap: timeout: "_env:LDAPTIMEOUT:5" user-defaults: - favourites: 12 + max-favourites: 12 theme: Default date-time-format: "%a %d %b %Y %R" date-format: "%d.%m.%Y" diff --git a/db.hs b/db.hs index 4a2a1bf7c..3bb77bcf5 100755 --- a/db.hs +++ b/db.hs @@ -66,8 +66,8 @@ fillDb = do winter2017 = TermIdentifier 2017 Winter summer2018 = TermIdentifier 2018 Summer gkleen <- insert User - { userPlugin = "LDAP" - , userIdent = "G.Kleen@campus.lmu.de" + { userIdent = "G.Kleen@campus.lmu.de" + , userAuthentication = AuthLDAP , userMatrikelnummer = Nothing , userEmail = "G.Kleen@campus.lmu.de" , userDisplayName = "Gregor Kleen" @@ -80,8 +80,8 @@ fillDb = do , userDownloadFiles = userDefaultDownloadFiles } fhamann <- insert User - { userPlugin = "LDAP" - , userIdent = "felix.hamann@campus.lmu.de" + { userIdent = "felix.hamann@campus.lmu.de" + , userAuthentication = AuthLDAP , userMatrikelnummer = Nothing , userEmail = "felix.hamann@campus.lmu.de" , userDisplayName = "Felix Hamann" @@ -94,8 +94,8 @@ fillDb = do , userDownloadFiles = userDefaultDownloadFiles } jost <- insert User - { userPlugin = "LDAP" - , userIdent = "jost@tcs.ifi.lmu.de" + { userIdent = "jost@tcs.ifi.lmu.de" + , userAuthentication = AuthLDAP , userMatrikelnummer = Nothing , userEmail = "jost@tcs.ifi.lmu.de" , userDisplayName = "Steffen Jost" @@ -108,8 +108,8 @@ fillDb = do , userDownloadFiles = userDefaultDownloadFiles } void . insert $ User - { userPlugin = "LDAP" - , userIdent = "max@campus.lmu.de" + { userIdent = "max@campus.lmu.de" + , userAuthentication = AuthLDAP , userMatrikelnummer = Nothing , userEmail = "max@campus.lmu.de" , userDisplayName = "Max Musterstudent" @@ -122,8 +122,8 @@ fillDb = do , userDownloadFiles = userDefaultDownloadFiles } void . insert $ User - { userPlugin = "LDAP" - , userIdent = "tester@campus.lmu.de" + { userIdent = "tester@campus.lmu.de" + , userAuthentication = AuthLDAP , userMatrikelnummer = Just "999" , userEmail = "tester@campus.lmu.de" , userDisplayName = "Tina Tester" @@ -196,11 +196,11 @@ fillDb = do void . insert $ DegreeCourse ffp sdMst sdInf void . insert $ Lecturer jost ffp void . insert $ Lecturer gkleen ffp - sheetkey <- insert $ Sheet ffp "AdHoc-Gruppen" Nothing NotGraded (Arbitrary 3) Nothing Nothing now now Nothing Nothing + sheetkey <- insert $ Sheet ffp "AdHoc-Gruppen" Nothing NotGraded (Arbitrary 3) Nothing Nothing now now Nothing Nothing (Upload True) insert_ $ SheetEdit gkleen now sheetkey - sheetkey <- insert $ Sheet ffp "Feste Gruppen" Nothing NotGraded RegisteredGroups Nothing Nothing now now Nothing Nothing + sheetkey <- insert $ Sheet ffp "Feste Gruppen" Nothing NotGraded RegisteredGroups Nothing Nothing now now Nothing Nothing (Upload True) insert_ $ SheetEdit gkleen now sheetkey - sheetkey <- insert $ Sheet ffp "Keine Gruppen" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing + sheetkey <- insert $ Sheet ffp "Keine Gruppen" Nothing NotGraded NoGroups Nothing Nothing now now Nothing Nothing (Upload True) insert_ $ SheetEdit gkleen now sheetkey -- EIP eip <- insert Course @@ -284,6 +284,7 @@ fillDb = do , sheetVisibleFrom = Just now , sheetActiveFrom = now , sheetActiveTo = (14 * nominalDay) `addUTCTime` now + , sheetUploadMode = Upload True , sheetHintFrom = Nothing , sheetSolutionFrom = Nothing } diff --git a/messages/dummy/de.msg b/messages/dummy/de.msg new file mode 100644 index 000000000..f3ca7cae1 --- /dev/null +++ b/messages/dummy/de.msg @@ -0,0 +1 @@ +DummyIdent: Nutzer-Kennung \ No newline at end of file diff --git a/messages/pw-hash/de.msg b/messages/pw-hash/de.msg new file mode 100644 index 000000000..9fb1eb5e4 --- /dev/null +++ b/messages/pw-hash/de.msg @@ -0,0 +1,2 @@ +PWHashIdent: Identifikation +PWHashPassword: Passwort \ No newline at end of file diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg index e66535980..1bd1ddd42 100644 --- a/messages/uniworx/de.msg +++ b/messages/uniworx/de.msg @@ -5,6 +5,7 @@ BtnRegister: Anmelden BtnDeregister: Abmelden BtnHijack: Sitzung übernehmen +Aborted: Abgebrochen Registered: Angemeldet RegisterFrom: Anmeldungen von RegisterTo: Anmeldungen bis @@ -41,10 +42,10 @@ CourseRegisterOk: Sie wurden angemeldet CourseDeregisterOk: Sie wurden abgemeldet CourseSecretWrong: Falsches Kennwort CourseSecret: Zugangspasswort -CourseNewOk tid@TermId ssh@SchoolId courseShortHand@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{courseShortHand} wurde erfolgreich erstellt. -CourseEditOk tid@TermId ssh@SchoolId courseShortHand@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{courseShortHand} wurde erfolgreich geändert. -CourseNewDupShort tid@TermId ssh@SchoolId courseShortHand@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{courseShortHand} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester. -CourseEditDupShort tid@TermId ssh@SchoolId courseShortHand@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{courseShortHand} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{courseShortHand} in diesem Semester. +CourseNewOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} wurde erfolgreich erstellt. +CourseEditOk tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} wurde erfolgreich geändert. +CourseNewDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} konnte nicht erstellt werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester. +CourseEditDupShort tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} konnte nicht geändert werden: Es gibt bereits einen anderen Kurs mit dem Kürzel #{csh} in diesem Semester. FFSheetName: Name TermCourseListHeading tid@TermId: Kursübersicht #{display tid} TermSchoolCourseListHeading tid@TermId school@SchoolName: Kursübersicht #{display tid} für #{display school} @@ -52,7 +53,7 @@ CourseListTitle: Alle Kurse TermCourseListTitle tid@TermId: Kurse #{display tid} TermSchoolCourseListTitle tid@TermId school@SchoolName: Kurse #{display tid} für #{display school} CourseNewHeading: Neuen Kurs anlegen -CourseEditHeading tid@TermId ssh@SchoolId courseShortHand@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{courseShortHand} editieren +CourseEditHeading tid@TermId ssh@SchoolId csh@CourseShorthand: Kurs #{display tid}-#{display ssh}-#{csh} editieren CourseEditTitle: Kurs editieren/anlegen CourseMembers: Teilnehmer CourseMembersCount num@Int64: #{display num} @@ -71,20 +72,25 @@ CourseRegisterFromTip: Ohne Datum ist keine Anmeldung möglich CourseRegisterToTip: Anmeldung darf auch ohne Begrenzung möglich sein CourseDeregisterUntilTip: Abmeldung darf auch ohne Begrenzung möglich sein +NoSuchTerm tid@TermId: Semester #{display tid} gibt es nicht. +NoSuchSchool ssh@SchoolId: Institut #{display ssh} gibt es nicht. +NoSuchCourseShorthand csh@CourseShorthand: Kein Kurs mit Kürzel #{display csh} bekannt. +NoSuchCourse: Keinen passenden Kurs gefunden. Sheet: Blatt -SheetList tid@TermId ssh@SchoolId courseShortHand@CourseShorthand: #{display tid}-#{display ssh}-#{courseShortHand} Übersicht Übungsblätter -SheetNewHeading tid@TermId ssh@SchoolId courseShortHand@CourseShorthand: #{display tid}-#{display ssh}-#{courseShortHand} Neues Übungsblatt anlegen -SheetNewOk tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: #{sheetName} wurde als neues Übungsblatt im Kurs #{display tid}-#{display ssh}-#{courseShortHand} erfolgreich erstellt. -SheetTitle tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{courseShortHand} #{sheetName} -SheetTitleNew tid@TermId ssh@SchoolId courseShortHand@CourseShorthand : #{display tid}-#{display ssh}-#{courseShortHand}: Neues Übungsblatt -SheetEditHead tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{courseShortHand} #{sheetName} editieren -SheetEditOk tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: Übungsblatt #{sheetName} aus Kurs #{display tid}-#{display ssh}-#{courseShortHand} wurde gespeichert. -SheetNameDup tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: Es gibt bereits ein Übungsblatt #{sheetName} in diesem Kurs #{display tid}-#{display ssh}-#{courseShortHand}. -SheetDelHead tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: #{sheetName} wirklich aus Kurs #{display tid}-#{display ssh}-#{courseShortHand} herauslöschen? +SheetList tid@TermId ssh@SchoolId csh@CourseShorthand: #{display tid}-#{display ssh}-#{csh} Übersicht Übungsblätter +SheetNewHeading tid@TermId ssh@SchoolId csh@CourseShorthand: #{display tid}-#{display ssh}-#{csh} Neues Übungsblatt anlegen +SheetNewOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{sheetName} wurde als neues Übungsblatt im Kurs #{display tid}-#{display ssh}-#{csh} erfolgreich erstellt. +SheetTitle tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh} #{sheetName} +SheetTitleNew tid@TermId ssh@SchoolId csh@CourseShorthand : #{display tid}-#{display ssh}-#{csh}: Neues Übungsblatt +SheetEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh} #{sheetName} editieren +SheetEditOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Übungsblatt #{sheetName} aus Kurs #{display tid}-#{display ssh}-#{csh} wurde gespeichert. +SheetNameDup tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Es gibt bereits ein Übungsblatt #{sheetName} in diesem Kurs #{display tid}-#{display ssh}-#{csh}. +SheetDelHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{sheetName} wirklich aus Kurs #{display tid}-#{display ssh}-#{csh} herauslöschen? SheetDelText submissionNo@Int: Dies kann nicht mehr rückgängig gemacht werden! Alle Einreichungen gehen ebenfalls verloren! Es gibt #{display submissionNo} Abgaben. -SheetDelOk tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{courseShortHand}: #{sheetName} gelöscht. +SheetDelOk tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh}: #{sheetName} gelöscht. +SheetUploadMode: Abgabe von Dateien SheetExercise: Aufgabenstellung SheetHint: Hinweis SheetHintFrom: Hinweis ab @@ -116,12 +122,12 @@ Deadline: Abgabe Done: Eingereicht Submission: Abgabenummer -SubmissionsCourse tid@TermId ssh@SchoolId courseShortHand@CourseShorthand: Alle Abgaben Kurs #{display tid}-#{display ssh}-#{courseShortHand} +SubmissionsCourse tid@TermId ssh@SchoolId csh@CourseShorthand: Alle Abgaben Kurs #{display tid}-#{display ssh}-#{csh} SubmissionsSheet sheetName@SheetName: Abgaben für #{sheetName} SubmissionWrongSheet: Abgabenummer gehört nicht zum angegebenen Übungsblatt. SubmissionAlreadyExists: Sie haben bereits eine Abgabe zu diesem Übungsblatt. -SubmissionEditHead tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{courseShortHand} #{sheetName}: Abgabe editieren/anlegen -CorrectionHead tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName cid@CryptoFileNameSubmission: #{display tid}-#{display ssh}-#{courseShortHand} #{sheetName}: Korrektur +SubmissionEditHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: #{display tid}-#{display ssh}-#{csh} #{sheetName}: Abgabe editieren/anlegen +CorrectionHead tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName cid@CryptoFileNameSubmission: #{display tid}-#{display ssh}-#{csh} #{sheetName}: Korrektur SubmissionMember g@Int: Mitabgebende(r) ##{display g} SubmissionArchive: Zip-Archiv der Abgabedatei(en) SubmissionFile: Datei zur Abgabe @@ -163,7 +169,7 @@ TooManyParticipants: Es wurden zu viele Mitabgebende angegeben AddCorrector: Zusätzlicher Korrektor CorrectorExists email@UserEmail: #{email} ist bereits als Korrektor eingetragen -SheetCorrectorsTitle tid@TermId ssh@SchoolId courseShortHand@CourseShorthand sheetName@SheetName: Korrektoren für #{display tid}-#{display ssh}-#{courseShortHand} #{sheetName} +SheetCorrectorsTitle tid@TermId ssh@SchoolId csh@CourseShorthand sheetName@SheetName: Korrektoren für #{display tid}-#{display ssh}-#{csh} #{sheetName} CountTutProp: Tutorien zählen gegen Proportion Corrector: Korrektor Correctors: Korrektoren @@ -184,7 +190,7 @@ Users: Benutzer HomeHeading: Aktuelle Termine LoginHeading: Authentifizierung LoginTitle: Authentifizierung -ProfileHeading: Benutzerprofil und Einstellungen +ProfileHeading: Benutzereinstellungen ProfileDataHeading: Gespeicherte Benutzerdaten ImpressumHeading: Impressum @@ -196,7 +202,7 @@ MatrikelNr: Matrikelnummer Theme: Oberflächen Design Favoriten: Anzahl gespeicherter Favoriten Plugin: Plugin -Ident: Identifizierung +Ident: Identifikation Settings: Individuelle Benutzereinstellungen SettingsUpdate: Einstellungen wurden gespeichert. @@ -218,11 +224,14 @@ NoCorrector: Kein Korrektor RemovedCorrections num@Int64: Korrektur-Daten wurden von #{display num} Abgaben entfernt. UpdatedAssignedCorrectorsAuto num@Int64: #{display num} Abgaben wurden unter den Korrektoren aufgeteilt. CouldNotAssignCorrectorsAuto num@Int64: #{display num} Abgaben konnten nicht automatisch zugewiesen werden: +SelfCorrectors num@Int64: #{display num} Abgaben wurden Abgebenden als eigenem Korrektor zugeteilt! + CorrectionsUploaded num@Int64: #{display num} Korrekturen wurden gespeichert: NoCorrectionsUploaded: In der hochgeladenen Datei wurden keine Korrekturen gefunden. RatingBy: Korrigiert von +AssignedTime: Zuteilung AchievedBonusPoints: Erreichte Bonuspunkte AchievedNormalPoints: Erreichte Punkte AchievedPassPoints: Erreichte Punkte @@ -280,8 +289,23 @@ SubmissionFilesIgnored: Es wurden Dateien in der hochgeladenen Abgabe ignoriert: SubmissionDoesNotExist smid@CryptoFileNameSubmission: Es existiert keine Abgabe mit Nummer #{toPathPiece smid}. LDAPLoginTitle: Campus-Login +PWHashLoginTitle: Uni2Work-Login +PWHashLoginNote: Dieses Formular ist zu verwenden, wenn Sie vom Uni2Work-Team spezielle Logindaten erhalten haben. Normale Nutzer melden sich bitte via Campus-Login an! DummyLoginTitle: Development-Login CorrectorNormal: Normal CorrectorMissing: Abwesend CorrectorExcused: Entschuldigt + +DayIsAHoliday tid@TermId date@Text: #{date} ist ein Feiertag +DayIsOutOfLecture tid@TermId date@Text: #{date} ist außerhalb der Vorlesungszeit des #{display tid} +DayIsOutOfTerm tid@TermId date@Text: #{date} liegt nicht im #{display tid} + +UploadModeNone: Kein Upload +UploadModeUnpack: Upload, einzelne Datei +UploadModeNoUnpack: Upload, ZIP-Archive entpacken + +SubmissionNoUploadExpected: Es ist keine Abgabe von Dateien vorgesehen. + +FieldPrimary: Hauptfach +FieldSecondary: Nebenfach diff --git a/models b/models index 341499e8f..594b69fad 100644 --- a/models +++ b/models @@ -1,6 +1,6 @@ User json - plugin Text - ident Text + ident (CI Text) + authentication AuthenticationMode matrikelnummer Text Maybe email (CI Text) displayName Text @@ -11,7 +11,7 @@ User json dateFormat DateTimeFormat "default='%d.%m.%Y'" timeFormat DateTimeFormat "default='%R'" downloadFiles Bool default=false - UniqueAuthentication plugin ident + UniqueAuthentication ident UniqueEmail email deriving Show UserAdmin @@ -41,7 +41,7 @@ StudyTerms Primary key Term json name TermIdentifier -- unTermKey :: TermId -> TermIdentifier - start Day -- TermKey :: TermIdentifier -< TermId + start Day -- TermKey :: TermIdentifier -> TermId end Day holidays [Day] lectureStart Day @@ -54,14 +54,14 @@ School json shorthand (CI Text) UniqueSchool name UniqueSchoolShorthand shorthand -- required for Normalisation of CI Text - Primary shorthand -- newtype Key School = School { unSchoolKey :: SchoolShorthand } + Primary shorthand -- newtype Key School = SchoolKey { unSchoolKey :: SchoolShorthand } deriving Eq DegreeCourse json course CourseId degree StudyDegreeId terms StudyTermsId UniqueDegreeCourse course degree terms -Course +Course name (CI Text) description Html Maybe linkExternal Text Maybe @@ -108,6 +108,7 @@ Sheet activeTo UTCTime hintFrom UTCTime Maybe solutionFrom UTCTime Maybe + uploadMode UploadMode CourseSheet course name SheetEdit user UserId @@ -132,10 +133,11 @@ File deriving Show Eq Submission sheet SheetId - ratingPoints Points Maybe - ratingComment Text Maybe - ratingBy UserId Maybe - ratingTime UTCTime Maybe + ratingPoints Points Maybe -- "Just" does not mean done + ratingComment Text Maybe -- "Just" does not mean done + ratingBy UserId Maybe -- assigned corrector + ratingAssigned UTCTime Maybe -- time assigned corrector + ratingTime UTCTime Maybe -- "Just" here indicates done! deriving Show SubmissionEdit user UserId diff --git a/routes b/routes index 014a25e28..6116665dc 100644 --- a/routes +++ b/routes @@ -39,7 +39,7 @@ /info VersionR GET !free /profile ProfileR GET POST !free !free -/profile/data ProfileDataR GET !free !free +/profile/data ProfileDataR GET POST !free !free /term TermShowR GET !free /term/current TermCurrentR GET !free @@ -55,10 +55,15 @@ -- For Pattern Synonyms see Foundation /course/ CourseListR GET !free !/course/new CourseNewR GET POST !lecturer +!/course/new/#{Maybe TermId}/#{Maybe SchoolId}/#{Maybe CourseShorthand} CourseNewTemplateR GET !lecturer /course/#TermId/#SchoolId/#CourseShorthand CourseR !lecturer: / CShowR GET !free /register CRegisterR POST !timeANDcapacity /edit CEditR GET POST + /delete CDeleteR GET POST !lecturerANDempty + /users CUsersR GET + /user/#CryptoUUIDUser CUserR GET + /correctors CHiWisR GET /subs CCorrectionsR GET POST /ex SheetListR GET !registered !materials !/ex/new SheetNewR GET POST @@ -77,10 +82,6 @@ /correctors SCorrR GET POST !/#SheetFileType/*FilePath SFileR GET !timeANDregistered !timeANDmaterials !corrector --- /user/#CryptoUUIDUser --- /users --- /correctors - /corrections CorrectionsR GET POST !corrector !lecturer /corrections/upload CorrectionsUploadR GET POST !corrector !lecturer diff --git a/src/Application.hs b/src/Application.hs index c5b69f55f..9c4cb5a54 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -206,13 +206,10 @@ handler h = getAppDevSettings >>= makeFoundation >>= flip unsafeHandler h db :: ReaderT SqlBackend (HandlerT UniWorX IO) a -> IO a db = handler . runDB -addPWEntry :: FilePath {-^ Password file -} - -> User +addPWEntry :: User -> Text {-^ Password -} -> IO () -addPWEntry pwFile User{..} (Text.encodeUtf8 -> pw) = do - (Text.decodeUtf8 -> pwHash) <- makePassword pw 14 - let pwEntry = PWEntry{ pwUser = User{ userPlugin = "PWFile", .. }, .. } - newUser = userIdent - c <- either (const []) id <$> Yaml.decodeFileEither pwFile - Yaml.encodeFile pwFile $ pwEntry : [ c' | c'@(PWEntry{pwUser=User{..}}) <- c, userIdent /= newUser ] +addPWEntry User{..} (Text.encodeUtf8 -> pw) = db $ do + PWHashConf{..} <- getsYesod $ appAuthPWHash . appSettings + (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength + void $ insert User{..} diff --git a/src/Auth/Dummy.hs b/src/Auth/Dummy.hs new file mode 100644 index 000000000..809db8647 --- /dev/null +++ b/src/Auth/Dummy.hs @@ -0,0 +1,63 @@ +{-# LANGUAGE NoImplicitPrelude + , RecordWildCards + , TemplateHaskell + , FlexibleContexts + , TypeFamilies + , OverloadedStrings + #-} + +module Auth.Dummy + ( dummyLogin + , DummyMessage(..) + ) where + +import Import.NoFoundation +import Database.Persist.Sql (SqlBackendCanRead) + +import Utils.Form + +import Data.CaseInsensitive (CI) +import qualified Data.CaseInsensitive as CI + + +data DummyMessage = MsgDummyIdent + + +dummyForm :: ( RenderMessage site FormMessage + , RenderMessage site DummyMessage + , YesodPersist site + , SqlBackendCanRead (YesodPersistBackend site) + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => AForm (HandlerT site IO) (CI Text) +dummyForm = areq (selectField userList) (fslI MsgDummyIdent) Nothing + <* submitButton + where + userList = fmap mkOptionList . runDB $ map toOption <$> selectList [] [Asc UserIdent] + toOption (Entity _ User{..}) = Option (CI.original userIdent) userIdent (CI.original userIdent) + +dummyLogin :: ( YesodAuth site + , YesodPersist site + , SqlBackendCanRead (YesodPersistBackend site) + , RenderMessage site FormMessage + , RenderMessage site DummyMessage + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => AuthPlugin site +dummyLogin = AuthPlugin{..} + where + apName = "dummy" + -- apDispatch :: Text -> [Text] -> HandlerT Auth (HandlerT site IO) TypedContent + apDispatch "POST" [] = do + ((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard dummyForm + case loginRes of + FormFailure errs -> do + lift . forM_ errs $ addMessage Error . toHtml + redirect LoginR + FormMissing -> redirect LoginR + FormSuccess ident -> + lift . setCredsRedirect $ Creds "dummy" (CI.original ident) [] + apDispatch _ _ = notFound + apLogin toMaster = do + (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard dummyForm + $(widgetFile "widgets/dummy-login-form") diff --git a/src/Auth/LDAP.hs b/src/Auth/LDAP.hs index ec0493e8f..32c185519 100644 --- a/src/Auth/LDAP.hs +++ b/src/Auth/LDAP.hs @@ -1,6 +1,7 @@ {-# LANGUAGE RecordWildCards , OverloadedStrings , TemplateHaskell + , ViewPatterns , TypeFamilies , FlexibleContexts , FlexibleInstances @@ -20,6 +21,9 @@ import Import.NoFoundation import Control.Lens import Network.Connection +import Data.CaseInsensitive (CI) +import qualified Data.CaseInsensitive as CI + import qualified Control.Monad.Catch as Exc import Utils.Form @@ -31,7 +35,10 @@ import qualified Data.Text.Encoding as Text import qualified Yesod.Auth.Message as Msg -data CampusLogin = CampusLogin { campusIdent, campusPassword :: Text } +data CampusLogin = CampusLogin + { campusIdent :: CI Text + , campusPassword :: Text + } data CampusMessage = MsgCampusIdentNote | MsgCampusIdent @@ -60,7 +67,7 @@ campusForm :: ( RenderMessage site FormMessage , Show (ButtonCssClass site) ) => AForm (HandlerT site IO) CampusLogin campusForm = CampusLogin - <$> areq textField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote) Nothing + <$> areq ciField (fslpI MsgCampusIdent "user.name@campus.lmu.de" & setTooltip MsgCampusIdentNote) Nothing <*> areq passwordField (fslI MsgCampusPassword) Nothing <* submitButton @@ -79,10 +86,10 @@ campusLogin conf@LdapConf{..} = AuthPlugin{..} ((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard campusForm case loginRes of FormFailure errs -> do - forM_ errs $ addMessage "error" . toHtml + forM_ errs $ addMessage Error . toHtml redirect LoginR FormMissing -> redirect LoginR - FormSuccess CampusLogin{..} -> do + FormSuccess CampusLogin{ campusIdent = CI.original -> campusIdent, ..} -> do ldapResult <- liftIO . Ldap.with ldapHost ldapPort $ \ldap -> do Ldap.bind ldap (Ldap.Dn campusIdent) (Ldap.Password $ Text.encodeUtf8 campusPassword) Ldap.bind ldap ldapDn ldapPassword diff --git a/src/Auth/PWFile.hs b/src/Auth/PWFile.hs deleted file mode 100644 index 541be7718..000000000 --- a/src/Auth/PWFile.hs +++ /dev/null @@ -1,60 +0,0 @@ -{-# LANGUAGE NoImplicitPrelude - , QuasiQuotes - , TemplateHaskell - , ViewPatterns - , RecordWildCards - , OverloadedStrings - , FlexibleContexts - , TypeFamilies - #-} - -module Auth.PWFile - ( maintenanceLogin - ) where - -import Import.NoFoundation -import Database.Persist.Sql (IsSqlBackend) - -import qualified Data.Yaml as Yaml - -import qualified Data.Text.Encoding as Text - -import Yesod.Auth.Util.PasswordStore (verifyPassword) - - -maintenanceLogin :: ( YesodAuth site - , YesodPersist site - , IsSqlBackend (YesodPersistBackend site) - , PersistUniqueWrite (YesodPersistBackend site) - ) => FilePath -> AuthPlugin site -maintenanceLogin fp = AuthPlugin{..} - where - apName = "PWFile" - apLogin = mempty - apDispatch "GET" [] = do - authData <- lookupBasicAuth - pwdata <- liftIO $ Yaml.decodeFileEither fp - - addHeader "WWW-Authenticate" [st|Basic realm="uni2work maintenance auth" charset="UTF-8"|] - - case pwdata of - Left err -> $logDebugS "Auth" $ tshow err - Right pws -> $logDebugS "Auth" $ tshow (length pws) <> " pw entries" - - case (authData, pwdata) of - (Nothing, _) -> do - notAuthenticated - (Just (usr, (Text.encodeUtf8 -> pw)), Right pwdata') - | [ PWEntry{ pwUser = pwUser@(User{..}), pwHash = (Text.encodeUtf8 -> pwHash) } ] - <- [ pwe | pwe@PWEntry{..} <- pwdata' - , let User{..} = pwUser - , userIdent == usr - , userPlugin == apName - ] - , verifyPassword pw pwHash - -> lift $ do - runDB . void $ insertUnique pwUser - setCredsRedirect $ Creds apName userIdent [] - _ -> permissionDenied "Invalid auth" - apDispatch _ _ = notFound - diff --git a/src/Auth/PWHash.hs b/src/Auth/PWHash.hs new file mode 100644 index 000000000..ba7198710 --- /dev/null +++ b/src/Auth/PWHash.hs @@ -0,0 +1,105 @@ +{-# LANGUAGE NoImplicitPrelude + , QuasiQuotes + , TemplateHaskell + , ViewPatterns + , RecordWildCards + , OverloadedStrings + , FlexibleContexts + , TypeFamilies + #-} + +module Auth.PWHash + ( hashLogin + , PWHashMessage(..) + ) where + +import Import.NoFoundation +import Database.Persist.Sql (SqlBackendCanRead) + +import Utils.Form + +import Data.CaseInsensitive (CI) +import qualified Data.CaseInsensitive as CI + +import Yesod.Auth.Util.PasswordStore (verifyPasswordWith) + +import qualified Yesod.Auth.Message as Msg + + +data HashLogin = HashLogin + { hashIdent :: CI Text + , hashPassword :: Text + } + +data PWHashMessage = MsgPWHashIdent + | MsgPWHashPassword + + +hashForm :: ( RenderMessage site FormMessage + , RenderMessage site PWHashMessage + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => AForm (HandlerT site IO) HashLogin +hashForm = HashLogin + <$> areq ciField (fslpI MsgPWHashIdent "Identifikation") Nothing + <*> areq passwordField (fslpI MsgPWHashPassword "Passwort") Nothing + <* submitButton + + +hashLogin :: ( YesodAuth site + , YesodPersist site + , SqlBackendCanRead (YesodPersistBackend site) + , RenderMessage site FormMessage + , RenderMessage site PWHashMessage + , Button site SubmitButton + , Show (ButtonCssClass site) + ) => PWHashAlgorithm -> AuthPlugin site +hashLogin pwHashAlgo = AuthPlugin{..} + where + apName = "PWHash" + apDispatch "POST" [] = do + ((loginRes, _), _) <- lift . runFormPost $ renderAForm FormStandard hashForm + case loginRes of + FormFailure errs -> do + forM_ errs $ addMessage Error . toHtml + redirect LoginR + FormMissing -> redirect LoginR + FormSuccess HashLogin{..} -> do + user <- lift . runDB . getBy $ UniqueAuthentication hashIdent + case user of + Just (Entity _ User{ userAuthentication = AuthPWHash{..}, userIdent = CI.original -> userIdent }) + | verifyPasswordWith pwHashAlgo (2^) (encodeUtf8 hashPassword) (encodeUtf8 authPWHash) -> -- (2^) is magic. + lift . setCredsRedirect $ Creds apName userIdent [] + other -> do + $logDebugS "PWHash" $ tshow other + loginErrorMessageI LoginR Msg.InvalidLogin + -- apDispatch "GET" [] = do + -- authData <- lookupBasicAuth + -- pwdata <- liftIO $ Yaml.decodeFileEither fp + + -- addHeader "WWW-Authenticate" [st|Basic realm="uni2work maintenance auth" charset="UTF-8"|] + + -- case pwdata of + -- Left err -> $logDebugS "Auth" $ tshow err + -- Right pws -> $logDebugS "Auth" $ tshow (length pws) <> " pw entries" + + -- case (authData, pwdata) of + -- (Nothing, _) -> do + -- notAuthenticated + -- (Just (usr, (Text.encodeUtf8 -> pw)), Right pwdata') + -- | [ PWEntry{ pwUser = pwUser@(User{..}), pwHash = (Text.encodeUtf8 -> pwHash) } ] + -- <- [ pwe | pwe@PWEntry{..} <- pwdata' + -- , let User{..} = pwUser + -- , userIdent == usr + -- , userPlugin == apName + -- ] + -- , verifyPassword pw pwHash + -- -> lift $ do + -- runDB . void $ insertUnique pwUser + -- setCredsRedirect $ Creds apName userIdent [] + -- _ -> permissionDenied "Invalid auth" + apDispatch _ _ = notFound + apLogin toMaster = do + (login, loginEnctype) <- handlerToWidget . generateFormPost $ renderAForm FormStandard hashForm + $(widgetFile "widgets/hash-login-form") + diff --git a/src/Foundation.hs b/src/Foundation.hs index 5f78d7f56..105c859ab 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -24,7 +24,8 @@ import Text.Jasmine (minifym) import Yesod.Auth.Message import Yesod.Auth.Dummy import Auth.LDAP -import Auth.PWFile +import Auth.PWHash +import Auth.Dummy import qualified Network.Wai as W (requestMethod, pathInfo) @@ -81,7 +82,7 @@ import Utils import Utils.Form import Utils.Lens -import Data.Aeson +import Data.Aeson hiding (Error) import Data.Aeson.TH import qualified Data.Yaml as Yaml @@ -166,6 +167,8 @@ data MenuTypes -- Semantische Rolle: -- Messages mkMessage "UniWorX" "messages/uniworx" "de" mkMessageVariant "UniWorX" "Campus" "messages/campus" "de" +mkMessageVariant "UniWorX" "Dummy" "messages/dummy" "de" +mkMessageVariant "UniWorX" "PWHash" "messages/pw-hash" "de" -- This instance is required to use forms. You can modify renderMessage to -- achieve customized and internationalized form validation messages. @@ -178,6 +181,12 @@ instance RenderMessage UniWorX TermIdentifier where Winter -> renderMessage' $ MsgWinterTerm year where renderMessage' = renderMessage foundation ls +instance RenderMessage UniWorX StudyFieldType where + renderMessage foundation ls = \case + FieldPrimary -> renderMessage' MsgFieldPrimary + FieldSecondary -> renderMessage' MsgFieldSecondary + where renderMessage' = renderMessage foundation ls + newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier deriving (Eq, Ord, Read, Show) @@ -302,7 +311,7 @@ knownTags = Map.fromList -- should not throw exceptions, i.e. no getBy404 or req [("free", trueAP) ,("deprecated", APHandler $ \r _ -> do $logWarnS "AccessControl" ("deprecated route: " <> tshow r) - addMessageI "error" MsgDeprecatedRoute + addMessageI Error MsgDeprecatedRoute allow <- appAllowDeprecated . appSettings <$> getYesod return $ bool (Unauthorized "Deprecated Route") Authorized allow ) @@ -677,7 +686,7 @@ instance YesodBreadcrumbs UniWorX where breadcrumb TermCurrentR = return ("Aktuell" , Just TermShowR) breadcrumb TermEditR = return ("Neu" , Just TermCurrentR) breadcrumb (TermEditExistR tid) = return ("Editieren" , Just $ TermCourseListR tid) - breadcrumb (TermCourseListR (unTermKey -> tid)) = getMessageRender <&> \mr -> (mr $ ShortTermIdentifier tid, Just TermShowR) + breadcrumb (TermCourseListR (unTermKey -> tid)) = getMessageRender <&> \mr -> (mr $ ShortTermIdentifier tid, Nothing) breadcrumb (TermSchoolCourseListR tid ssh) = return (CI.original $ unSchoolKey ssh, Just $ TermCourseListR tid) @@ -737,7 +746,7 @@ defaultLinks = -- Define the menu items of the header. , menuItemAccessCallback' = return True } , NavbarRight $ MenuItem - { menuItemLabel = "Profile" + { menuItemLabel = "Profil" , menuItemIcon = Just "cogs" , menuItemRoute = ProfileR , menuItemAccessCallback' = isJust <$> maybeAuthPair @@ -844,12 +853,6 @@ pageActions (CourseListR) = ] pageActions (CourseR tid ssh csh CShowR) = [ PageActionPrime $ MenuItem - { menuItemLabel = "Kurs Editieren" - , menuItemIcon = Nothing - , menuItemRoute = CourseR tid ssh csh CEditR - , menuItemAccessCallback' = return True - } - , PageActionPrime $ MenuItem { menuItemLabel = "Übungsblätter" , menuItemIcon = Nothing , menuItemRoute = CourseR tid ssh csh SheetListR @@ -871,12 +874,24 @@ pageActions (CourseR tid ssh csh CShowR) = , menuItemRoute = CourseR tid ssh csh CCorrectionsR , menuItemAccessCallback' = return True } - , PageActionSecondary $ MenuItem + , PageActionPrime $ MenuItem { menuItemLabel = "Neues Übungsblatt anlegen" , menuItemIcon = Nothing , menuItemRoute = CourseR tid ssh csh SheetNewR , menuItemAccessCallback' = return True } + , PageActionSecondary $ MenuItem + { menuItemLabel = "Kurs editieren" + , menuItemIcon = Nothing + , menuItemRoute = CourseR tid ssh csh CEditR + , menuItemAccessCallback' = return True + } + , PageActionSecondary $ MenuItem + { menuItemLabel = "Neuen Kurs klonen" + , menuItemIcon = Nothing + , menuItemRoute = CourseNewTemplateR (Just tid) (Just ssh) (Just csh) + , menuItemAccessCallback' = return True + } ] pageActions (CourseR tid ssh csh SheetListR) = [ PageActionPrime $ MenuItem @@ -1133,20 +1148,16 @@ instance YesodAuth UniWorX where authenticate Creds{..} = runDB $ do let - (userPlugin, userIdent) - | isDummy - , [dummyPlugin, dummyIdent] <- Text.splitOn ":" credsIdent - = (dummyPlugin, dummyIdent) - | otherwise - = (credsPlugin, credsIdent) + userIdent = CI.mk credsIdent + uAuth = UniqueAuthentication userIdent + isDummy = credsPlugin == "dummy" - isPWFile = credsPlugin == "PWFile" - uAuth = UniqueAuthentication userPlugin userIdent + isPWHash = credsPlugin == "PWHash" excHandlers - | isDummy || isPWFile + | isDummy || isPWHash = [ C.Handler $ \err -> do - addMessage "error" (toHtml $ tshow (err :: CampusUserException)) + addMessage Error (toHtml $ tshow (err :: CampusUserException)) $logErrorS "LDAP" $ tshow err acceptExisting ] @@ -1170,7 +1181,7 @@ instance YesodAuth UniWorX where flip catches excHandlers $ case appLdapConf of Just ldapConf -> fmap (either id id) . runExceptT $ do - ldapData <- campusUser ldapConf $ Creds userPlugin userIdent credsExtra + ldapData <- campusUser ldapConf $ Creds credsPlugin (CI.original userIdent) credsExtra $logDebugS "LDAP" $ "Successful LDAP lookup: " <> tshow ldapData let @@ -1178,6 +1189,10 @@ instance YesodAuth UniWorX where userEmail' = lookup (Attr "mail") ldapData userDisplayName' = lookup (Attr "displayName") ldapData userSurname' = lookup (Attr "sn") ldapData + + userAuthentication + | isPWHash = error "PWHash should only work for users that are already known" + | otherwise = AuthLDAP userEmail <- if | Just [bs] <- userEmail' @@ -1250,8 +1265,8 @@ instance YesodAuth UniWorX where authPlugins (appSettings -> AppSettings{..}) = catMaybes [ campusLogin <$> appLdapConf - , maintenanceLogin <$> appAuthPWFile - , authDummy <$ guard appAuthDummyLogin + , Just . hashLogin $ pwHashAlgorithm appAuthPWHash + , dummyLogin <$ guard appAuthDummyLogin ] authHttpManager = getHttpManager diff --git a/src/Handler/Admin.hs b/src/Handler/Admin.hs index d0682f855..156961629 100644 --- a/src/Handler/Admin.hs +++ b/src/Handler/Admin.hs @@ -53,9 +53,9 @@ postAdminTestR :: Handler Html postAdminTestR = do ((btnResult,_), _) <- runFormPost $ buttonForm case btnResult of - (FormSuccess CreateInf) -> setMessage "Informatik-Knopf gedrückt" - (FormSuccess CreateMath) -> addMessage "warning" "Knopf Mathematik erkannt" - _other -> return () + (FormSuccess CreateInf) -> addMessage Info "Informatik-Knopf gedrückt" + (FormSuccess CreateMath) -> addMessage Warning "Knopf Mathematik erkannt" + _other -> addMessage Warning "KEIN Knopf erkannt" getAdminTestR @@ -66,6 +66,6 @@ getAdminUserR uuid = do defaultLayout $ [whamlet|