Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5368f40b04 | ||
|
|
2f119e43bd | ||
|
|
78db95aa5f | ||
|
|
959c6dbc33 | ||
|
|
93169095c0 |
30194
client/package-lock.json
generated
30194
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@
|
|||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-router": "^5.2.0",
|
"react-router": "^5.2.0",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "4.0.1",
|
||||||
"subscriptions-transport-ws": "^0.9.18",
|
"subscriptions-transport-ws": "^0.9.18",
|
||||||
"web-vitals": "^0.2.4"
|
"web-vitals": "^0.2.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Header from './layout/Header';
|
import Header from './layout/Header';
|
||||||
|
import Login from './Login';
|
||||||
import AppointmentList from './appointment/AppointmentList';
|
import AppointmentList from './appointment/AppointmentList';
|
||||||
import CreateAppointment from './appointment/CreateAppointment';
|
import CreateAppointment from './appointment/CreateAppointment';
|
||||||
import UpdateAppointemnt from './appointment/UpdateAppointment';
|
import UpdateAppointemnt from './appointment/UpdateAppointment';
|
||||||
import Calendar from './Calendar';
|
import Calendar from './Calendar';
|
||||||
|
import Search from './Search';
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route } from 'react-router-dom';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
@@ -15,7 +17,9 @@ const App = () => {
|
|||||||
<Route exact path="/" component={AppointmentList} />
|
<Route exact path="/" component={AppointmentList} />
|
||||||
<Route exact path="/create" component={CreateAppointment} />
|
<Route exact path="/create" component={CreateAppointment} />
|
||||||
<Route exact path="/update/:_id" component={UpdateAppointemnt} />
|
<Route exact path="/update/:_id" component={UpdateAppointemnt} />
|
||||||
|
<Route exact path="/login" component={Login} />
|
||||||
<Route exact path="/calendar" component={Calendar} />
|
<Route exact path="/calendar" component={Calendar} />
|
||||||
|
<Route exact path="/search" component={Search} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -23,3 +27,42 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|
||||||
|
// // import logo from './../logo.svg';
|
||||||
|
// // import './../styles/App.css';
|
||||||
|
|
||||||
|
// import React, { Component } from 'react';
|
||||||
|
// import AppointmentList from './AppointmentList';
|
||||||
|
// import CreateAppointment from './CreateAppointment'
|
||||||
|
// // import Header from './Header';
|
||||||
|
// import Login from './Login'
|
||||||
|
// import Search from './Search';
|
||||||
|
// import { Redirect, Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
|
// const App = () => {
|
||||||
|
// return (
|
||||||
|
// <div className="center w85">
|
||||||
|
// <Header />
|
||||||
|
// <div className="ph3 pv1 background-gray">
|
||||||
|
// <Switch>
|
||||||
|
// <Route exact path="/" component={AppointmentList} />
|
||||||
|
// <Route
|
||||||
|
// exact
|
||||||
|
// path="/create"
|
||||||
|
// component={CreateAppointment}
|
||||||
|
// />
|
||||||
|
// <Route exact path="/login" component={Login} />
|
||||||
|
// <Route exact path="/search" component={Search} />
|
||||||
|
// <Route
|
||||||
|
// exact
|
||||||
|
// path="/new/:page"
|
||||||
|
// component={AppointmentList}
|
||||||
|
// />
|
||||||
|
// </Switch>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default App;
|
||||||
|
|||||||
133
client/src/components/Login.js
Normal file
133
client/src/components/Login.js
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router';
|
||||||
|
import { useMutation, gql } from '@apollo/client';
|
||||||
|
import { AUTH_TOKEN } from '../constants';
|
||||||
|
|
||||||
|
const SIGNUP_MUTATION = gql`
|
||||||
|
mutation SignupMutation(
|
||||||
|
$email: String!
|
||||||
|
$password: String!
|
||||||
|
$name: String!
|
||||||
|
) {
|
||||||
|
signup(
|
||||||
|
email: $email
|
||||||
|
password: $password
|
||||||
|
username: $name
|
||||||
|
) {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LOGIN_MUTATION = gql`
|
||||||
|
mutation LoginMutation(
|
||||||
|
$email: String!
|
||||||
|
$password: String!
|
||||||
|
) {
|
||||||
|
login(email: $email, password: $password) {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const [formState, setFormState] = useState({
|
||||||
|
login: true,
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
name: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const [login] = useMutation(LOGIN_MUTATION, {
|
||||||
|
variables: {
|
||||||
|
email: formState.email,
|
||||||
|
password: formState.password
|
||||||
|
},
|
||||||
|
onCompleted: ({ login }) => {
|
||||||
|
localStorage.setItem(AUTH_TOKEN, login.token);
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const [signup] = useMutation(SIGNUP_MUTATION, {
|
||||||
|
variables: {
|
||||||
|
name: formState.name,
|
||||||
|
email: formState.email,
|
||||||
|
password: formState.password
|
||||||
|
},
|
||||||
|
onCompleted: ({ signup }) => {
|
||||||
|
localStorage.setItem(AUTH_TOKEN, signup.token);
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4 className="mv3">
|
||||||
|
{formState.login ? 'Login' : 'Sign Up'}
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-column">
|
||||||
|
{!formState.login && (
|
||||||
|
<input
|
||||||
|
value={formState.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormState({
|
||||||
|
...formState,
|
||||||
|
name: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
placeholder="Your name"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
value={formState.email}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormState({
|
||||||
|
...formState,
|
||||||
|
email: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="text"
|
||||||
|
placeholder="Your email address"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={formState.password}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormState({
|
||||||
|
...formState,
|
||||||
|
password: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="password"
|
||||||
|
placeholder="Choose a safe password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex mt3">
|
||||||
|
<button
|
||||||
|
className="pointer mr2 button"
|
||||||
|
onClick={formState.login ? login : signup}
|
||||||
|
>
|
||||||
|
{formState.login ? 'login' : 'create account'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="pointer button"
|
||||||
|
onClick={(e) =>
|
||||||
|
setFormState({
|
||||||
|
...formState,
|
||||||
|
login: !formState.login
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formState.login
|
||||||
|
? 'need to create an account?'
|
||||||
|
: 'already have an account?'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
@@ -1,46 +1,39 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useLazyQuery } from '@apollo/client';
|
import { useLazyQuery } from '@apollo/client';
|
||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
|
import Appointment from './appointment/Appointment';
|
||||||
|
|
||||||
const FEED_SEARCH_QUERY = gql`
|
const FEED_SEARCH_QUERY = gql`
|
||||||
query FeedSearchQuery($filter: String!) {
|
query FeedSearchQuery($filter: String!) {
|
||||||
feed(filter: $filter) {
|
feed(filter: $filter) {
|
||||||
id
|
id
|
||||||
links {
|
appointments {
|
||||||
id
|
_id
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
type
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
const [executeSearch, { data }] = useLazyQuery(
|
const [executeSearch, { data }] = useLazyQuery(
|
||||||
FEED_SEARCH_QUERY
|
FEED_SEARCH_QUERY
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
Search
|
Search
|
||||||
<input
|
<input type="text" onChange={(e) => setSearchFilter(e.target.value)}/>
|
||||||
type="text"
|
<button onClick={() => executeSearch({ variables: { filter: searchFilter } })}>OK</button>
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
</div>
|
||||||
/>
|
{data &&
|
||||||
<button
|
data.feed.appointments.map((appointment, index) => (
|
||||||
onClick={() =>
|
<Appointment key={appointment.id} appointment={appointment} index={index} />
|
||||||
executeSearch({
|
))}
|
||||||
variables: { filter: searchFilter }
|
</>
|
||||||
})
|
);
|
||||||
}
|
|
||||||
>
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Search;
|
export default Search;
|
||||||
@@ -21,18 +21,116 @@ const Appointment = (props) => {
|
|||||||
onCompleted: () => history.push('/')
|
onCompleted: () => history.push('/')
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateAppointment = () => {
|
// const updateAppointment = () => {
|
||||||
let path = `/update/${appointment._id}`;
|
// let path = `/update/${appointment._id}`;
|
||||||
history.push(path);
|
// history.push(path);
|
||||||
}
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<b>{appointment.title}</b> starts at {appointment.start}, ends at {appointment.end}. It is described as "{appointment.description}"<button onClick={deleteAppointment}>DELETE</button><button onClick={updateAppointment}>EDIT</button>
|
<b>{appointment.title}</b> starts at {appointment.start}, ends at {appointment.end}. It is described as "{appointment.description}"<button onClick={deleteAppointment}>DELETE</button>
|
||||||
|
{/* <button onClick={updateAppointment}>EDIT</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Appointment;
|
export default Appointment;
|
||||||
|
|
||||||
|
// import { AUTH_TOKEN, APPOINTMENTS_PER_PAGE } from '../constants';
|
||||||
|
// import { timeDifferenceForDate } from '../utils'
|
||||||
|
// import { FEED_QUERY } from './AppointmentList'
|
||||||
|
|
||||||
|
// const FOLLOW_MUTATION = gql`
|
||||||
|
// mutation FollowMutation($appointmentId: ID!) {
|
||||||
|
// follow(followId: $followId) {
|
||||||
|
// id
|
||||||
|
// appointment {
|
||||||
|
// id
|
||||||
|
// follows {
|
||||||
|
// id
|
||||||
|
// user {
|
||||||
|
// id
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// user {
|
||||||
|
// id
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// const take = APPOINTMENTS_PER_PAGE;
|
||||||
|
// const skip = 0;
|
||||||
|
// const orderBy = { createdAt: 'desc' };
|
||||||
|
|
||||||
|
// const Appointment = (props) => {
|
||||||
|
// const { appointment } = props;
|
||||||
|
// const authToken = localStorage.getItem(AUTH_TOKEN);
|
||||||
|
// const take = APPOINTMENTS_PER_PAGE;
|
||||||
|
// const skip = 0;
|
||||||
|
// const orderBy = { createdAt: 'desc' };
|
||||||
|
|
||||||
|
// const [follow] = useMutation(FOLLOW_MUTATION, {
|
||||||
|
// variables: {
|
||||||
|
// appointmentId: appointment.id
|
||||||
|
// },
|
||||||
|
// update(cache, { data: { follow } }) {
|
||||||
|
// const { feed } = cache.readQuery({
|
||||||
|
// query: FEED_QUERY
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const updatedAppointments = feed.follows.map((feedFollow) => {
|
||||||
|
// if (feedFollow.id === appointment.id) {
|
||||||
|
// return {
|
||||||
|
// ...feedFollow,
|
||||||
|
// follows: [...feedFollow.follows, follow]
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// return feedFollow;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// cache.writeQuery({
|
||||||
|
// query: FEED_QUERY,
|
||||||
|
// data: {
|
||||||
|
// feed: {
|
||||||
|
// appointments: updatedAppointments
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="flex mt2 items-start">
|
||||||
|
// <div className="flex items-center">
|
||||||
|
// <span className="gray">{props.index + 1}.</span>
|
||||||
|
// {authToken && (
|
||||||
|
// <div
|
||||||
|
// className="ml1 gray f11"
|
||||||
|
// style={{ cursor: 'pointer' }}
|
||||||
|
// onClick={follow}
|
||||||
|
// >
|
||||||
|
// ▲
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// <div className="ml1">
|
||||||
|
// <div>
|
||||||
|
// {appointment.title} ({appointment.description})
|
||||||
|
// </div>
|
||||||
|
// {authToken && (
|
||||||
|
// <div className="f6 lh-copy gray">
|
||||||
|
// {appointment.follows.length} follows | by{' '}
|
||||||
|
// {follow.createdBy ? follow.createdBy.name : 'Unknown'}{' '}
|
||||||
|
// {timeDifferenceForDate(appointment.createdAt)}
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default Appointment;
|
||||||
@@ -16,7 +16,7 @@ export const APPOINTMENTS_QUERY = gql`
|
|||||||
|
|
||||||
const AppointmentList = () => {
|
const AppointmentList = () => {
|
||||||
|
|
||||||
const { data, loading } = useQuery(APPOINTMENTS_QUERY);
|
const { data } = useQuery(APPOINTMENTS_QUERY);
|
||||||
|
|
||||||
if (data !== undefined) {
|
if (data !== undefined) {
|
||||||
return (
|
return (
|
||||||
@@ -39,3 +39,175 @@ const AppointmentList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default AppointmentList;
|
export default AppointmentList;
|
||||||
|
|
||||||
|
// import { useHistory } from 'react-router';
|
||||||
|
// import { APPOINTMENTS_PER_PAGE } from '../constants';
|
||||||
|
// import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
// export const FEED_QUERY = gql`
|
||||||
|
// query AppointmentManyQuery(
|
||||||
|
// $take: Int
|
||||||
|
// $skip: Int
|
||||||
|
// $orderBy: AppointmentOrderByInput
|
||||||
|
// ) {
|
||||||
|
// appointmentMany(take: $take, skip: $skip, orderBy: $orderBy) {
|
||||||
|
// id
|
||||||
|
// appointments {
|
||||||
|
// id
|
||||||
|
// createdAt
|
||||||
|
// title
|
||||||
|
// # start
|
||||||
|
// # end
|
||||||
|
// description
|
||||||
|
// # createdBy {
|
||||||
|
// # id
|
||||||
|
// # name
|
||||||
|
// # }
|
||||||
|
// # follows {
|
||||||
|
// # id
|
||||||
|
// # user {
|
||||||
|
// # id
|
||||||
|
// # }
|
||||||
|
// # }
|
||||||
|
// }
|
||||||
|
// count
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// `;
|
||||||
|
|
||||||
|
// // const NEW_APPOINTMENTS_SUBSCRIPTION = gql`
|
||||||
|
// // subscription {
|
||||||
|
// // newAppointment {
|
||||||
|
// // id
|
||||||
|
// // url
|
||||||
|
// // description
|
||||||
|
// // createdAt
|
||||||
|
// // createdBy {
|
||||||
|
// // id
|
||||||
|
// // name
|
||||||
|
// // }
|
||||||
|
// // follows {
|
||||||
|
// // id
|
||||||
|
// // user {
|
||||||
|
// // id
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // }
|
||||||
|
// // `;
|
||||||
|
|
||||||
|
// const getQueryVariables = (isNewPage, page) => {
|
||||||
|
// const skip = isNewPage ? (page - 1) * APPOINTMENTS_PER_PAGE : 0;
|
||||||
|
// const take = isNewPage ? APPOINTMENTS_PER_PAGE : 100;
|
||||||
|
// const orderBy = { createdAt: 'desc' };
|
||||||
|
|
||||||
|
// return { take, skip, orderBy };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const AppointmentList = () => {
|
||||||
|
// const history = useHistory();
|
||||||
|
// const isNewPage = history.location.pathname.includes(
|
||||||
|
// 'new'
|
||||||
|
// );
|
||||||
|
// const pageIndexParams = history.location.pathname.split(
|
||||||
|
// '/'
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const page = parseInt(
|
||||||
|
// pageIndexParams[pageIndexParams.length - 1]
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const pageIndex = page ? (page - 1) * APPOINTMENTS_PER_PAGE : 0;
|
||||||
|
|
||||||
|
// const {
|
||||||
|
// data,
|
||||||
|
// loading,
|
||||||
|
// error,
|
||||||
|
// subscribeToMore
|
||||||
|
// } = useQuery(FEED_QUERY, {
|
||||||
|
// variables: getQueryVariables(isNewPage, page)
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // const { data } = useQuery(FEED_QUERY);
|
||||||
|
|
||||||
|
// const getAppointmentsToRender = (isNewPage, data) => {
|
||||||
|
// if (isNewPage) {
|
||||||
|
// return data.feed.appointments;
|
||||||
|
// }
|
||||||
|
// const rankedAppointments = data.feed.appointments.slice();
|
||||||
|
// rankedAppointments.sort(
|
||||||
|
// (l1, l2) => l2.follows.length - l1.follows.length
|
||||||
|
// );
|
||||||
|
// return rankedAppointments;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // subscribeToMore({
|
||||||
|
// // document: NEW_APPOINTMENTS_SUBSCRIPTION,
|
||||||
|
// // updateQuery: (prev, { subscriptionData }) => {
|
||||||
|
// // if (!subscriptionData.data) return prev;
|
||||||
|
// // const newAppointment = subscriptionData.data.newAppointment;
|
||||||
|
// // const exists = prev.feed.appointments.find(
|
||||||
|
// // ({ id }) => id === newAppointment.id
|
||||||
|
// // );
|
||||||
|
// // if (exists) return prev;
|
||||||
|
|
||||||
|
// // return Object.assign({}, prev, {
|
||||||
|
// // feed: {
|
||||||
|
// // appointments: [newAppointment, ...prev.feed.appointments],
|
||||||
|
// // count: prev.feed.appointments.length + 1,
|
||||||
|
// // __typename: prev.feed.__typename
|
||||||
|
// // }
|
||||||
|
// // });
|
||||||
|
// // }
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// {loading && <p>Loading...</p>}
|
||||||
|
// {error && <pre>{JSON.stringify(error, null, 2)}</pre>}
|
||||||
|
// {data && (
|
||||||
|
// <>
|
||||||
|
// {getAppointmentsToRender(isNewPage, data).map(
|
||||||
|
// (appointment, index) => (
|
||||||
|
// <Link
|
||||||
|
// key={appointment.id}
|
||||||
|
// link={appointment}
|
||||||
|
// index={index + pageIndex}
|
||||||
|
// />
|
||||||
|
// )
|
||||||
|
// )}
|
||||||
|
// {isNewPage && (
|
||||||
|
// <div className="flex ml4 mv3 gray">
|
||||||
|
// <div
|
||||||
|
// className="pointer mr2"
|
||||||
|
// onClick={() => {
|
||||||
|
// if (page > 1) {
|
||||||
|
// history.push(`/new/${page - 1}`);
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// Previous
|
||||||
|
// </div>
|
||||||
|
// <div
|
||||||
|
// className="pointer"
|
||||||
|
// onClick={() => {
|
||||||
|
// if (
|
||||||
|
// page <=
|
||||||
|
// data.feed.count / APPOINTMENTS_PER_PAGE
|
||||||
|
// ) {
|
||||||
|
// const nextPage = page + 1;
|
||||||
|
// history.push(`/new/${nextPage}`);
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// Next
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// )}
|
||||||
|
// </>
|
||||||
|
// )}
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default AppointmentList;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useMutation, gql } from '@apollo/client';
|
import { useMutation, gql } from '@apollo/client';
|
||||||
|
import { APPOINTMENTS_PER_PAGE } from '../../constants';
|
||||||
|
import { APPOINTMENTS_QUERY } from './AppointmentList';
|
||||||
import Datetime from 'react-datetime';
|
import Datetime from 'react-datetime';
|
||||||
import "react-datetime/css/react-datetime.css";
|
import "react-datetime/css/react-datetime.css";
|
||||||
|
|
||||||
@@ -41,6 +43,34 @@ const CreateAppointment = () => {
|
|||||||
start: formState.start,
|
start: formState.start,
|
||||||
end: formState.end
|
end: formState.end
|
||||||
},
|
},
|
||||||
|
update: (cache, { data: { createAppointment } }) => {
|
||||||
|
const take = APPOINTMENTS_PER_PAGE;
|
||||||
|
const skip = 0;
|
||||||
|
const orderBy = { createdAt: 'desc' };
|
||||||
|
|
||||||
|
const data = cache.readQuery({
|
||||||
|
query: APPOINTMENTS_QUERY,
|
||||||
|
variables: {
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
orderBy
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cache.writeQuery({
|
||||||
|
query: APPOINTMENTS_QUERY,
|
||||||
|
data: {
|
||||||
|
allAppointments: {
|
||||||
|
appointments: [createAppointment, ...data.allAppointments]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
orderBy
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
onCompleted: () => history.push('/')
|
onCompleted: () => history.push('/')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { useMutation, gql, useQuery } from '@apollo/client';
|
import { useMutation, gql, useQuery } from '@apollo/client';
|
||||||
|
// import { APPOINTMENTS_PER_PAGE } from '../../constants';
|
||||||
|
// import { APPOINTMENTS_QUERY } from './AppointmentList';
|
||||||
import Datetime from 'react-datetime';
|
import Datetime from 'react-datetime';
|
||||||
import "react-datetime/css/react-datetime.css";
|
import "react-datetime/css/react-datetime.css";
|
||||||
|
|
||||||
@@ -72,6 +74,10 @@ const UpdateAppointment = ({ match: { params: { _id } } }) => {
|
|||||||
if (data === undefined) {
|
if (data === undefined) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
} else {
|
} else {
|
||||||
|
// setFormState({
|
||||||
|
// formState.title= data.oneAppointment.title
|
||||||
|
// })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form
|
<form
|
||||||
@@ -89,10 +95,16 @@ const UpdateAppointment = ({ match: { params: { _id } } }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
readOnly
|
|
||||||
className="mb2"
|
className="mb2"
|
||||||
value={data.oneAppointment.title}
|
value={formState.title}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormState({
|
||||||
|
...formState,
|
||||||
|
title: e.target.value
|
||||||
|
})
|
||||||
|
}
|
||||||
type="text"
|
type="text"
|
||||||
|
placeholder="Input title"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="mb2"
|
className="mb2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { AUTH_TOKEN } from '../../constants';
|
import { AUTH_TOKEN } from '../../constants';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
@@ -17,6 +17,21 @@ const Header = () => {
|
|||||||
<div className="flex flex-fixed">
|
<div className="flex flex-fixed">
|
||||||
<Link to="/create" className="ml1 no-underline black">New</Link>
|
<Link to="/create" className="ml1 no-underline black">New</Link>
|
||||||
</div>
|
</div>
|
||||||
|
{/* <div className="flex flex-fixed">
|
||||||
|
<Link to="/search" className="ml1 no-underline black">Search</Link>
|
||||||
|
</div> */}
|
||||||
|
<div className="flex flex-fixed">
|
||||||
|
{authToken ? (
|
||||||
|
<div className="ml1 pointer black"
|
||||||
|
onClick={() => {
|
||||||
|
localStorage.removeItem(AUTH_TOKEN);
|
||||||
|
history.push(`/`);
|
||||||
|
}}
|
||||||
|
>Logout</div>
|
||||||
|
) : (
|
||||||
|
<Link to="/login" className="ml1 no-underline black">Login</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AUTH_TOKEN } from './constants';
|
|||||||
import App from './components/App';
|
import App from './components/App';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { setContext } from '@apollo/client/link/context';
|
import { setContext } from '@apollo/client/link/context';
|
||||||
|
// import * as serviceWorker from './serviceWorker';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ApolloProvider,
|
ApolloProvider,
|
||||||
ApolloClient,
|
ApolloClient,
|
||||||
@@ -13,7 +15,7 @@ import {
|
|||||||
} from '@apollo/client';
|
} from '@apollo/client';
|
||||||
|
|
||||||
const httpLink = createHttpLink({
|
const httpLink = createHttpLink({
|
||||||
uri: 'http://localhost:4000/djhb58fytkh476dk45yh49'
|
uri: 'http://localhost:4000/graphql'
|
||||||
});
|
});
|
||||||
|
|
||||||
const authLink = setContext((_, { headers }) => {
|
const authLink = setContext((_, { headers }) => {
|
||||||
@@ -39,3 +41,37 @@ ReactDOM.render(
|
|||||||
</BrowserRouter>,
|
</BrowserRouter>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
// serviceWorker.unregister();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import { split } from '@apollo/client';
|
||||||
|
// import { WebSocketLink } from '@apollo/client/link/ws';
|
||||||
|
// import { getMainDefinition } from '@apollo/client/utilities';
|
||||||
|
// // import AppointmentList from './components/AppointmentList';
|
||||||
|
|
||||||
|
// // export default App;
|
||||||
|
|
||||||
|
// const wsLink = new WebSocketLink({
|
||||||
|
// uri: `ws://localhost:4000/graphql`,
|
||||||
|
// options: {
|
||||||
|
// reconnect: true,
|
||||||
|
// connectionParams: {
|
||||||
|
// authToken: localStorage.getItem(AUTH_TOKEN)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const link = split(
|
||||||
|
// ({ query }) => {
|
||||||
|
// const { kind, operation } = getMainDefinition(query);
|
||||||
|
// return (
|
||||||
|
// kind === 'OperationDefinition' &&
|
||||||
|
// operation === 'subscription'
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// wsLink,
|
||||||
|
// authLink.concat(httpLink)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // reportWebVitals();
|
||||||
|
|||||||
13544
client/yarn.lock
13544
client/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -13,29 +13,29 @@
|
|||||||
"@babel/node": "^7.12.10",
|
"@babel/node": "^7.12.10",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"apollo-engine": "^1.1.2",
|
"apollo-engine": "^1.1.2",
|
||||||
"apollo-server": "^3.13.0",
|
"apollo-server": "^2.19.0",
|
||||||
"apollo-server-express": "^2.19.1",
|
"apollo-server-express": "^2.19.1",
|
||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.0.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.19.0",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"express": "^4.20.0",
|
"express": "^4.17.1",
|
||||||
"express-graphql": "^0.12.0",
|
"express-graphql": "*",
|
||||||
"graphql": "^15.4.0",
|
"graphql": "*",
|
||||||
"graphql-compose": "^7.23.0",
|
"graphql-compose": "*",
|
||||||
"graphql-compose-connection": "^8.0.1",
|
"graphql-compose-connection": "*",
|
||||||
"graphql-compose-mongoose": "^9.0.0",
|
"graphql-compose-mongoose": "*",
|
||||||
"graphql-depth-limit": "^1.1.0",
|
"graphql-depth-limit": "*",
|
||||||
"graphql-middleware": "^6.0.0",
|
"graphql-middleware": "*",
|
||||||
"graphql-tools": "^7.0.2",
|
"graphql-tools": "*",
|
||||||
"jsonwebtoken": "9.0.0",
|
"jsonwebtoken": "8.5.1",
|
||||||
"migrate": "^1.7.0",
|
"migrate": "^1.7.0",
|
||||||
"mocha": "^8.2.1",
|
"mocha": "^8.2.1",
|
||||||
"mongodb": "^3.6.10",
|
"mongodb": "^3.6.3",
|
||||||
"mongoose": "^6.13.6",
|
"mongoose": "^5.11.9",
|
||||||
"mongoose-bcrypt": "^1.9.0",
|
"mongoose-bcrypt": "^1.9.0",
|
||||||
"mongoose-timestamp": "^0.6.0",
|
"mongoose-timestamp": "^0.6.0",
|
||||||
"node-migrate": "^0.1.0"
|
"node-migrate": "^0.1.0"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import './utils/db.js';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
const moduleURL = new URL(import.meta.url);
|
const moduleURL = new URL(import.meta.url);
|
||||||
const __dirname = path.dirname(moduleURL.pathname);
|
const __dirname = path.dirname(moduleURL.pathname);
|
||||||
@@ -17,10 +18,33 @@ const pubsub = new PubSub();
|
|||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
function getTokenPayload(token) {
|
||||||
|
return jwt.verify(token, process.env.APP_SECRET);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserId(req, authToken) {
|
||||||
|
if (req) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (authHeader) {
|
||||||
|
const token = authHeader.replace('Bearer ', '');
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('No token found');
|
||||||
|
}
|
||||||
|
const { userId } = getTokenPayload(token);
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
} else if (authToken) {
|
||||||
|
const { userId } = getTokenPayload(authToken);
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
app.use('/djhb58fytkh476dk45yh49', graphqlHTTP({
|
app.use('/graphql', graphqlHTTP({
|
||||||
schema: schema,
|
schema,
|
||||||
validationRules: [depthLimit(3)],
|
validationRules: [depthLimit(3)],
|
||||||
graphiql: true
|
graphiql: true
|
||||||
}));
|
}));
|
||||||
@@ -30,13 +54,33 @@ const server = new ApolloServer({
|
|||||||
path.join(__dirname, 'schema.graphql'),
|
path.join(__dirname, 'schema.graphql'),
|
||||||
'utf8'
|
'utf8'
|
||||||
),
|
),
|
||||||
|
// schema,
|
||||||
cors: true,
|
cors: true,
|
||||||
playground: process.env.NODE_ENV === 'development' ? true : false,
|
playground: process.env.NODE_ENV === 'development' ? true : false,
|
||||||
context: ({ req }) => {
|
context: ({ req }) => ({
|
||||||
return {
|
|
||||||
...req,
|
...req,
|
||||||
mongoose,
|
mongoose,
|
||||||
pubsub
|
pubsub,
|
||||||
|
userId:
|
||||||
|
req && req.headers.authorization
|
||||||
|
? getUserId(req)
|
||||||
|
: null
|
||||||
|
}),
|
||||||
|
subscriptions: {
|
||||||
|
onConnect: (connectionParams) => {
|
||||||
|
if (connectionParams.authToken) {
|
||||||
|
return {
|
||||||
|
mongoose,
|
||||||
|
userId: getUserId(
|
||||||
|
null,
|
||||||
|
connectionParams.authToken
|
||||||
|
)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
mongoose
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
introspection: true,
|
introspection: true,
|
||||||
@@ -49,6 +93,7 @@ server.applyMiddleware({
|
|||||||
path: '/',
|
path: '/',
|
||||||
cors: true,
|
cors: true,
|
||||||
onHealthCheck: () =>
|
onHealthCheck: () =>
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
if (mongoose.connection.readyState > 0) {
|
if (mongoose.connection.readyState > 0) {
|
||||||
resolve();
|
resolve();
|
||||||
@@ -59,6 +104,5 @@ server.applyMiddleware({
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.listen({ port: process.env.PORT }, () => {
|
app.listen({ port: process.env.PORT }, () => {
|
||||||
console.log(`🚀 Server listening on port ${process.env.PORT}`);
|
console.log(`Server listening on port ${process.env.PORT}`);
|
||||||
console.log(`😷 Health checks available at ${process.env.HEALTH_ENDPOINT}`);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ const AppointmentSchema = new Schema({
|
|||||||
deleted: {
|
deleted: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false
|
required: false
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: "user",
|
||||||
|
required: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
export default mongoose.model('appointment', AppointmentSchema);
|
export default mongoose.model('appointment', AppointmentSchema);
|
||||||
33
server/src/models/user.js
Normal file
33
server/src/models/user.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
const UserSchema = new Schema({
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// hash the password
|
||||||
|
UserSchema.methods.generateHash = function (password) {
|
||||||
|
return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// checking if password is valid
|
||||||
|
UserSchema.methods.validPassword = function (password) {
|
||||||
|
return bcrypt.compareSync(password, this.password);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default mongoose.model('user', UserSchema);
|
||||||
@@ -1,32 +1,65 @@
|
|||||||
import Appointment from './models/appointment.js';
|
import Appointment from './models/appointment.js';
|
||||||
|
import User from './models/user.js';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { createAppointment } from './resolvers/Mutation';
|
||||||
|
|
||||||
export const resolvers = {
|
export const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
async allAppointments() {
|
async allAppointments() {
|
||||||
return await Appointment.find({ deleted: false })
|
return await Appointment.find({ deleted: false });
|
||||||
},
|
},
|
||||||
async oneAppointment(root, args, context, info) {
|
async oneAppointment(root, args) {
|
||||||
return await Appointment.findOne({
|
return await Appointment.findOne({
|
||||||
_id: args._id
|
_id: args._id
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async allUsers() {
|
||||||
|
return await User.find();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
async createAppointment(parent, args, context, info) {
|
async signup(root, args) {
|
||||||
console.log(context);
|
const user = await User.create(args);
|
||||||
args.deleted = false;
|
user.password = user.generateHash(args.password);
|
||||||
return await Appointment.create(args);
|
user.save();
|
||||||
|
|
||||||
|
const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user
|
||||||
|
};
|
||||||
},
|
},
|
||||||
async updateAppointment(parent, args, context, info) {
|
|
||||||
console.log(args);
|
async login(parent, args) {
|
||||||
|
const user = await User.findOne({
|
||||||
|
email: args.email
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('No such user found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.validPassword(args.password)) {
|
||||||
|
throw new Error('Invalid password');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async createAppointment(parent, args, context) {
|
||||||
|
return await createAppointment(parent, args, context);
|
||||||
|
},
|
||||||
|
async updateAppointment(parent, args) {
|
||||||
return await Appointment.findOneAndUpdate({
|
return await Appointment.findOneAndUpdate({
|
||||||
args
|
args
|
||||||
}, args, {
|
}, args, {
|
||||||
new: true
|
new: true
|
||||||
})
|
});
|
||||||
},
|
}
|
||||||
async deleteAppointment(parent, args, context, info) {
|
|
||||||
return await Appointment.findOneAndUpdate({ _id: args._id }, { deleted: true })
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -4,6 +4,13 @@ function createdBy(parent, args, context) {
|
|||||||
.createdBy();
|
.createdBy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function follows(parent, args, context) {
|
||||||
|
return context.mongo.appointment
|
||||||
|
.findUnique({ where: { id: parent.id } })
|
||||||
|
.follows();
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createdBy
|
createdBy,
|
||||||
|
follows
|
||||||
};
|
};
|
||||||
|
|||||||
16
server/src/resolvers/Follow.js
Normal file
16
server/src/resolvers/Follow.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
function appointment(parent, args, context) {
|
||||||
|
return context.mongo.follow
|
||||||
|
.findUnique({ where: { id: parent.id } })
|
||||||
|
.appointment();
|
||||||
|
}
|
||||||
|
|
||||||
|
function user(parent, args, context) {
|
||||||
|
return context.mongo.follow
|
||||||
|
.findUnique({ where: { id: parent.id } })
|
||||||
|
.user();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
appointment,
|
||||||
|
user
|
||||||
|
};
|
||||||
@@ -1,14 +1,87 @@
|
|||||||
function createAppointment(parent, args, context, info) {
|
const bcrypt = require('bcryptjs');
|
||||||
const newAppointment = context.mongo.appointment.create({
|
const jwt = require('jsonwebtoken');
|
||||||
data: {
|
import dotenv from 'dotenv';
|
||||||
title: args.title,
|
import appointment from '../models/appointment';
|
||||||
description: args.description
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
async function createAppointment(parent, args, context) {
|
||||||
|
const { userId } = context;
|
||||||
|
args.deleted = false;
|
||||||
|
args.createdBy = userId;
|
||||||
|
console.log(parent, args, context);
|
||||||
|
return await appointment.create(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signup(parent, args, context) {
|
||||||
|
const password = await bcrypt.hash(args.password, 10);
|
||||||
|
const user = await context.mongo.user.create({
|
||||||
|
data: { ...args, password }
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(parent, args, context) {
|
||||||
|
const user = await context.mongo.user.findUnique({
|
||||||
|
where: { email: args.email }
|
||||||
|
});
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('No such user found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = await bcrypt.compare(
|
||||||
|
args.password,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error('Invalid password');
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function follow(parent, args, context) {
|
||||||
|
const { userId } = context;
|
||||||
|
const follow = await context.mongo.follow.findUnique({
|
||||||
|
where: {
|
||||||
|
linkId_userId: {
|
||||||
|
linkId: Number(args.linkId),
|
||||||
|
userId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newAppointment;
|
if (follow) {
|
||||||
|
throw new Error(
|
||||||
|
`Already followed the appointment: ${args.linkId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFollow = context.mongo.follow.create({
|
||||||
|
data: {
|
||||||
|
user: { connect: { id: userId } },
|
||||||
|
link: { connect: { id: Number(args.linkId) } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
context.pubsub.publish('NEW_FOLLOW', newFollow);
|
||||||
|
|
||||||
|
return newFollow;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createAppointment
|
createAppointment,
|
||||||
|
signup,
|
||||||
|
login,
|
||||||
|
follow
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
async function feed(parent, args, context, info) {
|
async function feed(parent, args, context) {
|
||||||
|
|
||||||
const where = args.filter
|
const where = args.filter
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
function newLinkSubscribe(parent, args, context, info) {
|
function newAppointmentSubscribe(parent, args, context) {
|
||||||
return context.pubsub.asyncIterator("NEW_LINK")
|
return context.pubsub.asyncIterator("NEW_APPOINTMENT");
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAppointment = {
|
const newAppointment = {
|
||||||
subscribe: newLinkSubscribe,
|
subscribe: newAppointmentSubscribe,
|
||||||
resolve: payload => {
|
resolve: payload => payload,
|
||||||
return payload
|
};
|
||||||
},
|
|
||||||
|
function newFollowSubscribe(parent, args, context) {
|
||||||
|
return context.pubsub.asyncIterator("NEW_FOLLOW");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newFollow = {
|
||||||
|
subscribe: newFollowSubscribe,
|
||||||
|
resolve: payload => payload,
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
newAppointment,
|
newAppointment,
|
||||||
}
|
newFollow
|
||||||
|
};
|
||||||
@@ -8,6 +8,8 @@ type Query {
|
|||||||
): Feed!
|
): Feed!
|
||||||
allAppointments: [Appointment]
|
allAppointments: [Appointment]
|
||||||
oneAppointment(_id: ID!): Appointment
|
oneAppointment(_id: ID!): Appointment
|
||||||
|
allUsers: [User]
|
||||||
|
users: [User!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Feed {
|
type Feed {
|
||||||
@@ -35,10 +37,33 @@ type Mutation {
|
|||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
): Appointment
|
): Appointment
|
||||||
deleteAppointment(_id: ID!): Appointment
|
deleteAppointment(_id: ID!): Appointment
|
||||||
|
signup(email: String!, password: String!, username: String!): AuthPayload
|
||||||
|
login(email: String!, password: String!): AuthPayload
|
||||||
|
follow(appointmentId: ID!): Follow
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Subscription {
|
||||||
newAppointment: Appointment
|
newAppointment: Appointment
|
||||||
|
newFollow: Follow
|
||||||
|
}
|
||||||
|
|
||||||
|
#User Schemas
|
||||||
|
type User {
|
||||||
|
_id: ID!
|
||||||
|
username: String!
|
||||||
|
email: String!
|
||||||
|
password: String!
|
||||||
|
appointments: [Appointment!]!
|
||||||
|
}
|
||||||
|
input UserInput {
|
||||||
|
username: String!
|
||||||
|
email: String!
|
||||||
|
password: String!
|
||||||
|
appointments: [AppointmentInput!]!
|
||||||
|
}
|
||||||
|
type AuthPayload {
|
||||||
|
token: String
|
||||||
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
# Appointment schemas
|
# Appointment schemas
|
||||||
@@ -50,6 +75,8 @@ type Appointment {
|
|||||||
start: DateTime!
|
start: DateTime!
|
||||||
end: DateTime!
|
end: DateTime!
|
||||||
deleted: Boolean
|
deleted: Boolean
|
||||||
|
createdBy: User
|
||||||
|
# follows: [Follow!]!
|
||||||
}
|
}
|
||||||
input AppointmentInput {
|
input AppointmentInput {
|
||||||
title: String!
|
title: String!
|
||||||
@@ -64,6 +91,13 @@ input AppointmentOrderByInput {
|
|||||||
desc: Sort
|
desc: Sort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Follow schemas
|
||||||
|
type Follow {
|
||||||
|
_id: ID!
|
||||||
|
appointment: Appointment!
|
||||||
|
user: User!
|
||||||
|
}
|
||||||
|
|
||||||
# General-purpose schemas
|
# General-purpose schemas
|
||||||
enum Sort {
|
enum Sort {
|
||||||
asc
|
asc
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
const APP_SECRET = 'GraphQL-is-aw3some';
|
import jwt from 'jsonwebtoken';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
function getTokenPayload(token) {
|
||||||
|
return jwt.verify(token, process.env.APP_SECRET);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserId(req, authToken) {
|
||||||
|
if (req) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (authHeader) {
|
||||||
|
const token = authHeader.replace('Bearer ', '');
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('No token found');
|
||||||
|
}
|
||||||
|
const { userId } = getTokenPayload(token);
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
} else if (authToken) {
|
||||||
|
const { userId } = getTokenPayload(authToken);
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
APP_SECRET
|
getUserId
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
// const mongoose = require("mongoose");
|
||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
// const dotenv = require("../../.env");
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|||||||
14719
server/yarn.lock
14719
server/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user