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-router": "^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",
"web-vitals": "^0.2.4"
},

View File

@@ -1,9 +1,11 @@
import React from 'react';
import Header from './layout/Header';
import Login from './Login';
import AppointmentList from './appointment/AppointmentList';
import CreateAppointment from './appointment/CreateAppointment';
import UpdateAppointemnt from './appointment/UpdateAppointment';
import Calendar from './Calendar';
import Search from './Search';
import { Switch, Route } from 'react-router-dom';
const App = () => {
@@ -15,7 +17,9 @@ const App = () => {
<Route exact path="/" component={AppointmentList} />
<Route exact path="/create" component={CreateAppointment} />
<Route exact path="/update/:_id" component={UpdateAppointemnt} />
<Route exact path="/login" component={Login} />
<Route exact path="/calendar" component={Calendar} />
<Route exact path="/search" component={Search} />
</Switch>
</div>
</div>
@@ -23,3 +27,42 @@ const 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 { useLazyQuery } from '@apollo/client';
import gql from 'graphql-tag';
import Appointment from './appointment/Appointment';
const FEED_SEARCH_QUERY = gql`
query FeedSearchQuery($filter: String!) {
feed(filter: $filter) {
id
links {
id
appointments {
_id
title
description
type
}
}
}
`;
const Search = () => {
const [searchFilter, setSearchFilter] = useState('');
const [executeSearch, { data }] = useLazyQuery(
FEED_SEARCH_QUERY
);
return (
<>
<div>
Search
<input
type="text"
onChange={(e) => setSearchFilter(e.target.value)}
/>
<button
onClick={() =>
executeSearch({
variables: { filter: searchFilter }
})
}
>
OK
</button>
</div>
</>
);
const [searchFilter, setSearchFilter] = useState('');
const [executeSearch, { data }] = useLazyQuery(
FEED_SEARCH_QUERY
);
return (
<>
<div>
Search
<input type="text" onChange={(e) => setSearchFilter(e.target.value)}/>
<button onClick={() => executeSearch({ variables: { filter: searchFilter } })}>OK</button>
</div>
{data &&
data.feed.appointments.map((appointment, index) => (
<Appointment key={appointment.id} appointment={appointment} index={index} />
))}
</>
);
};
export default Search;

View File

@@ -21,18 +21,116 @@ const Appointment = (props) => {
onCompleted: () => history.push('/')
})
const updateAppointment = () => {
let path = `/update/${appointment._id}`;
history.push(path);
}
// const updateAppointment = () => {
// let path = `/update/${appointment._id}`;
// history.push(path);
// }
return (
<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>
);
};
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 { data, loading } = useQuery(APPOINTMENTS_QUERY);
const { data } = useQuery(APPOINTMENTS_QUERY);
if (data !== undefined) {
return (
@@ -39,3 +39,175 @@ const 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 { useHistory } from 'react-router';
import { useMutation, gql } from '@apollo/client';
import { APPOINTMENTS_PER_PAGE } from '../../constants';
import { APPOINTMENTS_QUERY } from './AppointmentList';
import Datetime from 'react-datetime';
import "react-datetime/css/react-datetime.css";
@@ -41,6 +43,34 @@ const CreateAppointment = () => {
start: formState.start,
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('/')
});

View File

@@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { useHistory } from 'react-router';
import { useMutation, gql, useQuery } from '@apollo/client';
// import { APPOINTMENTS_PER_PAGE } from '../../constants';
// import { APPOINTMENTS_QUERY } from './AppointmentList';
import Datetime from 'react-datetime';
import "react-datetime/css/react-datetime.css";
@@ -72,6 +74,10 @@ const UpdateAppointment = ({ match: { params: { _id } } }) => {
if (data === undefined) {
return <div>Loading...</div>
} else {
// setFormState({
// formState.title= data.oneAppointment.title
// })
return (
<div>
<form
@@ -89,10 +95,16 @@ const UpdateAppointment = ({ match: { params: { _id } } }) => {
type="text"
/>
<input
readOnly
className="mb2"
value={data.oneAppointment.title}
value={formState.title}
onChange={(e) =>
setFormState({
...formState,
title: e.target.value
})
}
type="text"
placeholder="Input title"
/>
<input
className="mb2"

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useHistory } from 'react-router';
import { Link, withRouter } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { AUTH_TOKEN } from '../../constants';
const Header = () => {
@@ -17,6 +17,21 @@ const Header = () => {
<div className="flex flex-fixed">
<Link to="/create" className="ml1 no-underline black">New</Link>
</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>
);
};

View File

@@ -5,6 +5,8 @@ import { AUTH_TOKEN } from './constants';
import App from './components/App';
import { BrowserRouter } from 'react-router-dom';
import { setContext } from '@apollo/client/link/context';
// import * as serviceWorker from './serviceWorker';
import {
ApolloProvider,
ApolloClient,
@@ -13,7 +15,7 @@ import {
} from '@apollo/client';
const httpLink = createHttpLink({
uri: 'http://localhost:4000/djhb58fytkh476dk45yh49'
uri: 'http://localhost:4000/graphql'
});
const authLink = setContext((_, { headers }) => {
@@ -39,3 +41,37 @@ ReactDOM.render(
</BrowserRouter>,
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/preset-env": "^7.12.11",
"apollo-engine": "^1.1.2",
"apollo-server": "^3.13.0",
"apollo-server": "^2.19.0",
"apollo-server-express": "^2.19.1",
"bcrypt": "^5.0.0",
"bcryptjs": "2.4.3",
"body-parser": "^1.20.3",
"body-parser": "^1.19.0",
"chai": "^4.2.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"esm": "^3.2.25",
"express": "^4.20.0",
"express-graphql": "^0.12.0",
"graphql": "^15.4.0",
"graphql-compose": "^7.23.0",
"graphql-compose-connection": "^8.0.1",
"graphql-compose-mongoose": "^9.0.0",
"graphql-depth-limit": "^1.1.0",
"graphql-middleware": "^6.0.0",
"graphql-tools": "^7.0.2",
"jsonwebtoken": "9.0.0",
"express": "^4.17.1",
"express-graphql": "*",
"graphql": "*",
"graphql-compose": "*",
"graphql-compose-connection": "*",
"graphql-compose-mongoose": "*",
"graphql-depth-limit": "*",
"graphql-middleware": "*",
"graphql-tools": "*",
"jsonwebtoken": "8.5.1",
"migrate": "^1.7.0",
"mocha": "^8.2.1",
"mongodb": "^3.6.10",
"mongoose": "^6.13.6",
"mongodb": "^3.6.3",
"mongoose": "^5.11.9",
"mongoose-bcrypt": "^1.9.0",
"mongoose-timestamp": "^0.6.0",
"node-migrate": "^0.1.0"

View File

@@ -9,6 +9,7 @@ import './utils/db.js';
import fs from 'fs';
import path from 'path';
import cors from 'cors';
import jwt from 'jsonwebtoken';
const moduleURL = new URL(import.meta.url);
const __dirname = path.dirname(moduleURL.pathname);
@@ -17,10 +18,33 @@ const pubsub = new PubSub();
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('/djhb58fytkh476dk45yh49', graphqlHTTP({
schema: schema,
app.use('/graphql', graphqlHTTP({
schema,
validationRules: [depthLimit(3)],
graphiql: true
}));
@@ -30,13 +54,33 @@ const server = new ApolloServer({
path.join(__dirname, 'schema.graphql'),
'utf8'
),
// schema,
cors: true,
playground: process.env.NODE_ENV === 'development' ? true : false,
context: ({ req }) => {
return {
context: ({ req }) => ({
...req,
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,
@@ -49,6 +93,7 @@ server.applyMiddleware({
path: '/',
cors: true,
onHealthCheck: () =>
// eslint-disable-next-line no-undef
new Promise((resolve, reject) => {
if (mongoose.connection.readyState > 0) {
resolve();
@@ -59,6 +104,5 @@ server.applyMiddleware({
});
app.listen({ port: process.env.PORT }, () => {
console.log(`🚀 Server listening on port ${process.env.PORT}`);
console.log(`😷 Health checks available at ${process.env.HEALTH_ENDPOINT}`);
console.log(`Server listening on port ${process.env.PORT}`);
});

View File

@@ -24,6 +24,11 @@ const AppointmentSchema = new Schema({
deleted: {
type: Boolean,
required: false
},
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "user",
required: false
}
});
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 User from './models/user.js';
import jwt from 'jsonwebtoken';
import { createAppointment } from './resolvers/Mutation';
export const resolvers = {
Query: {
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({
_id: args._id
});
},
async allUsers() {
return await User.find();
},
},
Mutation: {
async createAppointment(parent, args, context, info) {
console.log(context);
args.deleted = false;
return await Appointment.create(args);
async signup(root, args) {
const user = await User.create(args);
user.password = user.generateHash(args.password);
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({
args
}, args, {
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();
}
function follows(parent, args, context) {
return context.mongo.appointment
.findUnique({ where: { id: parent.id } })
.follows();
}
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 newAppointment = context.mongo.appointment.create({
data: {
title: args.title,
description: args.description
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
import dotenv from 'dotenv';
import appointment from '../models/appointment';
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 = {
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
? {

View File

@@ -1,14 +1,22 @@
function newLinkSubscribe(parent, args, context, info) {
return context.pubsub.asyncIterator("NEW_LINK")
function newAppointmentSubscribe(parent, args, context) {
return context.pubsub.asyncIterator("NEW_APPOINTMENT");
}
const newAppointment = {
subscribe: newLinkSubscribe,
resolve: payload => {
return payload
},
subscribe: newAppointmentSubscribe,
resolve: payload => payload,
};
function newFollowSubscribe(parent, args, context) {
return context.pubsub.asyncIterator("NEW_FOLLOW");
}
const newFollow = {
subscribe: newFollowSubscribe,
resolve: payload => payload,
};
module.exports = {
newAppointment,
}
newFollow
};

View File

@@ -8,6 +8,8 @@ type Query {
): Feed!
allAppointments: [Appointment]
oneAppointment(_id: ID!): Appointment
allUsers: [User]
users: [User!]!
}
type Feed {
@@ -35,10 +37,33 @@ type Mutation {
deleted: Boolean
): 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 {
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
@@ -50,6 +75,8 @@ type Appointment {
start: DateTime!
end: DateTime!
deleted: Boolean
createdBy: User
# follows: [Follow!]!
}
input AppointmentInput {
title: String!
@@ -64,6 +91,13 @@ input AppointmentOrderByInput {
desc: Sort
}
# Follow schemas
type Follow {
_id: ID!
appointment: Appointment!
user: User!
}
# General-purpose schemas
enum Sort {
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 = {
APP_SECRET
getUserId
};

View File

@@ -1,4 +1,7 @@
// const mongoose = require("mongoose");
import mongoose from 'mongoose';
// const dotenv = require("../../.env");
import dotenv from 'dotenv';
dotenv.config();

File diff suppressed because it is too large Load Diff