K
Kinde11mo ago
Colin

Best way to access the management API from Nuxt server routes?

Hello, I am kinda new to working with Kinde and am planning on using it for my future projects as it has been a very nice experience till now. I was trying to figure out the best way to access the management API from a Nuxt server route. My current use case is to add a role to a user from a stripe subscription webhook. It is written in the module's docs that the management API is a feature that has not yet seen the day but should still be accessible as it uses the Typescript SDK under the hood right? Here is the project I am working on: https://github.com/ColinEspinas/starter Have a wonderful day, Colin.
20 Replies
Daniel_Kinde
Daniel_Kinde11mo ago
Hi @Colin Thanks for reaching out, great looking starter setup! You're correct the Nuxt module does sit on top of our TS SDK, I will look at putting together and example for you and get back to you this week if thats ok?
Colin
ColinOP11mo ago
Thanks for your answer, I'll probably also tinker with it a bit in the meantime waiting for your example.
Daniel_Kinde
Daniel_Kinde11mo ago
@Colin Quick update from me, I have made some progress on getting this sorted, hoping to have a sharable solution soon (which could make it into the Module soon afterwards)
Colin
ColinOP11mo ago
Hey, this would be great, would love to help and contribute to this module once I am a bit more used to kinde. I've got some sort of working prototype on my end but it is not very pretty
Daniel_Kinde
Daniel_Kinde11mo ago
Hi @Colin I have a solution for you which works with the whole Kinde Management API. If you put the attached file the following location /server/utils/ You can then use it from any server route like this, there is no need to import, Nuxt auto imports will handle that for you.
const kindeManagementApi = await useKindeManagementApi();
const users = await kindeManagementApi.usersApi.getUsers();
const kindeManagementApi = await useKindeManagementApi();
const users = await kindeManagementApi.usersApi.getUsers();
Everything under kindeManagementApi is fully typed which should help you here. Let me know how you get on,.
Colin
ColinOP11mo ago
@Daniel_Kinde It looks a bit like what I've done but I couldn't get the token like you did. Do you use the same app client and secret for the management api as the nuxt app or do you create a new app for that? Here is what I've come up to for the user api (could transform in a general purpose one):
export async function useKindeUserApi(event: H3Event): Promise<UsersApi> {
const { authDomain, machineClientId, machineClientSecret } = useRuntimeConfig().kinde
const client = event.context.kinde as ({ sessionManager: SessionManager } & ACClient)

const kindeApiClient = createKindeServerClient(GrantType.CLIENT_CREDENTIALS, {
authDomain,
clientId: machineClientId,
clientSecret: machineSecretId,
audience: `${authDomain}/api`,
logoutRedirectURL: authDomain,
})

const token = await kindeApiClient.getToken(client.sessionManager)

const config = new Configuration({
basePath: authDomain,
accessToken: token,
headers: { Accept: 'application/json' },
})

return new UsersApi(config)
}
export async function useKindeUserApi(event: H3Event): Promise<UsersApi> {
const { authDomain, machineClientId, machineClientSecret } = useRuntimeConfig().kinde
const client = event.context.kinde as ({ sessionManager: SessionManager } & ACClient)

const kindeApiClient = createKindeServerClient(GrantType.CLIENT_CREDENTIALS, {
authDomain,
clientId: machineClientId,
clientSecret: machineSecretId,
audience: `${authDomain}/api`,
logoutRedirectURL: authDomain,
})

const token = await kindeApiClient.getToken(client.sessionManager)

const config = new Configuration({
basePath: authDomain,
accessToken: token,
headers: { Accept: 'application/json' },
})

return new UsersApi(config)
}
The issue here I think is that I am using the wrong session manager and can't get the token that way. I'll try your solution
Daniel_Kinde
Daniel_Kinde11mo ago
@Colin Yea, I opted for going to the API directly as I only wanted to token at this point. When I come to add the module may approach differently.
The one issue with my solution right now is it will get a new token every time the API is hit and won't cache it. That is something that would need to be improved.
I was able to get the list of users all ok using my implementation, once you're happy its working ok I will work to make it part of the core module.
Colin
ColinOP11mo ago
For the cache issue you could cache it with nitro's cached functions feature (https://nitro.unjs.io/guide/cache#function). I also recommend using the $fetch in nitro as it is made to play better with all serverless environments and runtimes (https://nitro.unjs.io/guide/fetch), not a very important change I guess but could prevent issues down the road. In the module I suggest naming the returned properties without the "Api" word as it would make more sense to use.
const users = await kindeManagementApi.users.getUsers();
// Instead of:
const users = await kindeManagementApi.usersApi.getUsers();
const users = await kindeManagementApi.users.getUsers();
// Instead of:
const users = await kindeManagementApi.usersApi.getUsers();
What do you think? I'll try to get it working with the cached function. For the client and secret id, I tought we were supposed to create a machine to machine app and get different ids from here, is it not the case ?
Daniel_Kinde
Daniel_Kinde11mo ago
Yes should 100% be using the $fetch, I was throwing together a solution for now to get you unblocked. The token generated is a M2M token you're correct, a normal user token can also be used here. The difference being that the M2M token will have no context of a logged in user. As for the naming of the Api modules, I have used the same names we use in the Next.js SDK for consistency, although I agree the Api part is redundant here as the unit composable is called kindeManagementApi, let me discuss with the team on naming here.
Colin
ColinOP11mo ago
@Daniel_Kinde Got something that seems to work (at least to get the user list)
const { kinde: config } = useRuntimeConfig()

