I'm working on the Pro MERN Stack: Full Stack Web App Development with Mongo, Express, React, and Node book, and I ran into CORS policy errors whenever I tried to go back to my homepage. It looks like the error is coming from my graphqlFetch file but I'm not so sure what the issue is. Any insight would be extremely helpful!!
CORS policy error message
GraphQLFetch error
1:
My GraphQLFetch code:
import fetch from 'isomorphic-fetch';
const dateRegex = new RegExp('^\\d\\d\\d\\d-\\d\\d-\\d\\d');
function jsonDateReviver(key, value) {
if (dateRegex.test(value)) return new Date(value);
return value;
}
export default async function
graphQLFetch(query, variables = {}, showError = null, cookie = null) {
const apiEndpoint = (__isBrowser__) // eslint-disable-line no-undef
? window.ENV.UI_API_ENDPOINT
: process.env.UI_SERVER_API_ENDPOINT;
try {
const headers = { 'Content-Type': 'application/json' };
if (cookie) headers.Cookie = cookie;
const response = await fetch(apiEndpoint, {
method: 'POST',
credentials: 'include',
headers,
body: JSON.stringify({ query, variables }),
});
const body = await response.text();
const result = JSON.parse(body, jsonDateReviver);
if (result.errors) {
const error = result.errors[0];
if (error.extensions.code === 'BAD_USER_INPUT') {
const details = error.extensions.exception.errors.join('\n ');
if (showError) showError(`${error.message}:\n ${details}`);
} else if (showError) {
showError(`${error.extensions.code}: ${error.message}`);
}
}
return result.data;
} catch (e) {
if (showError) showError(`Error in sending data to server: ${e.message}`);
return null;
}
}
My IssueEdit file
import React from 'react';
import { Link } from 'react-router-dom';
import { LinkContainer } from 'react-router-bootstrap';
import {
Col, Panel, Form, FormGroup, FormControl, ControlLabel,
ButtonToolbar, Button, Alert,
} from 'react-bootstrap';
import graphQLFetch from './graphQLFetch.js';
import NumInput from './NumInput.jsx';
import DateInput from './DateInput.jsx';
import TextInput from './TextInput.jsx';
import Toast from './Toast.jsx';
import store from './store.js';
export default class IssueEdit extends React.Component {
static async fetchData(match, search, showError) {
const query = `query issue($id: Int!) {
issue(id: $id) {
id title status owner
effort created due description
}
}`;
const { params: { id } } = match;
const result = await graphQLFetch(query, { id }, showError);
return result;
}
constructor() {
super();
const issue = store.initialData ? store.initialData.issue : null;
delete store.initialData;
this.state = {
issue,
invalidFields: {},
showingValidation: false,
toastVisible: false,
toastMessage: '',
toastType: 'success',
};
this.onChange = this.onChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.onValidityChange = this.onValidityChange.bind(this);
this.dismissValidation = this.dismissValidation.bind(this);
this.showValidation = this.showValidation.bind(this);
this.showSuccess = this.showSuccess.bind(this);
this.showError = this.showError.bind(this);
this.dismissToast = this.dismissToast.bind(this);
}
componentDidMount() {
const { issue } = this.state;
if (issue == null) this.loadData();
}
componentDidUpdate(prevProps) {
const { match: { params: { id: prevId } } } = prevProps;
const { match: { params: { id } } } = this.props;
if (id !== prevId) {
this.loadData();
}
}
onChange(event, naturalValue) {
const { name, value: textValue } = event.target;
const value = naturalValue === undefined ? textValue : naturalValue;
this.setState(prevState => ({
issue: { ...prevState.issue, [name]: value },
}));
}
onValidityChange(event, valid) {
const { name } = event.target;
this.setState((prevState) => {
const invalidFields = { ...prevState.invalidFields, [name]: !valid };
if (valid) delete invalidFields[name];
return { invalidFields };
});
}
async handleSubmit(e) {
e.preventDefault();
this.showValidation();
const { issue, invalidFields } = this.state;
if (Object.keys(invalidFields).length !== 0) return;
const query = `mutation issueUpdate(
$id: Int!
$changes: IssueUpdateInputs!
) {
issueUpdate(
id: $id
changes: $changes
) {
id title status owner
effort created due description
}
}`;
const { id, created, ...changes } = issue;
const data = await graphQLFetch(query, { changes, id }, this.showError);
if (data) {
this.setState({ issue: data.issueUpdate });
this.showSuccess('Updated issue successfully');
}
}
async loadData() {
const { match } = this.props;
const data = await IssueEdit.fetchData(match, null, this.showError);
this.setState({ issue: data ? data.issue : {}, invalidFields: {} });
}
showValidation() {
this.setState({ showingValidation: true });
}
dismissValidation() {
this.setState({ showingValidation: false });
}
showSuccess(message) {
this.setState({
toastVisible: true, toastMessage: message, toastType: 'success',
});
}
showError(message) {
this.setState({
toastVisible: true, toastMessage: message, toastType: 'danger',
});
}
dismissToast() {
this.setState({ toastVisible: false });
}
render() {
const { issue } = this.state;
if (issue == null) return null;
const { issue: { id } } = this.state;
const { match: { params: { id: propsId } } } = this.props;
if (id == null) {
if (propsId != null) {
return <h3>{`Issue with ID ${propsId} not found.`}</h3>;
}
return null;
}
const { invalidFields, showingValidation } = this.state;
let validationMessage;
if (Object.keys(invalidFields).length !== 0 && showingValidation) {
validationMessage = (
<Alert bsStyle="danger" onDismiss={this.dismissValidation}>
Please correct invalid fields before submitting.
</Alert>
);
}
const { issue: { title, status } } = this.state;
const { issue: { owner, effort, description } } = this.state;
const { issue: { created, due } } = this.state;
const { toastVisible, toastMessage, toastType } = this.state;
return (
<Panel>
<Panel.Heading>
<Panel.Title>{`Editing issue: ${id}`}</Panel.Title>
</Panel.Heading>
<Panel.Body>
<Form horizontal onSubmit={this.handleSubmit}>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Created</Col>
<Col sm={9}>
<FormControl.Static>
{created.toDateString()}
</FormControl.Static>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Status</Col>
<Col sm={9}>
<FormControl
componentClass="select"
name="status"
value={status}
onChange={this.onChange}
>
<option value="New">New</option>
<option value="Assigned">Assigned</option>
<option value="Fixed">Fixed</option>
<option value="Closed">Closed</option>
</FormControl>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Owner</Col>
<Col sm={9}>
<FormControl
componentClass={TextInput}
name="owner"
value={owner}
onChange={this.onChange}
key={id}
/>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Effort</Col>
<Col sm={9}>
<FormControl
componentClass={NumInput}
name="effort"
value={effort}
onChange={this.onChange}
key={id}
/>
</Col>
</FormGroup>
<FormGroup validationState={
invalidFields.due ? 'error' : null
}
>
<Col componentClass={ControlLabel} sm={3}>Due</Col>
<Col sm={9}>
<FormControl
componentClass={DateInput}
onValidityChange={this.onValidityChange}
name="due"
value={due}
onChange={this.onChange}
key={id}
/>
<FormControl.Feedback />
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Title</Col>
<Col sm={9}>
<FormControl
componentClass={TextInput}
size={50}
name="title"
value={title}
onChange={this.onChange}
key={id}
/>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} sm={3}>Description</Col>
<Col sm={9}>
<FormControl
componentClass={TextInput}
tag="textarea"
rows={4}
cols={50}
name="description"
value={description}
onChange={this.onChange}
key={id}
/>
</Col>
</FormGroup>
<FormGroup>
<Col smOffset={3} sm={6}>
<ButtonToolbar>
<Button bsStyle="primary" type="submit">Submit</Button>
<LinkContainer to="/issues">
<Button bsStyle="link">Back</Button>
</LinkContainer>
</ButtonToolbar>
</Col>
</FormGroup>
<FormGroup>
<Col smOffset={3} sm={9}>{validationMessage}</Col>
</FormGroup>
</Form>
</Panel.Body>
<Panel.Footer>
<Link to={`/edit/${id - 1}`}>Prev</Link>
{' | '}
<Link to={`/edit/${id + 1}`}>Next</Link>
</Panel.Footer>
<Toast
showing={toastVisible}
onDismiss={this.dismissToast}
bsStyle={toastType}
>
{toastMessage}
</Toast>
</Panel>
);
}
}
My IssueList file:
import React from 'react';
import URLSearchParams from 'url-search-params';
import { Panel } from 'react-bootstrap';
import IssueFilter from './IssueFilter.jsx';
import IssueTable from './IssueTable.jsx';
import IssueDetail from './IssueDetail.jsx';
import graphQLFetch from './graphQLFetch.js';
import withToast from './withToast.jsx';
import store from './store.js';
class IssueList extends React.Component {
static async fetchData(match, search, showError) {
const params = new URLSearchParams(search);
const vars = { hasSelection: false, selectedId: 0 };
if (params.get('status')) vars.status = params.get('status');
const effortMin = parseInt(params.get('effortMin'), 10);
if (!Number.isNaN(effortMin)) vars.effortMin = effortMin;
const effortMax = parseInt(params.get('effortMax'), 10);
if (!Number.isNaN(effortMax)) vars.effortMax = effortMax;
const { params: { id } } = match;
const idInt = parseInt(id, 10);
if (!Number.isNaN(idInt)) {
vars.hasSelection = true;
vars.selectedId = idInt;
}
const query = `query issueList(
$status: StatusType
$effortMin: Int
$effortMax: Int
$hasSelection: Boolean!
$selectedId: Int!
) {
issueList(
status: $status
effortMin: $effortMin
effortMax: $effortMax
) {
id title status owner
created effort due
}
issue(id: $selectedId) #include (if : $hasSelection) {
id description
}
}`;
const data = await graphQLFetch(query, vars, showError);
return data;
}
constructor() {
super();
const issues = store.initialData ? store.initialData.issueList : null;
const selectedIssue = store.initialData
? store.initialData.issue
: null;
delete store.initialData;
this.state = {
issues,
selectedIssue,
};
this.closeIssue = this.closeIssue.bind(this);
this.deleteIssue = this.deleteIssue.bind(this);
}
componentDidMount() {
const { issues } = this.state;
if (issues == null) this.loadData();
}
componentDidUpdate(prevProps) {
const {
location: { search: prevSearch },
match: { params: { id: prevId } },
} = prevProps;
const { location: { search }, match: { params: { id } } } = this.props;
if (prevSearch !== search || prevId !== id) {
this.loadData();
}
}
async loadData() {
const { location: { search }, match, showError } = this.props;
const data = await IssueList.fetchData(match, search, showError);
if (data) {
this.setState({ issues: data.issueList, selectedIssue: data.issue });
}
}
async closeIssue(index) {
const query = `mutation issueClose($id: Int!) {
issueUpdate(id: $id, changes: { status: Closed }) {
id title status owner
effort created due description
}
}`;
const { issues } = this.state;
const { showError } = this.props;
const data = await graphQLFetch(query, { id: issues[index].id },
showError);
if (data) {
this.setState((prevState) => {
const newList = [...prevState.issues];
newList[index] = data.issueUpdate;
return { issues: newList };
});
} else {
this.loadData();
}
}
async deleteIssue(index) {
const query = `mutation issueDelete($id: Int!) {
issueDelete(id: $id)
}`;
const { issues } = this.state;
const { location: { pathname, search }, history } = this.props;
const { id } = issues[index];
const { showSuccess, showError } = this.props;
const data = await graphQLFetch(query, { id }, showError);
if (data && data.issueDelete) {
this.setState((prevState) => {
const newList = [...prevState.issues];
if (pathname === `/issues/${id}`) {
history.push({ pathname: '/issues', search });
}
newList.splice(index, 1);
return { issues: newList };
});
showSuccess(`Deleted issue ${id} successfully.`);
} else {
this.loadData();
}
}
render() {
const { issues } = this.state;
if (issues == null) return null;
const { selectedIssue } = this.state;
return (
<React.Fragment>
<Panel>
<Panel.Heading>
<Panel.Title toggle>Filter</Panel.Title>
</Panel.Heading>
<Panel.Body collapsible>
<IssueFilter />
</Panel.Body>
</Panel>
<IssueTable
issues={issues}
closeIssue={this.closeIssue}
deleteIssue={this.deleteIssue}
/>
<IssueDetail issue={selectedIssue} />
</React.Fragment>
);
}
}
const IssueListWithToast = withToast(IssueList);
IssueListWithToast.fetchData = IssueList.fetchData;
export default IssueListWithToast;
You have to allow cross origin on your graphql server. For security reasons browsers didn't allow ajax request on cross origin. So lets say you're on localhost:3000 and your server is running on localhost:8000 so these two are on different origins.
So use cors module on Express server and allow Access-Control-Allow-Origin to "*"
Related
I've created a registration api that works fine using postman, I added some values there plus image an everything stores correctly, data an image, Later I started my React form and only text works, image is not currently sent to the api I guess.
Doing console.log() api in Node:
console.log(req.files);
with React Form: []
with Postman: an array of object that perfectly works
req.files output using postman
{
fieldname: 'images',
originalname: 'Screen Shot 2021-02-22 at 17.18.41.png',
encoding: '7bit',
mimetype: 'image/png',
destination: 'uploads/',
filename: '091f77f82fb805b1ede9f23205cc578e',
path: 'uploads/091f77f82fb805b1ede9f23205cc578e',
size: 37052
}
Here's my react classes:
httpService.js
import axios from "axios";
import { toast } from "react-toastify";
axios.interceptors.response.use(null, (error) => {
const expectedError =
error.response &&
error.response.status >= 400 &&
error.response.status < 500;
if (!expectedError) {
console.log("Loggind the error: ", error);
toast("An unexpected error ocurred.");
}
return Promise.reject(error);
});
export default {
post: axios.post
};
itemService.js
export function saveItem(item) {
const headers = {
'Content-Type': 'multipart/form-data',
};
return http.post(MY_ENDPOINT, item, headers);
}
itemForm.js
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { saveItem } from "../services/itemService";
class ItemForm extends Form {
state = {
data: { title: "", description: "", category: "", images: "" },
categories: [],
errors: {},
};
schema = {
_id: Joi.string(),
title: Joi.string().required().label("Title"),
description: Joi.string().required().label("Description"),
category: Joi.string().required().label("Category"),
images: Joi.required().label("Image"),
};
doSubmit = async () => {
console.log('form data> ', this.state.data); // This shows the correct object.
let formData = new FormData();
formData.append('title', this.state.data.title);
formData.append('description', this.state.data.description);
formData.append('category', this.state.data.category);
formData.append('images', this.state.data.images);
try {
await saveItem(formData);
} catch (ex) {
}
};
render() {
return (
<div>
<h1>New item</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("title", "Title")}
{this.renderInput("description", "Description")}
{this.renderSelect(
"category",
"title",
"Category",
this.state.categories
)}
{this.renderInputFile("images", "images", "file", false)}
{this.renderButton("Register")}
</form>
</div>
);
}
}
export default ItemForm;
form.jsx (The extended class)
import React, { Component } from "react";
import Joi from "joi-browser";
import Input from "./input";
import Select from "./select";
class Form extends Component {
state = {
data: {},
errors: {},
};
validate = () => {
const options = { abortEarly: false };
const { error } = Joi.validate(this.state.data, this.schema, options);
if (!error) return null;
const errors = {};
for (let item of error.details) errors[item.path[0]] = item.message;
return errors;
};
validateProperty = ({ name, value }) => {
const obj = { [name]: value };
const schema = { [name]: this.schema[name] };
const { error } = Joi.validate(obj, schema);
return error ? error.details[0].message : null;
};
handleSubmit = (e) => {
e.preventDefault();
const errors = this.validate();
this.setState({ errors: errors || {} });
if (errors) return;
this.doSubmit();
};
handleChange = ({ currentTarget: input }) => {
const errors = { ...this.state.errors };
const errorMessage = this.validateProperty(input);
if (errorMessage) errors[input.name] = errorMessage;
else delete errors[input.name];
const data = { ...this.state.data };
data[input.name] = input.value;
this.setState({ data, errors });
};
handleInputFileChange = ({ currentTarget: input }) => {
const errors = { ...this.state.errors };
const errorMessage = this.validateProperty(input);
if (errorMessage) errors[input.name] = errorMessage;
else delete errors[input.name];
const data = { ...this.state.data };
data[input.name] = input.value;
this.setState({ data, errors });
};
renderButton(label) {
return (
<button className="btn btn-primary" disabled={this.validate()}>
{label}
</button>
);
}
renderInput(name, label, type = "text", multiple = false) {
const { data, errors } = this.state;
return (
<Input
type={type}
name={name}
value={data[name]}
label={label}
onChange={this.handleChange}
error={errors[name]}
multiple={multiple}
/>
);
}
renderSelect(name, contentField, label, options) {
const { data, errors } = this.state;
return (
<Select
name={name}
value={data[name]}
label={label}
contentField={contentField}
options={options}
onChange={this.handleChange}
error={errors[name]}
/>
);
}
renderInputFile(name, label, type = "text", multiple = false) {
const { data, errors } = this.state;
return (
<Input
type={type}
name={name}
value={data[name]}
label={label}
onChange={this.handleInputFileChange}
error={errors[name]}
multiple={multiple}
accept="image/*"
/>
);
}
}
export default Form;
wassup Folks,
I got the following error from my component called "SearchReactTableContainer". I looked inside but everything looks well to many. Any Ideas how to fix this? I looked into the network conditions and the needed data from my resulting query is provided without any issues to my frontend.
SearchReactTableContainer.js
import React, { Component, Fragment } from "react";
import { has, isNil } from "ramda";
import CockpitSearchInput from "#components/SearchInput";
import SearchReactTable from "#cockpitComponents/SearchReactTable";
import {
MY_CASES_FILTER_KEY,
UNASSIGNED_FILTER_KEY
} from "#modules/CaseManager/CaseManagerDashboard/constants";
import { Col, Row } from "antd";
// TODO: TO BE REMOVED
const pagesCount = 3855;
export default class SearchReactTableContainer extends Component {
constructor(props) {
super(props);
const { limit, searchValue } = this.props;
const orMyCases = localStorage.getItem(MY_CASES_FILTER_KEY);
const orUnassignedCases = localStorage.getItem(UNASSIGNED_FILTER_KEY);
this.state = {
loading: false,
searchValue: searchValue || "",
data: [],
pageSize: limit,
pagesCount,
orMyCases: this.parseNonNullValue(orMyCases),
orUnassignedCases: this.parseNonNullValue(orUnassignedCases)
};
this.triggerFetchMore = this.triggerFetchMore.bind(this);
}
parseNonNullValue(value) {
try {
return !isNil(value) ? JSON.parse(value) : true;
} catch (err) {
console.error(err);
return false;
}
}
async fetchMoreFromGraphql(fetchMore, variables, tableKey) {
return await fetchMore({
variables,
updateQuery: (previousResult, { fetchMoreResult }) => {
if (!fetchMoreResult || !previousResult) return previousResult;
if (!(tableKey in previousResult)) {
return [];
}
return {
...previousResult,
...fetchMoreResult[tableKey]
};
}
});
}
async onFetchDataHandler(state) {
const { page, pageSize, sorted, filtered } = state;
const { tableKey, fetchMore } = this.props;
const { searchValue, orMyCases, orUnassignedCases } = this.state;
this.changeTableLoadingState();
const response = await this.fetchMoreFromGraphql(
fetchMore,
{
searchValue,
offset: page * pageSize,
limit: pageSize,
page,
pageSize,
sorted,
filtered,
filters: {
orMyCases,
orUnassignedCases
}
},
tableKey
);
let { data } = response;
const pagesCount =
searchValue.length === 0
? has("totalCount", data[tableKey])
? Math.ceil(data[tableKey].totalCount / pageSize)
: has("items", data[tableKey])
? Math.ceil(data[tableKey].items.length / pageSize)
: Math.ceil(data[tableKey].length / pageSize)
: this.state.pagesCount;
this.setState({
...this.state,
loading: false,
data: data[tableKey].items ? data[tableKey].items : data[tableKey],
pageIndex: page,
pageSize,
pagesCount
});
}
async triggerFetchMore(searchValue = "") {
const { tableKey, fetchMore, tableName } = this.props;
const { orMyCases, orUnassignedCases } = this.state;
this.changeTableLoadingState();
const variables = {
searchValue,
offset: 0,
limit: this.state.pageSize,
filters: {
orMyCases,
orUnassignedCases
}
};
const response = await this.fetchMoreFromGraphql(fetchMore, variables, tableKey);
const { data } = response;
try {
await this.setState(prevState => {
const { pageSize } = prevState;
const pagesCount =
searchValue.length === 0
? has("totalCount", data[tableKey])
? Math.ceil(data[tableKey].totalCount / pageSize)
: has("items", data[tableKey])
? Math.ceil(data[tableKey].items.length / pageSize)
: Math.ceil(data[tableKey].length / pageSize)
: this.state.pagesCount;
prevState = {
...prevState,
loading: false,
data: data[tableKey].items ? data[tableKey].items : data[tableKey],
searchValue,
pagesCount
};
return prevState;
});
localStorage.setItem(`${tableName}.searchValue`, searchValue);
} catch (err) {
console.log(err);
}
}
changeTableLoadingState() {
this.setState({ loading: !this.state.loading });
}
render() {
const {
columns,
dataFormatter,
search = true,
defaultSorted,
filterBlock
} = this.props;
const { data, pagesCount, pagesSize, loading, searchValue } = this.state;
let formattedData = data;
if (typeof dataFormatter === "function") {
formattedData = dataFormatter(data);
}
return (
<Fragment>
{search && (
<Row>
<Col span={6}>
<CockpitSearchInput
onChange={this.triggerFetchMore}
initialValue={searchValue}
/>
</Col>
</Row>
)}
{!isNil(filterBlock) && filterBlock(this)}
<SearchReactTable
{...this.props}
manual
data={formattedData}
columns={columns}
loading={loading}
// Request new data when things change
onFetchData={(state, instance) => this.onFetchDataHandler(state, instance)}
pages={pagesCount} // Display the total number of pages
pageSizeOptions={[5, 10, 20, 25, 50, 100]}
defaultPageSize={pagesSize}
className="table table-striped table-bordered table-hover"
defaultSorted={defaultSorted}
/>
</Fragment>
);
}
}
I'll add a screenshot showing me the actual issue in the developer tab.
Component that uses SearchReactTable
I am working on a small project that is a simple page with admin role and dashboard. I have user authentication and authorization with JWT token. My issue is that my Login.js form is faling to render whene there is no token that have user.role
So my code looks like this:
Login.js
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import axios from 'axios';
import { API_URL } from '../../../config';
import { authenticateUser, isAuthUser } from '../../../utils/utils';
import Layout from '../../layout/MainLayout/Layout';
import Button from '../../common/Buttons/Button';
class Login extends Component {
state = {
formData: {
email: '',
password: '',
},
userRedirect: false,
};
signIn = (user) => {
const config = {
headers: {
'Content-Type': 'application/json',
},
};
console.log('user axios', user.role);
axios
.post(`${API_URL}/login`, user, config)
.then((res) => authenticateUser(res.data));
this.setState({
formData: { email: '', password: '' },
userRedirect: true,
});
};
onChange = (e) => {
const { formData } = this.state;
//assign form data to new variable
let newFormData = { ...formData };
newFormData[e.target.name] = e.target.value;
this.setState({
formData: newFormData,
});
};
onSubmit = (e) => {
const { password, email } = this.state.formData;
e.preventDefault();
this.signIn({ email, password });
};
formRender = (email, password) => (
<form onSubmit={this.onSubmit}>
<div className='form-group'>
<label className='text-muted'>Email</label>
<input
type='email'
name='email'
value={email}
onChange={this.onChange}
className='form-control'></input>
</div>
<div className='form-group'>
<label className='text-muted'>Password</label>
<input
type='password'
name='password'
minLength='6'
value={password}
onChange={this.onChange}
className='form-control'></input>
</div>
<Button type='submit'>Login</Button>
</form>
);
redirectUser = () => {
const { userRedirect } = this.state;
const { user } = isAuthUser();
if (userRedirect === true) {
console.log('user role', user.role);
console.log('auth fuc', isAuthUser());
if (user.role === 2308) {
return <Redirect to='/admin/dashboard' />;
} else {
return <Redirect to='/users/me' />;
}
}
};
render() {
const { email, password } = this.state.formData;
return (
<Layout title='Login Form' description='Login to your account'>
{this.formRender(email, password)}
{this.redirectUser()}
</Layout>
);
}
}
export default Login;
Login form is working. All is getting messy when I am trying to get my role. Bu i have done some console logs, and redirectUser there is a user.role as it should based on user that is loging in.
I have two functions in utils.js, where I am checking if user is authenticated and if there is a JWT in local storage:
export const authenticateUser = (data) => {
if (typeof window !== 'undefined') {
localStorage.setItem('jwt', JSON.stringify(data));
}
};
//check if user is auth and there is jwt item in localstorage. menu render
export const isAuthUser = () => {
if (typeof window == 'undefined') {
return false;
}
if (localStorage.getItem('jwt')) {
return JSON.parse(localStorage.getItem('jwt'));
} else {
return false;
}
};
I did get that, my error comes from
<Layout title='Login Form' description='Login to your account'>
{this.formRender(email, password)}
{this.redirectUser()}
</Layout>
this.redirectUser() get me that role undefined. But i have no clue how i can get this working. From console logs I am getting user role.
Thanks for help
It seems that when there is no token in localStorage you are destructuring from a boolean. Here is what I mean:
redirectUser = () => {
const { userRedirect } = this.state;
const { user } = isAuthUser(); // isAuthUser() returns false when there is no jwt in the localStorage
if (userRedirect === true) {
console.log("user role", user.role);
console.log("auth fuc", isAuthUser());
if (user.role === 2308) {
return <Redirect to="/admin/dashboard" />;
} else {
return <Redirect to="/users/me" />;
}
}
};
You should at least check your result before destructuring. Try this:
redirectUser = () => {
const { userRedirect } = this.state;
const authData = isAuthUser();
if (authData === false) { // if jwt is not in localStorage
return <Redirect to="/sign-in-path-in-your-app" />;
} else if (userRedirect === true) {
const { user } = authData; // perform destructuring because here authData is not false but an object
console.log("user role", user.role);
console.log("auth fuc", isAuthUser());
if (user.role === 2308) {
return <Redirect to="/admin/dashboard" />;
} else {
return <Redirect to="/users/me" />;
}
}
};
I am trying to build a user system but I am getting confused with something since it does not work in real time.
I have created an example Sandbox to show my "issue" and my code. I didn't add any kind of validation stuff, it is just for example purposes.
Some of the issues (I see) are:
Button actions trigger on second click
Data won't refresh after created / deleted.
This is the <UsersPage /> component:
import React, { Fragment, useState, useEffect } from "react";
import { useMutation, useLazyQuery } from "#apollo/react-hooks";
import { ADD_USER, LIST_USERS, DELETE_USER } from "../../../config/constants";
import { useSnackbar } from "notistack";
import {
Grid,
Paper,
TextField,
Button,
Typography,
MenuItem,
FormHelperText
} from "#material-ui/core";
import AddUserIcon from "#material-ui/icons/PersonAdd";
import { withStyles } from "#material-ui/core/styles";
import PropTypes from "prop-types";
import Table from "../../Table";
const styles = theme => ({
grid: {
margin: theme.spacing(3)
},
icon: {
marginRight: theme.spacing(2)
},
form: {
width: "100%",
marginTop: theme.spacing(3),
overflowX: "auto",
padding: theme.spacing(2)
},
submit: {
margin: theme.spacing(2)
},
container: {
display: "flex",
flexWrap: "wrap"
},
textField: {
marginLeft: theme.spacing.unit,
marginRight: theme.spacing.unit
},
root: {
width: "100%",
marginTop: theme.spacing(3),
overflowX: "auto",
padding: theme.spacing(2)
},
title: {
margin: theme.spacing(2)
},
table: {
minWidth: 700
},
noRecords: {
textAlign: "center"
},
button: {
margin: theme.spacing.unit
}
});
const Users = props => {
const [idState, setIdState] = useState(null);
const [emailState, setEmailState] = useState("");
const [passwordState, setPasswordState] = useState("");
const [usersState, setUsersState] = useState([]);
const [errorsState, setErrorsState] = useState({});
const [loadingState, setLoadingState] = useState(false);
const [addUser, addUserResponse] = useMutation(ADD_USER);
const [loadUsers, usersResponse] = useLazyQuery(LIST_USERS);
const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER);
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
loadUsers();
if (usersResponse.called && usersResponse.loading) {
setLoadingState(true);
} else if (usersResponse.called && !usersResponse.loading) {
setLoadingState(false);
}
if (usersResponse.data) {
setUsersState(usersResponse.data.getUsers);
}
}, [usersResponse.called, usersResponse.loading, usersResponse.data]);
function handleSubmit(e) {
e.preventDefault();
if (idState) {
} else {
addUser({
variables: {
email: emailState,
password: passwordState
}
});
}
if (addUserResponse.called && addUserResponse.loading) {
enqueueSnackbar("Creating user");
}
if (addUserResponse.error) {
addUserResponse.error.graphQLErrors.map(exception => {
const error = exception.extensions.exception;
const messages = Object.values(error);
enqueueSnackbar(messages[0], { variant: "error" });
});
}
if (addUserResponse.data && addUserResponse.data.addUser) {
enqueueSnackbar("user created", { variant: "success" });
loadUsers();
}
}
function handleEdit(user) {
setIdState(user.id);
setEmailState(user.email);
}
async function handleDelete(data) {
if (typeof data === "object") {
data.map(id => {
deleteUser({ variables: { id } });
if (deleteUserResponse.data && deleteUserResponse.data.deleteUser) {
enqueueSnackbar("User deleted", { variant: "success" });
}
});
} else {
deleteUser({ variables: { id: data } });
if (deleteUserResponse.data && deleteUserResponse.data.deleteUser) {
enqueueSnackbar("User deleted", { variant: "success" });
}
}
}
function resetForm() {
setIdState(null);
setEmailState("");
}
const { classes } = props;
return (
<Fragment>
<Grid container spacing={8}>
<Grid item xs={3} className={classes.grid}>
<Paper className={classes.form}>
<Typography variant="h6" className={classes.title}>
{idState ? `Edit user: ${emailState}` : "Create user"}
</Typography>
<form className={classes.container} onSubmit={handleSubmit}>
<input type="hidden" name="id" value={idState} />
<TextField
className={classes.textField}
label="E-mail address"
type="email"
variant="outlined"
margin="normal"
autoComplete="email"
id="email"
name="email"
required={!idState}
fullWidth
onChange={e => setEmailState(e.target.value)}
value={emailState}
aria-describedby="email-error"
/>
<FormHelperText id="email-error">
{errorsState.email}
</FormHelperText>
<TextField
className={classes.textField}
label="Password"
variant="outlined"
margin="normal"
autoComplete="password"
id="password"
name="password"
required={!idState}
type="password"
fullWidth
onChange={e => setPasswordState(e.target.value)}
value={passwordState}
aria-describedby="password-error"
/>
<FormHelperText id="password-error">
{errorsState.password}
</FormHelperText>
<Button
variant="contained"
color="primary"
className={classes.submit}
size="large"
type="submit"
>
<AddUserIcon className={classes.icon} /> Save
</Button>
<Button
variant="contained"
color="secondary"
className={classes.submit}
type="button"
onClick={resetForm}
>
<AddUserIcon className={classes.icon} /> Add new
</Button>
</form>
</Paper>
</Grid>
<Grid item xs={8} className={classes.grid}>
<Paper className={classes.root}>
<Table
data={usersState}
className={classes.table}
columns={{
id: "ID",
email: "E-mail address"
}}
classes={classes}
title="Users"
handleEdit={handleEdit}
handleDelete={handleDelete}
filter={true}
loading={loadingState}
/>
</Paper>
</Grid>
</Grid>
</Fragment>
);
};
Users.propTypes = {
classes: PropTypes.object.isRequired
};
export default withStyles(styles)(Users);
In case you need more code, or editing:
Frontend sandbox: App / Code
Backend sandbox: App / Code
Any comments, suggestions or whatever will be appreciated.
Button actions trigger on second click
It's because you call
if (addUserResponse.called && addUserResponse.loading) {
enqueueSnackbar("Creating user");
}
right after you call addUser. The state didn't change when you check if (addUserResponse.called && addUserResponse.loading), the state is the same state before calling addUser.
When you click on the second time, you have the state after the first click and this if
if (addUserResponse.data && addUserResponse.data.addUser) {
enqueueSnackbar("user created", { variant: "success" });
loadUsers();
}
is true.
Solution:
Create a useEffect to handle addUser state and remove the if clauses from handleSubmit
useEffect(() => {
if (!addUserResponse.called) {
return;
}
if (addUserResponse.loading) {
enqueueSnackbar("Creating user");
return;
}
if (addUserResponse.error) {
addUserResponse.error.graphQLErrors.map(exception => {
const error = exception.extensions.exception;
const messages = Object.values(error);
enqueueSnackbar(messages[0], { variant: "error" });
});
return;
}
enqueueSnackbar("user created", { variant: "success" });
}, [addUserResponse.called, addUserResponse.loading]);
function handleSubmit(e) {
e.preventDefault();
if (idState) {
} else {
addUser({
variables: {
email: emailState,
password: passwordState
}
});
}
}
Data won't refresh after created / deleted.
You should update your cache after mutation because Apollo doesn't know if you're adding or deleting when you call a mutation.
Solution:
const [addUser, addUserResponse] = useMutation(ADD_USER, {
update: (cache, { data: { addUser } }) => {
// get current data cache
const cachedUsers = cache.readQuery({ query: LIST_USERS });
// create new users
const newUsers = [addUser, ...cachedUsers.getUsers];
// save newUsers on cache
cache.writeQuery({
query: LIST_USERS,
data: {
getUsers: newUsers
}
});
}
});
The same is true for delete user, expect newUsers will have current users filtered:
const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER, {
update: (cache, { data: { deleteUser } }) => {
const cachedUsers = cache.readQuery({ query: LIST_USERS });
// NOTE: this didn't work because deleteUser return true instead user.
// I'd suggest change your backend and deleteUser return user id to
// be able to perform this filter.
const newUsers = cachedUsers.getUsers.filter(
({ id }) => id !== deleteUser.id
);
cache.writeQuery({
query: LIST_USERS,
data: {
getUsers: newUsers
}
});
}
});
Note 1:
You don't need to call loadUsers more than one time. Because you update the cache when you perform a mutation, your data will always be the most recent. Because of that I'd call loadUsers this way:
useEffect(() => {
loadUsers();
}, []);
useEffect(() => {
if (usersResponse.called && usersResponse.loading) {
setLoadingState(true);
} else if (usersResponse.called && !usersResponse.loading) {
setLoadingState(false);
}
}, [usersResponse.called, usersResponse.loading]);
Note 2
You don't need to create a state for users, you already have one from usersResponse.data.getUsers, but it's your preference. In my case, I removed const [usersState, setUsersState] = useState([]); and added
const users =
usersResponse.data && usersResponse.data.getUsers
? usersResponse.data.getUsers
: [];
to pass to the table.
Edit October 10, 2019
The main change I did was creating a mutation called batchDeleteUsers that delete multiple users in a single call.
Updating server
I've made some changes to the server to get the app working. First, deleteUser returns User and I've created a mutation called batchDeleteUsers.
My current mutation schema:
type Mutation {
addUser(email: String!, password: String!): User
deleteUser(id: String!): User
batchDeleteUsers(ids: [String!]!): [User]
}
My current resolvers:
deleteUser: (root, { id }, context) => {
const user = USERSDB.find(user => user.id === id);
USERSDB = USERSDB.filter(user => user.id !== id);
return user;
},
batchDeleteUsers: (root, { ids }, context) => {
const users = USERSDB.filter(user => ids.includes(user.id));
USERSDB = USERSDB.filter(user => !ids.includes(user.id));
return users;
}
Updating App 1
Instead using useLazyQuery and calling it inside a useEffect, I'm using useQuery. This way we don't need to perform query inside useEffect, it's triggered when component initializes.
const usersResponse = useQuery(LIST_USERS);
Updating App 2
Below is how I create deleteUser and batchDeleteUsers mutations.
const [deleteUser, deleteUserResponse] = useMutation(DELETE_USER, {
update: (cache, { data: { deleteUser } }) => {
const cachedUsers = cache.readQuery({ query: LIST_USERS });
const newUsers = cachedUsers.getUsers.filter(
({ id }) => id !== deleteUser.id
);
cache.writeQuery({
query: LIST_USERS,
data: {
getUsers: newUsers
}
});
}
});
const [batchDeleteUsers, batchDeleteUsersResponse] = useMutation(
BATCH_DELETE_USERS,
{
update: (cache, { data: { batchDeleteUsers } }) => {
const cachedUsers = cache.readQuery({ query: LIST_USERS });
const newUsers = cachedUsers.getUsers.filter(({ id }) => {
return !batchDeleteUsers.map(({ id }) => id).includes(id);
});
cache.writeQuery({
query: LIST_USERS,
data: {
getUsers: newUsers
}
});
}
}
);
Updating App 3
This is how I handle the delete users' mutations lifecycle.
useEffect(() => {
if (!deleteUserResponse.called) {
return;
}
if (deleteUserResponse.loading) {
enqueueSnackbar("Deleting user");
return;
}
if (deleteUserResponse.error) {
deleteUserResponse.error.graphQLErrors.map(exception => {
const error = exception.extensions.exception;
const messages = Object.values(error);
enqueueSnackbar(messages[0], { variant: "error" });
});
return;
}
enqueueSnackbar("user deleted", { variant: "success" });
}, [deleteUserResponse.called, deleteUserResponse.loading]);
useEffect(() => {
if (!batchDeleteUsersResponse.called) {
return;
}
if (batchDeleteUsersResponse.loading) {
enqueueSnackbar("Deleting users");
return;
}
if (batchDeleteUsersResponse.error) {
batchDeleteUsersResponse.error.graphQLErrors.map(exception => {
const error = exception.extensions.exception;
const messages = Object.values(error);
enqueueSnackbar(messages[0], { variant: "error" });
});
return;
}
enqueueSnackbar("users deleted", { variant: "success" });
}, [batchDeleteUsersResponse.called, batchDeleteUsersResponse.loading]);
Updating App 4
Finally, this is how I handle delete users.
function handleDelete(data) {
if (typeof data === "object") {
batchDeleteUsers({ variables: { ids: data } });
} else {
deleteUser({ variables: { id: data } });
}
}
My server sandbox: code
My frontend sandbox: code
I have the following react code to call a post endpoint, the webapi does have a post and still I dont know why I get this error
import React, { Component } from 'react';
import { Row, Col, Tabs, Menu, Dropdown, Button, Icon, message, Input } from 'antd';
import Form from '../../components/uielements/form';
import PageHeader from '../../components/utility/pageHeader';
import Box from '../../components/utility/box';
import LayoutWrapper from '../../components/utility/layoutWrapper';
import ContentHolder from '../../components/utility/contentHolder';
import basicStyle from '../../settings/basicStyle';
import IntlMessages from '../../components/utility/intlMessages';
import { Cascader } from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
const FormItem = Form.Item;
class ExtractPageTemplate extends Component {
constructor(props) {
super(props);
this.state = {options:[], loading:false, selectedOptions:[], description:''};
this.loadData = this.loadData.bind(this);
this.enterLoading = this.enterLoading.bind(this);
this.onChange = this.onChange.bind(this);
this.handleChangeDescription= this.handleChangeDescription.bind(this);
}
handleChangeDescription(event){
this.setState({description : event.target.value});
}
enterLoading (){
this.setState({ loading: true });
const options = {
method: 'post',
body: JSON.stringify(
{
"SiteCollectionUrl": this.state.selectedOptions[0].value,
"PageName": this.state.selectedOptions[1].label,
"Description": this.state.Description
}),
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
adalApiFetch(fetch, "/Page/CreatePageTemplate", options)
.then(response =>{
if(response.status === 204){
Notification(
'success',
'Page tempate created',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Page template not created',
error
);
console.error(error);
});
}
componentDidMount() {
adalApiFetch(fetch, "/SiteCollection", {})
.then(response => response.json())
.then(json => {
console.log(json);
const firstLevelOptions = json.map(post => ({
value: post.Url,
label: post.Title,
isLeaf: false
}));
this.setState({
options: firstLevelOptions
});
});
}
onChange(value, selectedOptions) {
console.log("value:", value, "selectedOptions", selectedOptions);
this.setState({
selectedOptions: selectedOptions
});
}
loadData(selectedOptions){
console.log("loaddata", selectedOptions);
const targetOption = selectedOptions[selectedOptions.length - 1];
targetOption.loading = true;
const options = {
method: 'get',
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
adalApiFetch(fetch, "/Page/"+encodeURIComponent(targetOption.value.replace("https://","")), options)
.then(response => response.json())
.then(json => {
targetOption.loading = false;
console.log(json);
const secondLevelOptions = json.map(page => ({
value: page.Id,
label: page.Name,
isLeaf: true
}));
targetOption.children = secondLevelOptions;
this.setState({
options: [...this.state.options],
});
}
);
}
render(){
console.log("uepa" , this.props);
const { rowStyle, colStyle, gutter } = basicStyle;
const TabPane = Tabs.TabPane;
const { getFieldDecorator } = this.props.form;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 14 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 14,
offset: 6,
},
},
};
return (
<div>
<LayoutWrapper>
<PageHeader>{<IntlMessages id="pageTitles.PageAdministration" />}</PageHeader>
<Row style={rowStyle} gutter={gutter} justify="start">
<Col md={12} sm={12} xs={24} style={colStyle}>
<Box
title={<IntlMessages id="pageTitles.siteCollectionsTitle" />}
subtitle={<IntlMessages id="pageTitles.siteCollectionsTitle" />}
>
<ContentHolder>
<Cascader
options={this.state.options}
loadData={this.loadData}
onChange={this.onChange}
changeOnSelect
/>
<FormItem {...formItemLayout} label="Description " hasFeedback>
{getFieldDecorator('Description', {
rules: [
{
required: true,
message: 'Please input the page template description',
}
]
})(<Input name="Description" id="Description" onChange={this.handleChangeDescription} />)}
</FormItem>
<Button type="primary" loading={this.state.loading} onClick={this.enterLoading}>
Click me!
</Button>
</ContentHolder>
</Box>
</Col>
</Row>
</LayoutWrapper>
</div>
);
}
}
const WrappedExtractPageTemplate = Form.create()(ExtractPageTemplate );
export default WrappedExtractPageTemplate;
and my webapi code
namespace TenantManagementWebApi.Controllers
{
[Authorize]
[RoutePrefix("api/Page")]
public class PageController : ApiController
{
[HttpGet]
[Route("{*sitecollectionUrl}")]
public async Task<List<TenantManagementWebApi.Entities.Page>> Get(string sitecollectionUrl)
{
var tenant = await TenantHelper.GetActiveTenant();
var siteCollectionStore = CosmosStoreFactory.CreateForEntity<TenantManagementWebApi.Entities.SiteCollection>();
await siteCollectionStore.RemoveAsync(x => x.Title != string.Empty); // Removes all the entities that match the criteria
string domainUrl = tenant.TestSiteCollectionUrl;
string tenantName = domainUrl.Split('.')[0];
string tenantAdminUrl = tenantName + "-admin.sharepoint.com";
KeyVaultHelper keyVaultHelper = new KeyVaultHelper();
await keyVaultHelper.OnGetAsync(tenant.SecretIdentifier);
using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant("https://"+sitecollectionUrl, tenant.Email, keyVaultHelper.SecretValue))
{
var pagesLibrary = context.Web.GetListByUrl("SitePages") ?? context.Web.GetListByTitle("SitePages");
CamlQuery query = CamlQuery.CreateAllItemsQuery(100);
var pages = pagesLibrary.GetItems(query);
context.Load(pages);
context.ExecuteQuery();
List<TenantManagementWebApi.Entities.Page> listOfPages = new List<TenantManagementWebApi.Entities.Page>();
foreach(ListItem item in pages)
{
listOfPages.Add(new TenantManagementWebApi.Entities.Page() { Id = item.Id, Name = item.FieldValues["Title"]+".aspx" });
}
return listOfPages;
};
}
[HttpPost]
[Route("api/Page/CreatePageTemplate")]
public async Task<IHttpActionResult> CreatePageTemplate([FromBody]PageTemplateCreationModel model)
{
if (ModelState.IsValid)
{
var tenant = await TenantHelper.GetActiveTenant();
var siteCollectionStore = CosmosStoreFactory.CreateForEntity<TenantManagementWebApi.Entities.SiteCollection>();
await siteCollectionStore.RemoveAsync(x => x.Title != string.Empty); // Removes all the entities that match the criteria
string domainUrl = tenant.TestSiteCollectionUrl;
string tenantName = domainUrl.Split('.')[0];
string tenantAdminUrl = tenantName + "-admin.sharepoint.com";
KeyVaultHelper keyVaultHelper = new KeyVaultHelper();
await keyVaultHelper.OnGetAsync(tenant.SecretIdentifier);
using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetSharePointOnlineAuthenticatedContextTenant(model.SiteCollectionUrl, tenant.Email, keyVaultHelper.SecretValue))
{
try
{
var clientsidepageTemplateStore = CosmosStoreFactory.CreateForEntity<OfficeDevPnP.Core.Pages.ClientSidePage>();
var page = OfficeDevPnP.Core.Pages.ClientSidePage.Load(context, model.PageName);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var added = await clientsidepageTemplateStore.AddAsync(page);
return StatusCode(HttpStatusCode.NoContent);
//PageTemplate pageTemplate = new PageTemplate();
//pageTemplate.Name = model.Name;
// var page = OfficeDevPnP.Core.Pages.ClientSidePage.Load(context, "Home.aspx");
//int sectionOrder = 0;
//foreach (var section in page.Sections)
//{
// pageTemplate.Sections.Add(section);
// foreach (var column in section.Columns)
// {
// foreach(var webpart in column.Controls)
// {
// }
// }
// sectionOrder++;
//}
}
catch (System.Exception ex)
{
throw ex;
}
}
}
return BadRequest(ModelState);
}
}
}
The get endpoint works perfect
If you want to access CreatePageTemplate action you have to request url of following format: RoutePrefix + Route. In your case resulting url should be api/Page + api/Page/CreatePageTemplate = api/Page/api/Page/CreatePageTemplate. But apparently this code
adalApiFetch(fetch, "/Page/CreatePageTemplate", options)
requesting api/Page/CreatePageTemplate. So just update it to the following
adalApiFetch(fetch, "/Page/api/Page/CreatePageTemplate", options)
But if you intention is using original url you should only update your action
[HttpPost]
[Route("CreatePageTemplate")] //remove api/Page as it is already in RoutePrefix
public async Task<IHttpActionResult> CreatePageTemplate([FromBody]PageTemplateCreationModel model)