diff options
-rw-r--r-- | elm/Lib/Api.elm | 5 | ||||
-rw-r--r-- | elm/User/Login.elm | 2 | ||||
-rw-r--r-- | elm/User/PassReset.elm | 89 | ||||
-rw-r--r-- | elm/User/PassSet.elm | 94 | ||||
-rw-r--r-- | elm/User/Register.elm | 116 | ||||
-rw-r--r-- | lib/VNDB/Handler/Users.pm | 220 | ||||
-rw-r--r-- | lib/VNWeb/Elm.pm | 13 | ||||
-rw-r--r-- | lib/VNWeb/HTML.pm | 9 | ||||
-rw-r--r-- | lib/VNWeb/Misc/History.pm | 2 | ||||
-rw-r--r-- | lib/VNWeb/User/RegReset.pm | 143 |
10 files changed, 464 insertions, 229 deletions
diff --git a/elm/Lib/Api.elm b/elm/Lib/Api.elm index d1d0bc10..06072599 100644 --- a/elm/Lib/Api.elm +++ b/elm/Lib/Api.elm @@ -35,6 +35,11 @@ showResponse res = BadLogin -> "Invalid username or password." LoginThrottle -> "Action throttled, too many failed login attempts." InsecurePass -> "Your chosen password is in a database of leaked passwords, please choose another one." + BadEmail -> "Unknown email address." + Bot -> "Invalid answer to the anti-bot question." + 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." expectResponse : (Response -> msg) -> Http.Expect msg diff --git a/elm/User/Login.elm b/elm/User/Login.elm index 6a35f690..0adaef4f 100644 --- a/elm/User/Login.elm +++ b/elm/User/Login.elm @@ -72,7 +72,7 @@ type Msg update : Msg -> Model -> (Model, Cmd Msg) update msg model = case msg of - Username n -> ({ model | username = n }, Cmd.none) + Username n -> ({ model | username = String.toLower n }, Cmd.none) Password n -> ({ model | password = n }, Cmd.none) Newpass1 n -> ({ model | newpass1 = n, noteq = False }, Cmd.none) Newpass2 n -> ({ model | newpass2 = n, noteq = False }, Cmd.none) diff --git a/elm/User/PassReset.elm b/elm/User/PassReset.elm new file mode 100644 index 00000000..f1c36058 --- /dev/null +++ b/elm/User/PassReset.elm @@ -0,0 +1,89 @@ +module User.PassReset exposing (main) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Json.Encode as JE +import Browser +import Lib.Api as Api +import Gen.Api as GApi +import Gen.RegReset as GRR +import Lib.Html exposing (..) + + +main : Program () Model Msg +main = Browser.element + { init = always (init, Cmd.none) + , subscriptions = always Sub.none + , view = view + , update = update + } + + +type alias Model = + { email : String + , state : Api.State + , success : Bool + } + + +init : Model +init = + { email = "" + , state = Api.Normal + , success = False + } + + +encodeForm : Model -> JE.Value +encodeForm o = JE.object + [ ("email", JE.string o.email) ] + + +type Msg + = EMail String + | Submit + | Submitted GApi.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + EMail n -> ({ model | email = n }, Cmd.none) + + Submit -> ( { model | state = Api.Loading } + , Api.post "/u/newpass" (encodeForm model) Submitted ) + + Submitted GApi.Success -> ({ model | success = True }, Cmd.none) + Submitted e -> ({ model | state = Api.Error e }, Cmd.none) + + +view : Model -> Html Msg +view model = + if model.success + then + div [ class "mainbox" ] + [ h1 [] [ text "New password" ] + , div [ class "notice" ] + [ p [] [ text "Your password has been reset and instructions to set a new one should reach your mailbox in a few minutes." ] ] + ] + else + Html.form [ onSubmit Submit ] + [ div [ class "mainbox" ] + [ h1 [] [ text "Forgot Password" ] + , p [] + [ text "Forgot your password and can't login to VNDB anymore? " + , text "Don't worry! Just give us the email address you used to register on VNDB " + , 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 GRR.valEmail ] + ] + ] + ] + , div [ class "mainbox" ] + [ fieldset [ class "submit" ] [ submitButton "Submit" model.state True False ] + ] + ] diff --git a/elm/User/PassSet.elm b/elm/User/PassSet.elm new file mode 100644 index 00000000..0756196d --- /dev/null +++ b/elm/User/PassSet.elm @@ -0,0 +1,94 @@ +module User.PassSet exposing (main) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Json.Encode as JE +import Browser +import Browser.Navigation exposing (load) +import Lib.Api as Api +import Gen.Api as GApi +import Gen.RegReset as GRR +import Lib.Html exposing (..) + + +main : Program String Model Msg +main = Browser.element + { init = \url -> (init url, Cmd.none) + , subscriptions = always Sub.none + , view = view + , update = update + } + + +type alias Model = + { url : String + , newpass1 : String + , newpass2 : String + , state : Api.State + , noteq : Bool + } + + +init : String -> Model +init url = + { url = url + , newpass1 = "" + , newpass2 = "" + , state = Api.Normal + , noteq = False + } + + +encodeForm : Model -> JE.Value +encodeForm o = JE.object + [ ("password", JE.string o.newpass1) ] + + +type Msg + = Newpass1 String + | Newpass2 String + | Submit + | Submitted GApi.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Newpass1 n -> ({ model | newpass1 = n, noteq = False }, Cmd.none) + Newpass2 n -> ({ model | newpass2 = n, noteq = False }, Cmd.none) + + Submit -> + if model.newpass1 /= model.newpass2 + then ( { model | noteq = True }, Cmd.none) + else ( { model | state = Api.Loading } + , Api.post model.url (encodeForm model) Submitted ) + + Submitted GApi.Success -> (model, load "/") + Submitted e -> ({ model | state = Api.Error e }, Cmd.none) + + +view : Model -> Html Msg +view model = + Html.form [ onSubmit Submit ] + [ div [ class "mainbox" ] + [ 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 GRR.valPassword ] + ] + , tr [ class "newfield" ] + [ td [ class "label" ] [ label [ for "newpass2" ] [ text "Repeat" ]] + , td [ class "field" ] + [ inputPassword "newpass2" model.newpass2 Newpass2 GRR.valPassword + , if model.noteq then b [ class "standout" ] [ text "Passwords do not match" ] else text "" + ] + ] + ] + ] + , div [ class "mainbox" ] + [ fieldset [ class "submit" ] [ submitButton "Submit" model.state True False ] + ] + ] diff --git a/elm/User/Register.elm b/elm/User/Register.elm new file mode 100644 index 00000000..6e65893f --- /dev/null +++ b/elm/User/Register.elm @@ -0,0 +1,116 @@ +module User.Register exposing (main) + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Json.Encode as JE +import Browser +import Lib.Api as Api +import Gen.Api as GApi +import Gen.RegReset as GRR +import Lib.Html exposing (..) + + +main : Program () Model Msg +main = Browser.element + { init = always (init, Cmd.none) + , subscriptions = always Sub.none + , view = view + , update = update + } + + +type alias Model = + { username : String + , email : String + , vns : Int + , state : Api.State + , success : Bool + } + + +init : Model +init = + { username = "" + , email = "" + , vns = 0 + , state = Api.Normal + , success = False + } + + +encodeForm : Model -> JE.Value +encodeForm o = JE.object + [ ("username", JE.string o.username) + , ("email", JE.string o.email) + , ("vns", JE.int o.vns) ] + + +type Msg + = Username String + | EMail String + | VNs String + | Submit + | Submitted GApi.Response + + +update : Msg -> Model -> (Model, Cmd Msg) +update msg model = + case msg of + Username n -> ({ model | username = String.toLower n }, Cmd.none) + EMail n -> ({ model | email = n }, Cmd.none) + VNs n -> ({ model | vns = Maybe.withDefault model.vns (String.toInt n) }, Cmd.none) + + Submit -> ( { model | state = Api.Loading } + , Api.post "/u/register" (encodeForm model) Submitted ) + + Submitted GApi.Success -> ({ model | success = True }, Cmd.none) + Submitted e -> ({ model | state = Api.Error e }, Cmd.none) + + +view : Model -> Html Msg +view model = + if model.success + then + div [ class "mainbox" ] + [ h1 [] [ text "Account created" ] + , div [ class "notice" ] + [ p [] [ text "Your account has been created! In a few minutes, you should receive an email with instructions to set your password." ] ] + ] + else + Html.form [ onSubmit Submit ] + [ 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 GRR.valUsername ] + ] + , 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 GRR.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 GRR.valVns ] + ] + ] + ] + , div [ class "mainbox" ] + [ fieldset [ class "submit" ] [ submitButton "Submit" model.state True False ] + ] + ] diff --git a/lib/VNDB/Handler/Users.pm b/lib/VNDB/Handler/Users.pm index 08c34245..171b6b4f 100644 --- a/lib/VNDB/Handler/Users.pm +++ b/lib/VNDB/Handler/Users.pm @@ -13,11 +13,6 @@ use PWLookup; TUWF::register( qr{u([1-9]\d*)} => \&userpage, - qr{u/newpass} => \&newpass, - qr{u/newpass/sent} => \&newpass_sent, - qr{u([1-9]\d*)/setpass} => \&setpass, - qr{u/register} => \®ister, - qr{u/register/done} => \®ister_done, qr{u([1-9]\d*)/edit} => \&edit, qr{u([1-9]\d*)/posts} => \&posts, qr{u([1-9]\d*)/del(/[od])?} => \&delete, @@ -143,221 +138,6 @@ sub userpage { } -sub _check_throttle { - my $self = shift; - my $tm = $self->dbThrottleGet(norm_ip($self->reqIP)); - if($tm-time() > $self->{login_throttle}[1]) { - $self->htmlHeader(title => 'Login'); - div class => 'mainbox'; - h1 'Login'; - div class => 'warning'; - h2 'Maximum failed login attempts reached.'; - p; - txt 'Login has been temporarily disabled for your IP address. You can wait a few hours and try again,' - .' or you can try from a different IP address. If you forgot your password, you can still use the '; - a href => '/u/newpass', 'password reset'; - txt ' functionality. If you still have trouble logging in, send a mail to '; - a href => 'mailto:contact@vndb.org', 'contact@vndb.org'; - txt '.'; - end; - end; - end 'div'; - $self->htmlFooter; - return undef; - } - $tm -} - - -sub newpass { - my $self = shift; - - return $self->resRedirect('/', 'temp') if $self->authInfo->{id}; - - my($frm, $uid, $token); - if($self->reqMethod eq 'POST') { - return if !$self->authCheckCode; - $frm = $self->formValidate({ post => 'mail', template => 'email' }); - if(!$frm->{_err}) { - ($uid, $token) = $self->authResetPass($frm->{mail}); - $frm->{_err} = [ 'No user found with that email address' ] if !$uid; - } - if(!$frm->{_err}) { - my $u = $self->dbUserGet(uid => $uid)->[0]; - my $body = sprintf - "Hello %s,\n\nYour VNDB.org login has been disabled, you can now set a new password by following the link below:\n\n" - ."%s\n\nNow don't forget your password again! :-)\n\nvndb.org", - $u->{username}, $self->reqBaseURI()."/u$u->{id}/setpass?t=$token"; - $self->mail($body, - To => $frm->{mail}, - From => 'VNDB <noreply@vndb.org>', - Subject => "Password reset for $u->{username}", - ); - return $self->resRedirect('/u/newpass/sent', 'post'); - } - } - - $self->htmlHeader(title => 'Forgot password', noindex => 1); - div class => 'mainbox'; - h1 'Forgot password'; - p 'Forgot your password and can\'t login to VNDB anymore?' - .' Don\'t worry! Just give us the email address you used to register on VNDB,' - .' and we\'ll send you instructions to set a new password within a few minutes!'; - end; - $self->htmlForm({ frm => $frm, action => '/u/newpass' }, newpass => [ 'Reset password', - [ input => short => 'mail', name => 'Email' ], - ]); - $self->htmlFooter; -} - - -sub newpass_sent { - my $self = shift; - return $self->resRedirect('/', 'temp') if $self->authInfo->{id}; - $self->htmlHeader(title => 'New password', noindex => 1); - div class => 'mainbox'; - h1 'New password'; - div class => 'notice'; - p 'Your password has been reset and instructions to set a new one should reach your mailbox in a few minutes.'; - end; - end; - $self->htmlFooter; -} - - -# /u+/setpass had two modes: With a token (?t=xxx), to set the password after a -# 'register' or 'newpass', or without a token, after the user tried to log in -# with a weak password (that mode has been moved into v2rw). -sub setpass { - my($self, $uid) = @_; - return $self->resRedirect('/', 'temp') if $self->authInfo->{id}; - - my $t = $self->formValidate({param => 't', required => 0, regex => qr/^[a-f0-9]{40}$/i }); - return $self->resNotFound if $t->{_err}; - $t = $t->{t}; - - my $u = $self->dbUserGet(uid => $uid)->[0]; - return $self->resNotFound if !$u || ($t && !$self->authIsValidToken($u->{id}, $t)); - - my $tm = !$t && _check_throttle($self); - return if !$t && !defined $tm; - - my $frm; - if($self->reqMethod eq 'POST') { - return if !$self->authCheckCode("/u$u->{id}/setpass"); - $frm = $self->formValidate( - $t ? () : ( - { post => 'curpass', minlength => 4, maxlength => 500 }, - ), - { post => 'usrpass', minlength => 4, maxlength => 500 }, - { post => 'usrpass2', minlength => 4, maxlength => 500 }, - ); - push @{$frm->{_err}}, 'Passwords do not match' if $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}) { - $self->dbUserEdit($uid, email_confirmed => 1); - return if $self->authSetPass($uid, $frm->{usrpass}, "/u$uid", $t ? (token => $t) : (pass => $frm->{curpass})); - $self->dbThrottleSet(norm_ip($self->reqIP), $tm+$self->{login_throttle}[0]); - push @{$frm->{_err}}, 'Invalid password'; - } - } - - $self->htmlHeader(title => "Set password for $u->{username}", noindex => 1); - $self->htmlForm({ frm => $frm, action => "/u$u->{id}/setpass" }, setpass => [ "Set password for $u->{username}", - [ hidden => short => 't', value => $t||'' ], - $t ? ( - [ static => nolabel => 1, content => 'Now you can set a password for your account.' - .' You will be logged in automatically after your password has been saved.' ], - ) : ( - [ static => nolabel => 1, content => "Your current password is in a database of leaked passwords, please change your password to continue.<br><br>" ], - [ passwd => short => 'curpass', name => 'Current password' ], - ), - [ passwd => short => 'usrpass', name => 'Password' ], - [ passwd => short => 'usrpass2', name => 'Confirm password' ], - ]); - $self->htmlFooter; -} - - -sub register { - my $self = shift; - return $self->resRedirect('/', 'temp') if $self->authInfo->{id}; - - my $frm; - if($self->reqMethod eq 'POST') { - return if !$self->authCheckCode; - $frm = $self->formValidate( - { post => 'usrname', template => 'uname' }, - { post => 'mail', template => 'email' }, - { post => 'type', enum => [1..3] }, - { post => 'answer', template => 'uint' }, - ); - my $num = $self->{stats}{[qw|vn releases producers|]->[ $frm->{type} - 1 ]}; - push @{$frm->{_err}}, 'Question was not correctly answered. Are you sure you are a human?' - if !$frm->{_err} && ($frm->{answer} > $num*1.005 || $frm->{answer} < $num*0.995); - push @{$frm->{_err}}, 'Someone already has this username, please choose another name' - if $frm->{usrname} eq 'anonymous' || !$frm->{_err} && $self->dbUserGet(username => $frm->{usrname})->[0]{id}; - push @{$frm->{_err}}, 'Someone already registered with that email address' - if !$frm->{_err} && $self->dbUserEmailExists($frm->{mail}); - - # Use /32 match for IPv4 and /48 for IPv6. The /48 is fairly broad, so some - # users may have to wait a bit before they can register... - my $ip = $self->reqIP; - push @{$frm->{_err}}, 'You can only register one account from the same IP within 24 hours' - if !$frm->{_err} && $self->dbUserGet(ip => $ip =~ /:/ ? "$ip/48" : $ip, registered => time-24*3600)->[0]{id}; - - if(!$frm->{_err}) { - my $uid = $self->dbUserAdd($frm->{usrname}, $frm->{mail}); - my(undef, $token) = $self->authResetPass($frm->{mail}); - my $body = sprintf "Hello %s,\n\n" - ."Someone has registered an account on VNDB.org with your email address. To confirm your registration, follow the link below.\n\n" - ."%s\n\n" - ."If you don't remember creating an account on VNDB.org recently, please ignore this e-mail.\n\n" - ."vndb.org", - $frm->{usrname}, $self->reqBaseURI()."/u$uid/setpass?t=$token"; - $self->mail($body, - To => $frm->{mail}, - From => 'VNDB <noreply@vndb.org>', - Subject => "Confirm registration for $frm->{usrname}", - ); - return $self->resRedirect('/u/register/done', 'post'); - } - } - - $self->htmlHeader(title => 'Create an account', noindex => 1); - - my $type = $frm->{type} || floor(rand 3)+1; - $self->htmlForm({ frm => $frm, action => '/u/register' }, register => [ 'Create an account', - [ hidden => short => 'type', value => $type ], - [ input => short => 'usrname', name => 'Username' ], - [ static => content => 'Preferred username. Must be lowercase and can only consist of alphanumeric characters.' ], - [ input => short => 'mail', name => 'Email' ], - [ static => content => 'Your email address will only be used in case you lose your password.' - .' We will never send spam or newsletters unless you explicitly ask us for it or we get hacked.<br /><br />' ], - [ static => content => sprintf '<br /><br />How many %s do we have in the database? (Hint: look to your left)', - ['visual novels', 'releases', 'producers']->[$type-1] ], - [ input => short => 'answer', name => 'Answer' ], - ]); - $self->htmlFooter; -} - - -sub register_done { - my $self = shift; - return $self->resRedirect('/', 'temp') if $self->authInfo->{id}; - $self->htmlHeader(title => 'Account created', noindex => 1); - div class => 'mainbox'; - h1 'Account created'; - div class => 'notice'; - p 'Your account has been created! In a few minutes, you should receive an email with instructions to set your password.'; - end; - end; - $self->htmlFooter; -} - - sub edit { my($self, $uid) = @_; diff --git a/lib/VNWeb/Elm.pm b/lib/VNWeb/Elm.pm index 7838d194..a65844f8 100644 --- a/lib/VNWeb/Elm.pm +++ b/lib/VNWeb/Elm.pm @@ -39,6 +39,11 @@ my %apis = ( BadLogin => [], # Invalid user or pass LoginThrottle => [], # Too many failed login attempts InsecurePass => [], # Password is in a dictionary or breach database + BadEmail => [], # Unknown email address in password reset form + Bot => [], # User didn't pass bot verification + Taken => [], # Username already taken + DoubleEmail => [], # Account with same email already exists + DoubleIP => [], # Account with same IP already exists ); @@ -167,10 +172,10 @@ sub elm_form { my($name, $out, $in) = @_; my $data = ''; - $data .= def_type Recv => $out->analyze; - $data .= def_type Send => $in->analyze; - $data .= encoder encode => 'Send', $in->analyze; - $data .= def_validation val => $in->analyze; + $data .= def_type Recv => $out->analyze if $out; + $data .= def_type Send => $in->analyze if $in; + $data .= encoder encode => 'Send', $in->analyze if $in; + $data .= def_validation val => $in->analyze if $in; write_module $name, $data; } diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm index 02286750..005f9ee7 100644 --- a/lib/VNWeb/HTML.pm +++ b/lib/VNWeb/HTML.pm @@ -65,10 +65,12 @@ sub user_ { # Instantiate an Elm module -sub elm_($$$) { +sub elm_ { my($mod, $schema, $data) = @_; div_ 'data-elm-module' => $mod, - 'data-elm-flags' => JSON::XS->new->allow_nonref->encode($schema->analyze->coerce_for_json($data, unknown => 'remove')), ''; + $data ? ( + 'data-elm-flags' => JSON::XS->new->allow_nonref->encode($schema->analyze->coerce_for_json($data, unknown => 'remove')) + ) : (), ''; } @@ -97,7 +99,9 @@ sub _head_ { my $skin = tuwf->reqGet('skin') || auth->pref('skin') || config->{skin_default}; $skin = config->{skin_default} if !tuwf->{skins}{$skin}; + meta_ charset => 'utf-8'; title_ $o->{title}.' | vndb'; + base_ href => tuwf->reqBaseURI(); link_ rel => 'shortcut icon', href => '/favicon.ico', type => 'image/x-icon'; link_ rel => 'stylesheet', href => config->{url_static}.'/s/'.$skin.'/style.css?'.config->{version}, type => 'text/css', media => 'all'; link_ rel => 'search', type => 'application/opensearchdescription+xml', title => 'VNDB VN Search', href => tuwf->reqBaseURI().'/opensearch.xml'; @@ -107,7 +111,6 @@ sub _head_ { link_ rel => 'alternate', type => 'application/atom+xml', href => "/feeds/changes.atom", title => 'Recent Changes'; link_ rel => 'alternate', type => 'application/atom+xml', href => "/feeds/posts.atom", title => 'Recent Posts'; } - meta_ charset => 'utf-8'; meta_ name => 'csrf-token', content => auth->csrftoken; meta_ name => 'robots', content => 'noindex' if defined $o->{index} && !$o->{index}; diff --git a/lib/VNWeb/Misc/History.pm b/lib/VNWeb/Misc/History.pm index bb568c07..401a77aa 100644 --- a/lib/VNWeb/Misc/History.pm +++ b/lib/VNWeb/Misc/History.pm @@ -118,7 +118,7 @@ sub filters_ { r => { required => 0, default => 0, enum => [ 0, 1 ] }, # Include releases p => { page => 1 }, }}); - my $filt = tuwf->validate(get => $schema)->data; + my $filt = eval { tuwf->validate(get => $schema)->data } || tuwf->pass; $filt->{m} //= $type ? 0 : 1; # Exclude automated edits by default on the main 'recent changes' view. diff --git a/lib/VNWeb/User/RegReset.pm b/lib/VNWeb/User/RegReset.pm new file mode 100644 index 00000000..92808e95 --- /dev/null +++ b/lib/VNWeb/User/RegReset.pm @@ -0,0 +1,143 @@ +# User registration and password reset. These functions share some common code. +package VNWeb::User::RegReset; + +use VNWeb::Prelude; + + +# Generate some Elm code for the HTML5 validations, the Send and Recv types +# aren't used, they're simple enough to maintain manually. +elm_form RegReset => undef, form_compile(in => { + email => { email => 1 }, + password => { password => 1 }, + username => { username => 1 }, + vns => { uint => 1 }, +}); + + +TUWF::get '/u/newpass' => sub { + return tuwf->resRedirect('/', 'temp') if auth; + framework_ title => 'Password reset', index => 0, sub { + elm_ 'User.PassReset'; + }; +}; + + +json_api '/u/newpass', { + email => { email => 1 }, +}, sub { + my $data = shift; + + my($id, $token) = auth->resetpass($data->{email}); + return elm_BadEmail if !$id; + + my $name = tuwf->dbVali('SELECT username FROM users WHERE id =', \$id); + my $body = sprintf + "Hello %s," + ."\n\n" + ."Your VNDB.org login has been disabled, you can now set a new password by following the link below:" + ."\n\n" + ."%s" + ."\n\n" + ."Now don't forget your password again! :-)" + ."\n\n" + ."vndb.org", + $name, tuwf->reqBaseURI()."/u$id/setpass/$token"; + + tuwf->mail($body, + To => $data->{email}, + From => 'VNDB <noreply@vndb.org>', + Subject => "Password reset for $name", + ); + elm_Success +}; + + +# Compatibility with old the URL format +TUWF::get qr{/$RE{uid}/setpass}, sub { tuwf->resRedirect(sprintf('/u%d/setpass/%s', tuwf->capture('id'), tuwf->reqGet('t')||''), 'temp') }; + + +my $reset_url = qr{/$RE{uid}/setpass/(?<token>[a-f0-9]{40})}; + +TUWF::get $reset_url, sub { + return tuwf->resRedirect('/', 'temp') if auth; + + my $id = tuwf->capture('id'); + my $token = tuwf->capture('token'); + my $name = tuwf->dbVali('SELECT username FROM users WHERE id =', \$id); + + return tuwf->resNotFound if !$name || !auth->isvalidtoken($id, $token); + + framework_ title => 'Set password', index => 0, sub { + elm_ 'User.PassSet', tuwf->compile({}), tuwf->reqPath; + }; +}; + + +json_api $reset_url, { + password => { password => 1 }, +}, sub { + my $data = shift; + my $id = tuwf->capture('id'); + my $token = tuwf->capture('token'); + + return elm_InsecurePass if is_insecurepass($data->{password}); + die "Invalid reset token" if !auth->setpass($id, $token, undef, $data->{password}); + tuwf->dbExeci('UPDATE users SET email_confirmed = true WHERE id =', \$id); + elm_Success +}; + + +TUWF::get '/u/register', sub { + return tuwf->resRedirect('/', 'temp') if auth; + framework_ title => 'Register', index => 0, sub { + elm_ 'User.Register'; + }; +}; + + +json_api '/u/register', { + username => { username => 1 }, + email => { email => 1 }, + vns => { int => 1 }, +}, sub { + my $data = shift; + + my $num = tuwf->dbVali("SELECT count FROM stats_cache WHERE section = 'vn'"); + return elm_Bot if $data->{vns} < $num*0.995 || $data->{vns} > $num*1.005; + return elm_Taken if tuwf->dbVali('SELECT 1 FROM users WHERE username =', \$data->{username}); + return elm_DoubleEmail if tuwf->dbVali(select => sql_func user_emailexists => \$data->{email}); + + my $ip = tuwf->reqIP; + return elm_DoubleIP if tuwf->dbVali( + q{SELECT 1 FROM users WHERE registered >= NOW()-'1 day'::interval AND ip <<}, + $ip =~ /:/ ? \"$ip/48" : \"$ip/30" + ); + + my $id = tuwf->dbVali('INSERT INTO users', { + username => $data->{username}, + mail => $data->{email}, + ip => $ip, + }, 'RETURNING id'); + my(undef, $token) = auth->resetpass($data->{email}); + + my $body = sprintf + "Hello %s," + ."\n\n" + ."Someone has registered an account on VNDB.org with your email address. To confirm your registration, follow the link below." + ."\n\n" + ."%s" + ."\n\n" + ."If you don't remember creating an account on VNDB.org recently, please ignore this e-mail." + ."\n\n" + ."vndb.org", + $data->{username}, tuwf->reqBaseURI()."/u$id/setpass/$token"; + + tuwf->mail($body, + To => $data->{email}, + From => 'VNDB <noreply@vndb.org>', + Subject => "Confirm registration for $data->{username}", + ); + elm_Success +}; + +1; |