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
|
inhibit: 3600 # 60m
|
||||||
steps: 20
|
steps: 20
|
||||||
max-speedup: 3
|
max-speedup: 3
|
||||||
|
|
||||||
|
bot-mitigations:
|
||||||
|
- only-logged-in-table-sorting
|
||||||
|
|||||||
@ -993,6 +993,11 @@ th, td
|
|||||||
padding-right: 24px
|
padding-right: 24px
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
|
.table__th.presorted,
|
||||||
|
.table__th.presorted
|
||||||
|
position: relative
|
||||||
|
padding-right: 24px
|
||||||
|
|
||||||
.table__th.sortable::after,
|
.table__th.sortable::after,
|
||||||
.table__th.sortable::before
|
.table__th.sortable::before
|
||||||
content: ''
|
content: ''
|
||||||
@ -1022,6 +1027,28 @@ th, td
|
|||||||
.table__th.sorted-desc::after
|
.table__th.sorted-desc::after
|
||||||
border-bottom-color: white !important
|
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
|
\:root
|
||||||
--color-grey-light: #efefef
|
--color-grey-light: #efefef
|
||||||
--color-grey-lighter: #f5f5f5
|
--color-grey-lighter: #f5f5f5
|
||||||
@ -1044,6 +1071,22 @@ th, td
|
|||||||
border-right: 8px solid transparent
|
border-right: 8px solid transparent
|
||||||
border-bottom: 8px solid rgba(255, 255, 255, 0.4)
|
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
|
.course-header::before
|
||||||
// magic numbers to move arrow back in the right position after flipping it.
|
// magic numbers to move arrow back in the right position after flipping it.
|
||||||
this allows us to use the same border for the up and the down arrow
|
this allows us to use the same border for the up and the down arrow
|
||||||
@ -1053,6 +1096,12 @@ th, td
|
|||||||
.course-header::after
|
.course-header::after
|
||||||
transform: translateY(-150%)
|
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::before,
|
||||||
.course-header:hover::after
|
.course-header:hover::after
|
||||||
border-bottom-color: rgba(255, 255, 255, 0.7)
|
border-bottom-color: rgba(255, 255, 255, 0.7)
|
||||||
|
|||||||
@ -355,6 +355,9 @@ export class AsyncTable {
|
|||||||
_linkClickHandler = (event) => {
|
_linkClickHandler = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let url = this._getClickDestination(event.target);
|
let url = this._getClickDestination(event.target);
|
||||||
|
if (!url)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!url.match(/^http/)) {
|
if (!url.match(/^http/)) {
|
||||||
url = window.location.origin + window.location.pathname + url;
|
url = window.location.origin + window.location.pathname + url;
|
||||||
}
|
}
|
||||||
@ -363,7 +366,7 @@ export class AsyncTable {
|
|||||||
|
|
||||||
_getClickDestination(el) {
|
_getClickDestination(el) {
|
||||||
if (!el.matches('a') && !el.querySelector('a')) {
|
if (!el.matches('a') && !el.querySelector('a')) {
|
||||||
return '';
|
return null;
|
||||||
}
|
}
|
||||||
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
|
return el.getAttribute('href') || el.querySelector('a').getAttribute('href');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -216,7 +216,10 @@
|
|||||||
line-height: 1.4
|
line-height: 1.4
|
||||||
max-width: 85vw
|
max-width: 85vw
|
||||||
|
|
||||||
.course-header
|
.explanation
|
||||||
|
clear: both
|
||||||
|
|
||||||
|
.course-header, .inactive-course-header
|
||||||
float: left
|
float: left
|
||||||
background-color: var(--color-dark)
|
background-color: var(--color-dark)
|
||||||
position: relative
|
position: relative
|
||||||
|
|||||||
@ -3182,3 +3182,5 @@ WGFTextInput: Textfeld
|
|||||||
WGFFileUpload: Dateifeld
|
WGFFileUpload: Dateifeld
|
||||||
WorkflowGraphFormUploadIsDirectory: Upload ist Verzeichnis
|
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
|
WGFFileUpload: File field
|
||||||
WorkflowGraphFormUploadIsDirectory: Upload is a directory
|
WorkflowGraphFormUploadIsDirectory: Upload is a directory
|
||||||
WorkflowGraphFormInvalidNumberOfFiles: You need to upload exactly one file
|
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.
|
-- | 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 :: 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
|
dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> dbtIdent), dbtStyle = DBStyle{..}, .. } = do
|
||||||
|
doSorting <- or2M
|
||||||
|
(getsYesod . views _appBotMitigations $ Set.notMember SettingBotMitigationOnlyLoggedInTableSorting)
|
||||||
|
(is _Just <$> maybeAuthId)
|
||||||
|
|
||||||
let
|
let
|
||||||
sortingOptions = mkOptionList
|
sortingOptions = mkOptionList
|
||||||
[ Option t' (SortingSetting t d) t'
|
[ Option t' (SortingSetting t d) t'
|
||||||
@ -951,6 +955,7 @@ dbTable PSValidator{..} dbtable@DBTable{ dbtIdent = dbtIdent'@(toPathPiece -> db
|
|||||||
psShortcircuit <- (== Just dbtIdent') <$> lookupCustomHeader HeaderDBTableShortcircuit
|
psShortcircuit <- (== Just dbtIdent') <$> lookupCustomHeader HeaderDBTableShortcircuit
|
||||||
|
|
||||||
let
|
let
|
||||||
|
-- adjustPI = over _piSorting $ guardOnM doSorting -- probably not neccessary; not displaying the links should be enough for now
|
||||||
((errs, PaginationSettings{..}), paginationInput@PaginationInput{..})
|
((errs, PaginationSettings{..}), paginationInput@PaginationInput{..})
|
||||||
| FormSuccess pi <- piResult
|
| FormSuccess pi <- piResult
|
||||||
, not $ piIsUnset pi
|
, not $ piIsUnset pi
|
||||||
|
|||||||
@ -77,6 +77,8 @@ import Data.Conduit.Algorithms.FastCDC
|
|||||||
|
|
||||||
import Utils.Lens.TH
|
import Utils.Lens.TH
|
||||||
|
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
|
||||||
|
|
||||||
-- | Runtime settings to configure this application. These settings can be
|
-- | Runtime settings to configure this application. These settings can be
|
||||||
-- loaded from various sources: defaults, environment variables, config files,
|
-- loaded from various sources: defaults, environment variables, config files,
|
||||||
@ -217,6 +219,8 @@ data AppSettings = AppSettings
|
|||||||
|
|
||||||
, appFileSourceARCConf :: Maybe (ARCConf Int)
|
, appFileSourceARCConf :: Maybe (ARCConf Int)
|
||||||
, appFileSourcePrewarmConf :: Maybe PrewarmCacheConf
|
, appFileSourcePrewarmConf :: Maybe PrewarmCacheConf
|
||||||
|
|
||||||
|
, appBotMitigations :: Set SettingBotMitigation
|
||||||
} deriving Show
|
} deriving Show
|
||||||
|
|
||||||
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
|
data JobMode = JobsLocal { jobsAcceptOffload :: Bool }
|
||||||
@ -352,6 +356,11 @@ data PrewarmCacheConf = PrewarmCacheConf
|
|||||||
, precMaxSpeedup :: Rational
|
, precMaxSpeedup :: Rational
|
||||||
} deriving (Eq, Ord, Read, Show, Generic, Typeable)
|
} 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
|
nullaryPathPiece ''ApprootScope $ camelToPathPiece' 1
|
||||||
pathPieceJSON ''ApprootScope
|
pathPieceJSON ''ApprootScope
|
||||||
pathPieceJSONKey ''ApprootScope
|
pathPieceJSONKey ''ApprootScope
|
||||||
@ -388,6 +397,10 @@ deriveJSON defaultOptions
|
|||||||
|
|
||||||
makeLenses_ ''PrewarmCacheConf
|
makeLenses_ ''PrewarmCacheConf
|
||||||
|
|
||||||
|
nullaryPathPiece ''SettingBotMitigation $ camelToPathPiece' 3
|
||||||
|
pathPieceJSON ''SettingBotMitigation
|
||||||
|
pathPieceJSONKey ''SettingBotMitigation
|
||||||
|
|
||||||
instance FromJSON LdapConf where
|
instance FromJSON LdapConf where
|
||||||
parseJSON = withObject "LdapConf" $ \o -> do
|
parseJSON = withObject "LdapConf" $ \o -> do
|
||||||
ldapTls <- o .:? "tls"
|
ldapTls <- o .:? "tls"
|
||||||
@ -660,6 +673,8 @@ instance FromJSON AppSettings where
|
|||||||
]
|
]
|
||||||
appFileSourcePrewarmConf <- over (_Just . _precInhibit) (max 0) . assertM isValidPrewarmConf <$> o .:? "file-source-prewarm"
|
appFileSourcePrewarmConf <- over (_Just . _precInhibit) (max 0) . assertM isValidPrewarmConf <$> o .:? "file-source-prewarm"
|
||||||
|
|
||||||
|
appBotMitigations <- o .:? "bot-mitigations" .!= Set.empty
|
||||||
|
|
||||||
return AppSettings{..}
|
return AppSettings{..}
|
||||||
|
|
||||||
makeClassy_ ''AppSettings
|
makeClassy_ ''AppSettings
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
$newline never
|
$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}>
|
<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}>
|
||||||
$maybe flag <- sortableKey
|
$if doSorting
|
||||||
$case directions
|
$maybe flag <- sortableKey
|
||||||
$of [SortAsc]
|
$case directions
|
||||||
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting')) . substPi}>
|
$of [SortAsc]
|
||||||
^{widget}
|
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting')) . substPi}>
|
||||||
$of _
|
^{widget}
|
||||||
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting')) . substPi}>
|
$of _
|
||||||
^{widget}
|
<a .table__th-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting')) . substPi}>
|
||||||
$nothing
|
^{widget}
|
||||||
|
$nothing
|
||||||
|
^{widget}
|
||||||
|
$else
|
||||||
^{widget}
|
^{widget}
|
||||||
|
|||||||
@ -3,6 +3,9 @@ $newline never
|
|||||||
$maybe wdgt <- wHeaders
|
$maybe wdgt <- wHeaders
|
||||||
<div .course-teaser-header>
|
<div .course-teaser-header>
|
||||||
^{wdgt}
|
^{wdgt}
|
||||||
|
$if not doSorting
|
||||||
|
<p .explanation>
|
||||||
|
_{MsgCourseSortingOnlyLoggedIn}
|
||||||
$if null wRows && (dbsEmptyStyle == DBESHeading)
|
$if null wRows && (dbsEmptyStyle == DBESHeading)
|
||||||
<p>_{dbsEmptyMessage}
|
<p>_{dbsEmptyMessage}
|
||||||
$else
|
$else
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
$newline never
|
$newline never
|
||||||
|
|
||||||
$maybe flag <- sortableKey
|
$maybe flag <- sortableKey
|
||||||
<span .course-header *{attrs} :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc>
|
$if doSorting
|
||||||
$case directions
|
<span .course-header *{attrs} :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc>
|
||||||
$of [SortAsc]
|
$case directions
|
||||||
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting'))}>
|
$of [SortAsc]
|
||||||
^{widget}
|
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortDesc : piSorting'))}>
|
||||||
$of _
|
^{widget}
|
||||||
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting'))}>
|
$of _
|
||||||
^{widget}
|
<a .course-header-link rel=nofollow href=^{tblLink' $ setParams (wIdent "sorting") (map toPathPiece (SortingSetting flag SortAsc : piSorting'))}>
|
||||||
$nothing
|
^{widget}
|
||||||
|
$else
|
||||||
|
<span .inactive-course-header *{attrs} :isSorted SortAsc:.sorted-asc :isSorted SortDesc:.sorted-desc>
|
||||||
|
^{widget}
|
||||||
|
|||||||
Reference in New Issue
Block a user