5 Commits
main ... demo

Author SHA1 Message Date
Riccardo
5368f40b04 Progress 2021-08-29 10:39:39 +02:00
Riccardo
2f119e43bd Changes 2021-08-16 11:06:02 +02:00
Riccardo
78db95aa5f Some cleaning 2021-08-16 10:08:43 +02:00
Riccardo
959c6dbc33 Fix 2021-08-16 09:06:16 +02:00
Riccardo
93169095c0 gesJav-wocxo3-vedgyg 2021-08-16 08:54:17 +02:00
26 changed files with 27669 additions and 31784 deletions

30194
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}, },

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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('/')
}); });

View File

@@ -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"

View File

@@ -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>
); );
}; };

View File

@@ -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();

File diff suppressed because it is too large Load Diff

View File

@@ -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"
@@ -55,4 +55,4 @@
"nodemon": "^2.0.6", "nodemon": "^2.0.6",
"prettier": "^2.2.1" "prettier": "^2.2.1"
} }
} }

View File

@@ -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}`);
}); });

View File

@@ -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
View 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);

View File

@@ -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 })
},
} }
}; };

View File

@@ -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
}; };

View 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
};

View File

@@ -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
}; };

View File

@@ -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
? { ? {

View File

@@ -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
};

View File

@@ -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

View File

@@ -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
}; };

View File

@@ -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();

File diff suppressed because it is too large Load Diff