New MVC controller and view work on local but give a 404 error when published to dev

Robert Bell asked on December 11, 2020 18:53

I'm working on adding reset password functionality on our K12 site. I have created a simple controller and view that accepts the userid and reset token as parameters and displays a simple password/confirm password view to reset the users password. The URL is sent to the user via a email template. This is in a area called Coordinators. I'm using IIS 10 on local and dev.

This works locally when testing but when loaded to the dev server I get a 404 error. I have used RouteDebugger and can see the route on the dev site, but it will not give route information on a 404 page. The route works locally though.

I have added a second controller/view as a test incase there is a issue loading a backend resource. This controller just displays the text "Index". I get the same result. It works locally but fails on the dev server. Interesting note: If I log into the dev sever and access the page it works, but using the same URL from my local PC fails with the 404 error.

Local and the dev site point to the same Kentico database and the web.config files match in both environments.

I am missing something but I haven't been able to figure it out. Do I need to do something with Kentico or is this an IIS issue?

Below are examples of the code. reset path:

... /Coordinators/resetpassword/Reset?userId=3779&token=AQAAANCMnd8BFd ...

Route class:

public class CoordinatorsAreaRegistration : AreaRegistration 
{
    public override string AreaName 
    {
        get 
        {
            return "Coordinators";
        }
    }

    public override void RegisterArea(AreaRegistrationContext context) 
    {
        context.MapRoute(
            "Coordinator Search",
            "Coordinators/search",
            new { controller = "Search", action = "Index", searchText = UrlParameter.Optional, pageNumber = UrlParameter.Optional, indexes = "BCIndex;BCFileIndex" }
            );

        context.MapRoute(
            "Coordinator_Login",
            "Coordinators/login/{action}",
            new { controller = "login", action = "Index" }
            );

        context.MapRoute(
            "Coordinator_Logout",
            "Coordinators/logout/{action}",
            new { controller = "Logout", action = "Index" }
            );

        context.MapRoute(
            "Coordinator_Register",
            "Coordinators/register/{action}",
            new { controller = "register", action = "Index" }
            );

        context.MapRoute(
            "Coordinator_Reset_Password",
            "Coordinators/resetpassword/{action}",
            new { controller = "ResetPassword", action = "Index" }
            );

        context.MapRoute(
            "Coordinator_Forgot_Password",
            "Coordinators/forgotpassword/{action}",
            new { controller = "ForgotPassword", action = "Index" }
            );

        context.MapRoute(
            "Coordinator_UnlockAccount",
            "Coordinators/unlockaccount/{action}",
            new { controller = "UnlockAccount", action = "Index" }
            );

        context.MapRoute(
            "Coordinator_Profile",
            "Coordinators/profile/{action}",
            new { controller = "profile", action = "Index" }
            );

        context.MapRoute(
            "Coordinator_Directory",
            "Coordinators/user-directory/{action}",
            new { controller = "directory", action = "Index" }
            );

        context.MapRoute(
            "Coordinator_Default",
            "Coordinators/{controller}/{action}"
            );
    }
}

Controller class:

public class ResetPasswordController : BaseController
{
    public IAccountManager _accountManager { get; set; }

    public ILocalizationService _localizationService { get; }

    public ResetPasswordController(IAccountManager accountManager,
            IBusinessDependencies dependencies)
            : base(dependencies)
    {
        _accountManager = accountManager ?? throw new ArgumentNullException(nameof(accountManager));
        _localizationService = dependencies.LocalizationService ?? throw new ArgumentNullException(nameof(dependencies.LocalizationService));
    }

    [AllowAnonymous]
    [HttpGet]
    public ActionResult Index()
    {
        ResetPasswordViewModel model = new ResetPasswordViewModel();
        return View(model);
    }

    [AllowAnonymous]
    [HttpGet]
    public async Task<ActionResult> Reset(int? userId, string token)
    {
        var model = new ResetPasswordViewModel();
        IdentityManagerResult<ResetPasswordResultState, ResetPasswordViewModel> result = new IdentityManagerResult<ResetPasswordResultState, ResetPasswordViewModel>();
        try
        {
            // Verifies the parameters of the password reset request
            // True if the token is valid for the specified user, false if the token is invalid or has expired
            // By default, the generated tokens are single-use and expire in 1 day
            result = await _accountManager.VerifyResetPasswordTokenAsync(userId ?? 1, token);

            if (result.Success)
            {
                // If the password request is valid, displays the password reset form
                model = result.Data;
            }
            else
            {
                // If the password request is invalid, returns a view informing the user
                model.Status = _localizationService.Localize("membership.passwreset.failed");
            }
        }
        catch (InvalidOperationException)
        {
            // An InvalidOperationException occurs if a user with the given ID is not found
            // Returns a view informing the user that the password reset request is not valid
            model.Status = _localizationService.Localize("membership.passwreset.failed");
        }

        return View("Reset", model);
    }

