I am trying to create a Sveltekit app where users can log in. The login process is handled by a self-created API, so I would like to use the Auth.js Credentials Provider.
When I call the SignIn method as the FormAction in the +page.server.ts file, I get the error message 'window is not defined', which makes sense. Does this mean that Auth.js is not compatible with server-side rendering, or is there something else that I can adjust?
My Code:
//hooks.server.ts
SvelteKitAuth({
providers: [
// #ts-ignore
Credentials({
name: "credentials",
async authorize(credentials, req) {
// TODO: Call Api
const user = { id: "1", name: "J Smith", email: "jsmith#example.com" }
if (user) {
return user
} else {
return null
}
}
})
],
});
//+page.server.ts
export const actions = {
default: async ({ cookies, request }: { cookies: any, request: any }) => {
const data = await request.formData();
const credentials = {
username: data.get('username'),
password: data.get('password')
};
signIn('credentials', credentials)
.then(response => {
console.log('Success');
})
.catch(error => {
console.error('error', error);
});
}
};
I'm on a page that uses a vue component and one of my patch routes is saying I'm un-authenticated.
<template>
...
<button #click="editPost(attributes.attributes.post_id)"></button>
...
</template>
<script>
export default {
data() {
return {
info: null,
message: null,
postTitle: "",
postContent: ""
}
},
methods: {
editPost(id) { // not working, 401 unauthenticated
console.log('edit post clicked', id);
axios.patch('http://127.0.0.1:8000/api/posts/' + id, {
headers: {
Authorization: 'Bearer 3d58d6cd66e134a59b3a9373a2b4a233e55d00107b9251f654c5c92a2276a1c5'
}
})
.then((response) => {
this.info = response.data;
// this.message = 'Success';
console.log(this.info);
})
.catch((error) => {
console.log(error, error.response.data.message);
this.message = error.response.data.message;
})
},
deletePost(value){
console.log('delete post clicked', value);
}
},
mounted() {
axios.get('http://127.0.0.1:8000/api/posts', { // working as expected, authenticated
headers: {
Authorization: 'Bearer 3d58d6cd66e134a59b3a9373a2b4a233e55d00107b9251f654c5c92a2276a1c5'
}
})
.then((response) => {
this.info = response.data;
this.message = 'Success';
console.log(this.info);
})
.catch((error) => {
console.log(error, error.response.data.message);
this.message = error.response.data.message;
})
}
}
</script>
I dont understand how this can be since I have to authenticate to get the posts on page load and they load fine, with authentication using a Bearer token in header?
Why is the call to axios using a PATCH not working? The PATCH call works in postman fine also.
When I login I am redirected to secret page which needs JWT authentication. Data is loaded on secret page. And when I refresh the window - data is lost. How can I fix it?
I use eventBus to send a JWT token to sibling template.
Login view method on submit:
submitSignin() {
console.log("submit!");
this.submitted = true;
this.$v.$touch();
if (this.$v.$invalid) {
return; // stop here if form is invalid
}
axios
.post("http://localhost:3000/auth/login", this.authData)
.then((res) => {
this.token = res.data.token;
this.authData.email = "";
this.authData.password = "";
this.$v.$reset();
this.successMsg = "You Sign in Successfully!";
this.$router.push({ path: "/auth/all-users" });
this.$nextTick(() => {
eventBus.$emit("sendtoken", this.token);
});
})
.catch((err) => {
console.log(err.response.data.message);
this.errorMsg = err.response.data.message;
});
},
SecretPage view:
<script>
export default {
name: "SecretPage",
data() {
return {
users: [],
};
},
methods: {
loadUsers() {
let self = this;
eventBus.$on("sendtoken", (token) => {
axios
.get("http://localhost:3000/auth/all-users", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((response) => {
console.log(token);
console.log(response.data.users);
self.users = response.data.users;
})
.catch((err) => {
console.log(err);
});
});
},
},
mounted() {
this.loadUsers();
},
};
</script>
loaded users
I cannot correctly set my jwt token from my cookie to my Headers for an authenticaed gql request using apollo client.
I believe the problem is on my withApollo.js file, the one that wraps the App component on _app.js. The format of this file is based off of the wes bos advanced react nextjs graphql course. What happens is that nextauth saves the JWT as a cookie, and I can then grab the JWT from that cookie using a custom regex function. Then I try to set this token value to the authorization bearer header. The problem is that on the first load of a page with a gql query needing a jwt token, I get the error "Cannot read property 'cookie' of undefined". But, if I hit browser refresh, then suddenly it works and the token was successfully set to the header.
Some research led me to adding a setcontext link and so that's where I try to perform this operation. I tried to async await setting the token value but that doesn't seem to have helped. It just seems like the headers don't want to get set until on the refresh.
lib/withData.js
import { ApolloClient, ApolloLink, InMemoryCache } from '#apollo/client';
import { onError } from '#apollo/link-error';
import { getDataFromTree } from '#apollo/react-ssr';
import { createUploadLink } from 'apollo-upload-client';
import withApollo from 'next-with-apollo';
import { setContext } from 'apollo-link-context';
import { endpoint, prodEndpoint } from '../config';
import paginationField from './paginationField';
const getCookieValue = (name, cookie) =>
cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`)?.pop() || '';
let token;
function createClient(props) {
const { initialState, headers, ctx } = props;
console.log({ headers });
// console.log({ ctx });
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError)
console.log(
`[Network error]: ${networkError}. Backend is unreachable. Is it running?`
);
}),
setContext(async (request, previousContext) => {
token = await getCookieValue('token', headers.cookie);
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
};
}),
createUploadLink({
uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
fetchOptions: {
credentials: 'include',
},
headers,
}),
]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// TODO: We will add this together!
// allProducts: paginationField(),
},
},
},
}).restore(initialState || {}),
});
}
export default withApollo(createClient, { getDataFromTree });
page/_app.js
import { ApolloProvider } from '#apollo/client';
import NProgress from 'nprogress';
import Router from 'next/router';
import { Provider, getSession } from 'next-auth/client';
import { CookiesProvider } from 'react-cookie';
import nookies, { parseCookies } from 'nookies';
import Page from '../components/Page';
import '../components/styles/nprogress.css';
import withData from '../lib/withData';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
function MyApp({ Component, pageProps, apollo, user }) {
return (
<Provider session={pageProps.session}>
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} {...user} />
</Page>
</ApolloProvider>
</Provider>
);
}
MyApp.getInitialProps = async function ({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
pageProps.query = ctx.query;
const user = {};
const { req } = ctx;
const session = await getSession({ req });
if (session) {
user.email = session.user.email;
user.id = session.user.id;
user.isUser = !!session;
// Set
nookies.set(ctx, 'token', session.accessToken, {
maxAge: 30 * 24 * 60 * 60,
path: '/',
});
}
return {
pageProps,
user: user || null,
};
};
export default withData(MyApp);
api/auth/[...nextAuth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from 'axios';
const providers = [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await axios
.post('http://localhost:1337/auth/local', {
identifier: credentials.username,
password: credentials.password,
})
.then((res) => {
res.data.user.token = res.data.jwt;
return res.data.user;
}) // define user as res.data.user (will be referenced in callbacks)
.catch((error) => {
console.log('An error occurred:', error);
});
if (user) {
return user;
}
return null;
},
}),
];
const callbacks = {
// Getting the JWT token from API response
async jwt(token, user, account, profile, isNewUser) {
// WRITE TO TOKEN (from above sources)
if (user) {
const provider = account.provider || user.provider || null;
let response;
let data;
switch (provider) {
case 'google':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/google/callback?access_token=${account?.accessToken}`
);
data = await response.json();
if (data) {
token.accessToken = data.jwt;
token.id = data.user._id;
} else {
console.log('ERROR No data');
}
break;
case 'local':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/local/callback?access_token=${account?.accessToken}`
);
data = await response.json();
token.accessToken = user.token;
token.id = user.id;
break;
default:
console.log(`ERROR: Provider value is ${provider}`);
break;
}
}
return token;
},
async session(session, token) {
// WRITE TO SESSION (from token)
// console.log(token);
session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
redirect: async (url, baseUrl) => baseUrl,
};
const sessionPreferences = {
session: {
jwt: true,
},
};
const options = {
providers,
callbacks,
sessionPreferences,
};
export default (req, res) => NextAuth(req, res, options);
I have a simple vue app where I'm trying to add simple authentication. Inside my login.vue, I use axios to authenticate the user via ajax and store the token returned by the api in the store then redirect to a new page (ex: dashboard.vue).
The problem is that the token is saved but the view is not updated, can't call router.push() ...
Any ideas why isn't it working ? Thanks
Login.vue
methods: {
authenticate () {
var dataLogin = {
email: this.login,
password: this.password
}
var headers = { headers: { 'Content-type': 'application/json', 'Accept': 'application/json' } }
axios.post(config.apiUrl, dataLogin, headers)
.then(response => {
this.$store.dispatch('login', response.data).then(() => {
// if there is no error go to home page
if (!this.$store.getters.error) {
this.$router.push('/')
}
})
})
.catch(error => {
this.errorMessage = error.response.data.message
this.authError = true
})
}
}
The store function just save the token with localStorage
const actions = {
login (context, data) {
context.commit('authenticate', data)
}
}
const mutations = {
authenticate (state, data) {
localStorage.setItem('user-access_token', data.access_token)
}
}
You are calling a then() handler when you dispatch the action.
But your action does not return a promise.
So return a promise in your action as follows:
const actions = {
login (context, data) {
return new Promise((resolve, reject) => {
context.commit('authenticate', data)
resolve()
})
}
}
Also chain your promises for better readability
axios.post(config.apiUrl, dataLogin, headers)
.then(response => {
return this.$store.dispatch('login', response.data)
}).then(() => {
// if there is no error go to home page
if (!this.$store.getters.error) {
this.$router.push('/')
}
})
.catch(error => {
this.errorMessage = error.response.data.message
this.authError = true
})