Strapi returns 404 for custom route only when deployed to Heroku - heroku

I have created a custom route in Strapi v4 called "user-screens". Locally I hit it with my FE code and it returns some data as expected. However when I deploy it to Heroku and attempt to access the endpoint with code also deployed to Heroku it returns a 404. I've tailed the Heroku logs and can see that the endpoint is hit on the server side, but the logs don't give anymore info other than it returned a 404.
I am doing other non custom route api calls and these all work fine on Heroku. I am able to auth, save the token, and hit the api with the JWT token and all other endpoints return data. This is only happening on my custom route when deployed to Heroku. I've set up cors with the appropriate origins, and I am wondering if I need to add something to my policies and middlewares in the custom route. I have verified the permissions and verified the route is accessible to authenticated users in the Strapi admin.
Here is my route:
module.exports = {
routes: [
{
method: "GET",
path: "/user-screens",
handler: "user-screens.getUserScreens",
config: {
policies: [],
middlewares: [],
},
},
],
};
And my controller:
"use strict";
/**
* A set of functions called "actions" for `user-screens`
*/
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
ctx.send(result);
});
},
};

For anyone facing this, the answer was to change how I returned the ctx response from a 'send' to a 'return' from the controller method. I am not sure why this works locally and not on Heroku, but this fixes it:
New controller code:
module.exports = {
getUserScreens: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
}
return strapi.entityService
.findMany("api::screen.screen", {
owner: user.id,
populate: ["image"],
})
.then((result) => {
return result;
})
.catch((error) => {
return error;
});
},
};

Related

Okta signout don't work in Angular Applicaiton

I am using OKTA SDK for the angular following enter link description here
this documentation. I am also using OktaCallbackComponent and OktaAuthService for authentication.
I can log in successfully. after a successful login OKTA redirects me to OktaCallbackComponent where they store some keys in localstroge and finally, I get navigated to my main page.
now when I click on the logout button from the application it does not work. As I see it the page loads and immediately navigates to the callback component and again navigates to the main page. whereas I want the login page should come to the user.
this is my logout function.
async logout(){
this.oktaAuth.tokenManager.clear()
await this.oktaAuth.signOut();
this.router.navigate(['/login']);
this.toastr.success('Logout Successfully', 'See you next time' , {timeOut: 5000});
}
can anyone help me with what could be the issue.
{
path: 'main',
component: OpDataTableComponent,
canActivate: [ OktaAuthGuard ],
data: {
title: 'Main Page'
}
},
{
path: CALLBACK_PATH,
component: OktaCallbackComponent,
// Later: Add a component
},
{
path: 'login',
// component: LoginComponent,
component:OktaLoginComponent,
canActivate: [checkAfterLoginService],
data: {
title: 'Login Page'
}
}
CheckAfterLoginService
export class checkAfterLoginService {
constructor(private oktaAuth: OktaAuthService,private tokenService: TokenService, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | Observable<boolean> | Promise<boolean> {
if(this.oktaAuth.isAuthenticated())
{
return this.router.navigateByUrl('/main');
}else
{
return false;
}
}
}
Okta configuration.
const ISSUER = 'https://...../oauth2/default';
const HOST = window.location.host;
const REDIRECT_URI = 'https://..../callback';
const SCOPES = 'openid profile email';
const config = {
issuer: ISSUER,
clientId: '.....',
redirectUri: REDIRECT_URI,
scopes: SCOPES.split(/\s+/)
};
P.s logout URL added to the application setting is https://../login route.
how can I solve the issue or what could be the issue? your help is much appreciated.
Try changing your logout() method to be as follows:
async logout(){
await this.oktaAuth.signOut();
this.router.navigate(['/login']);
this.toastr.success('Logout Successfully', 'See you next time' , {timeOut: 5000});
}
You're currently clearing the tokens manually, which makes our underlying Auth JS SDK thinking you've already logged out. this.oktaAuth.signOut() should clean up the tokens for you. If you still want to clear them manually, make sure and do it after signOut().
I had similar issues, but I wanted to redirect back to the okta login screen. This is what worked for my situation.
public logout(): void {
const oktaBaseUrl: string = `${environment.okta.issuer}/v1`;
const oktaTokenStorage: any = JSON.parse(localStorage.getItem('okta-token-storage'));
const oktaIdToken = oktaTokenStorage?.idToken;
this.oktaAuth.logout();
window.location.href = `${oktaBaseUrl}/logout?id_token_hint=${oktaIdToken.idToken}&post_logout_redirect_uri=${environment.okta.postLogoutRedirectUri}`;
}

Laravel Sanctum/Vuex Uncaught Error Vue Router Navigation Guard

Can anyone advise on this issue?
I've got a Laravel app with a Vue front-end, connecting to the API using Laravel Sanctum. (within the same application) I'm trying to set up the authentication guards so routes can only be accessed after authentication with the API.
I'm connecting through the state action like so:
async getAuthenticatedUser({ commit }, params) {
await axios.get('api/auth-user', { params })
.then(response => {
commit('SET_AUTHENTICATED', true)
commit('SET_AUTHENTICATED_USER', response.data)
localStorage.setItem('is-authenticated', 'true')
return Promise.resolve(response.data)
})
.catch(error => {
commit('SET_AUTHENTICATED', false)
commit('SET_AUTHENTICATED_USER', [])
localStorage.removeItem('is-authenticated')
return Promise.reject(error.response.data)
})
},
The state authenticated property is set as follows:
const state = () => ({
authenticated: localStorage.getItem('is-authenticated') || false,
authUser: [],
})
I have the following guard checking the auth. If I'm not signed in the app correctly redirects me back to the login screen when I access a route with the requiresAuth attribute.
However when I attempt to log in, I get Redirected when going from "/login" to "/dashboard" via a navigation guard.
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => record.meta.requiresAuth)) {
if (store.getters["Auth/isAuthenticated"]) {
next()
return
}
next('/login')
}
if (to.matched.some((record) => record.meta.requiresVisitor)) {
if (! store.getters["Auth/isAuthenticated"]) {
next()
return
}
next('/dashboard')
}
next()
})
If it's safe to ignore, and you are using vue-router ^3.4.0, you can do:
import VueRouter from 'vue-router'
const { isNavigationFailure, NavigationFailureType } = VueRouter
...
this.$router.push(fullPath).catch(error => {
if (!isNavigationFailure(error, NavigationFailureType.redirected)) {
throw Error(error)
}
})

