I'm trying to use NextAuth and Strapi in my app, but NextAuth session only shows email for the user.
When I call the Strapi login API directly:
axios
.post(`${process.env.STRAPI_URL}/api/auth/local`, {
identifier: "email#provider.com",
password: "test123",
})
.then((response) => {
console.log("User profile", response.data.user);
})
I get this object (response.data.user) in console:
{
"id": 4,
"username": "theusername",
"email": "email#provider.com",
"provider": "local",
"confirmed": true,
"blocked": false,
"createdAt": "2022-09-18T17:02:43.581Z",
"updatedAt": "2022-09-27T16:39:22.993Z"
}
But when I try to sign in with NextAuth:
import axios from "axios";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export default NextAuth({
// Configure one or more authentication providers
providers: [
CredentialsProvider({
name: "Sign in with Email",
credentials: {
email: { label: "Email", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
// return (
if (credentials == null) return null;
try {
/* I tried `...rest` but no luck. also console log doesn't work here and I can't see the actual response from the API call */
const { user, jwt } =
(await axios
.post(
`${process.env.STRAPI_URL}/api/auth/local`,
{
identifier: credentials.email,
password: credentials.password,
}
)
.then((response) => {
return response.data;
})
.catch((error) => {
console.log(error.response);
throw new Error(error.response.data.message);
})) || null;
return { jwt, ...user };
} catch (error) {
console.warn(error);
// Sign In Fail
// return null;
}
// );
},
}),
],
callbacks: {
session: async ({ session, token }) => {
session.id = token.id;
session.jwt = token.jwt;
return Promise.resolve(session);
},
jwt: async ({ token, user }) => {
if (user) {
token.id = user.id;
token.jwt = user.jwt;
}
return Promise.resolve(token);
},
},
});
I only get email for the user:
{
"user": {
"email": "email#provider.com"
},
"expires": "2022-10-27T20:03:20.177Z",
"id": 4,
"jwt": "somejwtcodethatichangedhereforsecurity"
}
How can I have other properties, like username, in the user returned from NextAuth?
It's possible, by modifying callbacks:
callbacks: {
session: async ({ session, token }) => {
session.id = token.id;
session.jwt = token.jwt;
session.user.username = token.username /* added */
return Promise.resolve(session);
},
jwt: async ({ token, user }) => {
if (user) {
token.id = user.id;
token.jwt = user.jwt;
token.username = user.username /* added */
}
return Promise.resolve(token);
},
},
Related
I'm using nuxt 3 with #sidebase/nuxt-auth for my PKCE OAUTH2 authentication flow with my Laravel API which uses Laravel passport. I am done with the implementation of the authentication flow from the Laravel side, and done with the nuxt side, getting the token and saving it.
Then I have created a custom interceptor with ofetch, in order to send the access_token that I fetch from the session at every request.
but when the access token is expired, it is not getting refreshed automatically.
am i missing some configuration? or refresh token is a custom logic i have to write?
I've tried the JWT callback in the #sidebase/nuxt-auth but it didn't work.
This is my current auth configuration:
import {NuxtAuthHandler} from '#auth'
import useCustomFetch from "~/composables/useCustomFetch";
export default NuxtAuthHandler({
providers: [
{
id: 'passport',
name: 'Passport',
type: 'oauth',
version: '2.0',
authorization: {
url: "https://example.com/oauth/authorize",
params: {
scope: '',
prompt: 'front',
},
},
clientSecret: 'awd',
clientId: "96695f40-1578-4b7c-974b-181e0344dcac",
token: 'https://example.com/api/v1/oauth/token',
userinfo: 'https://example.com/api/v1/user',
checks: ['pkce'],
profile(profile: { success: { user: any } }) {
const data = profile.success.user
return {
id: data.id,
name: data.first_name + " " + data.last_name,
email: data.email,
};
},
}
],
cookies: {
},
callbacks: {
async jwt({token, account, user}) {
if (account && user) {
return {
access_token: account.access_token,
refresh_token: account.refresh_token,
accessTokenExpires: account.expires_at,
user
}
}
// #ts-ignore
if (Date.now() < token.accessTokenExpires * 1000) {
return token
}
return await refreshAccessToken(token);
},
async session({session, token}) {
// #ts-ignore
session.user = token.user
// #ts-ignore
session.access_token = token.access_token
// #ts-ignore
session.error = token.error
return session
},
},
events: {
async signOut() {
try {
await useCustomFetch('/oauth/tokens/revoke', {
method: 'POST'
})
} catch (e) {
console.log(e);
}
},
}
})
async function refreshAccessToken(token: any) {
try {
const url = "https://example.com/api/v1/oauth/token";
// #ts-ignore
const refreshedToken: AuthResponse = await $fetch(url, {
headers: {
"Content-Type": "application/json",
},
method: "POST",
body: {
grant_type: 'refresh_token',
refresh_token: token.refresh_token,
client_id: "96695f40-1578-4b7c-974b-181e0344dcac"
}
});
token.access_token = refreshedToken.access_token;
token.accessTokenExpires = Date.now() + refreshedToken.expires_at * 1000;
token.refresh_token = refreshedToken.refresh_token;
return {
...token
}
} catch (error) {
console.log(error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
interface AuthResponse {
access_token: string,
refresh_token: string,
token_type: string,
expires_at: number,
}
as per the title, I am having problem trying to enable graphql subscription in my loopback 4 application.
Here is my code that I've done so far.
index.ts
export async function main(options: ApplicationConfig = {}) {
const app = new BackendLb4Application(options)
await app.boot()
await app.start()
const url = app.restServer.url;
const oas: Oas3 = <Oas3><unknown>await app.restServer.getApiSpec()
const {schema} = await createGraphQLSchema(oas, {
operationIdFieldNames: true,
baseUrl: url,
createSubscriptionsFromCallbacks: true,
})
const handler = graphqlHTTP( (request:any, response:any, graphQLParams: any) => ({
schema,
pretty: true,
graphiql: true
}))
app.mountExpressRouter(graphqlPath, handler);
const pubsub = new PubSub()
const ws = createServer(app);
ws.listen(PORT, () => {
new SubscriptionServer(
{
execute,
subscribe,
schema,
onConnect: (params: any, socket: any, ctx: any) => {
console.log(params, 'here on onconnect')
// Add pubsub to context to be used by GraphQL subscribe field
return { pubsub }
}
},
{
server: ws,
path: '/subscriptions'
}
)
})
return app
}
Here is my schema
type Subscription {
"""
Equivalent to PATCH onNotificationUpdate
"""
postRequestQueryCallbackUrlApiNotification(secondInputInput: SecondInputInput): String
"""
Equivalent to PATCH onNotificationUpdate
"""
postRequestQueryCallbackUrlOnNotificationUpdate(firstInputInput: FirstInputInput): String
}
Here is an example of my controller
#patch('/notification-update', {
operationId: 'notificationUpdate',
description: '**GraphQL notificationUpdate**',
callbacks:[ {
onNotificationUpdate: {
//'{$request.query.callbackUrl}/onNotificationUpdate': {
post: {
requestBody: {
operationId: 'notificationUpdateCallback',
description: 'rasjad',
content: {
'application/json': {
schema: {
title: "firstInput",
type: 'object',
properties: {
userData: {
type: "string"
}
}
}
}
}
},
responses: {
'200': {
description: 'response to subscription',
}
}
}
},
// }
}],
responses: {
'200': {
description: 'Notification PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Notification, {partial: true}),
},
},
})
notification: Notification,
#param.where(Notification) where?: Where<Notification>,
): Promise<Count> {
return this.notificationRepository.update(notification, where);
}
Ive defined the callbacks object in my controller which will then create a subscription in my schema. Tested it out on graphiql but did not work.
I am not sure where to go from here. Do I need a custom resolver or something? Not sure.
Appreciate it if anyone could help on this.
Just in case someone else is looking to do the same thing.
I switched out graphqlHTTP with Apollo Server to create my graphql server.
So my final index.ts looks like this.
export async function main(options: ApplicationConfig = {}) {
const lb4Application = new BackendLb4Application(options)
await lb4Application.boot()
await lb4Application.migrateSchema()
await lb4Application.start()
const url = lb4Application.restServer.url;
const graphqlPath = '/graphql'
// Get the OpenApiSpec
const oas: Oas3 = <Oas3><unknown>await lb4Application.restServer.getApiSpec()
// Create GraphQl Schema from OpenApiSpec
const {schema} = await createGraphQLSchema(oas, {
strict: false,
viewer: true,
baseUrl: url,
headers: {
'X-Origin': 'GraphQL'
},
createSubscriptionsFromCallbacks: true,
customResolvers: {
"lb4-title": {
"your-path":{
patch: (obj, args, context, info) => {
const num = Math.floor(Math.random() * 10);
pubsub.publish("something", { yourMethodName: {count: num} }).catch((err: any) => {
console.log(err)
})
return {count: 1}
}
}
}
},
customSubscriptionResolvers: {
"lb4-title" : {
"yourMethodName": {
post: {
subscribe: () => pubsub.asyncIterator("something"),
resolve: (obj: any, args: any, context, info) => {
console.log(obj, 'obj')
}
}
}
}
}
})
const app = express();
const server = new ApolloServer({
schema,
plugins: [{
async serverWillStart() {
return {
async drainServer() {
subscriptionServers.close();
}
};
}
}],
})
const subscriptionServers = SubscriptionServer.create(
{
// This is the `schema` we just created.
schema,
// These are imported from `graphql`.
execute,
subscribe,
},
{
server: lb4Application.restServer.httpServer?.server,
path: server.graphqlPath,
//path: server.graphqlPath,
}
);
await server.start();
server.applyMiddleware({ app, path: "/" });
lb4Application.mountExpressRouter('/graphql', app);
return lb4Application
}
Also you will need to define the callbacks object in your controller like so.
#patch('/something-update', {
operationId: 'somethingUpdate',
description: '**GraphQL somethingUpdate**',
callbacks:[
{
yourMethodName: {
post: {
responses: {
'200': {
description: 'response to subscription',
content: {'application/json': {schema: CountSchema}},
}
}
}
},
}
],
responses: {
'200': {
description: 'Something PATCH success count',
content: {'application/json': {schema: CountSchema}},
},
},
})
async updateAll(
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Something, {partial: true}),
},
},
})
something: Something,
#param.where(Something) where?: Where<Something>,
): Promise<Count> {
return this.somethingRepository.updateAll(something, where);
}
And that is it. You can test it out from the GraphQL Playground and play around with the subscriptions.
For the time being, I am fine with defining customResolvers and customSubscriptionResolvers but I'm pretty sure I can automate this two objects from the controllers.
Cheers!
I store logged in user data in localstorage. I also validate JWT token in axios interceptor and if it's expired I will refresh it. so I need to update store with new user data and JWT token and in order to do that, I need to call redux action that I have in my Auth module.
AuthRedux.js
export const actionTypes = {
Login: "[Login] Action",
Logout: "[Logout] Action",
UserRequested: "[Request User] Action",
UserLoaded: "[Load User] Auth API",
SetUser: "[Set User] Action",
};
const initialAuthState = {
user: undefined,
authToken: undefined,
};
export const reducer = persistReducer(
{ storage, key: "myapp-auth", whitelist: ["user", "authToken"] },
(state = initialAuthState, action) => {
switch (action.type) {
case actionTypes.Login: {
const { authToken } = action.payload;
return { authToken, user: undefined };
}
case actionTypes.Logout: {
// TODO: Change this code. Actions in reducer aren't allowed.
return initialAuthState;
}
case actionTypes.UserLoaded: {
const { user } = action.payload;
return { ...state, user };
}
case actionTypes.SetUser: {
const { user } = action.payload;
return { ...state, user };
}
default:
return state;
}
}
);
export const actions = {
login: (authToken) => ({ type: actionTypes.Login, payload: { authToken } }),
logout: () => ({ type: actionTypes.Logout }),
requestUser: (user) => ({ type: actionTypes.UserRequested, payload: { user } }),
fulfillUser: (user) => ({ type: actionTypes.UserLoaded, payload: { user } }),
setUser: (user) => ({ type: actionTypes.SetUser, payload: { user } }),
};
export function* saga() {
yield takeLatest(actionTypes.Login, function* loginSaga() {
yield put(actions.requestUser());
});
yield takeLatest(actionTypes.UserRequested, function* userRequested() {
const { data: user } = yield getUserByToken();
yield put(actions.fulfillUser(user));
});
}
AxiosInterceptor.js
export default function setupAxios(axios, store, props) {
axios.interceptors.request.use(
config => {
const {
auth: { authToken }
} = store.getState();
if (authToken) {
config.headers.Authorization = `Bearer ${authToken}`;
}
return config;
},
err => Promise.reject(err)
);
axios.interceptors.response.use(
(response) => {
console.log(props);
return response;
},
function (error) {
const originalRequest = error.config;
if (error.response?.status === 401) {
if (error.response.data === "refresh_token") {
// refresh token and set new user data
// question is how can I call redux setUser action in here and update state
}
else if (error.response.data === "invalid_token") {
window.localStorage.clear();
window.location.href = '/auth/login';
}
else { }
}
if (!originalRequest._retry) {
originalRequest._retry = true;
return axios(originalRequest);
}
return Promise.reject(error);
}
);
}
My question is how can I call reducer setUser action in interceptor and update state
You can dispatch actions from outside of a component when you have access to the store with store.dispatch(anAction), in your case you can do:
store.dispatch(setUser())
I am trying to manually set the status on my formik instance upon receiving an error response from server:
const formik = useFormik({
initialValues: {
email: "",
password: "",
},
onSubmit: async ({ email, password }, { setStatus }) => {
try {
const response = await loginUser({
variables: {
email,
password,
},
});
} catch (error) {
await setStatus({ email: error.message, password: error.message });
console.log(formik.status);
}
formik.resetForm();
},
validationSchema: Yup.object().shape({
email: Yup.string(),
password: Yup.string(),
}),
});
Logging status prints undefined.
Why is status not being set?
Figured it out.
Using resetForm clears both form status and errors.
To get around this I have manually cleared both fields instead:
initialValues={{
email: "",
password: "",
}}
onSubmit={async ({ email, password }, { setStatus, setFieldValue }) => {
try {
const response = await loginUser({
variables: {
email,
password,
},
});
} catch (error) {
setStatus({
email: "Shit",
});
setFieldValue("email", "");
setFieldValue("password", "");
}
}}
validationSchema={Yup.object().shape({
email: Yup.string(),
password: Yup.string(),
})}
I have issue with very slowly getting data from laravel api to vue view, I did tutorial where I have:
import axios from 'axios';
const client = axios.create({
baseURL: '/api',
});
export default {
all(params) {
return client.get('users', params);
},
find(id) {
return client.get(`users/${id}`);
},
update(id, data) {
return client.put(`users/${id}`, data);
},
delete(id) {
return client.delete(`users/${id}`);
},
};
<script>
import api from "../api/users";
export default {
data() {
return {
message: null,
loaded: false,
saving: false,
user: {
id: null,
name: "",
email: ""
}
};
},
methods: {
onDelete() {
this.saving = true;
api.delete(this.user.id).then(response => {
this.message = "User Deleted";
setTimeout(() => this.$router.push({ name: "users.index" }), 1000);
});
},
onSubmit(event) {
this.saving = true;
api
.update(this.user.id, {
name: this.user.name,
email: this.user.email
})
.then(response => {
this.message = "User updated";
setTimeout(() => (this.message = null), 10000);
this.user = response.data.data;
})
.catch(error => {
console.log(error);
})
.then(_ => (this.saving = false));
}
},
created() {
api.find(this.$route.params.id).then(response => {
this.loaded = true;
this.user = response.data.data;
});
}
};
</script>
It's load data from api very slowly I see firstly empty inputs in view and after some short time I see data, if I open api data from laravel I see data immediately, so my question is How speed up it? Or maby I did something wrong?
Whenever I am using an API with Vue, I usually make most of my API calls before opening the Vue then passing it in like this.
<vue-component :user="'{!! $user_data !!}'"></vue-component>
But if you have to do it in the Vue component, I am not sure if this will show improvement over your method but I would set it up with the "mounted" like so.
export default {
mounted() {
api.find(this.$route.params.id).then(response => {
this.loaded = true;
this.user = response.data.data;
});
}
}
Also heres a good tutorial on Axios and how to use HTTP Requets with Vue.
Hopefully this answered your question, good luck!