Next.js and AWS Amplify getting Error: No Current User - getServerSideProps - graphql

getServerSideProps is displaying "No Current User", but I'm logged in my app.
type Post
#model
#key(
name: "postsByUsername"
fields: ["username"]
queryField: "postsByUsername"
)
#auth(
rules: [
{ allow: owner, ownerField: "username" }
{ allow: private, operations: [read] }
{ allow: public, operations: [read] }
]
) {
id: ID!
username: String
image: S3Object
}
I'm able to use AWS AppSync's website to actually query the posts by username, it returned values. I logged in using the same login I'm in for my app.
export async function getServerSideProps() {
const SSR = withSSRContext();
const { data } = await SSR.API.graphql({
query: postsByUsername,
variables: {
username: "username" // checked in dynamodb, same username as signed in.
},
authMode: "AMAZON_COGNITO_USER_POOLS",
});
return {
props: {
posts: data.postsByUsername.items,
},
};
}
I've also added ssr in my _app.js:
import { Amplify } from "aws-amplify";
import awsExports from "../../aws-exports";
Amplify.configure({ ...awsExports, ssr: true });
in aws-exports.js:
const awsmobile = {
"aws_project_region": "xxxxx",
"aws_appsync_graphqlEndpoint": "https://...",
"aws_appsync_region": "xxxxx",
"aws_appsync_authenticationType": "AMAZON_COGNITO_USER_POOLS",
"aws_appsync_apiKey": "xxx-xxxxxxxx...",
"aws_cognito_identity_pool_id": "xxxxxx",
"aws_cognito_region": "xxxxx",
"aws_user_pools_id": "xxxxx",
"aws_user_pools_web_client_id": "xxxxxx",
"oauth": {},
"aws_user_files_s3_bucket": "xxxxxx",
"aws_user_files_s3_bucket_region": "xxxxxx"
};
everything in awsmobile were autogenerated.

You need to pass through the request context in your withSSRContext function, so your getServerSideProps function should look as follows
export async function getServerSideProps({ req }) {
const SSR = withSSRContext({ req });
...
}
Reference:
https://docs.amplify.aws/lib/ssr/q/platform/js/#withssrcontext

Related

Strapi update username from custom controller

I am trying to create a custom controller to update the user profile.
I created the routing file and the corresponding controller.
Routing file: server/src/api/profile/routes/profile.js
module.exports = {
routes: [
{
method: 'GET',
path: '/profile',
handler: 'profile.getProfile',
},
{
method: 'PUT',
path: '/profile',
handler: 'profile.updateProfile',
},
]
}
Controller: src/api/profile/controllers/profile.js
async updateProfile(ctx) {
try {
const { id } = ctx.state?.user;
const user = strapi.query('admin::user').update({
where: { id },
data: {
username: "testUsername"
}
})
ctx.body = "User updated"
} catch(error) {
ctx.badRequest("Something went wrong", { error })
}
},
The above code returns "User updated", but the username does not update. I am executing the PUT call with a correct Bearer authorisation token and the user permissions for that user are set to enable "updateProfile".
Oddly enough, the same code, when changed to update a different API item, works perfectly fine:
async updateArticle(ctx) {
try {
const { id } = ctx.state?.user;
const article = strapi.query('api::article.article').update({
where: { author: id },
data: {
title: "New title"
}
})
ctx.body = article
} catch(error) {
ctx.badRequest("Something went wrong", { error })
}
},
I am also confused by different syntaxes appearing in the official Strapi documentation, for example some docs mention:
strapi.query('admin::user').update({ id }, data)
But in other places in the documentation its:
strapi.plugins['users-permissions'].services.user.update({ id });
And then elsewhere:
strapi.query('user', 'users-permissions').update(params, values);
Another question is: do I need to sanitise the input / output in any way? If yes, how? Importing sanitizeEntity from "Strapi-utils" doesn't work, but it's mentioned in several places on the internet.
Additionally, I cannot find a list of all ctx properties. Where can I read what is the difference between ctx.body and ctx.send?
The lack of good documentation is really hindering my development. Any help with this will be greatly appreciated.