AngularFireAuthGuard redirectUrl after login

I use firebase and AngularFireAuthGuard to protect specific routes, so that only authenticated users are allowed to access them.
In particular, my MainComponent and MgmtComponent should only be accessible to AUTHENTICATED users.
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['/login']);
const routes: Routes = [
{ path: 'teams/:teamId/sessions/:sessionId',
component: MainComponent,
canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectUnauthorizedToLogin }
},
{ path: 'mgmt',
component: MgmtComponent,
canActivate: [AngularFireAuthGuard], data: { authGuardPipe: redirectUnauthorizedToLogin }
},
{
path: 'login',
component: LoginComponent
}
];
My Problem is, that the user is not redirected back to the originally requested URL, after a successful login.
So what I want/expect is:
user goes to /mgmt
as the user is not authenticated he is automatically redirected to /login
user authenticates (e.g. via google or Facebook OAuth)
user is automatically redirected back to the originally requested page (/mgmt)
Steps 1-3 work fine, but step 4 is missing.
Now that the feature request is in, you can do this using the auth guard. However, the docs are unclear, so here is how I did it.
/** add redirect URL to login */
const redirectUnauthorizedToLogin = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
return redirectUnauthorizedTo(`/login?redirectTo=${state.url}`);
};
/** Uses the redirectTo query parameter if available to redirect logged in users, or defaults to '/' */
const redirectLoggedInToPreviousPage = (next: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
let redirectUrl = '/';
try {
const redirectToUrl = new URL(state.url, location.origin);
const params = new URLSearchParams(redirectToUrl.search);
redirectUrl = params.get('redirectTo') || '/';
} catch (err) {
// invalid URL
}
return redirectLoggedInTo(redirectUrl);
};
This is an open feature request, the angularfire team is working on it: https://github.com/angular/angularfire/pull/2448
Meanwhile I found this workaround:
In the app-routing-module.ts instead of
const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['/login']);
I use following to store the url in the sessionStorage:
const redirectUnauthorizedToLogin = (route: ActivatedRouteSnapshot) => {
const path = route.pathFromRoot.map(v => v.url.map(segment => segment.toString()).join('/')).join('/');
return pipe(
loggedIn,
tap((isLoggedIn) => {
if (!isLoggedIn) {
console.log('Saving afterLogin path', path);
sessionStorage.setItem('afterLogin', path);
}
}),
map(loggedIn => loggedIn || ['/login'])
);
};
In the LoginComponent I read the value from the session storage to redirect:
sessionStorage.getItem('afterLogin');
this.router.navigateByUrl(redirectUrl);

GraphQL mutation "Cannot set headers after they are sent to the client"

