summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2019-10-02 14:21:12 +0200
committerYorhel <git@yorhel.nl>2019-10-02 14:21:15 +0200
commit7d8274f0332b1c92825988c6393037bf7eb12af3 (patch)
treecdcc33c92f764b3199fffeef990d71e6085dff06
parent1a9a4b4bdb8f3b6d7d0ad12032c17c44a8287a09 (diff)
v2rw: Convert user preferences form
And add a small 'formField' function to shrink the Elm form generation code a bit.
-rw-r--r--Makefile1
-rw-r--r--elm/DocEdit.elm43
-rw-r--r--elm/Lib/Api.elm1
-rw-r--r--elm/Lib/Html.elm27
-rw-r--r--elm/User/Edit.elm192
-rw-r--r--elm/User/Login.elm44
-rw-r--r--elm/User/PassReset.elm5
-rw-r--r--elm/User/PassSet.elm15
-rw-r--r--elm/User/Register.elm36
-rw-r--r--lib/VNDB/Handler/Users.pm129
-rw-r--r--lib/VNWeb/Elm.pm9
-rw-r--r--lib/VNWeb/User/Edit.pm101
12 files changed, 375 insertions, 228 deletions
diff --git a/Makefile b/Makefile
index c28a78b7..71fb3f4a 100644
--- a/Makefile
+++ b/Makefile
@@ -53,6 +53,7 @@ prod: all ${PROD}
clean:
rm -f ${ALL_CLEAN} ${PROD}
rm -f static/f/icons.png
+ rm -rf elm/Gen/
rm -f elm3/Lib/Gen.elm
rm -rf elm/elm-stuff/build-artifacts
rm -rf elm3/elm-stuff/build-artifacts
diff --git a/elm/DocEdit.elm b/elm/DocEdit.elm
index f7cbac61..1dcd174c 100644
--- a/elm/DocEdit.elm
+++ b/elm/DocEdit.elm
@@ -9,13 +9,10 @@ import Json.Encode as JE
import Lib.Html exposing (..)
import Lib.Api as Api
import Lib.Ffi as Ffi
+import Lib.Editsum as Editsum
import Gen.Api as GApi
import Gen.DocEdit as GD
---import Lib.Api as Api
---import Lib.Ffi as Ffi
-
-import Lib.Editsum as Editsum
main : Program GD.Recv Model Msg
main = Browser.element
@@ -100,28 +97,24 @@ view model =
[ div [ class "mainbox" ]
[ h1 [] [ text <| "Edit d" ++ String.fromInt model.id ]
, table [ class "formtable" ]
- [ tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "title" ] [ text "Title" ]]
- , td [ class "field" ] [ inputText "title" model.title Title (style "width" "300px" :: GD.valTitle) ]
- ]
- , tr [ class "newfield" ]
- [ td [ class "field", colspan 2 ]
- [ br [] []
- , text "Contents (HTML and MultiMarkdown supported, which is "
- , a [ href "https://daringfireball.net/projects/markdown/basics", target "_blank" ] [ text "Markdown" ]
- , text " with some "
- , a [ href "http://fletcher.github.io/MultiMarkdown-5/syntax.html", target "_blank" ][ text "extensions" ]
- , text ")."
- , br [] []
- , a [ href "#", style "float" "right", onClickN Preview ]
- [ text <| if model.preview == "" then "Preview" else "Edit"
- , if model.state == Api.Loading then div [ class "spinner" ] [] else text ""
- ]
- , br [] []
- , if model.preview == ""
- then inputTextArea "content" model.content Content ([rows 50, cols 90, style "width" "850px"] ++ GD.valContent)
- else div [ class "docs preview", style "width" "850px", Ffi.innerHtml model.preview ] []
+ [ formField "title::Title" [ inputText "title" model.title Title (style "width" "300px" :: GD.valTitle) ]
+ , formField "none"
+ [ br_ 1
+ , b [] [ text "Contents" ]
+ , br_ 1
+ , text "HTML and MultiMarkdown supported, which is "
+ , a [ href "https://daringfireball.net/projects/markdown/basics", target "_blank" ] [ text "Markdown" ]
+ , text " with some "
+ , a [ href "http://fletcher.github.io/MultiMarkdown-5/syntax.html", target "_blank" ][ text "extensions" ]
+ , text "."
+ , a [ href "#", style "float" "right", onClickN Preview ]
+ [ text <| if model.preview == "" then "Preview" else "Edit"
+ , if model.state == Api.Loading then div [ class "spinner" ] [] else text ""
]
+ , br_ 1
+ , if model.preview == ""
+ then inputTextArea "content" model.content Content ([rows 50, cols 90, style "width" "850px"] ++ GD.valContent)
+ else div [ class "docs preview", style "width" "850px", Ffi.innerHtml model.preview ] []
]
]
]
diff --git a/elm/Lib/Api.elm b/elm/Lib/Api.elm
index 06072599..4df99fde 100644
--- a/elm/Lib/Api.elm
+++ b/elm/Lib/Api.elm
@@ -40,6 +40,7 @@ showResponse res =
Taken -> "Username already taken, please choose a different name."
DoubleEmail -> "Email address already used for another account."
DoubleIP -> "You can only register one account from the same IP within 24 hours."
+ BadCurPass -> "Current password is invalid."
expectResponse : (Response -> msg) -> Http.Expect msg
diff --git a/elm/Lib/Html.elm b/elm/Lib/Html.elm
index 41f6e376..fd5ebf5d 100644
--- a/elm/Lib/Html.elm
+++ b/elm/Lib/Html.elm
@@ -13,6 +13,10 @@ onClickN : m -> Attribute m
onClickN action = custom "click" (JD.succeed { message = action, stopPropagation = True, preventDefault = True})
+-- Multi-<br> (ugly but oh, so, convenient)
+br_ : Int -> Html m
+br_ n = if n == 1 then br [] [] else span [] <| List.repeat n <| br [] []
+
-- Submit button with loading indicator and error message display
submitButton : String -> Api.State -> Bool -> Bool -> Html m
submitButton val state valid load = div []
@@ -85,3 +89,26 @@ inputCheck nam val onch = input (
]
++ (if nam == "" then [] else [ id nam, name nam ])
) []
+
+
+-- Generate a form field (table row) with a label. The `label` string can be:
+--
+-- "none" -> To generate a full-width field (colspan=2)
+-- "" -> Empty label
+-- "Some string" -> Text label
+-- "input::String" -> Label that refers to the named input
+--
+-- (Yeah, stringly typed arguments; I wish Elm had typeclasses)
+formField : String -> List (Html m) -> Html m
+formField lbl cont =
+ tr [ class "newfield" ]
+ [ if lbl == "none"
+ then text ""
+ else
+ td [ class "label" ]
+ [ case String.split "::" lbl of
+ [name, txt] -> label [ for name ] [ text txt ]
+ txt -> text <| String.concat txt
+ ]
+ , td (class "field" :: if lbl == "none" then [ colspan 2 ] else []) cont
+ ]
diff --git a/elm/User/Edit.elm b/elm/User/Edit.elm
new file mode 100644
index 00000000..4bdffad9
--- /dev/null
+++ b/elm/User/Edit.elm
@@ -0,0 +1,192 @@
+module User.Edit exposing (main)
+
+import Bitwise exposing (..)
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Browser
+import Browser.Navigation exposing (load)
+import Lib.Html exposing (..)
+import Lib.Api as Api
+import Gen.Api as GApi
+import Gen.Types as GT
+import Gen.UserEdit as GUE
+
+
+main : Program GUE.Send Model Msg
+main = Browser.element
+ { init = \e -> (init e, Cmd.none)
+ , view = view
+ , update = update
+ , subscriptions = always Sub.none
+ }
+
+
+type alias Model =
+ { state : Api.State
+ , data : GUE.Send
+ , cpass : Bool
+ , pass1 : String
+ , pass2 : String
+ , opass : String
+ , passNeq : Bool
+ }
+
+
+init : GUE.Send -> Model
+init d =
+ { state = Api.Normal
+ , data = d
+ , cpass = False
+ , pass1 = ""
+ , pass2 = ""
+ , opass = ""
+ , passNeq = False
+ }
+
+
+type Data
+ = Username String
+ | EMail String
+ | Perm Int Bool
+ | IgnVotes Bool
+ | HideList Bool
+ | ShowNsfw Bool
+ | TraitsSexual Bool
+ | Spoilers String
+ | TagsAll Bool
+ | TagsCont Bool
+ | TagsEro Bool
+ | TagsTech Bool
+ | Skin String
+ | Css String
+
+
+updateData : Data -> GUE.Send -> GUE.Send
+updateData msg model =
+ case msg of
+ Username n -> { model | username = n }
+ EMail n -> { model | email = n }
+ Perm n b -> { model | perm = if b then or model.perm n else and model.perm (complement n) }
+ IgnVotes n -> { model | ign_votes = n }
+ HideList b -> { model | hide_list = b }
+ ShowNsfw b -> { model | show_nsfw = b }
+ TraitsSexual b -> { model | traits_sexual = b }
+ Spoilers n -> { model | spoilers = Maybe.withDefault model.spoilers (String.toInt n) }
+ TagsAll b -> { model | tags_all = b }
+ TagsCont b -> { model | tags_cont = b }
+ TagsEro b -> { model | tags_ero = b }
+ TagsTech b -> { model | tags_tech = b }
+ Skin n -> { model | skin = n }
+ Css n -> { model | customcss = n }
+
+
+type Msg
+ = Set Data
+ | CPass Bool
+ | OPass String
+ | Pass1 String
+ | Pass2 String
+ | Submit
+ | Submitted GApi.Response
+
+
+-- Synchronizes model.data.password with model.stuff
+fixup : Model -> Model
+fixup model =
+ let
+ data = model.data
+ ndata = { data | password = if model.cpass && model.pass1 == model.pass2 then Just { old = model.opass, new = model.pass1 } else Nothing }
+ in { model | data = ndata }
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Set d -> ({ model | data = updateData d model.data }, Cmd.none)
+ CPass b -> (fixup { model | cpass = b, passNeq = False }, Cmd.none)
+ OPass n -> (fixup { model | opass = n, passNeq = False }, Cmd.none)
+ Pass1 n -> (fixup { model | pass1 = n, passNeq = False }, Cmd.none)
+ Pass2 n -> (fixup { model | pass2 = n, passNeq = False }, Cmd.none)
+
+ Submit ->
+ if model.cpass && model.pass1 /= model.pass2
+ then ({ model | passNeq = True }, Cmd.none )
+ else ({ model | state = Api.Loading }, Api.post "/u/edit" (GUE.encode model.data) Submitted)
+
+ -- TODO: This reload is only necessary for the skin and customcss options to apply, but it's nicer to do that directly from JS.
+ Submitted GApi.Success -> (model, load <| "/u" ++ String.fromInt model.data.id ++ "/edit")
+ Submitted r -> ({ model | state = Api.Error r }, Cmd.none)
+
+
+
+view : Model -> Html Msg
+view model =
+ let
+ modform =
+ [ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "Admin options" ] ]
+ , formField "username::Username" [ inputText "username" model.data.username (Set << Username) GUE.valUsername ]
+ , formField "Permissions"
+ <| List.intersperse (br_ 1)
+ <| List.map (\(n,s) -> label [] [ inputCheck "" (and model.data.perm n > 0) (Set << Perm n), text (" " ++ s) ])
+ GT.userPerms
+ , formField "Other" [ label [] [ inputCheck "" model.data.ign_votes (Set << IgnVotes), text " Ignore votes in VN statistics" ] ]
+ ]
+
+ passform =
+ [ formField "opass::Old password" [ inputPassword "opass" model.opass OPass GUE.valPasswordOld ]
+ , formField "pass1::New password" [ inputPassword "pass1" model.pass1 Pass1 GUE.valPasswordNew ]
+ , formField "pass2::Repeat"
+ [ inputPassword "pass2" model.pass2 Pass2 GUE.valPasswordNew
+ , br_ 1
+ , if model.passNeq
+ then b [ class "standout" ] [ text "Passwords do not match" ]
+ else text ""
+ ]
+ ]
+
+ in Html.form [ onSubmit Submit ]
+ [ div [ class "mainbox" ]
+ [ h1 [] [ text <| if model.data.authmod then "Edit " ++ model.data.username else "My preferences" ]
+ , table [ class "formtable" ] <|
+ [ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "General" ] ]
+ , formField "Username" [ text model.data.username ]
+ , formField "email::E-Mail" [ inputText "email" model.data.email (Set << EMail) GUE.valEmail ]
+ ]
+ ++ (if model.data.authmod then modform else []) ++
+ [ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "Password" ] ]
+ , formField "" [ label [] [ inputCheck "" model.cpass CPass, text " Change password" ] ]
+ ] ++ (if model.cpass then passform else [])
+ ++
+ [ tr [ class "newpart" ] [ td [ colspan 2 ] [ text "Preferences" ] ]
+ , formField "Privacy"
+ [ label []
+ [ inputCheck "" model.data.hide_list (Set << HideList)
+ , text " Don't allow others to see my visual novel list, vote list and wishlist and exclude these lists from the database dumps and API."
+ ]
+ ]
+ , formField "NSFW" [ label [] [ inputCheck "" model.data.show_nsfw (Set << ShowNsfw), text " Show NSFW images by default" ] ]
+ , formField "" [ label [] [ inputCheck "" model.data.traits_sexual (Set << TraitsSexual), text " Show sexual traits by default on character pages" ] ]
+ , formField "Tags" [ label [] [ inputCheck "" model.data.tags_all (Set << TagsAll), text " Show all tags by default on visual novel pages (don't summarize)" ] ]
+ , formField ""
+ [ text "Default tag categories on visual novel pages:", br_ 1
+ , label [] [ inputCheck "" model.data.tags_cont (Set << TagsCont), text " Content" ], br_ 1
+ , label [] [ inputCheck "" model.data.tags_ero (Set << TagsEro ), text " Sexual content" ], br_ 1
+ , label [] [ inputCheck "" model.data.tags_tech (Set << TagsTech), text " Technical" ]
+ ]
+ , formField "spoil::Spoiler level"
+ [ inputSelect "spoil" (String.fromInt model.data.spoilers) (Set << Spoilers) []
+ [ ("0", "Hide spoilers")
+ , ("1", "Show only minor spoilers")
+ , ("2", "Show all spoilers")
+ ]
+ ]
+ , formField "skin::Skin" [ inputSelect "skin" model.data.skin (Set << Skin) [ style "width" "300px" ] GT.skins ]
+ , formField "css::Custom CSS" [ inputTextArea "css" model.data.customcss (Set << Css) ([ rows 5, cols 60 ] ++ GUE.valCustomcss) ]
+ ]
+
+ ]
+ , div [ class "mainbox" ]
+ [ fieldset [ class "submit" ] [ submitButton "Submit" model.state (not model.passNeq) False ]
+ ]
+ ]
diff --git a/elm/User/Login.elm b/elm/User/Login.elm
index 224f84fb..e3771777 100644
--- a/elm/User/Login.elm
+++ b/elm/User/Login.elm
@@ -98,21 +98,15 @@ view model =
div [ class "mainbox" ]
[ h1 [] [ text "Login" ]
, table [ class "formtable" ]
- [ tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "username" ] [ text "Username" ]]
- , td [ class "field" ] [ inputText "username" model.username Username GUE.valUsername ]
+ [ formField "username::Username"
+ [ inputText "username" model.username Username GUE.valUsername
+ , br_ 1
+ , a [ href "/u/register" ] [ text "No account yet?" ]
]
- , tr []
- [ td [] []
- , td [ class "field" ] [ a [ href "/u/register" ] [ text "No account yet?" ] ]
- ]
- , tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "password" ] [ text "Password" ]]
- , td [ class "field" ] [ inputPassword "password" model.password Password GUE.valPassword ]
- ]
- , tr []
- [ td [] []
- , td [ class "field" ] [ a [ href "/u/newpass" ] [ text "Forgot your password?" ] ]
+ , formField "password::Password"
+ [ inputPassword "password" model.password Password GUE.valPasswordOld
+ , br_ 1
+ , a [ href "/u/newpass" ] [ text "Forgot your password?" ]
]
]
, if model.state == Api.Normal || model.state == Api.Loading
@@ -122,14 +116,13 @@ view model =
, text "If you have not used this login form since October 2014, your account has likely been disabled. You can "
, a [ href "/u/newpass" ] [ text "reset your password" ]
, text " to regain access."
- , br [] []
- , br [] []
+ , br_ 2
, text "Still having trouble? Send a mail to "
, a [ href <| "mailto:" ++ adminEMail ] [ text adminEMail ]
, text ". But keep in mind that I can only help you if the email address associated with your account is correct"
, text " and you still have access to it. Without that, there is no way to prove that the account is yours."
]
- ]
+ ]
changeBox =
div [ class "mainbox" ]
@@ -139,19 +132,14 @@ view model =
, text "Your current password is in a public database of leaked passwords. You need to change it before you can continue."
]
, table [ class "formtable" ]
- [ tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "newpass1" ] [ text "New password" ]]
- , td [ class "field" ] [ inputPassword "newpass1" model.newpass1 Newpass1 GUE.valPassword ]
- ]
- , tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "newpass2" ] [ text "Repeat" ]]
- , td [ class "field" ]
- [ inputPassword "newpass2" model.newpass2 Newpass2 GUE.valPassword
- , if model.noteq then b [ class "standout" ] [ text "Passwords do not match" ] else text ""
- ]
+ [ formField "newpass1::New password" [ inputPassword "newpass1" model.newpass1 Newpass1 GUE.valPasswordNew ]
+ , formField "newpass2::Repeat"
+ [ inputPassword "newpass2" model.newpass2 Newpass2 GUE.valPasswordNew
+ , br_ 1
+ , if model.noteq then b [ class "standout" ] [ text "Passwords do not match" ] else text ""
]
]
- ]
+ ]
in Html.form [ onSubmit Submit ]
[ if model.insecure then changeBox else loginBox
diff --git a/elm/User/PassReset.elm b/elm/User/PassReset.elm
index dd520494..f22a16c8 100644
--- a/elm/User/PassReset.elm
+++ b/elm/User/PassReset.elm
@@ -77,10 +77,7 @@ view model =
, text " and we'll send you instructions to set a new password within a few minutes!"
]
, table [ class "formtable" ]
- [ tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "email" ] [ text "E-Mail" ]]
- , td [ class "field" ] [ inputText "email" model.email EMail GUE.valEmail ]
- ]
+ [ formField "email::E-Mail" [ inputText "email" model.email EMail GUE.valEmail ]
]
]
, div [ class "mainbox" ]
diff --git a/elm/User/PassSet.elm b/elm/User/PassSet.elm
index 3662dcd3..a04cfe33 100644
--- a/elm/User/PassSet.elm
+++ b/elm/User/PassSet.elm
@@ -75,16 +75,11 @@ view model =
[ h1 [] [ text "Set your password" ]
, p [] [ text "Now you can set a password for your account. You will be logged in automatically after your password has been saved." ]
, table [ class "formtable" ]
- [ tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "newpass1" ] [ text "New password" ]]
- , td [ class "field" ] [ inputPassword "newpass1" model.newpass1 Newpass1 GUE.valPassword ]
- ]
- , tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "newpass2" ] [ text "Repeat" ]]
- , td [ class "field" ]
- [ inputPassword "newpass2" model.newpass2 Newpass2 GUE.valPassword
- , if model.noteq then b [ class "standout" ] [ text "Passwords do not match" ] else text ""
- ]
+ [ formField "newpass1::New password" [ inputPassword "newpass1" model.newpass1 Newpass1 GUE.valPasswordNew ]
+ , formField "newpass2::Repeat"
+ [ inputPassword "newpass2" model.newpass2 Newpass2 GUE.valPasswordNew
+ , br_ 1
+ , if model.noteq then b [ class "standout" ] [ text "Passwords do not match" ] else text ""
]
]
]
diff --git a/elm/User/Register.elm b/elm/User/Register.elm
index 65571475..f3a28d70 100644
--- a/elm/User/Register.elm
+++ b/elm/User/Register.elm
@@ -82,32 +82,20 @@ view model =
[ div [ class "mainbox" ]
[ h1 [] [ text "Create an account" ]
, table [ class "formtable" ]
- [ tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "username" ] [ text "Username" ]]
- , td [ class "field" ] [ inputText "username" model.username Username GUE.valUsername ]
+ [ formField "username::Username"
+ [ inputText "username" model.username Username GUE.valUsername
+ , br_ 1
+ , text "Preferred username. Must be lowercase and can only consist of alphanumeric characters."
]
- , tr []
- [ td [] []
- , td [ class "field" ] [ text "Preferred username. Must be lowercase and can only consist of alphanumeric characters." ]
- ]
- , tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "email" ] [ text "E-Mail" ]]
- , td [ class "field" ] [ inputText "email" model.email EMail GUE.valEmail ]
- ]
- , tr []
- [ td [] []
- , td [ class "field" ]
- [ text "Your email address will only be used in case you lose your password. "
- , text "We will never send spam or newsletters unless you explicitly ask us for it or we get hacked."
- , br [] []
- , br [] []
- , text "Anti-bot question: How many visual novels do we have in the database? (Hint: look to your left)"
- ]
- ]
- , tr [ class "newfield" ]
- [ td [ class "label" ] [ label [ for "vns" ] [ text "Answer" ]]
- , td [ class "field" ] [ inputText "vns" (if model.vns == 0 then "" else String.fromInt model.vns) VNs [] ]
+ , formField "email::E-Mail"
+ [ inputText "email" model.email EMail GUE.valEmail
+ , br_ 1
+ , text "Your email address will only be used in case you lose your password. "
+ , text "We will never send spam or newsletters unless you explicitly ask us for it or we get hacked."
+ , br_ 3
+ , text "Anti-bot question: How many visual novels do we have in the database? (Hint: look to your left)"
]
+ , formField "vns::Answer" [ inputText "vns" (if model.vns == 0 then "" else String.fromInt model.vns) VNs [] ]
]
]
, div [ class "mainbox" ]
diff --git a/lib/VNDB/Handler/Users.pm b/lib/VNDB/Handler/Users.pm
index 171b6b4f..2557b9d1 100644
--- a/lib/VNDB/Handler/Users.pm
+++ b/lib/VNDB/Handler/Users.pm
@@ -13,7 +13,6 @@ use PWLookup;
TUWF::register(
qr{u([1-9]\d*)} => \&userpage,
- qr{u([1-9]\d*)/edit} => \&edit,
qr{u([1-9]\d*)/posts} => \&posts,
qr{u([1-9]\d*)/del(/[od])?} => \&delete,
qr{u/(all|[0a-z])} => \&list,
@@ -138,134 +137,6 @@ sub userpage {
}
-sub edit {
- my($self, $uid) = @_;
-
- # are we allowed to edit this user?
- return $self->htmlDenied if !$self->authInfo->{id} || $self->authInfo->{id} != $uid && !$self->authCan('usermod');
-
- # fetch user info (cached if uid == loggedin uid)
- my $u = $self->dbUserGet(uid => $uid, what => 'extended prefs')->[0];
- return $self->resNotFound if !$u->{id};
-
- # check POST data
- my $frm;
- if($self->reqMethod eq 'POST') {
- return if !$self->authCheckCode;
- $frm = $self->formValidate(
- $self->authCan('usermod') ? (
- { post => 'usrname', template => 'uname' },
- { post => 'perms', required => 0, multi => 1, enum => [ keys %{$self->{permissions}} ] },
- { post => 'ign_votes', required => 0, default => 0 },
- ) : (),
- { post => 'mail', template => 'email' },
- { post => 'curpass', required => 0, minlength => 4, maxlength => 500, default => '' },
- { post => 'usrpass', required => 0, minlength => 4, maxlength => 500 },
- { post => 'usrpass2', required => 0, minlength => 4, maxlength => 500 },
- { post => 'hide_list', required => 0, default => 0, enum => [0,1] },
- { post => 'show_nsfw', required => 0, default => 0, enum => [0,1] },
- { post => 'traits_sexual', required => 0, default => 0, enum => [0,1] },
- { post => 'tags_all', required => 0, default => 0, enum => [0,1] },
- { post => 'tags_cat', required => 0, multi => 1, enum => [qw|cont ero tech|] },
- { post => 'spoilers', required => 0, default => 0, enum => [0..2] },
- { post => 'skin', required => 0, default => $self->{skin_default}, enum => [ keys %{$self->{skins}} ] },
- { post => 'customcss', required => 0, maxlength => 2000, default => '' },
- );
- push @{$frm->{_err}}, 'Passwords do not match'
- if ($frm->{usrpass} || $frm->{usrpass2}) && (!$frm->{usrpass} || !$frm->{usrpass2} || $frm->{usrpass} ne $frm->{usrpass2});
- push @{$frm->{_err}}, 'Your chosen password is in a database of leaked passwords, please choose another one'
- if $self->{password_db} && PWLookup::lookup($self->{password_db}, $frm->{usrpass});
-
- if(!$frm->{_err}) {
- $frm->{skin} = '' if $frm->{skin} eq $self->{skin_default};
- $self->dbUserPrefSet($uid, $_ => $frm->{$_}) for (qw|skin customcss show_nsfw traits_sexual tags_all hide_list spoilers|);
-
- my $tags_cat = join(',', sort @{$frm->{tags_cat}}) || 'none';
- $self->dbUserPrefSet($uid, tags_cat => $tags_cat eq $self->{default_tags_cat} ? '' : $tags_cat);
-
- my %o;
- if($self->authCan('usermod')) {
- $o{username} = $frm->{usrname} if $frm->{usrname};
- $o{ign_votes} = $frm->{ign_votes} ? 1 : 0;
-
- my $perm = 0;
- $perm |= $self->{permissions}{$_} for(@{ delete $frm->{perms} });
- $self->dbUserSetPerm($u->{id}, $self->authInfo->{id}, auth->token(), $perm);
- }
- $self->dbUserSetMail($u->{id}, $self->authInfo->{id}, auth->token(), $frm->{mail});
- $self->dbUserEdit($uid, %o);
- $self->authAdminSetPass($u->{id}, $frm->{usrpass}) if $frm->{usrpass} && $self->authInfo->{id} != $u->{id};
-
- if($frm->{usrpass} && $self->authInfo->{id} == $u->{id}) {
- # Bit ugly: On incorrect password, all other changes are still saved.
- my $ok = $self->authSetPass($u->{id}, $frm->{usrpass}, "/u$uid/edit?d=1", pass => $frm->{curpass});
- return if $ok;
- push @{$frm->{_err}}, 'Invalid password';
- } else {
- return $self->resRedirect("/u$uid/edit?d=1", 'post');
- }
- }
- }
-
- # fill out default values
- $frm->{usrname} ||= $u->{username};
- $frm->{mail} ||= $self->dbUserGetMail($u->{id}, $self->authInfo->{id}, auth->token);
- $frm->{perms} ||= [ grep $u->{perm} & $self->{permissions}{$_}, keys %{$self->{permissions}} ];
- $frm->{$_} //= $u->{prefs}{$_} for(qw|skin customcss show_nsfw traits_sexual tags_all hide_list spoilers|);
- $frm->{tags_cat} ||= [ split /,/, $u->{prefs}{tags_cat}||$self->{default_tags_cat} ];
- $frm->{ign_votes} = $u->{ign_votes} if !defined $frm->{ign_votes};
- $frm->{skin} ||= $self->{skin_default};
- $frm->{usrpass} = $frm->{usrpass2} = $frm->{curpass} = '';
-
- # create the page
- $self->htmlHeader(title => 'My account', noindex => 1);
- $self->htmlMainTabs('u', $u, 'edit');
- if($self->reqGet('d')) {
- div class => 'mainbox';
- h1 'Settings saved';
- div class => 'notice';
- p 'Settings successfully saved.';
- end;
- end
- }
- $self->htmlForm({ frm => $frm, action => "/u$uid/edit" }, useredit => [ 'My account',
- [ part => title => 'General info' ],
- $self->authCan('usermod') ? (
- [ input => short => 'usrname', name => 'Username' ],
- [ select => short => 'perms', name => 'Permissions', multi => 1, size => (scalar keys %{$self->{permissions}}), options => [
- map [ $_, $_ ], sort keys %{$self->{permissions}} ] ],
- [ check => short => 'ign_votes', name => 'Ignore votes in VN statistics' ],
- ) : (
- [ static => label => 'Username', content => $frm->{usrname} ],
- ),
- [ input => short => 'mail', name => 'Email' ],
-
- [ part => title => 'Change password' ],
- [ static => content => 'Leave blank to keep your current password' ],
- [ passwd => short => 'curpass', name => 'Current Password' ],
- [ passwd => short => 'usrpass', name => 'New Password' ],
- [ passwd => short => 'usrpass2', name => 'Confirm password' ],
-
- [ part => title => 'Options' ],
- [ check => short => 'hide_list', name =>
- qq{Don't allow other people to see my <a href="/u$uid/list">visual novel list</a>,
- <a href="/u$uid/votes">votes</a> and <a href="/u$uid/wish">wishlist</a>,
- and exclude these lists from the <a href="/d14">database dumps</a> and <a href="/d11">API</a>.} ],
- [ check => short => 'show_nsfw', name => 'Disable warnings for images that are not safe for work.' ],
- [ check => short => 'traits_sexual', name => 'Show sexual traits by default on character pages.' ],
- [ check => short => 'tags_all', name => 'Show all tags by default on visual novel pages.' ],
- [ select => short => 'tags_cat', name => 'Tag categories', multi => 1, size => 3,
- options => [ map [ $_, $TAG_CATEGORY{$_} ], keys %TAG_CATEGORY ] ],
- [ select => short => 'spoilers', name => 'Spoiler level', options => [
- [0, 'Hide spoilers'], [1, 'Show only minor spoilers'], [2, 'Show all spoilers'] ]],
- [ select => short => 'skin', name => 'Preferred skin', width => 300, options => [
- map [ $_, $self->{skins}{$_}[0].($self->debug?" [$_]":'') ], sort { $self->{skins}{$a}[0] cmp $self->{skins}{$b}[0] } keys %{$self->{skins}} ] ],
- [ textarea => short => 'customcss', name => 'Additional <a href="http://en.wikipedia.org/wiki/Cascading_Style_Sheets">CSS</a>' ],
- ]);
- $self->htmlFooter;
-}
-
-
sub posts {
my($self, $uid) = @_;
diff --git a/lib/VNWeb/Elm.pm b/lib/VNWeb/Elm.pm
index a65844f8..78fad8c8 100644
--- a/lib/VNWeb/Elm.pm
+++ b/lib/VNWeb/Elm.pm
@@ -44,6 +44,7 @@ my %apis = (
Taken => [], # Username already taken
DoubleEmail => [], # Account with same email already exists
DoubleIP => [], # Account with same IP already exists
+ BadCurPass => [], # Current password is incorrect when changing password
);
@@ -106,8 +107,8 @@ sub def_validation {
$v{required} ? 'A.required True' : (),
$v{minlength} ? "A.minlength $v{minlength}" : (),
$v{maxlength} ? "A.maxlength $v{maxlength}" : (),
- $v{min} ? "A.min $v{min}" : (),
- $v{max} ? "A.max $v{max}" : (),
+ $v{min} ? 'A.min '.string($v{min}) : (),
+ $v{max} ? 'A.max '.string($v{max}) : (),
$v{pattern} ? 'A.pattern '.string($v{pattern}) : ()
).']' if !$obj->{keys};
$data;
@@ -228,6 +229,10 @@ sub write_types {
$data .= def urlStatic => String => string config->{url_static};
$data .= def adminEMail => String => string config->{admin_email};
+ $data .= def userPerms => 'List (Int, String)' => list map tuple(VNWeb::Auth::listPerms->{$_}, string $_), sort keys VNWeb::Auth::listPerms->%*;
+ $data .= def skins => 'List (String, String)' =>
+ list map tuple(string $_, string tuwf->{skins}{$_}[0]),
+ sort { tuwf->{skins}{$a}[0] cmp tuwf->{skins}{$b}[0] } keys tuwf->{skins}->%*;
write_module Types => $data;
}
diff --git a/lib/VNWeb/User/Edit.pm b/lib/VNWeb/User/Edit.pm
index 96945d0c..1af1c6c8 100644
--- a/lib/VNWeb/User/Edit.pm
+++ b/lib/VNWeb/User/Edit.pm
@@ -2,11 +2,100 @@ package VNWeb::User::Edit;
use VNWeb::Prelude;
-# Some validations in this form are also used by Login.elm, PassReset.elm, PassSet.elm and Register.elm
-elm_form UserEdit => undef, form_compile(in => {
- email => { email => 1 },
- password => { password => 1 },
- username => { username => 1 },
-});
+
+my $FORM = form_compile in => {
+ username => { username => 1 },
+ email => { email => 1 },
+ perm => { uint => 1, func => sub { ($_[0] & ~auth->allPerms) == 0 } },
+ ign_votes => { anybool => 1 },
+ hide_list => { anybool => 1 },
+ show_nsfw => { anybool => 1 },
+ traits_sexual => { anybool => 1 },
+ tags_all => { anybool => 1 },
+ tags_cont => { anybool => 1 },
+ tags_ero => { anybool => 1 },
+ tags_tech => { anybool => 1 },
+ spoilers => { uint => 1, range => [ 0, 2 ] },
+ skin => { enum => tuwf->{skins} },
+ customcss => { required => 0, default => '', maxlength => 2000 },
+
+ password => { _when => 'in', required => 0, type => 'hash', keys => {
+ old => { password => 1 },
+ new => { password => 1 }
+ } },
+
+ id => { uint => 1 },
+ # This is technically only used for Perl->Elm data, but also received from
+ # Elm in order to make the Send and Recv types equivalent.
+ authmod => { anybool => 1 },
+};
+
+# Some validations in this form are also used by other User.* Elm modules.
+elm_form UserEdit => undef, $FORM;
+
+
+TUWF::get qr{/$RE{uid}/edit}, sub {
+ my $u = tuwf->dbRowi('SELECT id, username, perm, ign_votes FROM users WHERE id =', \tuwf->capture('id'));
+
+ return tuwf->resNotFound if !can_edit u => $u;
+
+ $u->{email} = tuwf->dbVali(select => sql_func user_getmail => \$u->{id}, \auth->uid, sql_fromhex auth->token);
+ $u->{authmod} = auth->permUsermod;
+ $u->{password} = undef;
+
+ # Let's not disclose this (though it's not hard to find out through other means)
+ if(!auth->permUsermod) {
+ $u->{ign_votes} = 0;
+ $u->{perm} = auth->defaultPerms;
+ }
+
+ my $prefs = { map +($_->{key}, $_->{value}), @{ tuwf->dbAlli('SELECT key, value FROM users_prefs WHERE uid =', \$u->{id}) }};
+ $u->{$_} = $prefs->{$_}||'' for qw/hide_list show_nsfw traits_sexual tags_all spoilers skin customcss/;
+ $u->{spoilers} ||= 0;
+ $u->{skin} ||= config->{skin_default};
+ $u->{"tags_$_"} = (($prefs->{tags_cat}||'cont,tech') =~ /$_/) for qw/cont ero tech/;
+
+ my $title = $u->{id} == auth->uid ? 'My Account' : "Edit $u->{username}";
+ framework_ title => $title, index => 0, type => 'u', dbobj => $u, tab => 'edit',
+ sub {
+ elm_ 'User.Edit', $FORM, $u;
+ };
+};
+
+
+json_api qr{/u/edit}, $FORM, sub {
+ my $data = shift;
+
+ return elm_Unauth if !can_edit u => $data;
+
+ if(auth->permUsermod) {
+ tuwf->dbExeci(update => users => set => {
+ username => $data->{username},
+ ign_votes => $data->{ign_votes},
+ email_confirmed => 1,
+ }, where => { id => $data->{id} });
+ tuwf->dbExeci(select => sql_func user_setperm => \$data->{id}, \auth->uid, sql_fromhex(auth->token), \$data->{perm});
+ }
+
+ if($data->{password}) {
+ return elm_InsecurePass if is_insecurepass $data->{password}{new};
+
+ if(auth->uid == $data->{id}) {
+ return elm_BadCurPass if !auth->setpass($data->{id}, undef, $data->{password}{old}, $data->{password}{new});
+ } else {
+ tuwf->dbExeci(select => sql_func user_admin_setpass => \$data->{id}, \auth->uid,
+ sql_fromhex(auth->token), sql_fromhex auth->_preparepass($data->{password}{new})
+ );
+ }
+ }
+
+ tuwf->dbExeci(select => sql_func user_setmail => \$data->{id}, \auth->uid, sql_fromhex(auth->token), \$data->{email});
+
+ $data->{skin} = '' if $data->{skin} eq config->{skin_default};
+ auth->prefSet($_, $data->{$_}, $data->{id}) for qw/hide_list show_nsfw traits_sexual tags_all spoilers skin customcss/;
+ auth->prefSet(tags_cat => join(',', map $data->{"tags_$_"} ? $_ : (), qw/cont ero tech/), $data->{id});
+
+ elm_Success
+};
1;