React-Apollo Authentication in Next.js app - graphql

I am trying to setup authentication in an app using React-Apollo and Next.js. It is setup to return a cookie containing a jwt token after sending in the credentials, which works perfectly well. When trying to run a query for the current user, it comes back as null. If I refresh the page, the query works and I get back the current user. I did notice that the cache is updated with the correct data even though the response is null.
Component:
class Index extends Component{
constructor(props){
super(props)
this.usernameInput = ''
this.passwordInput = ''
this.state = {
error: null,
}
}
getCurrentAccount = async () => {
return await this.props.getCurrentAccount
}
sendCreds = async (e) => {
e.preventDefault()
const result = await this.props.authenticate({
variables: {
username: this.usernameInput,
password: this.passwordInput,
},
update: (store, {data: { currentAccount } } ) => {
const data = store.readQuery({
query: QUERY_CURRENT_ACCOUNT,
})
console.log('after authenitcation', data)
}
})
if (result.error) {
console.log('error on authentication', result.error)
this.setState({error: 'There was an error with our server.'})
} else if (result) {
//this.login()
}
}
login = async () => {
const data = await this.getCurrentAccount()
if (data.error) {
console.log('error on fetching account', data.error)
this.setState({error: 'There was an error with our server.'})
} else if (data.currentAccount.role) {
Router.push('/dashboard')
} else {
this.setState({error: 'The ID or password you entered is incorrect.'})
}
}
render() {
return (
<form onSubmit={this.sendCreds}>
{ this.state.error ? <FormError>{this.state.error}</FormError> : null}
<fieldset>
<InputField id="username" label="usrname" placeholder="username" pattern="^[a-zA-Z0-9_]*$" required inputValue={val=>this.usernameInput = val}/>
<InputField id="password" type="password" label="Password" placeholder="Password" required inputValue={val=>this.passwordInput = val}/>
</fieldset>
<ButtonPrimary type="submit" label="Send Credentials" width="full"/>
<ButtonPrimary label="Get Current User to Login" width="full" clickAction={()=>this.login()}/>
<footer>
<Link href="forgot-username">
<a>Forgot my username</a>
</Link>
<Link href="forgot-password">
<a>Forgot my password</a>
</Link>
</footer>
</form>
)}
}
GrahpQL:
const MUTATION_AUTH = gql`
mutation authenticate($username: String!, $password: String!) {
authenticate(input: {
username: $username,
password: $password
}) {
jwtToken
}
}
`
const QUERY_CURRENT_ACCOUNT = gql`
{
currentAccount {
account {
firstName
profileImageUrl
}
role
}
}
`
export default withData(compose(
graphql(MUTATION_AUTH, { name: 'authenticate' }),
graphql(QUERY_CURRENT_ACCOUNT, { name: 'getCurrentAccount'} )
)
(withApollo(Index)))

Related

How to make a graphql entry in a psql database

