diff options
author | Yorhel <git@yorhel.nl> | 2019-10-02 14:21:12 +0200 |
---|---|---|
committer | Yorhel <git@yorhel.nl> | 2019-10-02 14:21:15 +0200 |
commit | 7d8274f0332b1c92825988c6393037bf7eb12af3 (patch) | |
tree | cdcc33c92f764b3199fffeef990d71e6085dff06 | |
parent | 1a9a4b4bdb8f3b6d7d0ad12032c17c44a8287a09 (diff) |
v2rw: Convert user preferences form
And add a small 'formField' function to shrink the Elm form generation
code a bit.
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | elm/DocEdit.elm | 43 | ||||
-rw-r--r-- | elm/Lib/Api.elm | 1 | ||||
-rw-r--r-- | elm/Lib/Html.elm | 27 | ||||
-rw-r--r-- | elm/User/Edit.elm | 192 | ||||
-rw-r--r-- | elm/User/Login.elm | 44 | ||||
-rw-r--r-- | elm/User/PassReset.elm | 5 | ||||
-rw-r--r-- | elm/User/PassSet.elm | 15 | ||||
-rw-r--r-- | elm/User/Register.elm | 36 | ||||
-rw-r--r-- | lib/VNDB/Handler/Users.pm | 129 | ||||
-rw-r--r-- | lib/VNWeb/Elm.pm | 9 | ||||
-rw-r--r-- | lib/VNWeb/User/Edit.pm | 101 |
12 files changed, 375 insertions, 228 deletions
@@ -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; |