feat(bot-mitigations): only logged in table sorting
This commit is contained in:
parent
8b532a2b3b
commit
fb6ae089c6
@ -281,3 +281,6 @@ file-source-prewarm:
|
||||
inhibit: 3600 # 60m
|
||||
steps: 20
|
||||
max-speedup: 3
|
||||
|
||||
bot-mitigations:
|
||||
- only-logged-in-table-sorting
|
||||
|
||||
@ -993,6 +993,11 @@ th, td
|
||||
padding-right: 24px
|
||||
cursor: pointer
|
||||
|
||||
.table__th.presorted,
|
||||
.table__th.presorted
|
||||
position: relative
|
||||
padding-right: 24px
|
||||
|
||||
.table__th.sortable::after,
|
||||
.table__th.sortable::before
|
||||
content: ''
|
||||
@ -1022,6 +1027,28 @@ th, td
|
||||
.table__th.sorted-desc::after
|
||||
border-bottom-color: white !important
|
||||
|
||||
.table__th.presorted::before,
|
||||
.table__th.presorted::after
|
||||
content: ''
|
||||
position: absolute
|
||||
top: 50%
|
||||
right: 4px
|
||||
width: 0
|
||||
height: 0
|
||||
border-left: 8px solid transparent
|
||||
border-right: 8px solid transparent
|
||||
border-bottom: 8px solid rgba(255, 255, 255, 0.2)
|
||||
|
||||
.table__th.presorted.sorted-asc::before,
|
||||
.table__th.presorted.sorted-desc::after
|
||||
border-bottom-color: white !important
|
||||
|
||||
.table__th.presorted::before
|
||||
transform: translateY(150%) scale(1, -1)
|
||||
transform-origin: top
|
||||
.table__th.presorted::after
|
||||
transform: translateY(-150%)
|
||||
|
||||
\:root
|
||||
--color-grey-light: #efefef
|
||||
--color-grey-lighter: #f5f5f5
|
||||
@ -1043,6 +1070,22 @@ th, td
|
||||
border-left: 8px solid transparent
|
||||
border-right: 8px solid transparent
|
||||
border-bottom: 8px solid rgba(255, 255, 255, 0.4)
|
||||
|
||||
.inactive-course-header::before,
|
||||
.inactive-course-header::after
|
||||
content: ''
|
||||
position: absolute
|
||||
right: 10px
|
||||
top: 20px
|
||||
width: 0
|
||||
height: 0
|
||||
border-left: 8px solid transparent
|
||||
border-right: 8px solid transparent
|
||||
border-bottom: 8px solid rgba(255,255,255, 0.2)
|
||||
|
||||
.inactive-course-header.sorted-asc::before,
|
||||
.inactive-course-header.sorted-desc::after
|
||||
border-bottom-color: white !important
|
||||
|
||||
.course-header::before
|
||||
// magic numbers to move arrow back in the right position after flipping it.
|
||||
@ -1053,6 +1096,12 @@ th, td
|
||||
.course-header::after
|
||||
transform: translateY(-150%)
|
||||
|
||||
.inactive-course-header::before
|
||||
transform: translateY(150%) scale(1, -1)
|
||||
transform-origin: top
|
||||
.inactive-course-header::after
|
||||
transform: translateY(-150%)
|
||||
|
||||
.course-header:hover::before,
|
||||
.course-header:hover::after
|
||||
border-bottom-color: rgba(255, 255, 255, 0.7)
|
||||
|
||||
@ -355,6 +355,9 @@ export class AsyncTable {
|
||||
_linkClickHandler = (event) => {
|
||||
event.preventDefault();
|
||||
let url = this._getClickDestination(event.target);
|
||||
if (!url)
|
||||
return;
|
||||
|
||||
if (!url.match(/^http/)) {
|
||||
url = window.location.origin + window.location.pathname + url;
|
||||
}
|
||||
@ -363,7 +366,7 @@ export class AsyncTable {
|
||||
|
||||
_getClickDestination(el) {
|
||||
if (!el.matches('a') && !el.querySelector('a')) {
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
|
||||
}
|
||||
|
||||
@ -216,7 +216,10 @@
|
||||
line-height: 1.4
|
||||
max-width: 85vw
|
||||
|
||||
.course-header
|
||||
.explanation
|
||||
clear: both
|
||||
|
||||
.course-header, .inactive-course-header
|
||||
float: left
|
||||
background-color: var(--color-dark)
|
||||
position: relative
|
||||
|
||||
@ -3181,4 +3181,6 @@ UrlFieldCouldNotParseAbsolute: Konnte nicht als absolute URL interpretiert werde
|
||||
WGFTextInput: Textfeld
|
||||
WGFFileUpload: Dateifeld
|
||||
WorkflowGraphFormUploadIsDirectory: Upload ist Verzeichnis
|
||||
WorkflowGraphFormInvalidNumberOfFiles: Es muss genau eine Datei hochgeladen werden
|
||||
WorkflowGraphFormInvalidNumberOfFiles: Es muss genau eine Datei hochgeladen werden
|
||||
|
||||
CourseSortingOnlyLoggedIn: Das Benutzerinterface zur Sortierung dieser Tabelle ist nur für eingeloggte Benutzer aktiv
|
||||
@ -3182,3 +3182,5 @@ WGFTextInput: Text field
|
||||
WGFFileUpload: File field
|
||||
WorkflowGraphFormUploadIsDirectory: Upload is a directory
|
||||
WorkflowGraphFormInvalidNumberOfFiles: You need to upload exactly one file
|
||||
|
||||
CourseSortingOnlyLoggedIn: The user interface for sorting this table is only active for logged in users
|
||||
|
||||
@ -897,6 +897,10 @@ instance IsDBTable m a => IsString (DBCell m a) where
|
||||
-- | DB-backed tables with pagination, may short-circuit a handler if the frontend only asks for the table content, i.e. handler actions after calls to dbTable may not happen at all.
|
||||
dbTable :: forall m x. IsDBTable m x => PSValidator m x -> DBTable m x -> DB (DBResult m x)
|
||||
dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> dbtIdent), dbtStyle = DBStyle{..}, .. } = do
|
||||
doSorting <- or2M
|
||||
(getsYesod . views _appBotMitigations $ Set.notMember SettingBotMitigationOnlyLoggedInTableSorting)
|
||||
(is _Just <$> maybeAuthId)
|
||||
|
||||
let
|
||||
sortingOptions = mkOptionList
|
||||
[ Option t' (SortingSetting t d) t'
|
||||
@ -951,6 +955,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
||||
psShortcircuit <- (== Just dbtIdent') <$> lookupCustomHeader HeaderDBTableShortcircuit
|
||||
|
||||
let
|
||||
-- adjustPI = over _piSorting $ guardOnM doSorting -- probably not neccessary; not displaying the links should be enough for now
|
||||
((errs, PaginationSettings{..}), paginationInput@PaginationInput{..})
|
||||
| FormSuccess pi <- piResult
|
||||
, not $ piIsUnset pi
|
||||
|
||||
@ -77,6 +77,8 @@ import Data.Conduit.Algorithms.FastCDC
|
||||
|
||||
import Utils.Lens.TH
|
||||
|
||||
import qualified Data.Set as Set
|
||||
|
||||
|
||||
-- | Runtime settings to configure this application. These settings can be
|
||||
-- loaded from various sources: defaults, environment variables, config files,
|
||||
@ -217,6 +219,8 @@ data AppSettings = AppSettings
|
||||
|
||||
, appFileSourceARCConf :: Maybe (ARCConf Int)
|
||||
, appFileSourcePrewarmConf :: Maybe PrewarmCacheConf
|
||||
|
||||
, appBotMitigations :: Set SettingBotMitigation
|
||||
} deriving Show
|
||||
|
||||
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
|
||||
@ -352,6 +356,11 @@ data PrewarmCacheConf = PrewarmCacheConf
|
||||
, precMaxSpeedup :: Rational
|
||||
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
||||
|
||||
data SettingBotMitigation
|
||||
= SettingBotMitigationOnlyLoggedInTableSorting
|
||||
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
|
||||
deriving anyclass (Universe, Finite)
|
||||
|
||||
nullaryPathPiece ''ApprootScope $ camelToPathPiece' 1
|
||||
pathPieceJSON ''ApprootScope
|
||||
pathPieceJSONKey ''ApprootScope
|
||||
@ -388,6 +397,10 @@ deriveJSON defaultOptions
|
||||
|
||||
makeLenses_ ''PrewarmCacheConf
|
||||
|
||||
nullaryPathPiece ''SettingBotMitigation $ camelToPathPiece' 3
|
||||
pathPieceJSON ''SettingBotMitigation
|
||||
pathPieceJSONKey ''SettingBotMitigation
|
||||
|
||||
instance FromJSON LdapConf where
|
||||
parseJSON = withObject "LdapConf" $ \o -> do
|
||||
ldapTls <- o .:? "tls"
|
||||
@ -660,6 +673,8 @@ instance FromJSON AppSettings where
|
||||
]
|
||||
appFileSourcePrewarmConf <- over (_Just . _precInhibit) (max 0) . assertM isValidPrewarmConf <$> o .:? "file-source-prewarm"
|
||||
|
||||
appBotMitigations <- o .:? "bot-mitigations" .!= Set.empty
|
||||
|
||||
return AppSettings{..}
|
||||
|
||||
makeClassy_ ''AppSettings
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
$newline never
|
||||
<th .table__th *{attrs} :isSortable:.sortable :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc :is _Just sortableKey:uw-hide-column-header=#{maybe "" toPathPiece sortableKey} :cellSize /= 1:colspan=#{cellSize}>
|
||||
$maybe flag <- sortableKey
|
||||
$case directions
|
||||
$of [SortAsc]
|
||||
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting')) . substPi}>
|
||||
^{widget}
|
||||
$of _
|
||||
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting')) . substPi}>
|
||||
^{widget}
|
||||
$nothing
|
||||
<th .table__th *{attrs} :isSortable && doSorting:.sortable :isSortable && not doSorting:.presorted :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc :is _Just sortableKey:uw-hide-column-header=#{maybe "" toPathPiece sortableKey} :cellSize /= 1:colspan=#{cellSize}>
|
||||
$if doSorting
|
||||
$maybe flag <- sortableKey
|
||||
$case directions
|
||||
$of [SortAsc]
|
||||
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting')) . substPi}>
|
||||
^{widget}
|
||||
$of _
|
||||
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting')) . substPi}>
|
||||
^{widget}
|
||||
$nothing
|
||||
^{widget}
|
||||
$else
|
||||
^{widget}
|
||||
|
||||
@ -3,6 +3,9 @@ $newline never
|
||||
$maybe wdgt <- wHeaders
|
||||
<div .course-teaser-header>
|
||||
^{wdgt}
|
||||
$if not doSorting
|
||||
<p .explanation>
|
||||
_{MsgCourseSortingOnlyLoggedIn}
|
||||
$if null wRows && (dbsEmptyStyle == DBESHeading)
|
||||
<p>_{dbsEmptyMessage}
|
||||
$else
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
$newline never
|
||||
|
||||
$maybe flag <- sortableKey
|
||||
<span .course-header *{attrs} :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc>
|
||||
$case directions
|
||||
$of [SortAsc]
|
||||
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting'))}>
|
||||
^{widget}
|
||||
$of _
|
||||
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting'))}>
|
||||
^{widget}
|
||||
$nothing
|
||||
$if doSorting
|
||||
<span .course-header *{attrs} :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc>
|
||||
$case directions
|
||||
$of [SortAsc]
|
||||
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting'))}>
|
||||
^{widget}
|
||||
$of _
|
||||
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting'))}>
|
||||
^{widget}
|
||||
$else
|
||||
<span .inactive-course-header *{attrs} :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc>
|
||||
^{widget}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user