Rails backend / Vue 3 Frontend - Handle subdomains - ruby-on-rails

i'm currently building a multitenant SaaS application with Rails 7.
I started using rails as a monolith application without caring so much about frontend, the problem is that step by step it's getting bigger and bigger and i think it would be worth to split the backend and fronted to have a Rails REST API application and a Vue 3 app as frontend using vue-router
The problem that now i'm facing is this:
How can i handle different subdomains? I will explain myself better:
Every subdomain ('subdomain.example.com') will search for the correct tenant looking based on the subdomain
If the subdomain is 'www' or there's no subdomain then the landing page / blog will be shown
Possibly i would expand the check on a possible second subdomain (client.subdomain.example.com) this will be needed for a Cloudflare for SaaS configuration when it occurs to bind a client own domain (clientdomain.com) to my application
I've looked for a while and the only solution i found out is:
https://www.appsloveworld.com/vuejs/100/6/vue-js-vue-router-subdomains
so i tried to adapt it to my needs with something like this inside my main.js:
const app = createApp(App)
const host = window.location.host;
const parts = host.split('.');
const domainLength = 3; // route1.example.com => domain length = 3
const router = () => {
let routes;
console.log(parts[0] === 'www')
if (parts[0] === 'www') {
console.log('landing')
routes = landing_router;
} else if (parts.length === (domainLength - 1) && parts[0] != 'www') {
console.log('dashboard')
routes = dashboard_router;
} else {
console.log('landing')
routes = landing_router;
}
return routes;
};
app.use(createPinia())
app.use(router())
app.mount('#app')
And then i created 2 different routers (i'm not handling the third point of the list yet)
Is this solution good? Is there a better way to do it? Or should i drop the idea and stick to rails?
Thanks everyone

Related

Microsoft.Azure.Functions.Worker.Middleware.IFunctionsWorkerMiddleware Unit Testing

Looking for some examples on how to setup unit testing around IFunctionsWorkerMiddleware. All the examples I've found are for MVC and not function apps.
Example for MVC, but looking for .net 6 using function apps
using var host = await new HostBuilder() .ConfigureWebHost(webBuilder => { webBuilder .UseTestServer() .ConfigureServices(services => { services.AddMyServices(); }) .Configure(app => { app.UseMiddleware<MyMiddleware>(); }); }) .StartAsync(); var response = await host.GetTestClient().GetAsync("/")
Thanks in advance.
In order to test the middleware we need to spin up a host using HostBuilder; as part of configuring host builder we can stipulate it needs to use the test server (an in-memory webserver).  This is what we will make request against an what should be executing the middleware. Every single example I've found and tried are all for MVC  and nothing for function apps.  and every attempt pretty much results in a gRPC issue (gRPC is spun uo by the function app process by default, I cannot find where/how to not set it up).

.NET Core 2.0 dual authentication (Mixing MVC and API)

I am a back-end developer who typically develops APIs and and administration panels all under the same project. I primary use Laravel/PHP, however I recently started delving into .NET Core.
In PHP (Laravel), I could tie my API endpoints and webpages to the same controller actions. For example, An API consumer should be able to create a blog post using the API endpoint. Also, the administrator should be able to create a blog post using the web UI, which should follow the same validation logic, etc.
Here is some pseudocode for the create action.
class BlogPostController
{
//Create a new blog post
function create(request)
{
authorizeAction(); //Make sure the current user is authorized to create a blog post
validateFields(request); //Make sure the posted data is valid for a blog post
//Create the blog post
blogPost = new BlogPost(request);
if(request.isAPI)
return json(blogPost); //Return the new blog post as a json string
return view(blogPost); //Return an HTML representation of the blog post (in administration panel)
}
}
In Laravel, since all routes can have different middleware, I could define two routes to the above action eg POST /api/blogpost and POST /blogpost, each with different authentication schemes. (api would authorize via a JWT token, and the web url would auth via a cookie).
I'm having trouble doing something similar in .NET Core since different authentication schemes cannot be applied via separate middleware as of Version 2 (as far as I know).
My solution to this problem would be adding two authentication schemes like so:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); // => remove default claims
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie(cfg=>cfg.SlidingExpiration = true)
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = Configuration["JwtIssuer"],
ValidAudience = Configuration["JwtIssuer"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
ClockSkew = TimeSpan.Zero
};
});
And then on my controller use:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme + "," + CookieAuthenticationDefaults.AuthenticationScheme)]
This would effectively challenge a JWT token and a cookie and use whichever one it found. This seems a bit messy to me (and a Microsoft employee would agree)
Am I approaching this whole problem wrong? To me it makes sense to keep all of the logic in one controller and just return json or HTML depending on the type of request. I could have a separate controller for the API endpoints and the webpages, but it seems like a lot of duplication of code. The other option would be to ditch the idea completely and just build an API/Single Page Application that consumes the API.
Is this simply bad design, or is there any elegant way of using different authentication schemes based on the route?

