safer mod notes

This commit is contained in:
Thibault Duplessis 2020-03-03 15:22:48 -06:00
parent ee2d375985
commit 2563a13e98
14 changed files with 66 additions and 73 deletions

View file

@ -404,7 +404,10 @@ final class User(
_ ?? { user => _ ?? { user =>
env.user.forms.note.bindFromRequest.fold( env.user.forms.note.bindFromRequest.fold(
e => err(e)(user), e => err(e)(user),
data => env.user.noteApi.write(user, data.text, me, data.mod && isGranted(_.ModNote, me)) inject suc data => {
val isMod = data.mod && isGranted(_.ModNote, me)
env.user.noteApi.write(user, data.text, me, isMod, isMod && ~data.dox)
} inject suc
) )
} }
} }

View file

@ -99,22 +99,26 @@ trait FormHelper { self: I18nHelper =>
)( )(
div( div(
span(cls := "form-check-input")( span(cls := "form-check-input")(
st.input( cmnToggle(id(field), field.name, field.value.has("true"), disabled)
st.id := id(field),
name := field.name,
value := "true",
tpe := "checkbox",
cls := "form-control cmn-toggle",
field.value.has("true") option checked,
disabled option st.disabled
),
label(`for` := id(field))
), ),
groupLabel(field)(labelContent) groupLabel(field)(labelContent)
), ),
help map { helper(_) } help map { helper(_) }
) )
def cmnToggle(fieldId: String, fieldName: String, checked: Boolean, disabled: Boolean = false, value: String = "true") = frag(
st.input(
st.id := fieldId,
name := fieldName,
st.value := value,
tpe := "checkbox",
cls := "form-control cmn-toggle",
checked option st.checked,
disabled option st.disabled
),
label(`for` := fieldId)
)
def select( def select(
field: Field, field: Field,
options: Iterable[(Any, String)], options: Iterable[(Any, String)],

View file

@ -206,8 +206,7 @@ object inquiry {
div(cls := "actions close")( div(cls := "actions close")(
span(cls := "switcher", title := "Automatically open next report")( span(cls := "switcher", title := "Automatically open next report")(
span(cls := "switch")( span(cls := "switch")(
input(id := "auto-next", cls := "cmn-toggle", tpe := "checkbox", checked), form3.cmnToggle("auto-next", "auto-next", true)
label(`for` := "auto-next")
) )
), ),
postForm( postForm(

View file

@ -41,17 +41,7 @@ object permissions {
s"Granted by package: $p" s"Granted by package: $p"
} }
})( })(
span( span(form3.cmnToggle(id, "permissions[]", checked = u.roles.contains(perm.dbKey), value = perm.dbKey)),
input(
st.id := id,
cls := "cmn-toggle",
tpe := "checkbox",
name := "permissions[]",
value := perm.dbKey,
u.roles.contains(perm.dbKey) option checked
),
label(`for` := id)
),
label(`for` := id)(perm.name) label(`for` := id)(perm.name)
) )
} }

View file

@ -39,17 +39,7 @@ object create {
} }
val id = s"oauth-scope-${scope.key.replace(":", "_")}" val id = s"oauth-scope-${scope.key.replace(":", "_")}"
div( div(
span( span(form3.cmnToggle(id, s"${form("scopes").name}[]", value = scope.key, checked = false, disabled = disabled)),
input(
st.id := id,
cls := "cmn-toggle",
tpe := "checkbox",
name := s"${form("scopes").name}[]",
value := scope.key,
disabled option st.disabled
),
label(`for` := id)
),
label(`for` := id, st.title := disabled.option("You already have played games!"))(scope.name) label(`for` := id, st.title := disabled.option("You already have played games!"))(scope.name)
) )
} }

View file

@ -92,10 +92,7 @@ object bits {
"round-toggle-autoswitch" |> { id => "round-toggle-autoswitch" |> { id =>
span(cls := "move-on switcher", st.title := trans.automaticallyProceedToNextGameAfterMoving.txt())( span(cls := "move-on switcher", st.title := trans.automaticallyProceedToNextGameAfterMoving.txt())(
label(`for` := id)(trans.autoSwitch()), label(`for` := id)(trans.autoSwitch()),
span(cls := "switch")( span(cls := "switch")(form3.cmnToggle(id, id, false))
input(st.id := id, cls := "cmn-toggle", tpe := "checkbox"),
label(`for` := id)
)
) )
} }
), ),