I am trying to figure out how to make a graphql entry in a psql database.
I am stuck and am not getting any feedback from console logs at any point in my attempt. I'm stuck for what to try next (or where to look for a tutorial showing how this step is supposed to work).
I have a table in my prisma schema called 'issue'. I am trying to create an 'issue' entry.
I have made a form with:
import * as React from "react"
import { Box, Center, Heading, Button, } from "#chakra-ui/react"
import { Select, OptionBase, GroupBase } from "chakra-react-select";
import { groupedIssueCategories } from "../components/issue/categories"
import { gql } from "#apollo/client"
import Head from 'next/head'
import { IssueInput, useAllIssuesQuery, useCreateIssueMutation } from "lib/graphql"
import * as c from "#chakra-ui/react"
import { Input } from "components/Input"
// import { Select } from "components/Select"
import { HomeLayout } from "components/HomeLayout"
import { Limiter } from "components/Limiter"
import { Form } from "components/Form"
import Yup from "lib/yup"
import { useForm } from "lib/hooks/useForm"
import { useMe } from "lib/hooks/useMe"
import { useToast } from "lib/hooks/useToast"
interface GroupedRiskOption extends OptionBase {
label: string
value: string
}
const _ = gql`
mutation CreateIssue($data: IssueInput!) {
createIssue(data: $data) {
id
title
issueCategory
description
userId
}
}
query AllIssues {
allIssues {
id
title
issueId
description
userId
}
}
`
export default function Issue() {
const toast = useToast()
const { me, loading: meLoading } = useMe()
const [createIssue] = useCreateIssueMutation()
const { data: issues, refetch } = useAllIssuesQuery()
const IssueSchema = Yup.object().shape({
title: Yup.string().required("Title is required"),
issueCategory: Yup.string().required("Category is required"),
description: Yup.string().required("Description is required"),
})
const form = useForm({ schema: IssueSchema })
const onSubmit = (data: IssueInput) => {
console.log(data)
return form.handler(() => createIssue({ variables: { data: { ...data, userId: me?.id || ""} } }), {
onSuccess: async () => {
toast({
title: "Issue created",
description: "Your issue has been created",
status: "success",
})
refetch()
form.reset()
},
})
}
if (meLoading)
return (
<c.Center>
<c.Spinner />
</c.Center>
)
if (!me) return null
return (
<Box>
<Head>
<title>Create Issue</title>
</Head>
<Limiter pt={20} minH="calc(100vh - 65px)">
<Center flexDir="column">
<Heading as="h1" size="3xl" fontWeight="extrabold" px="3rem" lineHeight="1.2" letterSpacing="tight" color="brand.orange">
Create Issue
</Heading>
<Form onSubmit={onSubmit} {...form}>
<c.Stack spacing={2}>
<c.Heading>Issues</c.Heading>
<Input autoFocus name="title" label="Title" placeholder="Eg: climate change" />
<Input name="description" label="Description" placeholder="Eg: Issues relating to climate change" />
<Select<GroupedRiskOption, true, GroupBase<GroupedRiskOption>>
// isMulti
name="issueCategory"
options={groupedIssueCategories}
placeholder="Select issue categories"
closeMenuOnSelect={false}
/>
<Button
color="brand.orange"
type="submit"
isFullWidth
isDisabled={form.formState.isSubmitting ||
!form.formState.isDirty}
isLoading={form.formState.isSubmitting}
>
Create Issue
</Button>
<c.List>
{/* {issues.allIssues.map((issue) => (
<c.ListItem key={issue.id}>
{issue.title}
{issue.issueCategory}
{issue.description}
</c.ListItem>
))} */}
</c.List>
</c.Stack>
</Form>
</Center>
</Limiter>
</Box>
)
}
Issue.getLayout = (page: React.ReactNode) => <HomeLayout>{page}</HomeLayout>
I have a create issue mutation in my lib/graphql:
export function useCreateIssueMutation(baseOptions?: Apollo.MutationHookOptions<CreateIssueMutation, CreateIssueMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateIssueMutation, CreateIssueMutationVariables>(CreateIssueDocument, options);
}
export type CreateIssueMutationHookResult = ReturnType<typeof useCreateIssueMutation>;
export type CreateIssueMutationResult = Apollo.MutationResult<CreateIssueMutation>;
export type CreateIssueMutationOptions = Apollo.BaseMutationOptions<CreateIssueMutation, CreateIssueMutationVariables>;
When I click submit, nothing happens in the console. I can't log the data, and I can't see any errors, either in the terminal or in the console.
Can anyone give me a steer on where to look for insights as to what is going wrong. There is no data in the database, the onSuccess step doesn't get performed.

Next JS Login form not working. Back end used is springboot