Apollo cache redirect for field with no arguments

I have a login mutation tha looks similiar to this:
mutation login($password: String!, $email: String) {
login(password: $password, email: $email) {
jwt
user {
account {
id
email
}
}
}
}
On the other hand, I have a query for getting the account details. The backend verifies which user it is by means of the JWT token that is send with the request, so no need for sending the account id as an argument.
query GetUser {
user {
account {
id
email
}
}
}
The issue I am facing is now: Apollo is making a network request every time as GetUser has no argument. I would prever to query from cache first. So I thought, I could redirect as described here.
First, I was facing the issue that user field does not return an id directly so I have defined a type policy as such:
const typePolicies: TypePolicies = {
User: {
keyFields: ["account", ["id"]],
},
}
So regarding the redirect I have add the following to the type policy:
const typePolicies: TypePolicies = {
User: {
keyFields: ["account", ["id"]],
},
Query: {
fields: {
user(_, { toReference }) {
return toReference({
__typename: "User",
account: {
id: "1234",
},
})
},
},
},
}
This works, however there is a fixed id of course. Is there any way to solve this issue by always redirecting to the user object that was queried during login?
Or is it better to add the id as argument to the GetUser query?
I have solve this by means of the readField function:
Query: {
fields: {
user(_, { toReference, readField }) {
return readField({
fieldName: "user",
from: toReference({
__typename: "LoginMutationResponse",
errors: null,
}),
})
},
},
},
What happens if the reference cannot be found? It seems like apollo is not making a request then. Is that right? How can this be solved?
I have noticed this in my tests as the login query is not executed.

AppSync with AWS4 and mixed authentication type (UserPools and IAM)

I have an AppSync GraphQL API defined. The API is using Cognito UserPools as primary authentication. This part works fine.
I am trying to execute the request from the Nodejs Lambda function using axios and aws4. The mutation I am trying to hit is configure something like this in my schema (omitting most of the schema):
AppSyncSchema:
Type: AWS::AppSync::GraphQLSchema
Properties:
ApiId: !GetAtt MyApi.ApiId
Definition: |
type Something {
someId: ID!
}
input SomethingInput {
someId: ID!
}
type Mutation {
doSomething(input: SomethingInput!): Something #aws_iam
}
I have configure the Lambda function execution role to have appsync:GraphQL permission:
"Action": [
"appsync:GraphQL"
],
"Resource": "arn:aws:appsync:eu-west-2:xxxx:apis/yyyy/*",
"Effect": "Allow",
"Sid": "AllowAppSyncExecution"
From different articles online, I have put together some request that my Nodejs Typescript function then tries to execute:
const doSomething = `mutation doSomething($input: SomethingInput!) {
doSomething(input: $input) {
someId
}
}`;
const data = {
operationName: 'doSomething',
query: doSomething,
variables: {
input: { someId: 'abc' },
},
};
const body = JSON.stringify(data);
const signOptions = {
method: 'POST',
url: 'https://zzzz.appsync-api.eu-west-2.amazonaws.com/graphql,
host: 'zzzz.appsync-api.eu-west-2.amazonaws.com',
path: '/graphql',
region: process.env.AWS_REGION,
service: 'appsync',
headers: {
'content-type': 'application/json',
},
body, // used by aws4
data, // used by axios
};
const creds = {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
sessionToken: process.env.AWS_SESSION_TOKEN,
};
const signed = aws4.sign(signOptions, creds);
delete signed.headers.Host;
delete signed.headers['Content-Length'];
console.log(signed);
console.log('Got signed headers');
try {
await axios(signed);
} catch (err) {
console.error('Fialed to execute AppSync request', err);
throw err;
}
I am always getting a Request failed with status code 401 response back. The AppSync logs don't reveal much more:
{
"logType": "RequestSummary",
"requestId": "11111111",
"graphQLAPIId": "some id",
"statusCode": 401,
"latency": 7079000
}
What am I missing in configuring this correctly? Is there something I need to add to allow AWS IAM only for a specific mutation or do you see any issue with the way I am trying to execute the request?
These are the articles I was referencing:
manually-triggering-appsync-apollo-subscriptions
calling-amazon-api-gateway-authenticated-methods-with-axios-and-aws4
Got it working. I had to add additional authentication provider to the AWS::AppSync::GraphQLApi. In CloudFormation, this looks like an additional property:
Type: AWS::AppSync::GraphQLApi
Properties:
Name: !Ref MyApi
AuthenticationType: AMAZON_COGNITO_USER_POOLS
UserPoolConfig:
UserPoolId: !Ref MyPool
AwsRegion: !Ref MyRegion
DefaultAction: ALLOW
AdditionalAuthenticationProviders:
- AuthenticationType: AWS_IAM
After making this work, I got a response from AppSync GraphQL but it contained GraphQL errors for all fields in the response object:
Not Authorized to access someId on type Something
To get around this I had to also allow IAM on this type in the GraphQL Schema:
type Something #aws_cognito_user_pools #aws_iam {
someId: ID!
}

