summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--elm/Lib/Api.elm5
-rw-r--r--elm/User/Login.elm2
-rw-r--r--elm/User/PassReset.elm89
-rw-r--r--elm/User/PassSet.elm94
-rw-r--r--elm/User/Register.elm116
-rw-r--r--lib/VNDB/Handler/Users.pm220
-rw-r--r--lib/VNWeb/Elm.pm13
-rw-r--r--lib/VNWeb/HTML.pm9
-rw-r--r--lib/VNWeb/Misc/History.pm2
-rw-r--r--lib/VNWeb/User/RegReset.pm143
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} => \&register,
- qr{u/register/done} => \&register_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;