    [AllowAnonymous]
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Reset(ResetPasswordViewModel model)
    {
        IdentityManagerResult<ResetPasswordResultState> result = new IdentityManagerResult<ResetPasswordResultState>();
        // Validates the received password data based on the view model
        if (ModelState.IsValid)
        {
            // setup the default status message
            result.Success = false;
            result.ResultState = ResetPasswordResultState.PasswordNotReset;
            model.Status = _localizationService.Localize("membership.passwordreset.failed");

            try
            {
                result = await _accountManager.ResetPasswordAsync(model);
                // Changes the user's password if the provided reset token is valid
                if (result.Success)
                {
                    // If the password change was successful, displays a view and email informing the user

                    var emailMessage = new EmailMessage();
                    var macroResolver = MacroResolver.GetInstance();
                    emailMessage.Recipients = model.UserName;
                    EmailSender.SendEmail(SiteContext.CurrentSiteName, emailMessage, "Membership.PasswordReset", macroResolver, false);


                    model.Status = _localizationService.Localize("membership.passwordreset.success").Replace("%username%", model.UserName);
                    return Redirect("/coordinators/login");
                }
                else
                {
                    // Occurs if the reset token is invalid
                    // Returns a view informing the user that the password reset failed
                    model.Status = _localizationService.Localize("membership.passwordreset.failed");
                }
            }
            catch (InvalidOperationException)
            {
                // An InvalidOperationException occurs if a user with the given ID is not found
                // Returns a view informing the user that the password reset failed
                model.Status = _localizationService.Localize("membership.passwordreset.failed");
            }
        }
        return View(model);
    }
}

View class(Reset.cshtml):

@model #####.Areas.Coordinators.Models.ResetPasswordViewModel
@{ Layout = "~/Areas/Coordinators/Views/Shared/_Layout.cshtml"; }

<div class="container">
    @using (Html.BeginForm())
    {
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div><span class="required-field"></span> - denotes a required field</div>
        @Html.AntiForgeryToken()
        <input type="hidden" name="ResetToken" value="@Model.ResetToken" />
        <input type="hidden" name="UserName" value="@Model.UserName" />
        <div class="form-group">
            @Html.LabelFor(m => m.Password) @if (ModelMetadata.FromStringExpression("UserName", ViewData).IsRequired)
            {<span class='required-field'></span>}
            @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" })
            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        </div>
        <div class="form-group">
            @Html.LabelFor(m => m.PasswordVerify) @if (ModelMetadata.FromStringExpression("UserName", ViewData).IsRequired)
            {<span class='required-field'></span>}
            @Html.ValidationMessageFor(m => m.PasswordVerify, "", new { @class = "text-danger" })
            @Html.PasswordFor(m => m.PasswordVerify, new { @class = "form-control" })
        </div>
        <div>
            <button type="submit" class="btn btn-green">Submit</button>
        </div>
        <div class="form-group">
            <span class="">@Html.DisplayFor(m => m.Status)</span>
        </div>
    }
</div>

Correct Answer

Robert Bell answered on December 16, 2020 00:06

The issue was discovered today.

The resetpassword view was calling the layout page and in the nav section the layout was trying to build a breadcrumb trail from Kentico pages. The reset password controller is out side of Kentico and returned a 404. This would not happen if the page was called locally only over the network. In my case I change the method to return EmptyResult() and the page is now loading.

I got thrown off by it working locally and expecting an error when remote debugging the site. Stepping through the layout page would have found the issue sooner.

Working Breadcrumb code:

    public ActionResult Index(string pagePath)
    {
        var page = _breadcrumbRepository.GetDocuments()
                .WhereEquals("DocumentUrlPath", pagePath)
                .Or()
                .WhereEquals("NodeAliasPath", pagePath)
                .PublishedVersion(false)
                .OnCurrentSite()
                .FirstOrDefault();
        var breadcrumbParts = new Dictionary<string, string>();

        if (page == null)
        {
            var pageAlias = _breadcrumbRepository.GetDocumentAliases().WhereEquals("AliasUrlPath", pagePath).FirstOrDefault();

            if (pageAlias == null)
                return new EmptyResult();

            page = _breadcrumbRepository.GetDocuments().WhereEquals("DocumentNodeId", pageAlias.AliasNodeID).FirstOrDefault();
        }

        if (page != null)
        {
            breadcrumbParts = page.DocumentsOnPath.Where(d => !string.IsNullOrEmpty(d.DocumentName)).ToDictionary(d => d.DocumentName, d => d.NodeAliasPath);
        }

        return PartialView("_breadcrumb", breadcrumbParts);
    }
}
0 votesVote for this answer Unmark Correct answer

Recent Answers


Brenden Kehren answered on December 14, 2020 19:35

Check your URL's in the Site's settings. Assuming you're working on localhost and have a domain name setup in your presentation URL, this will cause problems if you're sharing a database with the server code.

0 votesVote for this answer Mark as a Correct answer

Robert Bell answered on December 15, 2020 16:23

HI Brenden,

Thank you for responding, the dev server is the presentation URL in settings.

The other controllers work fine using local host, login, forgotpassword, etc., and they work when pushed to the dev server. I think there is something specific to resetpasswod that I'm not seeing. I should be able to input the URL manually to open the page and this is when I get the 404 on the dev server, but I hit the controller, and it works, when I debug locally, http://localhost/coordinators/ResetPassword/reset.

I'll check to see if I can get a development license for my pc and move away from localhost and test again.

0 votesVote for this answer Mark as a Correct answer

   Please, sign in to be able to submit a new answer.