I'm implementing graphql login mutation to authenticate user login credential. Mutation verifies the password with bcrypt then sends a cookie to the client, which will render user profile based on whether the cookie is a buyer or owner user).
GraphQL Login Mutation Code:
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
loginUser: {
type: UserType,
args: {
email: { type: GraphQLString },
password: { type: GraphQLString }
},
resolve: function (parent, args, { req, res }) {
User.findOne({ email: args.email }, (err, user) => {
if (user) {
bcrypt.compare(args.password, user.password).then(isMatch => {
if (isMatch) {
if (!user.owner) {
res.cookie('cookie', "buyer", { maxAge: 900000, httpOnly: false, path: '/' });
} else {
res.cookie('cookie', "owner", { maxAge: 900000, httpOnly: false, path: '/' });
}
return res.status(200).json('Successful login');
} else {
console.log('Incorrect password');
}
});
}
});
}
}
}
});
Server.js:
app.use("/graphql",
(req, res) => {
return graphqlHTTP({
schema,
graphiql: true,
context: { req, res },
})(req, res);
});
Error message:
(node:10630) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
[0] at ServerResponse.setHeader (_http_outgoing.js:470:11)
[0] at ServerResponse.header (/Users/xxx/xxx/server/node_modules/express/lib/response.js:771:10)
[0] at ServerResponse.append (/Users/xxx/xxx/server/node_modules/express/lib/response.js:732:15)
[0] at ServerResponse.res.cookie (/Users/xxx/xxx/server/node_modules/express/lib/response.js:857:8)
[0] at bcrypt.compare.then.isMatch (/Users/xxx/xxx/server/schema/schema.js:89:41)
I've done some research on this error, but can't seem to find a relevant answer. The issue seems to lie within response body being executing more than once, thus "cannot set headers after they are sent to the client". Since I'm sending both res.cookie() and res.status(200), how could I fix this problem?
express-graphql already sets the status and sends a response for you -- there's no need to call either res.status or res.json inside your resolver.
GraphQL always returns a status of 200, unless the requested query was invalid, in which case it returns a status of 400. If errors occur while executing the request, they will be included the response (in an errors array separate from the returned data) but the status will still be 200. This is all by design -- see additional discussion here.
Instead of calling res.json, your resolver should return a value of the appropriate type (in this particular case UserType), or a Promise that will resolve to this value.
Additionally, you shouldn't utilize callbacks inside resolvers since they are not compatible with Promises. If the bcrypt library you're using supports using Promises, use the appropriate API. If it doesn't, switch to a library that does (like bcryptjs) or wrap your callback inside a Promise. Ditto for whatever ORM you're using.
In the end, your resolver should look something like this:
resolve: function (parent, args, { req, res }) {
const user = await User.findOne({ email: args.email })
if (user) {
const isMatch = await bcrypt.compare(args.password, user.password)
if (isMatch) {
const cookieValue = user.owner ? 'owner' : 'buyer'
res.cookie('cookie', cookieValue, { maxAge: 900000, httpOnly: false, path: '/' })
return user
}
}
// If you want an error returned in the response, just throw it
throw new Error('Invalid credentials')
}

strapi - restrict user to fetch only data related to him

Usually, a logged-in user gets all entries of a Content Type.
I created a "snippets" content type (_id,name,content,users<<->>snippets)
<<->> means "has and belongs to many" relation.
I created some test users and make a request:
curl -H 'Authorization: Bearer eyJ...' http://localhost:1337/snippets/
Main Problem: an authenticated user should only see the entries assigned to him. Instead, a logged-in user gets all snippets, which is bad.
How is it possible to modify the fetchAll(ctx.query); query to take that into account so it does something like fetchAll(ctx.state.user.id); at the /-route->find-method ?
The basic find method is here:
find: async (ctx) => {
if (ctx.query._q) {
return strapi.services.snippet.search(ctx.query);
} else {
return strapi.services.snippet.fetchAll(ctx.query);
}
},
Sub-Question: Does strapi even know which user is logged in when I do Bearer-Token Authentication ?
You could set up a /snippets/me route under the snippets config.
That route could call the Snippets.me controller method which would check for the user then query snippets based on the user.
So in api/snippet/config/routes.json there would be something like :
{
"method": "GET",
"path": "/snippets/me",
"handler": "Snippets.me",
"config": {
"policies": []
}
},
Then in the controller (api/snippet/controllers/Snippet.js), you could do something like:
me: async (ctx) => {
const user = ctx.state.user;
if (!user) {
return ctx.badRequest(null, [{ messages: [{ id: 'No authorization header was found' }] }]);
}
const data = await strapi.services.snippet.fetch({user:user.id});
if(!data){
return ctx.notFound();
}
ctx.send(data);
},
Then you would give authenticated users permissions for the me route not for the overall snippets route.
The above is correct, except with newer versions of strapi. Use find and not fetch :)
const data = await strapi.services.snippet.find({ user: user.id });
Strapi v3.0.0-beta.20
A possibility would be to extend the query used by find and findOne in the controllers with a restriction regarding the logged in user. In this case you might also want to adapt the count endpoint to be consistent.
This would result in:
withOwnerQuery: (ctx, ownerPath) => {
const user = ctx.state.user;
if (!user) {
ctx.badRequest(null, [
{ messages: [{ id: "No authorization header was found" }] },
]);
return null;
}
return { ...ctx.query, [ownerPath]: user.id };
};
find: async (ctx) => {
ctx.query = withOwnerQuery(ctx, "owner.id");
if (ctx.query._q) {
return strapi.services.snippet.search(ctx.query);
} else {
return strapi.services.snippet.fetchAll(ctx.query);
}
},
// analogous for for findOne
Depending on your usage of controllers and services you could achieve the same thing via adapting the service methods.
This kind of solution would work with the GraphQL plugin.

Resources