const getApiToken = defineCachedFunction(async () => {
const response = await $fetch<{ access_token: string }>(`${config.authDomain}/oauth2/token`, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: config.clientId,
client_secret: config.clientSecret,
audience: `${config.authDomain}/api`,
}),
})
return response.access_token
}, {
maxAge: 86400,
name: 'kindeApiToken',
})

export async function useKindeManagementApi() {
const apiToken = await getApiToken()

const configuration = new Configuration({
basePath: config.authDomain,
accessToken: apiToken,
headers: { Accept: 'application/json' },
})

return {
users: new UsersApi(configuration),
oauth: new OAuthApi(configuration),
subscribers: new SubscribersApi(configuration),
organizations: new OrganizationsApi(configuration),
connectedApps: new ConnectedAppsApi(configuration),
featureFlags: new FeatureFlagsApi(configuration),
environments: new EnvironmentsApi(configuration),
permissions: new PermissionsApi(configuration),
roles: new RolesApi(configuration),
business: new BusinessApi(configuration),
industries: new IndustriesApi(configuration),
timezones: new TimezonesApi(configuration),
applications: new ApplicationsApi(configuration),
callbacks: new CallbacksApi(configuration),
apis: new APIsApi(configuration),
}
}
const { kinde: config } = useRuntimeConfig()

const getApiToken = defineCachedFunction(async () => {
const response = await $fetch<{ access_token: string }>(`${config.authDomain}/oauth2/token`, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: config.clientId,
client_secret: config.clientSecret,
audience: `${config.authDomain}/api`,
}),
})
return response.access_token
}, {
maxAge: 86400,
name: 'kindeApiToken',
})

export async function useKindeManagementApi() {
const apiToken = await getApiToken()

const configuration = new Configuration({
basePath: config.authDomain,
accessToken: apiToken,
headers: { Accept: 'application/json' },
})

return {
users: new UsersApi(configuration),
oauth: new OAuthApi(configuration),
subscribers: new SubscribersApi(configuration),
organizations: new OrganizationsApi(configuration),
connectedApps: new ConnectedAppsApi(configuration),
featureFlags: new FeatureFlagsApi(configuration),
environments: new EnvironmentsApi(configuration),
permissions: new PermissionsApi(configuration),
roles: new RolesApi(configuration),
business: new BusinessApi(configuration),
industries: new IndustriesApi(configuration),
timezones: new TimezonesApi(configuration),
applications: new ApplicationsApi(configuration),
callbacks: new CallbacksApi(configuration),
apis: new APIsApi(configuration),
}
}
What do you think ? Token is cached for 86400 seconds as this is the default in the kinde dashboard but could be set as an env variable I also removed the return type as it is infered in my setup and didn't want it to be too big on discord but I think it should definitely be added back if implemented in the module
Daniel_Kinde
Daniel_Kinde11mo ago
Looks great. Use that for now and I will work to get it in the module
Colin
ColinOP11mo ago
Thanks for the help, I'll make sure to check the PR for this @Daniel_Kinde So I've been trying to add a role to a user from the management API and can't get it to work, I get a 400. Here is the code for that:
const kindeManagementApi = await useKindeManagementApi()
const { roles } = await kindeManagementApi.roles.getRoles()
const proRoleId = roles?.find(role => role.key === 'pro')?.id
await kindeManagementApi.organizations.createOrganizationUserRole({
orgCode: kinde.defaultOrg,
userId,
createOrganizationUserRoleRequest: {
roleId: proRoleId,
},
}).catch(error => console.log(error))
const kindeManagementApi = await useKindeManagementApi()
const { roles } = await kindeManagementApi.roles.getRoles()
const proRoleId = roles?.find(role => role.key === 'pro')?.id
await kindeManagementApi.organizations.createOrganizationUserRole({
orgCode: kinde.defaultOrg,
userId,
createOrganizationUserRoleRequest: {
roleId: proRoleId,
},
}).catch(error => console.log(error))
userId coming from the getUserProfile().id from the kinde client of the nitro app
Daniel_Kinde
Daniel_Kinde11mo ago
I am looking into this @Colin , will get back to you once I have an answer. @Colin Can I check something? Does the user you're adding the role to already have the role assigned to it?
Colin
ColinOP11mo ago
@Daniel_Kinde Oh I think I see what is happening, Nuxt is calling my endpoint 2 times in a row (once server and once client side) the second time the role is already there so it throws Is there any way I can check for that instead of receiving a generic bad request error ?
Daniel_Kinde
Daniel_Kinde11mo ago
@Colin I am not seeing the double request happening. If I remove the role from the user, it works ok. If you have in the server utils and running from the API, it wouldn't fire from the client?
Colin
ColinOP11mo ago
Because I am fetching my endpoint from the client right now with a useFetch() I suppose I'll just check the roles on the user before trying to add the role, could be great to have a way to know the issue from the error.
Colin
ColinOP11mo ago
So I basically just created a util to handle that:
Colin
ColinOP11mo ago
Thanks for all the help you've provided could have been stuck on this for a long time haha @Daniel_Kinde Okay I keep finding strange things, now on the front side, when I do client.getOrganization() or client.getUser() I get null when trying to get the org code, also when I do client.getPermissions() I get an empty array for permissions but with the right org code and finally client.getClaim('roles') has a value of null. I do have a role that adds a permission so it should be there Maybe I should open new a new feed as this is not the same issue
Daniel_Kinde
Daniel_Kinde11mo ago
Have you got any code examples you can share on how you're going about this?
Colin
ColinOP11mo ago
Just like the docs
const kindeClient = useKindeClient()