Hi we have purchased a theme. In the theme they have include JWT based login but used fake db(dummy values) and local storage. I want to change it to our logic which uses JWT based authentication. Backend used is spring boot.
I could not fully understand the code here. In the docs, they mentioned to make changes in the authcontext file only. However, it didnt worked after I made changes in the handlelogin function(authcontext.js file). Also I gave the loginEndpoint as the backend API URL, but not sure what to replace in place of meEndpoint. If anybody gets any better idea, please try to help. Will it work properly if me make changes in the handleLogin function of authContext.js file alone? or do we have to make changes in the initAuth async function that is defined inside useEffect hook? Also what is the onSubmit function(the function which executes when login button is clicked) doing
please find the code in AuthContext.js file
import { createContext, useEffect, useState } from 'react'
// ** Next Import
import { useRouter } from 'next/router'
// ** Axios
import axios from 'axios'
// ** Config
import authConfig from 'src/configs/auth'
// ** Defaults
const defaultProvider = {
user: null,
loading: true,
setUser: () => null,
setLoading: () => Boolean,
isInitialized: false,
login: () => Promise.resolve(),
logout: () => Promise.resolve(),
setIsInitialized: () => Boolean,
register: () => Promise.resolve(),
token:null,
setToken:()=>null
}
const AuthContext = createContext(defaultProvider)
const AuthProvider = ({ children }) => {
// ** States
const [user, setUser] = useState(defaultProvider.user)
const [loading, setLoading] = useState(defaultProvider.loading)
const [isInitialized, setIsInitialized] = useState(defaultProvider.isInitialized)
const [token,setToken]=useState(defaultProvider.token)
// ** Hooks
const router = useRouter()
useEffect(() => {
const initAuth = async () => {
setIsInitialized(true)
const storedToken = window.localStorage.getItem(authConfig.storageTokenKeyName)
if (storedToken) {
setLoading(true)
await axios
.get(authConfig.meEndpoint, {
headers: {
Authorization: storedToken
}
})
.then(async response => {
setLoading(false)
setUser({ ...response.data.userData })
})
} else {
setLoading(false)
}
}
initAuth()
}, [])
const handleLogin = (params, errorCallback) => {
axios
.post(authConfig.loginEndpoint, params)
.then(async res => {
window.localStorage.setItem(authConfig.storageTokenKeyName, res.data.accessToken)
})
.then(() => {
axios
.get(authConfig.meEndpoint, {
headers: {
Authorization: window.localStorage.getItem(authConfig.storageTokenKeyName)
}
})
.then(async response => {
const returnUrl = router.query.returnUrl
setUser({ ...response.data.userData })
await window.localStorage.setItem('userData', JSON.stringify(response.data.userData))
const redirectURL = returnUrl && returnUrl !== '/' ? returnUrl : '/'
router.replace(redirectURL)
})
})
.catch(err => {
if (errorCallback) errorCallback(err)
})
}
const values = {
user,
loading,
setUser,
setLoading,
isInitialized,
setIsInitialized,
login: handleLogin,
logout: handleLogout,
register: handleRegister
}
return <AuthContext.Provider value={values}>{children}</AuthContext.Provider>
}
export { AuthContext, AuthProvider }
the authConfig file as below:-
export default {
meEndpoint: '/auth/me',
loginEndpoint: 'qortex-dev-backoffice-portal.westus2.cloudapp.azure.com:8080/auth-user/login',
registerEndpoint: '/jwt/register',
storageTokenKeyName: 'accessToken'
}
the handlesubmit function executes on login button click as shown below
const onSubmit = data => {
const { email, password } = data
auth.login({ email, password }, () => {
setError('email', {
type: 'manual',
message: 'Email or Password is invalid'
})
})
}
Login form controls code as shown below
<form noValidate autoComplete='off' onSubmit={handleSubmit(onSubmit)}>
<FormControl fullWidth sx={{ mb: 4 }}>
<Controller
name='email'
control={control}
rules={{ required: true }}
render={({ field: { value, onChange, onBlur } }) => (
<TextField
autoFocus
label='Email'
value={value}
onBlur={onBlur}
onChange={onChange}
error={Boolean(errors.email)}
placeholder='admin#materio.com'
/>
)}
/>
{errors.email && <FormHelperText sx={{ color: 'error.main' }}>{errors.email.message}</FormHelperText>}
</FormControl>
<FormControl fullWidth>
<InputLabel htmlFor='auth-login-v2-password' error={Boolean(errors.password)}>
Password
</InputLabel>
<Controller
name='password'
control={control}
rules={{ required: true }}
render={({ field: { value, onChange, onBlur } }) => (
<OutlinedInput
value={value}
onBlur={onBlur}
label='Password'
onChange={onChange}
id='auth-login-v2-password'
error={Boolean(errors.password)}
type={showPassword ? 'text' : 'password'}
endAdornment={
<InputAdornment position='end'>
<IconButton
edge='end'
onMouseDown={e => e.preventDefault()}
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOutline /> : <EyeOffOutline />}
</IconButton>
</InputAdornment>
}
/>
)}
/>
{errors.password && (
<FormHelperText sx={{ color: 'error.main' }} id=''>
{errors.password.message}
</FormHelperText>
)}
</FormControl>

Why does RTK query response handling not work?

