Hey there,
I have a User model in my database where the schema looks something like this (only included some properties):
model User {
id String @id @default(uuid())
createdAt DateTime @default(now())
email String @unique
togglKey String? @unique
}
In my SDL the user is queryable and the users.sdl.ts looks something like this:
type User {
id: String!
createdAt: DateTime!
email: String!
togglKey: String
}
type Query {
user(id: String!): User @requireAuth
}
So far so good. The to query a user, you have to be authenticated. But the field togglKey
really should only be queryable by the user who that property belongs to.
I thought “well, let’s just do a custom directive for this”, but after studying the documentation I am in a bit of a bind:
- the validator directive does not have access to the resolved value
- the transformer cannot be async (which i would need to do, to check if the currentUser is also the one who the record is for).
Any ideas how to resolve this?
1 Like
Got something working here - let me know if there is a better way!
When inspecting the context
that I receive in the validator directive, there is the context.params.query
which contains the original GraphQL query which itself contains the id of the user that I want to query.
So, with a little regex magic I can extract the ID and then compare it to the id of the context.currentUser
.
This is what my directive now looks like:
import {
createValidatorDirective,
RedwoodGraphQLError,
ValidatorDirectiveFunc,
} from '@redwoodjs/graphql-server'
export const schema = gql`
"""
Use @requireSameUser to validate access to a field, query or mutation.
"""
directive @requireSameUser on FIELD_DEFINITION
`
const validate: ValidatorDirectiveFunc = ({ context }) => {
const regex = /id:\s*"([a-f\d-]+)"/;
// Use the match method to extract the id value
//@ts-ignore
const match = context.params.query.match(regex);
// The extracted id value will be in the second element of the match array
const requestedUserId = match ? match[1] : null;
if (requestedUserId !== context.currentUser?.id) throw new RedwoodGraphQLError("Sorry, can't let you do this.")
}
const requireSameUser = createValidatorDirective(schema, validate)
export default requireSameUser
1 Like
You could do something much simpler.
Instead of querying for a user by and seeing if the user id is the current user via a directive — just create an authenticated service called “me” or something that takes no params.
Then in the service fetch the user profile data for the context.currentUser.id.
Or if you want to protect any other service never pass in the user id — just get it from the context and use it in your data fetch.
3 Likes
Wow, yeah that is a much more elegant solution. For others, the me
service looks like this:
export const me: QueryResolvers["me"] = () => {
return db.user.findUniqueOrThrow({
where: { id: context.currentUser?.id }
})
}
TIL: The context is also available in the services!
In general, I suppose I need to create more GQL types for specific use cases. I guess I was abusing the default User type before.
Thanks so much!
2 Likes
The solution did work for me.
Just beware that when I imported the context, I needed to restart the server.
import { context } from '@redwoodjs/graphql-server'
1 Like