#28 Multiple Razor Page Request Handlers
Thursday, April 12, 2018
In this article we will create the end points for the user to be able to register and submit a request when/if they forgot the password/username. In order to do this we will be confronted with a problem when trying to post back to a razor page that is not the origin of the request. To solve this problem we add multiple get/post handlers to our razor pages and see how we can tell the server which handler we are trying to reach.
Parts
- Part 30: User Database
- Part 29: Canonical Url
- Part 28: Razor Page Handlers
- Part 27: Send Screen Finished
- Part 26: Stroke Dashoffset
- Part 25: Send Screen
- Part 24: Forgot Link Behaviour
- Part 23: Animating Controls (cont.)
- Part 22: Animating Controls
- Part 21: Hiding Control Groups
- Part 20: ModelState Errors (cont.)
- Part 19: ModelState Errors
- Part 18: Animating Info Pages (cont.)
- Part 17: Animating Info Pages
- Part 16: Keyboard Navigation
- Part 15: Accessing the DOM
- Part 14: All About the Username
- Part 13: CSRF Attacks
- Part 12: Http Requests
- Part 11: HMR
- Part 10: Color Inputs And Buttons
- Part 9: Animating Sub-Actions
- Part 8: Form Validation (cont.)
- Part 7: Form Validation
- Part 6: Form Group
- Part 5: Authenticator Validators
- Part 4: Authenticator Inputs
- Part 3: First Angular Component
- Part 2: Webpack
- Part 1: Beginning
You can view a completed example of the authenticator component here.
Adding additional request methods
-
WebUi
-
Source
-
components
-
authenticator
- authenticator.component.ts
- authenticator-http.service.ts
-
authenticator
-
components
-
Source
First thing we are going to do is add the ability to make a request to the server for registering, forgot password and
forgot username (a). To make it easier to take advantage of the type
system we will define a few additional interfaces for the payload for these request. Now that we have the ability to
make these request we need to modify our authenticator component to use them. We just need to modify our
onClickSend
method to make the appropriate request
(b).
authenticator-http.service.ts (1)
...
export interface IForgotPayload {
email: string;
}
interface ILoginRegisterPayload {
password: string;
username: string;
}
export interface ILoginPayload extends ILoginRegisterPayload {
rememberme: boolean;
}
export interface IRegisterPayload extends ILoginRegisterPayload {
email: string;
emailConfirm: string;
passwordConfirm: string;
}
export class AuthenticatorHttpService ... {
...
public forgotPassword = (data: IForgotPayload, success: (response: Response) => void, error: (response: Response) => void, $finally?: (response: Response) => void) => {
this.post(AuthenticatorHttpService.forgotPasswordUrl, data, success, error, $finally);
}
public forgotUsername = (data: IForgotPayload, success: (response: Response) => void, error: (response: Response) => void, $finally?: (response: Response) => void) => {
this.post(AuthenticatorHttpService.forgotUsernameUrl, data, success, error, $finally);
}
...
public register = (data: IRegisterPayload, success: (response: Response) => void, error: (response: Response) => void, $finally?: (response: Response) => void) => {
this.post(AuthenticatorHttpService.registerUrl, data, success, error, $finally);
}
...
}
authenticator.component.ts (1)
...
export class AuthenticatorComponent ... {
...
private onClickSend = () => {
...
if (this.isLogin) {
if (this.isSubActionForgotPassword) {
this.forgotPassword();
return;
}
if (this.isSubActionForgotUsername) {
this.forgotUsername();
return;
}
this.login();
} else if (this.isRegister) {
this.register();
}
}
...
private forgotPassword = () => {
this._authenticatorHttpService.forgotPassword(
{
email: this.controlEmail.value
},
() => {
this._sendScreen.success("Request successful you will be redirected shortly.");
},
() => {
this.xhrFailure();
this._sendScreen.failure("Error using the provided email.");
});
}
private forgotUsername = () => {
this._authenticatorHttpService.forgotUsername(
{
email: this.controlEmail.value
},
() => {
this._sendScreen.success("Request successful you will be redirected shortly.");
},
() => {
this.xhrFailure();
this._sendScreen.failure("Error using the provided email.");
});
}
...
private login = () => {
...
this._authenticatorHttpService.login(this._xhrData,
() => {
this._sendScreen.success("Login successful you will be redirected shortly.");
},
...);
}
...
private register = () => {
this._authenticatorHttpService.register(
{
email: this.controlEmail.value,
emailConfirm: this.controlEmailConfirm.value,
password: this.controlPassword.value,
passwordConfirm: this.controlPasswordConfirm.value,
username: this.controlUsername.value
},
() => {
this._sendScreen.success("Registration successful you will be redirected shortly.");
},
() => {
this.xhrFailure();
this._sendScreen.failure("Error using the provided information.");
});
}
...
}
With those changes made we can now make a register request to the correct end point with the required payload (c), as well as, a forgot password request (d) and a forgot username request (e). You may also notice the regsiter request returns a status code of 200 and the other two return 404. This is because many articles back we created razor pages for both the login and register pages but we have not created them for the forgot requests which we will take care of next.
Exception processing 404
-
WebUi
-
Source
-
services
- http.service.ts
-
services
-
Source
Currently if the server returns an response without a response object when our base http service attempts to process it an exception is thrown. To deal with this we are going use a try catch block within in the process error method (f).
http.service.ts (1)
...
export abstract class HttpService ... {
...
private processError = (response: Response) => {
let json: IErrorResponse;
try {
json = response.json() as IErrorResponse;
} catch (e) {
return;
}
...
}
...
}
The forgot razor pages
-
WebUi
-
Pages
-
Account
- Forgot-Password.cshtml
- Forgot-Username.cshtml
-
Account
-
Pages
Next thing we are going to do is create the forgot password and forgot username razor pages. For my urls I like to have a hyphen
separate words to make it easier to read but if you try to create a razor page with a hyphen in it Visual Studio will either
raise and error or replace the hyphen with an underscore depending on how you create the page. In order to get around this
problem I create the pages without the hyphen and once it is created I rename the page itself to whatever I wanted it to be
originally. For example in the present case I will create two pages, one named ForgotPassword.cshtml
and the other named ForgotUsername.cshtml
. Once they are created I will rename them to
Forgot-Password.cshtml
(g) and
Forgot-Username.cshtml
(h).
Forgot-Password.cshtml (1)
@page
@{
ViewBag.Title = "Forgot Password Email Sent";
}
<div>
<h1>@ViewBag.Title</h1>
<p>
Please check your email to reset your password. If you have not received the email shortly please check your spam folder.
</p>
</div>
Forgot-Username.cshtml (1)
@page
@{
ViewBag.Title = "Forgot Username Email Sent";
}
<div>
<h1>@ViewBag.Title</h1>
<p>
Please check your email for your username. If you have not received the email shortly please check your spam folder.
</p>
</div>
The forgot and register request models
-
WebUi
-
Models
-
Account
- AccountForgotRequest.cs
- AccountRegisterRequest.cs
-
Account
-
Models
When a user makes a register or forgot password/username request we will need to be able to bind the information that they send in to a c# class just as we have previously done for login requests. This means it is now time to create a couple of classes that we can bind forgot requests (k) and register requests (l) to. In both models we have also include some data annotations on the properties to aid in validation. We will add more latter on if needed.
AccountForgotRequest.cs (1)
using System.ComponentModel.DataAnnotations;
namespace WebUi.Models.Account
{
public class AccountForgotRequest
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
}
AccountRegisterRequest.cs (1)
using System.ComponentModel.DataAnnotations;
namespace WebUi.Models.Account
{
public class AccountRegisterRequest
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Compare("Email", ErrorMessage = "The email and confirmation email must match.")]
public string EmailConfirm { get; set; }
[Required]
public string Password { get; set; }
[Compare("Password", ErrorMessage = "The password and confirmation password must match.")]
public string PasswordConfirm { get; set; }
[MaxLength(100, ErrorMessage = "Maximum length is 100")]
[MinLength(8, ErrorMessage = "Minimum length is 8")]
public string Username { get; set; }
}
}
Time for the post methods
-
WebUi
-
Pages
-
Account
- Forgot-Password.cshtml.cs
- Forgot-Username.cshtml.cs
- Register.cshtml.cs
-
Account
-
Pages
With everything in place it is now time to create the post methods for sending the forgot password (m), forgot username (n) and register (o) requests to. Each of them is a simple method where we are just returning an ok response that should contain the information that was sent in so that we can test that the binding is happening properly.
Forgot-Password.cshtml.cs (1)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebUi.Models.Account;
namespace WebUi.Pages.Account
{
public class ForgotPasswordModel : PageModel
{
public void OnGet()
{
}
public IActionResult OnPost([FromBody] AccountForgotRequest request)
{
return new OkObjectResult(request);
}
}
}
Forgot-Username.cshtml.cs (1)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebUi.Models.Account;
namespace WebUi.Pages.Account
{
public class ForgotUsernameModel : PageModel
{
public void OnGet()
{
}
public IActionResult OnPost([FromBody] AccountForgotRequest request)
{
return new OkObjectResult(request);
}
}
}
Register.cshtml.cs (1)
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using WebUi.Models.Account;
namespace WebUi.Pages.Account
{
public class RegisterModel : PageModel
{
public void OnGet()
{
}
public IActionResult OnPost([FromBody] AccountRegisterRequest request)
{
return new OkObjectResult(request);
}
}
}
The register request and response behave just as we expect but unfortunately when a forgot request is made (p) no matter what we try to do the response is always to send back the get view (p) even though we can clearly see that we are making a post request to the correct end point.
Down but not out
-
WebUi
-
Controllers
- AccountController.cs
-
Pages
-
Account
- Forgot-Password.cshtml.cs
- Forgot-Username.cshtml.cs
- Login.cshtml.cs
- Register.cshtml.cs
-
Account
-
Controllers
If I stop and think about it what I was attempting to do might actually be contrary to the intended use of the razer pages in the first place. The razor page is supposed to symbolize a physical page so we should probably be responding to every request that can be made on that page in the code behind for the page itself. To achieve this we will remove the post request method from the forgot password (r) and (s) code behinds, as well as, removing the check username action from the account controller (t).
Forgot-Password.cshtml.cs (2)
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WebUi.Pages.Account
{
public class ForgotPasswordModel : PageModel
{
public void OnGet()
{
}
}
}
Forgot-Username.cshtml.cs (2)
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace WebUi.Pages.Account
{
public class ForgotUsernameModel : PageModel
{
public void OnGet()
{
}
}
}
AccountController.cs
using Microsoft.AspNetCore.Mvc;
namespace WebUi.Controllers
{
[Route("account")]
public class AccountController : Controller
{
}
}
Since the forgot actions are sub-actions that the user can perform when trying to login we will move them to the login code behind (u) and for the same reason we will move the check username availability action to the register code behind (v).
Login.cshtml.cs (1)
...
namespace WebUi.Pages.Account
{
public class LoginModel : PageModel
{
...
public IActionResult OnPostForgotPassword([FromBody] AccountForgotRequest request)
{
return new OkObjectResult(new
{
message = "forgot password",
request
});
}
public IActionResult OnPostForgotUsername([FromBody] AccountForgotRequest request)
{
return new OkObjectResult(new
{
message = "forgot username",
request
});
}
}
}
Register.cshtml.cs (2)
using System.Threading;
...
namespace WebUi.Pages.Account
{
public class RegisterModel : PageModel
{
...
public IActionResult OnGetCheckUsernameAvailability(string username)
{
Thread.Sleep(2000);
if (username == "aaaaaaaa")
{
return new BadRequestObjectResult(new
{
Message = username
});
}
return new OkObjectResult(new
{
Message = username
});
}
...
}
}
Don't forget the anti-forgery token
-
WebUi
- Startup.cs
We do not want to forget that we need to check for the anti-forgery token when a user performs a non-safe action such as a post request. Previously we were doing this by applying an attribute to any action that we wanted to check the token for before performing the action but there is a better way. We will apply the attribute globally in the startup class (w). Since the attribute is being applied globally we will be eliminating the risk that we forget to apply it when we need to and in addition to this it will automatically not be applied to idempotent actions such as get requests.
Startup.cs (1)
...
using Microsoft.AspNetCore.Mvc;
...
namespace WebUi
{
public class Startup
{
public void ConfigureServices(...)
{
...
services.AddMvc(x =>
{
x.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
}
}
}
Just to be sure the framework is still looking for our anti-forgery token we can temporarily remove it from our request headers and perform a login request. When we do this we see the server responds with a 400 bad request (x). Once we once again add the token to the request and resend it we receive a 200 ok response (y).
Razor page handler
-
WebUi
-
Source
-
components
-
authenticator
- authenticator-http.service.ts
-
authenticator
-
services
- http.service.ts
-
components
-
Source
Now how do we tell the framework just what end point we are trying to reach? We do this by including a key value pair within the query string of our request. Because of this we will start by changing the get and post methods of our base http service (z). We will change the methods so that they take in an object with the required properties instead a comma delimited list of arguments, which we probably should have done originally anyway. In addition to all of the arguments that we originally had we will include an optional argument that we can use to specify the name of the handler that we want to to handle the request. If we specify the handler the base service will automatically include it within the query string of the request for both the get and post requests. With those changes we need to update our authenticator http service (aa).
http.service.ts (2)
...
import loDashIsNil = require("lodash/isNil");
...
interface IHttpRequest {
data: Object;
error: (response: Response) => void;
finally?: (response: Response) => void;
razorPageHandler?: string;
success: (response: Response) => void;
url: string;
}
...
export abstract class HttpService ... {
...
protected get = (request: IHttpRequest) => {
const data = request.data as any;
if (loDashIsNil(request.razorPageHandler) === false) {
data.handler = request.razorPageHandler;
}
this.doPromise(this.http.get(this.urlWithQueryString(request.url, data), this._requestOptions), request);
}
protected post = (request: IHttpRequest) => {
const url = loDashIsNil(request.razorPageHandler)
? request.url
: this.urlWithQueryString(request.url, { handler: request.razorPageHandler });
this.doPromise(this.http.post(url, request.data, this._requestOptions), request);
}
private doPromise = (observable: Observable<Response>, request: IHttpRequest) => {
observable
.toPromise()
.then((response: Response) => {
request.success(response);
if (loDashIsNil(request.finally) === false) {
request.finally(response);
}
})
.catch((response: Response) => {
this.processError(response);
request.error(response);
if (loDashIsNil(request.finally) === false) {
request.finally(response);
}
});
}
...
}
authenticator-http.service.ts (2)
...
export class AuthenticatorHttpService ... {
private static readonly checkUsernameUrl = "/account/check-username-availability"; // <- Remove
private static readonly forgotPasswordUrl = "/account/forgot-password"; // <- Remove
private static readonly forgotUsernameUrl = "/account/forgot-username"; // <- Remove
...
public checkUsernameAvailability = (...) => {
this.get({
data: { username: username },
error: error,
finally: $finally,
razorPageHandler: "CheckUsernameAvailability",
success: success,
url: AuthenticatorHttpService.registerUrl
});
}
public forgotPassword = (...) => {
this.post({
data: data,
error: error,
finally: $finally,
razorPageHandler: "ForgotPassword",
success: success,
url: AuthenticatorHttpService.loginUrl
});
}
public forgotUsername = (...) => {
this.post({
data: data,
error: error,
finally: $finally,
razorPageHandler: "ForgotUsername",
success: success,
url: AuthenticatorHttpService.loginUrl
});
}
public login = (...) => {
this.post({
data: data,
error: error,
finally: $finally,
success: success,
url: AuthenticatorHttpService.loginUrl
});
}
public register = (...) => {
this.post({
data: data,
error: error,
finally: $finally,
success: success,
url: AuthenticatorHttpService.registerUrl
});
}
}
Now that we can specify the handler when we send in a forgot password request the framework correctly binds it and returns it (bb) and does the same for a forgot username request (cc).