const { data: permissions } = await useAsyncData(async () => {
const { permissions } = (await kindeClient?.getPermissions()) ?? {}
return permissions
})

console.log(permissions.value)
const kindeClient = useKindeClient()

const { data: permissions } = await useAsyncData(async () => {
const { permissions } = (await kindeClient?.getPermissions()) ?? {}
return permissions
})

console.log(permissions.value)
Replace the .getPermissions() by any other method and that is how I tested Just to give more context, I checked the boxes to add the roles claim in the access token, my user does have a role and this role has a permission that should be granted A bit more insight
const kindeClient = useKindeClient()

const { data: permissions } = await useAsyncData(async () => {
const { permissions } = (await kindeClient?.getPermissions()) ?? {}
return permissions
})
console.log(permissions.value)

const { data: hasAccess } = await useAsyncData(async () => {
return (await kindeClient?.getPermission('unlimited-tasks')) ?? {}
})
console.log(hasAccess.value.isGranted)

const { data: orgs } = await useAsyncData(async () => {
const orgs = (await kindeClient?.getUserOrganizations()) ?? {}
return orgs
})

console.log(orgs.value)

const { data: org } = await useAsyncData(async () => {
const org = (await kindeClient?.getOrganization()) ?? {}
return org
})

console.log(org.value)
const kindeClient = useKindeClient()

const { data: permissions } = await useAsyncData(async () => {
const { permissions } = (await kindeClient?.getPermissions()) ?? {}
return permissions
})
console.log(permissions.value)

const { data: hasAccess } = await useAsyncData(async () => {
return (await kindeClient?.getPermission('unlimited-tasks')) ?? {}
})
console.log(hasAccess.value.isGranted)

const { data: orgs } = await useAsyncData(async () => {
const orgs = (await kindeClient?.getUserOrganizations()) ?? {}
return orgs
})

console.log(orgs.value)

const { data: org } = await useAsyncData(async () => {
const org = (await kindeClient?.getOrganization()) ?? {}
return org
})

console.log(org.value)
Returns:
[] <- permissions
false <- hasAccess.isGranted
null <- orgs
{ orgCode: 'org_4b7dc412b0c' } <- org
[] <- permissions
false <- hasAccess.isGranted
null <- orgs
{ orgCode: 'org_4b7dc412b0c' } <- org
Seems like today getOrganization works, not changed anything and yesterday it was null Also getUser does not have any data about the org anymore and returns the same thing as getUserProfile Did you do an update of the API? I mean we should probably take this to another thread right ? So I think it was because I did not refresh my tokens This is really a problem, I just logged in another account, cameback to the original one and roles and permissions are empty again I created a new thread about this matter https://discord.com/channels/1070212618549219328/1206007767865888789 as my initial issue with the management API is fixed
Want results from more Discord servers?
Add your server