I tried to use the RTK query on my login request, but I got some trouble when printing out the result. Here is my code.
authRTK.ts
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/query/react";
import { loginForm, UserResponse } from "../type/type";
import { RootState } from "./store";
export const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:3001',
prepareHeaders: (headers, { getState }) => {
// By default, if we have a token in the store, let's use that for authenticated requests
const token = (getState() as RootState).auth.token;
if (token) {
headers.set("authentication", `Bearer ${token}`);
}
return headers;
}
}),
endpoints: (build) => ({
login: build.mutation<UserResponse, loginForm>({
query: (credentials) => ({
url: "login",
method: "POST",
body: credentials
}),
transformResponse: (response: { data: UserResponse }) => {
return response.data
},
}),
protected: build.mutation({
query: () => "protected"
})
})
});
export const { useLoginMutation,useProtectedMutation } = api;
store.ts
import { configureStore } from '#reduxjs/toolkit'
import cartReducer from './cartRedux';
import userReducer from './authRedux';
import { api } from './authRTK';
export const store = configureStore({
reducer:{
cart: cartReducer,
auth: userReducer,
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),//getDefaultMiddleware
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Login.tsx
const Login = () => {
const [login, { isLoading,error,isError}] = useLoginMutation();
const [showPassword,setShowPassword] = useState<boolean>(false);
return (
<Container>
<Wrapper>
{/* <button onClick={()=>testCookie()}>測試一下cookie</button> */}
<Title>SIGN IN</Title>
<Formik
initialValues={{ email: "", password: "" }}
validationSchema={Yup.object({
password: Yup.string()
.min(8, 'Must be 8 characters or higher')
.required(),
email: Yup.string().email('Invalid email address').required(),
})}
onSubmit = { async (values, actions) => {
try{
const result = await login(values);
if("data" in result){
console.log(result.data)
}else{
console.log((result.error as RequestError).data) ////this will printout the expected result , but I have to cast error to RequestError type to print the nested data inside , and I can't use this data else where like error above
console.log(error) //This printout undefined,mean there's no error data inside,but not supposed to happen
console.log(isError) //print out false , but supposed to be true
}
}catch(err){
console.log(err)
}
}}>
{({
errors,
values,
handleChange,
handleBlur,
handleSubmit,
validateField
}) => (
<Form onSubmit={handleSubmit}>
<InputContainer>
<Input
onChange={handleChange}
onBlur={handleBlur}
value={values.email}
type="text"
name="email"
placeholder="Email"
data-testid="email"
/>
</InputContainer>
{errors.email && <Error data-testid="emailError">{errors.email}</Error>}
<InputContainer>
<Input
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
type={showPassword ? "text" : "password"}
name="password"
placeholder="Password"
data-testid="password"
/>
{showPassword ? <VisibilityOff onClick={()=>setShowPassword(false) }/> : <Visibility onClick={()=>setShowPassword(true) }/> }
</InputContainer>
{errors.password && <Error data-testid="passwordError">{errors.password}</Error>}
<Button
data-testid="submit"
type="submit">Submit</Button>
</Form>
)}
</Formik>
</Wrapper>
</Container>
);
};
export default Login;
So My main problems are with the login.tsx,Error didn't work as expected, and my response data have to determine if "data" is in it, even though I used transformResponse.
BTW my response type looks like below
RequestError:
{
data:string;
status:string
}
data is not the data from your response. It is the data property of the trigger function result.
trigger always returns an object in the form { data: ... } or { error: ... }.
So without your transformResult you would end up with result.data.data instead of result.data.
You can also unwrap that, to directly get the data and throw an error in the error case, but that's not the default as it might lead to uncaught promise rejection errors if you don't handle it.
async (values, actions) => {
try{
const result = await login(values).unwrap();
console.log(result.data)
} catch(err){
console.log(err)
}
}

Auth0 - Refreshing token for google-oauth2 connections

I have a SPA react application where I am using auth0 for authentication. I would like to do a silent authentication and get a new token whenever site is refreshed, like it is suggested in this answer.
I have an Auth class responsible for handling tokens:
import auth0 from 'auth0-js'
import { authConfig } from '../config'
export default class Auth {
accessToken
idToken
expiresAt
auth0 = new auth0.WebAuth({
domain: authConfig.domain,
clientID: authConfig.clientId,
redirectUri: authConfig.callbackUrl,
responseType: 'token id_token',
scope: 'openid'
})
constructor(history) {
this.history = history
this.login = this.login.bind(this)
this.logout = this.logout.bind(this)
this.handleAuthentication = this.handleAuthentication.bind(this)
this.isAuthenticated = this.isAuthenticated.bind(this)
this.getAccessToken = this.getAccessToken.bind(this)
this.getIdToken = this.getIdToken.bind(this)
this.renewSession = this.renewSession.bind(this)
}
login() {
this.auth0.authorize()
}
handleAuthentication() {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
console.log('Access token: ', authResult.accessToken)
console.log('id token: ', authResult.idToken)
this.setSession(authResult)
} else if (err) {
this.history.replace('/')
console.log(err)
alert(`Error: ${err.error}. Check the console for further details.`)
}
})
}
getAccessToken() {
return this.accessToken
}
getIdToken() {
return this.idToken
}
setSession(authResult) {
// Set isLoggedIn flag in localStorage
localStorage.setItem('isLoggedIn', 'true')
// Set the time that the access token will expire at
let expiresAt = authResult.expiresIn * 1000 + new Date().getTime()
this.accessToken = authResult.accessToken
this.idToken = authResult.idToken
this.expiresAt = expiresAt
// navigate to the home route
this.history.replace('/')
}
renewSession(cb) {
this.auth0.checkSession({}, (err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult)
cb(err, authResult)
} else if (err) {
this.logout()
console.log(
`Could not get a new token (${err.error}: ${err.error_description}).`
)
}
})
}
logout() {
// Remove tokens and expiry time
this.accessToken = null
this.idToken = null
this.expiresAt = 0
// Remove isLoggedIn flag from localStorage
localStorage.removeItem('isLoggedIn')
this.auth0.logout({
return_to: `${window.location.origin}/login`
})
}
isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
let expiresAt = this.expiresAt
return new Date().getTime() < expiresAt
}
}
In my main App component in componentDidMount I am calling the renewSession method from Auth class:
export default class App extends Component<AppProps, AppState> {
constructor(props: AppProps) {
super(props)
this.handleLogin = this.handleLogin.bind(this)
this.handleLogout = this.handleLogout.bind(this)
this.createNewPost = this.createNewPost.bind(this)
}
state: AppState = {
tokenRenewed: false
}
componentDidMount() {
this.props.auth.renewSession(() => {
this.setState({ tokenRenewed: true })
})
}
handleLogin() {
this.props.auth.login()
}
handleLogout() {
this.props.auth.logout()
}
async createNewPost() {
const idToken = this.props.auth.getIdToken()
try {
const newPost = await createPost(idToken)
this.props.history.push(`/posts/${newPost.postId}/edit`)
} catch {
alert('Post creation failed')
}
}
render() {
if (!this.state.tokenRenewed) return 'loading...'
const userAuthenticated = this.props.auth.isAuthenticated()
return (
<div>
<Segment vertical>
<Grid container stackable verticalAlign="middle">
<Grid.Row>
<Grid.Column width={16}>
<Router history={this.props.history}>
{this.generateMenu(userAuthenticated)}
{this.generateCurrentPage(userAuthenticated)}
</Router>
</Grid.Column>
</Grid.Row>
</Grid>
</Segment>
</div>
)
}
And this are my routes:
<Router history={history}>
<Switch>
<Route
path="/callback"
render={props => {
handleAuthentication(props)
return <Callback />
}}
/>
<Route
path="/login"
render={props => {
return <LogIn auth={auth} {...props} />
}}
/>
<Route
path="/"
render={props => {
return <App auth={auth} {...props} />
}}
/>
</Switch>
</Router>
That works fine if I login with username/password. But, if I use a social login like Google/Gmail then whenever I login in to the app, I get an error from auth0.checkSession in Auth class:
Could not get a new token (login_required: Login required).
How can I make this work with Google/Gmail login as well?

laravel vue getting info by hidden field

I need to pass logged user id to back-end and I have vuex store so I can get my user info like {{currentUser.id}} the problem is i cannot pass it to back-end it gives me validation error that user_id is required while i have this hidden input in my form
<input type="hidden" name="user_id" :value="currentUser.id">
for normal inputs i have v-model like v-model="project.title" which is not possible to use on hidden fields.
The question here is how can I pass my user_id to back-end?
Code
<script>
import validate from 'validate.js';
export default {
data: function () {
return {
project: {
title: '',
body: '',
attachment: '',
projectclass: '',
deadline: '',
user_id: '',
csrf: document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
},
errors: null
}
},
computed: {
currentUser() {
return this.$store.getters.currentUser;
}
},
methods: {
add() {
this.errors = null;
const errors = validate(this.$data.project);
if(errors) {
this.errors = errors;
return;
}
axios.post('/api/projects/new', this.$data.project)
.then((response) => {
this.$router.push('/projects');
});
}
}
}
</script>
This happens because user_id in this.$data.project dosn't get updated.
Instead of having hidden input you can just do
add() {
this.errors = null;
const errors = validate(Object.assign(this.$data.project, {user_id: this.currentUser.id}));
if(errors) {
this.errors = errors;
return;
}
axios.post('/api/projects/new', Object.assign(this.$data.project, {user_id: this.currentUser.id}))
.then((response) => {
this.$router.push('/projects');
});
}

Resources