Relay commitUpdate callback with follow-up mutation and missing fragment

I have two GraphQL/Relay mutations that work fine separately. The first one creates an item. The second one runs a procedure for connecting two items.
GraphQL
createOrganization(
input: CreateOrganizationInput!
): CreateOrganizationPayload
createOrganizationMember(
input: CreateOrganizationMemberInput!
): CreateOrganizationMemberPayload
input CreateOrganizationInput {
clientMutationId: String
organization: OrganizationInput!
}
input CreateOrganizationMemberInput {
clientMutationId: String
organizationMember: OrganizationMemberInput!
}
# Represents a user’s membership in an organization.
input OrganizationMemberInput {
# The organization which the user is a part of.
organizationId: Uuid!
# The user who is a member of the given organization.
memberId: Uuid!
}
type CreateOrganizationPayload {
clientMutationId: String
# The `Organization` that was created by this mutation.
organization: Organization
# An edge for our `Organization`. May be used by Relay 1.
organizationEdge(
orderBy: OrganizationsOrderBy = PRIMARY_KEY_ASC
): OrganizationsEdge
# Our root query field type. Allows us to run any query from our mutation payload.
query: Query
}
I would like to be able to run the createOrganization mutation and then connect the user to the organization with the createOrganizationMember mutation. The second mutation takes two arguments, one of which is the newly created edge.
I tried passing the edge into the mutation, but it expects the mutation to be able to getFragment. How can I get the fragment for the payload edge so it can be passed into a mutation?
React-Relay
Relay.Store.commitUpdate(
new CreateOrganizationMutation({
organizationData: data,
user,
query,
}), {
onSuccess: response => {
Relay.Store.commitUpdate(
new CreateOrganizationMemberMutation({
organization: response.createOrganization.organizationEdge.node,
user,
})
);
},
}
);
fragments: {
user: () => Relay.QL`
fragment on User {
${CreateOrganizationMutation.getFragment('user')},
${CreateOrganizationMemberMutation.getFragment('user')},
}
`,
I solved this problem without changing any GraphQL:
I created a new Relay container, route, and queries object. It is configured as a
child route for the container where the first of two mutation occurs. The id for
the new edge is passed as a parameter via the route pathname. A router state
variable is also passed.
Routes
import {Route} from 'react-router';
function prepareProfileParams (params, {location}) {
return {
...params,
userId: localStorage.getItem('user_uuid'),
};
}
// ProfileContainer has the component CreateOrganizationForm, which calls
// the createOrganization mutation
<Route
path={'profile'}
component={ProfileContainer}
queries={ProfileQueries}
prepareParams={prepareProfileParams}
onEnter={loginBouncer}
renderLoading={renderLoading}
>
<Route path={'join-organization'}>
<Route
path={':organizationId'}
component={JoinOrganizationContainer}
queries={JoinOrganizationQueries}
renderLoading={renderLoading}
/>
</Route>
</Route>
CreateOrganizationForm.js
Relay.Store.commitUpdate(
new CreateOrganizationMutation({
organizationData: data,
user,
query,
}), {
onSuccess: response => {
const organizationId = response.createOrganization.organizationEdge.node.rowId;
router.push({
pathname: `/profile/join-organization/${organizationId}`,
state: {
isAdmin: true,
},
});
},
}
);
The new Relay container JoinOrganizationContainer will hook into a lifecycle
method to call the second mutation that we needed. The second mutation has an
onSuccess callback which does router.push to the page for the new object we
created with the first mutation.
JoinOrganizationContainer.js
import React from 'react';
import Relay from 'react-relay';
import CreateOrganizationMemberMutation from './mutations/CreateOrganizationMemberMutation';
class JoinOrganizationContainer extends React.Component {
static propTypes = {
user: React.PropTypes.object,
organization: React.PropTypes.object,
};
static contextTypes = {
router: React.PropTypes.object,
location: React.PropTypes.object,
};
componentWillMount () {
const {user, organization} = this.props;
const {router, location} = this.context;
Relay.Store.commitUpdate(
new CreateOrganizationMemberMutation({
user,
organization,
isAdmin: location.state.isAdmin,
}), {
onSuccess: response => {
router.replace(`/organization/${organization.id}`);
},
}
);
}
render () {
console.log('Joining organization...');
return null;
}
}
export default Relay.createContainer(JoinOrganizationContainer, {
initialVariables: {
userId: null,
organizationId: null,
},
fragments: {
user: () => Relay.QL`
fragment on User {
${CreateOrganizationMemberMutation.getFragment('user')},
}
`,
organization: () => Relay.QL`
fragment on Organization {
id,
${CreateOrganizationMemberMutation.getFragment('organization')},
}
`,
},
});
JoinOrganizationQueries.js
import Relay from 'react-relay';
export default {
user: () => Relay.QL`
query { userByRowId(rowId: $userId) }
`,
organization: () => Relay.QL`
query { organizationByRowId(rowId: $organizationId) }
`,
};
One unexpected benefit of doing things this way is that there is now a shareable url that can be used as an invite link for joining an organization in this app. If the user is logged in and goes to the link: <host>/profile/join-organization/<organizationRowId>, the mutation will run that joins the person as a member. In this use case, router.state.isAdmin is false, so the new membership will be disabled as an admin.

Relayjs Graphql user authentication

Is it possible to authenticate users with different roles solely trough a graphql server in combination with relay & react?
I looked around, and couldn't find much info about this topic.
In my current setup, the login features with different roles, are still going trough a traditional REST API... ('secured' with json web tokens).
I did it in one of my app, basically you just need a User Interface, this one return null on the first root query if nobody is logged in, and you can then update it with a login mutation passing in the credentials.
The main problem is to get cookies or session inside the post relay request since it does'nt handle the cookie field in the request.
Here is my client mutation:
export default class LoginMutation extends Relay.Mutation {
static fragments = {
user: () => Relay.QL`
fragment on User {
id,
mail
}
`,
};
getMutation() {
return Relay.QL`mutation{Login}`;
}
getVariables() {
return {
mail: this.props.credentials.pseudo,
password: this.props.credentials.password,
};
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
user: this.props.user.id,
}
}];
}
getOptimisticResponse() {
return {
mail: this.props.credentials.pseudo,
};
}
getFatQuery() {
return Relay.QL`
fragment on LoginPayload {
user {
userID,
mail
}
}
`;
}
}
and here is my schema side mutation
var LoginMutation = mutationWithClientMutationId({
name: 'Login',
inputFields: {
mail: {
type: new GraphQLNonNull(GraphQLString)
},
password: {
type: new GraphQLNonNull(GraphQLString)
}
},
outputFields: {
user: {
type: GraphQLUser,
resolve: (newUser) => newUser
}
},
mutateAndGetPayload: (credentials, {
rootValue
}) => co(function*() {
var newUser = yield getUserByCredentials(credentials, rootValue);
console.log('schema:loginmutation');
delete newUser.id;
return newUser;
})
});
to keep my users logged through page refresh I send my own request and fill it with a cookie field... This is for now the only way to make it work...

Resources