View file

@ -175,15 +175,7 @@ private object bits {
) )
), ),
td(cls := "single")( td(cls := "single")(
st.input( form3.cmnToggle(form3.id(field), field.name, checked = field.value.has("1"), value = "1")
tpe := "checkbox",
cls := "cmn-toggle",
id := form3.id(field),
name := field.name,
value := "1",
field.value.has("1") option checked
),
label(`for` := form3.id(field))
) )
) )
} }

View file

@ -508,7 +508,7 @@ object mod {
othersWithEmail.others.map { othersWithEmail.others.map {
case lila.security.UserSpy.OtherUser(o, byIp, byFp) => case lila.security.UserSpy.OtherUser(o, byIp, byFp) =>
val dox = isGranted(_.Doxing) || (o.lameOrAlt && !o.hasTitle) val dox = isGranted(_.Doxing) || (o.lameOrAlt && !o.hasTitle)
val myNotes = notes.filter(_.to == o.id) val userNotes = notes.filter(n => n.to == o.id && (ctx.me.exists(n.isFrom) || isGranted(_.Doxing)))
tr(o == u option (cls := "same"))( tr(o == u option (cls := "same"))(
if (dox || o == u) td(dataSort := o.id)(userLink(o, withBestRating = true, params = "?mod")) if (dox || o == u) td(dataSort := o.id)(userLink(o, withBestRating = true, params = "?mod"))
else td, else td,
@ -527,14 +527,14 @@ object mod {
markTd(o.marks.ipban ?? 1, ipban(cls := "is-red")), markTd(o.marks.ipban ?? 1, ipban(cls := "is-red")),
markTd(o.disabled ?? 1, closed), markTd(o.disabled ?? 1, closed),
markTd(o.marks.reportban ?? 1, reportban), markTd(o.marks.reportban ?? 1, reportban),
myNotes.nonEmpty option { userNotes.nonEmpty option {
td(dataSort := myNotes.size)( td(dataSort := userNotes.size)(
a(href := s"${routes.User.show(o.username)}?notes")( a(href := s"${routes.User.show(o.username)}?notes")(
notesText( notesText(
title := s"Notes from ${myNotes.map(_.from).map(usernameOrId).mkString(", ")}", title := s"Notes from ${userNotes.map(_.from).map(usernameOrId).mkString(", ")}",
cls := "is-green" cls := "is-green"
), ),
myNotes.size userNotes.size
) )
) )
} getOrElse td(dataSort := 0), } getOrElse td(dataSort := 0),

View file

@ -128,29 +128,32 @@ object header {
postForm(action := s"${routes.User.writeNote(u.username)}?note")( postForm(action := s"${routes.User.writeNote(u.username)}?note")(
textarea( textarea(
name := "text", name := "text",
placeholder := "Write a note about this user only you and your friends can read" placeholder := "Write a private note about this user"
), ),
submitButton(cls := "button")(trans.send()), if (isGranted(_.ModNote)) div(cls := "mod-note")(
if (isGranted(_.ModNote)) submitButton(cls := "button")(trans.send()),
label(style := "margin-left: 1em;")( div(
input( div(form3.cmnToggle("note-mod", "mod", true)),
tpe := "checkbox", label(`for` := "note-mod")("For moderators only")
name := "mod", ),
checked, isGranted(_.Doxing) option div(
value := "true", div(form3.cmnToggle("note-dox", "dox", false)),
style := "vertical-align: middle;" label(`for` := "note-dox")("Doxing info")
),
"For moderators only"
) )
else input(tpe := "hidden", name := "mod", value := "false") )
else frag(
input(tpe := "hidden", name := "mod", value := "false"),
submitButton(cls := "button")(trans.send()),
)
), ),
social.notes.isEmpty option div("No note yet"), social.notes.isEmpty option div("No note yet"),
social.notes.map { note => social.notes.filter(n => ctx.me.exists(n.isFrom) || isGranted(_.Doxing)).map { note =>
div(cls := "note")( div(cls := "note")(
p(cls := "note__text")(richText(note.text)), p(cls := "note__text")(richText(note.text)),
p(cls := "note__meta")( p(cls := "note__meta")(
userIdLink(note.from.some), userIdLink(note.from.some),
br, br,
note.dox option "dox ",
momentFromNow(note.date), momentFromNow(note.date),
(ctx.me.exists(note.isFrom) && !note.mod) option frag( (ctx.me.exists(note.isFrom) && !note.mod) option frag(
br, br,

View file

@ -83,7 +83,8 @@ object Permission {
SeeInsight, SeeInsight,
UserSearch, UserSearch,
RemoveRanking, RemoveRanking,
ModMessage ModMessage,
ModNote
), ),
"Hunter" "Hunter"
) )
@ -92,7 +93,6 @@ object Permission {
extends Permission( extends Permission(
"DOXING", "DOXING",
List( List(
ModNote,
ViewIpPrint ViewIpPrint
), ),
"Doxing" "Doxing"

View file

@ -11,11 +11,12 @@ final class DataForm(authenticator: Authenticator) {
val note = Form( val note = Form(
mapping( mapping(
"text" -> text(minLength = 3, maxLength = 2000), "text" -> text(minLength = 3, maxLength = 2000),
"mod" -> boolean "mod" -> boolean,
"dox" -> optional(boolean)
)(NoteData.apply)(NoteData.unapply) )(NoteData.apply)(NoteData.unapply)
) )
case class NoteData(text: String, mod: Boolean) case class NoteData(text: String, mod: Boolean, dox: Option[Boolean])
def username(user: User): Form[String] = def username(user: User): Form[String] =
Form( Form(

View file

@ -9,6 +9,7 @@ case class Note(
to: User.ID, to: User.ID,
text: String, text: String,
mod: Boolean, mod: Boolean,
dox: Boolean,
date: DateTime date: DateTime
) { ) {
def userIds = List(from, to) def userIds = List(from, to)
@ -50,7 +51,7 @@ final class NoteApi(
.sort($sort desc "date") .sort($sort desc "date")
.list[Note](50) .list[Note](50)
def write(to: User, text: String, from: User, modOnly: Boolean) = { def write(to: User, text: String, from: User, modOnly: Boolean, dox: Boolean) = {
val note = Note( val note = Note(
_id = ornicar.scalalib.Random nextString 8, _id = ornicar.scalalib.Random nextString 8,
@ -58,6 +59,7 @@ final class NoteApi(
to = to.id, to = to.id,
text = text, text = text,
mod = modOnly, mod = modOnly,
dox = modOnly && dox,
date = DateTime.now date = DateTime.now
) )
@ -85,7 +87,7 @@ final class NoteApi(
def lichessWrite(to: User, text: String) = def lichessWrite(to: User, text: String) =
userRepo.lichess flatMap { userRepo.lichess flatMap {
_ ?? { _ ?? {
write(to, text, _, true) write(to, text, _, true, false)
} }
} }

View file

@ -4,6 +4,7 @@
@import '../../../common/css/component/hover-text'; @import '../../../common/css/component/hover-text';
@import '../../../common/css/component/crosstable'; @import '../../../common/css/component/crosstable';
@import '../../../common/css/component/flash'; @import '../../../common/css/component/flash';
@import '../../../common/css/form/cmn-toggle';
@import '../../../common/css/base/scrollbar'; @import '../../../common/css/base/scrollbar';
@import '../../../game/css/row'; @import '../../../game/css/row';
@import '../user/show'; @import '../user/show';

View file

@ -21,4 +21,15 @@
min-height: 2.7em; min-height: 2.7em;
} }
} }
.mod-note {
@extend %flex-center;
> div {
@extend %flex-center;
margin-left: 1.5em;
> label {
margin-left: .5em;
cursor: pointer;
}
}
}
} }