Closed domharrington closed 1 year ago
I just tried to do this via the signIn callback, and similarly to the profile() function it only works to modify the user object pre-save the very first time it's called. On subsequent calls, this has no effect:
export const authOptions = {
callbacks: {
signIn({ user, profile, account }) {
user.provider = account.provider;
switch (account.provider) {
case 'github':
user.username = profile.login;
break;
case 'bitbucket':
user.username = profile.username;
break;
}
return true;
},
}
}
In the end I went with this which will append the user with account.provider
and username
properties extracted from the profile. This works for both new users (the prisma call fails and we handle it) and existing users (via the prisma update() call).
import { Prisma, PrismaClient } from '@prisma/client'
const client = new PrismaClient()
export const authOptions = {
callbacks: {
async signIn({ user, profile, account }) {
user.provider = account.provider;
switch (account.provider) {
case "github":
user.username = profile.login;
break;
case "bitbucket":
user.username = profile.username;
break;
}
try {
await client.users.update({
where: { email: user.email },
data: { provider: user.provider, username: user.username },
})
} catch (e) {
// Rethrow any error that isn't known. This error will get thrown
// if we're trying to update a user in the database that doesn't exist
// yet i.e. they're a new user.
// https://www.prisma.io/docs/reference/api-reference/error-reference#p2025
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code !== 'P2025') {
throw e;
}
}
}
return true;
},
}
}
For a simplified example with a single provider (github), you could do this:
import { Prisma, PrismaClient } from '@prisma/client'
const client = new PrismaClient()
export const authOptions = {
callbacks: {
async signIn({ user, profile, account }) {
user.provider = account.provider;
user.username = profile.login;
try {
await client.users.update({
where: { email: user.email },
data: { provider: user.provider, username: user.username },
})
} catch (e) {
// Rethrow any error that isn't known. This error will get thrown
// if we're trying to update a user in the database that doesn't exist
// yet i.e. they're a new user.
// https://www.prisma.io/docs/reference/api-reference/error-reference#p2025
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code !== 'P2025') {
throw e;
}
}
}
return true;
},
}
}
This could also be extended to add other custom default properties, not from account/profile. I hope this helps someone! Or if there's anything wrong with this approach, lmk!
I hope we can come up with an approach that isn't so verbose together - happy to submit a fix if you can offer some advice on what API design will be accepted.
There is a standard way, please refer to the documentation: https://next-auth.js.org/getting-started/client#updating-the-session
Hey, thanks for responding! This is client side only though, no? Is there any way to do this from the server side? And please correct me if i'm wrong, but this only updates the current active session not the user that's stored in the database?
I'm hoping to save the username that comes back from the oauth login - what would be the recommended flow using update()
? I'd have to append the username onto the jwt token during sign in (callbacks.jwt()
) then i'd have to append this onto the session (callbacks.session()
) then when i'm on the frontend, I'd call update()
on the session based on this value? I may be misunderstanding here, but I probably wouldn't need to do this because that value would already be in the session at that point and I'm looking to update the user stored in the database. I hope that makes sense.
Cheers
Interested in a server side solution as well!
The solution i posted here has been working for me! If it's a new user, you can just update the user object passed in, if it's an existing user you can update the user with your adapter. I actually simplified my code above to use the adapter instead of prisma directly:
async signIn({ user, profile, account }) {
user.provider = account.provider;
user.username = profile.login;
if (user.id) {
await adapter.updateUser({
id: user.id,
provider: user.provider,
username: user.username,
});
}
return true;
},
Hope this helps!
The solution i posted here has been working for me! If it's a new user, you can just update the user object passed in, if it's an existing user you can update the user with your adapter. I actually simplified my code above to use the adapter instead of prisma directly:
async signIn({ user, profile, account }) { user.provider = account.provider; user.username = profile.login; if (user.id) { await adapter.updateUser({ id: user.id, provider: user.provider, username: user.username, }); } return true; },
Hope this helps!
Hey @domharrington thanks for the solution. Just a quick one, have you extended the AdapterUser type anywhere? are you using the adapter that you are passing to the config? im getting type errors (for additional fields that i have added for the user)
@sheeni17 I wasn't using TypeScript in this project, so I'm not sure on the proper way to type this, but yeah you'd probably have to extend it if it's not typed on there.
Hey @domharrington thanks for the solution. Just a quick one, have you extended the AdapterUser type anywhere? are you using the adapter that you are passing to the config? im getting type errors (for additional fields that i have added for the user)
You can extend AdapterUser
like so:
import { AdapterUser } from '@auth/core/adapters';
declare module '@auth/core/adapters' {
interface AdapterUser {
customField?: string;
}
}
I actually simplified my code above to use the adapter instead of prisma directly:
await adapter.updateUser(...)
~@domharrington I'm missing something... where is adapter
being defined?~
Never mind, I just had to use config.adapter
š¤¦āāļø
Hey everyone, I'm running into the same issue where adapter
is not defined in my signIn
callback function. I've tried to follow the guidance from above, but I still can't seem to get it working correctly.
Here's my setup:
async signIn({ user, account, profile, email, credentials }) {
await adapter.updateUser({
id: user.id,
});
return true;
}
And my auth.ts
file looks like this:
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import { SupabaseAdapter } from "@auth/supabase-adapter";
export const { handlers, signIn, signOut, auth } = NextAuth({
debug: true,
adapter: SupabaseAdapter({
url: '',
secret: '',
}),
session: {
strategy: "database",
},
providers: [
//... other providers
],
});
However, I keep getting the error: Cannot find name 'adapter'
.
I saw that someone mentioned using config.adapter
, but I'm not sure where exactly to define or use it within this context. Any help or further guidance would be greatly appreciated!
Thank you in advance!
Hey @Netyson, a couple suggestions-
I recommend putting anything that isn't directly related to "should we allow this person to sign in" in the events:
parameter instead of callbacks:
. I originally had my db updates in callbacks.signIn
but found out the hard way that if there was an issue communicating with the database it resulted in the entire auth module telling everyone they were not authorized to sign in (because the return was falsy).
Secondly, if you put the auth config into its own variable it will help, because you need to reference the value of your config adapter (authConfig.adapter
) in your signIn
function (as I noted in my comment just above yours š) - that's why your code is complaining it's not defined.
Try this (note I like to use authConfig
instead of config
as my variable name to help keep things organized, but you can call it whatever you like):
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import { SupabaseAdapter } from "@auth/supabase-adapter";
export const authConfig = {
debug: true,
adapter: SupabaseAdapter({
url: '',
secret: '',
}),
session: {
strategy: "database",
},
providers: [
//... other providers
],
events: {
signIn: async (message) => {
const { account, profile, user, isNewUser } = message;
const { updateUser } = authConfig.adapter || {};
if (updateUser != null) {
await updateUser({ id: user.id });
}
}
},
});
export const { handlers, signIn, signOut, auth } = NextAuth(authConfig);
Here are the docs for events: https://authjs.dev/reference/core#events
Hey @gnowland,
Thanks to your comments and suggestions, I managed to fix my issue and update the user information on every login. Your help was really aweseome!
Cheers!
Description š
This feature request/issue seems to come up a lot. I would like there to be a (standardised) way to update the user that's stored in the database on, or after initial creation. My use case is that I would like to store a
username
property on the users collection, but we already have some existing users who have logged into the system. Overriding the provider.profile() function is possible (and would work for new users), but for existing users the updated information doesn't save to the database.How to reproduce āļø
Due to the extensibility of this module, there are a ton of different ways to implement this currently:
Override profile() in your oauth provider:
Add another db update call to one of the callbacks (signIn() or jwt())
Override the adapter methods (createUser/updateUser)
Is there a recommended approach here? Have I missed something in the docs? It would be great to have a unified and consistent way to do this across providers. Maybe via an extendProfile() callback at the provider level:
That way you dont have to mess around with overriding profiles per provider and whatever is returned from this function should be saved to the database each time.
Related issues: https://github.com/nextauthjs/next-auth/issues/7100 https://github.com/nextauthjs/next-auth/issues/6959 https://github.com/nextauthjs/next-auth/issues/6952#issuecomment-1471751706
Similar feature requests: https://github.com/nextauthjs/next-auth/discussions/7548 https://github.com/nextauthjs/next-auth/discussions/1194 https://github.com/nextauthjs/next-auth/discussions/4865
Contributing šš½
Yes, I am willing to help implement this feature in a PR