In my Angular app, if I load the home page / and then navigate to, say, /products, it works fine (it's a lazy-loaded module). But if now I reload the page, the browser makes a GET /products call to the server, which results in a 404.
The solution is to send index.html and the Angular app is back on rails. So in Express I do app.all("*", (req,res) => { res.sendFile("index.html") }) and it works.
How to do the same thing in Nest?
There is a #All decorator, but each controller in a given component handles a subroute, for instance #Controller("cats") will match /cats routes, so if I add #All in this controller, it will match only /cats/*, not *.
Must I really create a whole separate module with a controller, just for this? That's what I did
#Controller() // Matches "/"
export class GenericController {
#All() // Matches "*" on all methods GET, POST...
genericFunction(){
console.log("Generic route reached")
}
}
And in my main module :
#Module({
imports: [
ItemsModule, // Other routes like /items
GenericModule, // Generic "*" route last
],
})
It works, but it seems overkill. Is this the way to go or is there a simpler trick?
So, will be best to use global-scoped exception filter.
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.useGlobalFilters(new NotFoundExceptionFilter());
await app.listen(3000);
}
bootstrap();
NotFoundExceptionFilter:
import { ExceptionFilter, Catch, NotFoundException } from '#nestjs/common';
import { HttpException } from '#nestjs/common';
#Catch(NotFoundException)
export class NotFoundExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
// here return `index.html`
}
}
Maybe it will not work, will test later
You don't need to create a separated GenericModule. However, GenericController is fully valid and you approach is definitely a good one. The question is rather what would you like to achieve using this generic route. If handling "Route not found" error is your requirement, a better choice is an exception filter.
An alternative answer in 2022.
I've solved this by specifying the routes in the order I want them evaluated. In my instance I am using a fallback route to catch all requests, but if I need custom processing I want to create a route which superceeds the fallback route.
However, in defining a catchall route /api/:resource in the AppController, I found the fallback route would overwrite all other routes.
My solution to this is to define the fallback route in it's own module and ensure that it is appended to the list of modules. This way it is created last and will only catch what falls through.
#router.ts
import {RouterModule} from '#nestjs/core/router';
import {ContentBlockModule} from './content_block/content_block.module';
import {FallbackModule} from './fallback/fallback.module';
const APIRoutesWithFallbackRoute = RouterModule.register([
{
// This lets me avoid prepending my routes with /api prefixes
path: 'api',
// Overload the /api/content_blocks route and foward it to the custom module
children: [
{
path: 'content_blocks',
module: ContentBlockModule,
},
],
},
{ //Fallback Route catches any post to /api/:resource
path: 'api',
module: FallbackModule,
},
]);
#app.module
App module imports the fallback module. Important Ensure FallbackModule is the last module to be declaired or it will overwrite routes that are included after it.
import {Module} from '#nestjs/common';
import {AppService} from './app.service';
import {APIRoutesWithFallbackRoute} from './APIRoutesWithFallbackRoute';
import {ContentBlockModule} from './content_block/content_block.module';
import {FallbackModule} from './fallback/fallback.module';
// APIRoutes include first, Fallback Routes prepended.
#Module({
imports: [APIRoutesWithFallbackRoute, ContentBlockModule, FallbackModule],
controllers: [],
providers: [AppService],
})
export class AppModule {}
FallbackController
import {Controller, Post, Req, Res} from '#nestjs/common';
import {defaultHandler} from 'ra-data-simple-prisma';
import {FallbackService} from './fallback.service';
#Controller()
export class FallbackController {
constructor(private readonly prisma: FallbackService) {}
#Post(':resource')
fallback(#Req() req, #Res() res) {
// return this.appService.getData();
console.log('executing from the default fallback route');
return defaultHandler(req, res, this.prisma);
}
}
ContentBlockController
The content block controller, included here for completeness.
#Controller()
export class ContentBlockController {
constructor(
private readonly contentBlockService: ContentBlockService,
private readonly prisma: PrismaService,
) {}
#Post()
async create(
#Body() contentBlock: content_blocks,
#Req() req: Request,
#Res() res: Response,
): Promise<void> {
console.log('executing from the resource specific route');
// lean on my service to do heavy business logic
const [model, values] = await this.contentBlockService.createContentBlock(
contentBlock,
);
// inject custom logic...
const alteredRequest: CreateRequest = {
...req,
body: {
...req.body,
params: {
data: values,
},
},
};
return createHandler(alteredRequest, res, model);
}
}
Using this system I am able to define a single route to handle 90% of the routes necessary to expose my Prisma models to my private API. And if I need custom logic I have full control.
Related
My script I'm using axios and vuex but it was necessary to make a change from formData to Json in the script and with that it's returning from the POST/loginB2B 200 api, but it doesn't insert in the localstorage so it doesn't direct to the dashboard page.
**Auth.js**
import axios from "axios";
const state = {
user: null,
};
const getters = {
isAuthenticated: (state) => !!state.user,
StateUser: (state) => state.user,
};
async LogIn({commit}, user) {
await axios.post("loginB2B", user);
await commit("setUser", user.get("email"));
},
async LogOut({ commit }) {
let user = null;
commit("logout", user);
},
};
**Login.vue**
methods: {
...mapActions(["LogIn"]),
async submit() {
/*const User = new FormData();
User.append("email", this.form.username)
User.append("password", this.form.password)*/
try {
await this.LogIn({
"email": this.form.username,
"password": this.form.password
})
this.$router.push("/dashboard")
this.showError = false
} catch (error) {
this.showError = true
}
},
},
app.vue
name: "App",
created() {
const currentPath = this.$router.history.current.path;
if (window.localStorage.getItem("authenticated") === "false") {
this.$router.push("/login");
}
if (currentPath === "/") {
this.$router.push("/dashboard");
}
},
};
The api /loginB2B returns 200 but it doesn't create the storage to redirect to the dashboard.
I use this example, but I need to pass json instead of formData:
https://www.smashingmagazine.com/2020/10/authentication-in-vue-js/
There are a couple of problems here:
You do a window.localStorage.getItem call, but you never do a window.localStorage.setItem call anywhere that we can see, so that item is probably always empty. There also does not seem to be a good reason to use localStorage here, because you can just access your vuex store. I noticed in the link you provided that they use the vuex-persistedstate package. This does store stuff in localStorage by default under the vuex key, but you should not manually query that.
You are using the created lifecycle hook in App.vue, which usually is the main component that is mounted when you start the application. This also means that the code in this lifecycle hook is executed before you log in, or really do anything in the application. Instead use Route Navigation Guards from vue-router (https://router.vuejs.org/guide/advanced/navigation-guards.html).
Unrelated, but you are not checking the response from your axios post call, which means you are relying on this call always returning a status code that is not between 200 and 299, and that nothing and no-one will ever change the range of status codes that result in an error and which codes result in a response. It's not uncommon to widen the range of "successful" status codes and perform their own global code based on that. It's also not uncommon for these kind of endpoints to return a 200 OK status code with a response body that indicates that no login took place, to make it easier on the frontend to display something useful to the user. That may result in people logging in with invalid credentials.
Unrelated, but vuex mutations are always synchronous. You never should await them.
There's no easy way to solve your problem, so I would suggest making it robust from the get-go.
To properly solve your issue I would suggest using a global navigation guard in router.js, mark with the meta key which routes require authentication and which do not, and let the global navigation guard decide if it lets you load a new route or not. It looks like the article you linked goes a similar route. For completeness sake I will post it here as well for anyone visiting.
First of all, modify your router file under router/index.js to contain meta information about the routes you include. Load the store by importing it from the file where you define your store. We will then use the Global Navigation Guard beforeEach to check if the user may continue to that route.
We define the requiresAuth meta key for each route to check if we need to redirect someone if they are not logged in.
router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import store from '../store';
Vue.use(VueRouter);
const routes = [
{
path: '/',
name: 'Dashboard',
component: Dashboard,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'Login',
component: Login,
meta: {
requiresAuth: false
}
}
];
// Create a router with the routes we just defined
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// This navigation guard is called everytime you go to a new route,
// including the first route you try to load
router.beforeEach((to, from, next) => {
// to is the route object that we want to go to
const requiresAuthentication = to.meta.requiresAuth;
// Figure out if we are logged in
const userIsLoggedIn = store.getters['isAuthenticated']; // (maybe auth/isAuthenticated if you are using modules)
if (
(!requiresAuthentication) ||
(requiresAuthentication && userIsLoggedIn)
) {
// We meet the requirements to go to our intended destination, so we call
// the function next without any arguments to go where we intended to go
next();
// Then we return so we do not run any other code
return;
}
// Oh dear, we did try to access a route while we did not have the required
// permissions. Let's redirect the user to the login page by calling next
// with an object like you would do with `this.$router.push(..)`.
next({ name: 'Login' });
});
export default router;
Now you can remove the created hook from App.vue. Now when you manually change the url in the address bar, or use this.$router.push(..) or this.$router.replace(..) it will check this function, and redirect you to the login page if you are not allowed to access it.
I found this question about determine the routes. While the first answer is exactly what I need, and it works
import { Controller, Get, Request } from "#nestjs/common";
import { Request as ExpressRequest, Router } from "express";
#Get()
root(#Request() req: ExpressRequest) {
const router = req.app._router as Router;
return {
routes: router.stack
.map(layer => {
if(layer.route) {
const path = layer.route?.path;
const method = layer.route?.stack[0].method;
return `${method.toUpperCase()} ${path}`
}
})
.filter(item => item !== undefined)
}
}
I want to be able to unit test this.
My end to end test works fine
it('/api (GET) test expected routes', async done => {
const ResponseData = await request(app.getHttpServer())
.get('/api')
.set('Accept', 'application/json');
expect(ResponseData.status).toBe(200);
expect(ResponseData.headers['content-type']).toContain('json');
expect(ResponseData.body.routes.length).toBeGreaterThan(2);
done(); // Call this to finish the test
});
The problem I am having, is how to create and pass the Request part that is needed for the root() call for a unit test. The ExpressRequest is not a class or anything to simply create, and then assign values. It is currently a large definition. I assume there must be an easy way to create one, but I have not found it yet.
You can make use of the #golevelup/ts-jest package to help create mocks of objects. It can take an interface as a generic and return an entire jest mock that is compatible with the type.
I have a vue.js app with a router that prevents the pages from been open without authorization using the following code:
import Router from 'vue-router';
import store from '../store/index';
function guardAuth(to, from, next) {
if (store.state.authorizationToken) {
next();
} else {
next({
name: 'login',
query: { redirect: to.fullPath },
});
}
}
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'toroot',
redirect: 'login',
},
{
path: '/overview',
component: Overview,
beforeEnter: guardAuth,
},
....
and a store mutation that is called when an API call fails:
import axios from 'axios';
import Cookies from 'js-cookie';
import router from '../router/index';
export default new Vuex.Store({
state: {
mutations: {
handleApiFail(state, err) {
if (err && !axios.isCancel(err) && state.authorizationToken) {
// Block subsequent logout calls.
state.authorizationToken = null;
// Clear the token cookie just in case.
Cookies.set('authorizationToken', null);
// Stop the current and subsequent requests.
state.cancellationSource.cancel('Authorization token has expired.');
router.push({ name: 'login', query: { expired: '1', redirect: window.location.pathname } });
}
},
as you can see from the code above 'router' imports 'store' and 'store' imports 'router' and as far as I see this causes 'store' to be undefined inside 'guardAuth()'. Obviously, I can get rid of this cyclic dependency by moving 'handleApiFail' to a separate '.js' file, but I am not sure that it is a good idea. Is there a better solution or some common approach for haling this sutiation? Should 'handleApiFail' be a mutation or a simple function? Can a mutation use 'router'? Do I really need to get rid of the cyclic dependency (for example, in C++ I does not)?
It be better handleapi fail in separate function than mutation. and if you want to check it before entering route. you could use beforeEnter() on your route.
check this docs about beforeEnter or another route properties
Store mutation methods should not perform any logic at all. Stores are only used to hold your global application state, they should not perform any logic like authorizing the user or navigating through your application. What you'll want to do is move the logic out of the store and into the component that does the authorization check. From there just do something like $store.commit('unauthorized') and $store.commit('authorized', user). Should look like this:
sendAuthRequest.then(
(success) => {
$store.commit('authorized', <userVariable>);
$router.push(...);
}, (failure) => {
$store.commit('unauthorized');
$router.push(...);
}
);
Instead of getting redirects from 301.json I want to make a request to my api which returns my json.
I am using the #nuxtjs/axios module.
const redirects = require('../301.json');
export default function (req, res, next) {
const redirect = redirects.find(r => r.from === req.url);
if (redirect) {
console.log('redirect: ${redirect.from} => ${redirect.to}');
res.writeHead(301, { Location: redirect.to });
res.end();
} else {
next();
}
}
Original answer
To build on #Dominooch's answer, if you want to return just JSON, you can use the .json() helper. It automatically sets the content-type to application/json and stringify's an object you pass it.
edit:
To clarify what we're doing here, we're replacing your 301.json entirely and using nuxt's way of creating middleware to:
define a generic handler that you can reuse for any route
defining explicitly which paths will use your handler (what I'm assuming you're 301.json is doing)
If 301.json is really just an array of paths that you want to redirect, then you can just use .map() but i'd personally not, because it's not immediately clear which paths are getting redirected (see my last sample)
That said, the very last thing I would avoid is making a global middleware (fires for every request) that checks to see if the path is included in your array. <- Will make route handling longer for each item in the array. Using .map() will make nuxt do the route matching for you (which it already does anyways) instead of sending every request through your handler.
// some-api-endpoint.js
import axios from 'axios'
export default {
path: '/endpoint'
handler: async (req, res) => {
const { data } = await axios.get('some-request')
res.json(data)
}
}
Then in your nuxt.config.js:
// nuxt.config.js
module.exports = {
// some other exported properties ...
serverMiddleware: [
{ path: '/endpoint', handler: '~/path/to/some-api-endpoint.js' },
]
}
If 301.json is really just an array of paths:
// nuxt.config.js
const routes = require('../301.json');
module.exports = {
// some other exported properties ...
serverMiddleware: routes.map(path =>
({ path, handler: '~/path/to/some-api-endpoint.js' }))
}
Or if you have other middleware:
// nuxt.config.js
const routes = require('../301.json');
module.exports = {
// some other exported properties ...
serverMiddleware: [
...routes.map(path =>
({ path, handler: '~/path/to/some-api-endpoint.js' })),
... // Other middlewares
}
Here's what I did and it seems to work:
//uri-path.js
import axios from 'axios'
export default {
path: '/uri/path',
async handler (req, res) {
const { data } = await axios.get('http://127.0.0.1:8000/uri/path')
res.setHeader('Content-Type', 'text/html')
res.end(data)
}
}
We are developing a component that handles OpenID Connect's implicit flow.
In step 5 of the flow, the "Authorization Server sends the End-User back to the Client with an ID Token and, if requested, an Access Token." We would like our component to handle that request, which will be to ~/openid-login.
How do we configure Aurelia to have it route to a function in our component?
export class OpenId {
// how do we route ~/openid-login to this?
public handleRequest() {
}
}
Note: Here is the work in progress.
Using a navStrategy within your routeConfig will allow you to do what ever you like before navigating to a page. See below:
import { autoinject } from 'aurelia-framework';
import { RouterConfiguration, Router, NavigationInstruction } from 'aurelia-router';
#autoinject
export class App {
router: Router;
configureRouter(config: RouterConfiguration, router: Router) {
let openIdNavStrat = (instruction: NavigationInstruction) => {
console.log('Do whatever we would like to do.');
// then redirect to where ever you would like.
instruction.config.moduleId = 'login';
}
config.map([
{ route: ['', 'login'], moduleId: 'login' },
{ route: 'openid-login', navigationStrategy: openIdNavStrat },
]);
this.router = router;
}
}
There is documentation on Navigation Strategies here: http://aurelia.io/hub.html#/doc/article/aurelia/router/latest/router-configuration/3