Angular 2 - How to have different values of a var/const for different environments?

I am developing a Angular 2/Ionic 2 + JEE 7 project and I have a very specific scenario:
I have a httpClient layer that encapsulates every call to backend, there is a const named REST_BASE_PATHthat I would like to point to my localhost when in development environment and to a specific address when in production.
That said I would like to know what is the best and most automatic way of accomplish that..
You could define a custom request options to centralize this:
export class AppRequestOptions extends BaseRequestOptions {
constructor(private #Inject('webApiBaseUrl') webApiBaseUrl:string) {
}
merge(options?:RequestOptionsArgs):RequestOptions {
options.url = this.webApiBaseUrl + options.url;
return super.merge(options);
}
}
The webApiBaseUrl value you inject could be defined when bootstrapping your application:
bootstrap(AppComponent, [
HTTP_PROVIDERS,
provide('webApiBaseUrl', { useValue: 'https://bookapi.apispark.net/v1' })
]);
Set base url for angular 2 http requests
You need to update the variable when package your application with the value for the production environment.
Here is a question regarding packaging that could help you at this level:
How do I actually deploy an Angular 2 + Typescript + systemjs app?

Refreshing the browser bypasses angular on routerProvider based URLs

I have a grails app backing an angularjs front-end. They are deployed as a single WAR. I've removed the context path from the app so that it runs on http://localhost:8080.
I have a list of articles and I have the $routeProvider setup to redirect / to /articles at which point the controller takes over and pulls the list via $http. Pretty standard stuff.
Initially, I was using the default location provider config in that hashes (#) are used in the URL. I've changed it via
$locationProvider.html5Mode(true);
and everything still works. However, if I change the URL directly in the address bar and hit enter, or if I just refresh the browser when it is at /articles, the server side takes over and I just get my list of articles as json. No angular. I understand why this happens and for now what I've done is detected a non-ajax request on the server and am issuing a redirect to / which will allow angular to kick into gear.
I'm wondering if this is the right thing. Or is there something else I can do that is a better practice.
Redirecting is the right solution.
I was able to make it work using url mapping. So far it works :-)
I started with something like this:
"/**" (controller: 'app', action: 'index')
with app/index being the angular app page. But this will also match everything else (e.g. /$controller/$action). I had to explicitly map each $controller/$action to the correct controller. Not so good... ;-)
To solve this problem I'm prefixing all uris with /client for angular routes and /server for grails uris. This makes url mapping easy and it helps to distinguish angular routes from template uris etc.
My final url mapping looks like this:
class UrlMappings {
static excludes = [
"/lib/**",
"/css/**",
"/js/**"
]
static mappings = {
// - all client uris (routes) will start with '/client/',
// - all server uris (load) will start with '/server/'
// redirect /$appName/ to /$appName/client
"/" (controller: 'redirect', action: 'redirectTo') {
to = '/client/'
permanent = true
}
// redirect any angular route
"/client/**" (controller: 'app', action: 'index')
// standard controller/action mapping
"/server/$controller/$action/$id?" {
constraints {
}
}
}
}
You can't redirect directly in the url mapping, so I use a simple controller:
class RedirectController {
def redirectTo () {
redirect (uri: params.to, permanent: params.permanent)
}
}
The routing entries look like this:
$routeProvider.when ('/client/login', {templateUrl: './server/security/login'});
just answered on this question here
angularjs html5mode refresh page get 404
be sure that you rewrite rules on server work correctly

