So, I have an MVC5 site that uses the default routing template {controller}/{action}/{id} and this works fine. Most everything in the site requires a login (i.e. [Authorize] attribute is used almost everywhere), and this works fine.
Well, now I have a need to allow anonymous access to select pages when a certain kind of link pattern is used: App/{token}/{action}. The {token} is a random string associated with something in my database. I can issue and deactivate these tokens at will.
I got this new App/{token}/{action} routing working by implementing a custom RouteBase that parses the incoming URL for these tokens, and, crucially, adds the the token value to the RouteData.DataTokens so that my App controller can make use of it without needing an explicit action argument for it. So, I added this new route to the route table ahead of the default routing like this:
// new route here
routes.Add("AppToken", new AnonAppAccessRoute());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Here is the problem/question: adding this now route has now made my default route stop working -- everything is now going through AnonAppAccessRoute which of course is meant to work only for a few things. I don't understand how to make my AnonAppAccessRoute apply only to URLs with a certain pattern. The MapRoute method accepts a URL pattern, but Adding a route doesn't seem to let you put a filter on it. What am I missing? I've looked around quite a bit at various blogs and documentation about routing, but I've not found good info about using the DataTokens collection (which I feel is important to my approach), and I'm not seeing a good explanation of the difference between Adding a route explicitly vs calling MapRoute.
Here's the code of my custom RouteBase:
public class AnonAppAccessRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string[] pathElements = httpContext.Request.Path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if (pathElements.Length > 0)
{
string token = TryGetArrayElement(pathElements, 1);
if (!string.IsNullOrEmpty(token))
{
result = new RouteData(this, new MvcRouteHandler());
result.DataTokens.Add("appToken", token);
result.Values.Add("controller", "App");
result.Values.Add("action", TryGetArrayElement(pathElements, 2, "Index"));
}
}
return result;
}
private string TryGetArrayElement(string[] array, int index, string defaultValue = null)
{
try
{
return array[index];
}
catch
{
return defaultValue;
}
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
I got this to work by dropping the custom RouteBase and instead used this MapRoute call like this:
routes.MapRoute(
name: "AppAnon",
url: "App/{token}/{action}",
defaults: new { controller = "App", action = "Index" }
);
Then, in my App controller, I did this in the Initialize override:
protected AppToken _appToken = null;
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
string token = requestContext.RouteData.Values["token"]?.ToString();
_appToken = Db.FindWhere<AppToken>("[Token]=#token", new { token });
if (!_appToken?.IsActive ?? false) throw new Exception("The token is not found or inactive.");
}
This way, my "token" is available to all controller actions via the _appToken variable, and already validated. I did not need to use RouteData.DataTokens. Note that my Db.FindWhere statement is ORM-specific and not really related to the question -- it's just how I look up a database record.
Related
I'm trying to get the edit URL of a content as a string from backend, the catch is I'm inside a workflow activity, so I can't use Url.Action... or Url.ItemEditLink... or other UrlHelpers as if it were a controller or a view. Also, although I'm inside a workflow, the contents I need it for are not part of the workflowContext or the activityContext, so I can't use those or tokens either.
A solution could be to get the content metadata and the site baseUrl and try to build it manually, but I think this way is prone to errors.
Thanks.
This is how I build a Uri in an activity:
public class MyClass : Task
{
private readonly RequestContext _requestContext;
...
public MyActivity(RequestContext requestContext, ...)
{
_requestContext = requestContext;
...
}
...
public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext)
{
var content = ... get using ID
var helper = new UrlHelper(_requestContext);
var baseurl = new Uri(_orchardServices.WorkContext.CurrentSite.BaseUrl);
Uri completeurl = new Uri(baseurl, helper.ItemDisplayUrl(content));
yield return T("Done");
}
}
Turns out that I actually do build the Uri semi-manually, but I haven't had issues with this method. You may be able to use just the ItemDisplayUrl for navigation inside of Orchard; I had to get the full URL because the string gets sent to an outside program (Slack).
I'm trying to implement some domain name logic in my existing MVC5 app. The problem I'm running in to is if I try to use my custom subclass from Route, it doesn't respect the Namespaces field and throws an error because I have 2 different User controllers.
As a control, this works perfectly fine:
routes.MapRoute("Login",
"login/",
new { controller = "User", action = "Login" },
new[] { "Quotes.Web.Controllers" });
My DomainRoute class inherits from Route and just adds a Domain property. Here is the relevant constructor:
public DomainRoute(string domain, string url, object defaults, string[] namespaces = null)
: base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
Domain = domain;
DataTokens = new RouteValueDictionary {["Namespaces"] = namespaces};
}
and I register it like:
var loginRoute = new DomainRoute(
domain,
"login/",
new { controller = "User", action = "Login" },
new[] { "Quotes.Web.Controllers" });
routes.Add("Login", loginRoute);
DataTokens looks identical between the working version and my broken version yet it seems to ignore the fact that my DomainRoute has a Namespace entry
Multiple types were found that match the controller named 'User'. This can happen if the route that services this request ('login/') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
What am I missing?
I think,this will help you, i had the same issue, solved this by adding the below code
var dataTokens = new RouteValueDictionary();
var ns = new string[] {"MyProject.Controllers"};
dataTokens["Namespaces"] = ns;
routes.Add("Default", new CultureRoute(
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
null /*constraints*/,
dataTokens
));
I switched my DomainRoute class with the much improved version found here: https://gist.github.com/IDisposable/77f11c6f7693f9d181bb
Now my route creation is just:
var clientRoutes = new DomainRouteCollection("mydomain",
"Quotes.Web.Controllers",
routes);
clientRoutes.MapRoute("Login", "login/", new { controller = "User", action = "Login" });
...which is more concise and even more importantly, it works.
In My MVC5 application I have 3 areas. My project structure as following
I have implemented an ActionFilter class to validate whether user has granted the permission for particular action methods. My ActionFilter class stay out of areas folder. I want to check user permission within the OnActionExecuting method and redirect to PermissionDenied action method which has implemented on ErrorControl. However it does not recognize within areas and gives an error message mentioning "No controller and action method found within the area"
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if(!GrantPermission(filterContext))
{
Controller contr = (BaseController)filterContext.Controller;
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary {
{ "area", "" },
{ "controller", "Error" },
{ "action", "PermissionDenied" }
});
filterContext.Result.ExecuteResult(contr.ControllerContext);
}
base.OnActionExecuting(filterContext);
}
Can anyone help me to get this solved. this one already has ruined my day.
Area is not a route value. It is put into the RouteData.DataTokens dictionary instead. But there is no way to set it from RedirectToRouteResult.
Instead, you could use the UrlHelper to generate the URL much as you would in an ActionLink. The UrlHelper will work out what the virtual path of your Area is. Then, you can just use RedirectResult to get to that URL.
if (!GrantPermission(filterContext))
{
Controller contr = (BaseController)filterContext.Controller;
var urlHelper = new UrlHelper(filterContext.RequestContext);
var redirectUrl = urlHelper.Action("PermissionDenied", "Error", new { area = "" });
filterContext.Result = new RedirectResult(redirectUrl);
filterContext.Result.ExecuteResult(contr.ControllerContext);
}
base.OnActionExecuting(filterContext);
NOTE: The correct way to authorize an action is to use either IAuthorizationFilter or better yet, inherit AuthorizeAttribute. Authorization filters run before action filters do. Also, they will execute the result handler automatically for you.
This question already has answers here:
Create route for root path, '/', with ServiceStack
(3 answers)
Closed 3 years ago.
I've got a Fallback DTO that looks like the following:
[FallbackRoute("/{Path*}")]
public class Fallback
{
public string Path { get; set; }
}
Now, in my Service I would like to redirect to an HTML5 compliant URL, and this is what I've tried:
public object Get(Fallback fallback)
{
return this.Redirect("/#!/" + fallback.Path);
}
It is working all fine and dandy, except for the fact that query parameters are not passed along with the path. Using Request.QueryString does not work as no matter what I do it is empty. Here's what my current (non-working) solution looks like:
public object Get(Fallback fallback)
{
StringBuilder sb = new StringBuilder("?");
foreach (KeyValuePair<string, string> item in Request.QueryString)
{
sb.Append(item.Key).Append("=").Append(item.Value).Append("&");
}
var s = "/#!/" + fallback.Path + sb.ToString();
return this.Redirect(s);
}
TL;DR: I want to pass on query strings along with fallback path.
EDIT: It turns out I had two problems; now going to mysite.com/url/that/does/not/exist?random=param correctly redirects the request to mysite.com/#!/url/that/does/not/exist?random=param& after I changed the above loop to:
foreach (string key in Request.QueryString)
{
sb.Append(key).Append("=").Append(Request.QueryString[key]).Append("&");
}
But the fallback is still not being called at root, meaning mysite.com/?random=param won't trigger anything.
In essence, what I want to do is to have ServiceStack look for query strings at root, e.g., mysite.com/?key=value, apply some logic and then fire off a redirect. The purpose of this is in order for crawler bots to be able to query the site with a _escaped_fragment_ parameter and then be presented with an HTML snapshot prepared by a server. This is in order for the bots to be able to index single-page applications (more on this).
I'm thinking perhaps the FallbackRoute function won't cover this and I need to resort to overriding the CatchAllHandler.
I managed to find a solution thanks to this answer.
First create an EndpointHostConfig object in your AppHost:
var config = new EndpointHostConfig
{
...
};
Then, add a RawHttpHandler:
config.RawHttpHandlers.Add(r =>
{
var crawl = r.QueryString["_escaped_fragment_"];
if (crawl != null)
{
HttpContext.Current.RewritePath("/location_of_snapshots/" + crawl);
}
return null;
});
Going to mysite.com/?_escaped_fragment_=home?key=value will fire off a redirection to mysite.com/location_of_snapshots/home?key=value, which should satisfy the AJAX crawling bots.
N.B. It's possible some logic needs to be applied to the redirection to ensure that there won't be double forward slashes. I have yet to test that.
I am trying to make a custom route but I cannot get it working and even though everything seems okay it always returns 404.
So here are the route defined.
It is defined first before the default and according to route debugger this is the route that gets hit.(Matched Route: Game/{id}/{title})
routes.Add(
"GamesDefault",
new Route("Game/{id}/{title}",
new RouteValueDictionary(new { controller = "Games", action = "ShowGame" }),
new DefaultMvcRouteHandler(urlTranslator, urlFoundAction)));
Here is the path Im trying to reach: /Game/5/test
And this is the Controller declaration. The GamesController is placed in the Controllers folder and its view are in Views/Games/showGames.cshtml.
public GamesController()
{
}
public ActionResult ShowGames(int id, string title)
{
return View(title);
}
The DefaultMvcRouteHandler doesnt do anything fancy.
public class DefaultMvcRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MvcHandler(requestContext);
}
}
The default route works without problems, and I have tried everything I can find like changing the name of the route so it doesnt match any folders or anything like that.
If anyone have any ideas on what more to try I would be most grateful.
As per my comment you are passing incorrect default route values for the controller and action values.
Update your route like so:
routes.Add(
"GamesDefault",
new Route("Game/{id}/{title}",
new RouteValueDictionary(new { controller = "GamesController", action = "ShowGames" }),
new DefaultMvcRouteHandler(urlTranslator, urlFoundAction)));