ASP.net MVC SPA routing

I'm planning to build a SPA with asp.net MVC4 but I'm not quite sure how I have to Setup my Project because of the Routing. Most SPA's work with hashrouting like this mypage/#/News/today for instance.
What would happen if the browses directly to mypage/News/today if I haven't specified a Controller named News with an action today?
The App should handle both types of Routing, how can I achieve this?
Do I have to build my App in a classic way, like Adding several Controllers with appropriate Actions and views and also build a clientside MVC structure with knockout, jquery etc?
You'll have to let all routes to "pages" fall through to let your SPA handle them (including essentially fake 404s if it's not to a real page in your SPA), but at the same time, need to make sure that you get the correct responses for API calls and/or file requests.
Below is the setup I have (I am using Vue as the js framework but that doesn't matter much for this, and not at all for the server-side piece).
First, add this to your Startup.cs, in addition to your default route setup:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.Use(async (context, next) =>
{
await next();
var path = context.Request.Path.Value;
// If there's no available file and the request doesn't contain an extension, we're probably trying to access a page
if (context.Response.StatusCode == (int)HttpStatusCode.NotFound && !Path.HasExtension(path) && !path.StartsWith("/api"))
{
context.Request.Path = "/Home/SpaRedirect"; // attempts to redirect to the URL within the SPA
context.Response.StatusCode = (int)HttpStatusCode.OK; // Make sure we update the status code, otherwise it returns 404
await next();
}
});
...
}
So the newly added SpaRedirect to HomeController looks like this, and just stores the requested URL in ViewData...
public IActionResult SpaRedirect()
{
ViewData["RequestUrl"] = HttpContext.Request.Path;
return View("Index");
}
Then in Index.cshtml, just capture that requested url in session storage so we have it available on the client-side:
<script src="~/dist/main.js" asp-append-version="true">
sessionStorage.setItem("redirectAttempt", #ViewData["RequestUrl"]);
</script>
Then in your boot script file (the entry-point for your SPA), add something like:
let redirectAttemptUrl = sessionStorage.getItem("redirectAttempt");
if (redirectAttemptUrl) {
router.push(redirectAttemptUrl);
sessionStorage.removeItem("redirectAttempt");
}
Which just checks for the presence of a requested url, and then the SPA's router attempts to navigate to it (in the example above it is a vue-router), then removes it from storage.
So this way, if a user attempts to navigate directly to a URL by entering it in the url bar (or via a bookmark) the app will load and take them to the right place, IF it exists... which takes us to the last piece...
Finally, you have to handle "404s" within your SPA, which is done by adding a catch-all route to your routes defs that takes user to a 404 component page you set up, which for Vue would look like this:
// adding an explicit 404 path as well for programmatically handling when something is not found within the app, i.e. return this.$router.push('/404')
{ path: '/404', component: NotFound, name: '404', alias: '*' }, // remove alias to not show the actual url that resulted in our little 404 here
{ path: '*', redirect: '/404' }, // this is the catch-all path to take us to our 404 page
Caveat: I'm no expert so could be missing something, would love additional comments on how to improve this. One thing that this doesn't handle is if the user is ALREADY in the SPA and for some reason edits the URL directly to navigate to someplace else, it would still trigger a server call and full reload, which ideally wouldn't be the case, but this is a pretty trivial issue I'd say.

Resources