├── .eslintrc.json
├── .gitignore
├── .graphqlconfig.yml
├── README.md
├── client
├── apollo
│ ├── mutations.js
│ └── queries.js
├── components
│ ├── Auth.jsx
│ ├── Dashboard.jsx
│ ├── Header.jsx
│ ├── Landing.jsx
│ └── ProtectedRoute.jsx
├── index.html
└── index.jsx
├── database
├── datamodel.graphql
└── prisma.yml
├── package.json
├── src
├── index.js
├── resolvers
│ ├── Mutation.js
│ └── Query.js
└── schema.graphql
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 | "rules": {
4 | "no-param-reassign": ["error", { "props": false }]
5 | },
6 | "globals": {
7 | "document": false
8 | },
9 | "env": {
10 | "browser": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 | src/**/*.css
12 | dist
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
26 | .cache
27 |
28 | prisma.graphql
29 |
--------------------------------------------------------------------------------
/.graphqlconfig.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | app:
3 | schemaPath: src/schema.graphql
4 | extensions:
5 | endpoints:
6 | default: http://localhost:4000
7 | database:
8 | schemaPath: src/generated/prisma.graphql
9 | extensions:
10 | prisma: database/prisma.yml
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fullstack Prisma & Apollo
2 | I'm building this to create a _dead simple_ 'fullstack' example of Graphql. Its demonstrating React, Apollo, graphql-yoga, and Prisma.
3 |
4 | Whenever I come to these kind of examples I always look to see how they handle user authentication, so all this app does is handles user signup and login. It persists user state using a session cookie, the goal is for this to serve a good base template to add whatever flavor or application on top of it.
5 |
6 | ## Prerequisites
7 | Install dependencies with `npm install` or `yarn install`.
8 |
9 | Be sure to have prisma and nodemon installed globally.
10 | ```shell
11 | npm install -g prisma graphql-cli nodemon
12 | ```
13 | or
14 | ```shell
15 | yarn global prisma graphql-cli nodemon
16 | ```
17 |
18 | ## Generating Prisma
19 | After logging into Prisma's CLI:
20 | ```shell
21 | prisma deploy database
22 | ```
23 |
24 | Select Demo server which will give you an endpoint like `https://us1.prisma.sh/myusername-12345/my-project/dev`.
25 |
26 | Place a `.env` file in the database folder listing the variables in `database/prisma.yml`. For example:
27 | ```
28 | APP_SECRET=mysecret123
29 | DB_URL=https://us1.prisma.sh/myusername-12345/my-project/dev
30 | ```
31 |
32 | ## Running the dev server
33 | ```shell
34 | node src/index.js
35 | ```
36 |
37 | Then go to `localhost:4000` to access the playground.
38 |
39 | ## Authorization
40 | I'm saving the user's id in a session using express-session.
41 | graphql-yoga
42 | https://github.com/prisma/graphql-yoga/tree/master/examples/authentication/express-session
43 |
44 |
45 | ## Why Sessions
46 | [Many](https://www.apollographql.com/docs/react/recipes/authentication.html) [examples](https://www.howtographql.com/graphql-js/6-authentication/) of using Apollo or graphql demonstrate saving a the user's JWT packet in respnse header or ... localstorage 😳. After reading quite a [few](https://www.rdegges.com/2018/please-stop-using-local-storage/) [posts](http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/) on the subject, I wanted to show a production ready, battle tested solution like sessions. This required a few caveats in the codebase such as query for checking if the user is logged in on page load.
47 |
48 | - For ApolloClient, had to switch from using apollo-boost because of [sessions not being persisted](https://github.com/apollographql/apollo-client/issues/4018#issuecomment-439654182). Manually installing `apollo-boost`'s constituent parts fixed the problem.
49 |
--------------------------------------------------------------------------------
/client/apollo/mutations.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const SIGNUP = gql`
4 | mutation SignupMutation($name: String!, $email: String!, $password: String!) {
5 | signup(name: $name, email: $email, password: $password) {
6 | id
7 | email
8 | __typename
9 | }
10 | }
11 | `;
12 |
13 | export const LOGIN = gql`
14 | mutation LoginMutation($email: String!, $password: String!) {
15 | login(email: $email, password: $password) {
16 | id
17 | email
18 | __typename
19 | }
20 | }
21 | `;
22 |
23 | export const LOGOUT = gql`
24 | mutation LogoutMutation {
25 | logout {
26 | status
27 | __typename
28 | }
29 | }
30 | `;
31 |
--------------------------------------------------------------------------------
/client/apollo/queries.js:
--------------------------------------------------------------------------------
1 | import { gql } from 'apollo-boost';
2 |
3 | export const GET_AUTH_STATUS = gql`
4 | {
5 | isLoggedIn {
6 | status
7 | __typename
8 | }
9 | }
10 | `;
11 |
12 | export const GET_USER = gql`
13 | {
14 | user {
15 | id
16 | name
17 | email
18 | __typename
19 | }
20 | }
21 | `;
22 |
--------------------------------------------------------------------------------
/client/components/Auth.jsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Mutation } from 'react-apollo';
4 | import { SIGNUP, LOGIN } from '../apollo/mutations';
5 | import { GET_AUTH_STATUS } from '../apollo/queries';
6 |
7 | class Auth extends PureComponent {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = {
12 | email: '',
13 | password: '',
14 | name: '',
15 | isSignup: props.type === 'SIGNUP',
16 | };
17 | }
18 |
19 | render() {
20 | const {
21 | email,
22 | password,
23 | name,
24 | isSignup,
25 | } = this.state;
26 |
27 | return (
28 |
29 |
{isSignup ? 'Sign Up' : 'Log In'}
30 |
31 | {
35 | cache.writeQuery({
36 | query: GET_AUTH_STATUS,
37 | data: {
38 | isLoggedIn: {
39 | status: true,
40 | __typename: 'AuthStatus',
41 | },
42 | },
43 | });
44 | }}
45 | >
46 | {
47 | mutation => (
48 |
101 | )
102 | }
103 |
104 |
105 | );
106 | }
107 | }
108 |
109 | Auth.propTypes = {
110 | type: PropTypes.string.isRequired,
111 | };
112 |
113 | export default Auth;
114 |
--------------------------------------------------------------------------------
/client/components/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Query } from 'react-apollo';
3 | import { GET_USER } from '../apollo/queries';
4 |
5 | const Dashboard = () => (
6 |
9 | {
10 | ({ loading, error, data }) => {
11 | if (loading) return loading...;
12 | if (error) return error.;
13 |
14 | return (
15 |
16 | Welcome to your dashboard
17 | {' '}
18 | {data.user.email}
19 |
20 | );
21 | }
22 | }
23 |
24 |
25 | );
26 |
27 | export default Dashboard;
28 |
--------------------------------------------------------------------------------
/client/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { Query, Mutation } from 'react-apollo';
4 | import { GET_AUTH_STATUS } from '../apollo/queries';
5 | import { LOGOUT } from '../apollo/mutations';
6 |
7 | const logoutUpdate = (cache, { data }) => {
8 | cache.writeQuery({
9 | query: GET_AUTH_STATUS,
10 | data: { isLoggedIn: data.logout },
11 | });
12 | };
13 |
14 | const Header = () => (
15 |
18 | {
19 | ({ loading, error, data }) => (
20 |
21 | HEADER
22 | {' - '}
23 | {
24 | (!loading && !data.isLoggedIn.status)
25 | ? (
26 |
27 | sign up
28 | {' - '}
29 | log in
30 |
31 | )
32 | : (
33 |
37 | {logout => }
38 |
39 | )
40 | }
41 | {
42 | error && error.
43 | }
44 |
45 | )
46 | }
47 |
48 | );
49 |
50 | export default Header;
51 |
--------------------------------------------------------------------------------
/client/components/Landing.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Landing = () => (
4 |
5 | Welcome to the home page!
6 |
7 | );
8 |
9 | export default Landing;
10 |
--------------------------------------------------------------------------------
/client/components/ProtectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Query } from 'react-apollo';
3 | import { Redirect, Route } from 'react-router-dom';
4 | import { GET_AUTH_STATUS } from '../apollo/queries';
5 |
6 | const ProtectedRoute = ({
7 | component: Component,
8 | unAuthenticatedOnly,
9 | authenticatedOnly,
10 | ...rest
11 | }) => (
12 | (
15 |
18 | {
19 | ({ loading, error, data }) => {
20 | if (loading) return loading...;
21 | if (error) return error.;
22 |
23 | if (authenticatedOnly && !data.isLoggedIn.status) return ;
24 | if (unAuthenticatedOnly && data.isLoggedIn.status) return ;
25 |
26 | return ;
27 | }
28 | }
29 |
30 | )}
31 | />
32 | );
33 |
34 | export default ProtectedRoute;
35 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Boilerplate
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import { InMemoryCache } from 'apollo-cache-inmemory';
4 | import { ApolloClient } from 'apollo-client';
5 | import { HttpLink } from 'apollo-link-http';
6 | import { ApolloProvider } from 'react-apollo';
7 | import { BrowserRouter as Router, Route } from 'react-router-dom';
8 | import Auth from './components/Auth';
9 | import Header from './components/Header';
10 | import Landing from './components/Landing';
11 | import Dashboard from './components/Dashboard';
12 | import ProtectedRoute from './components/ProtectedRoute';
13 |
14 | const client = new ApolloClient({
15 | cache: new InMemoryCache(),
16 | link: new HttpLink({
17 | credentials: 'include',
18 | uri: 'http://localhost:4000',
19 | }),
20 | });
21 |
22 | const App = () => (
23 |
24 |
25 |
26 |
27 | } />
28 | } />
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 |
37 | render(, document.getElementById('app'));
38 |
--------------------------------------------------------------------------------
/database/datamodel.graphql:
--------------------------------------------------------------------------------
1 | type User {
2 | id: ID! @unique
3 | name: String!
4 | email: String! @unique
5 | password: String!
6 | }
7 |
--------------------------------------------------------------------------------
/database/prisma.yml:
--------------------------------------------------------------------------------
1 | endpoint: ${env:DB_URL}
2 | datamodel: datamodel.graphql
3 | secret: ${env:APP_SECRET}
4 |
5 | hooks:
6 | post-deploy:
7 | - graphql get-schema --project database
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fullstack-prisma-apollo",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "dev": "NODE_ENV=development concurrently --kill-others-on-fail \"nodemon src/index.js\" \"parcel client/index.html\"",
8 | "build": "parcel build ./client/index.html/",
9 | "test": "echo \"Error: no test specified\" && exit 1"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/captDaylight/fullstack-prisma-apollo.git"
14 | },
15 | "author": "paul christophe",
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/captDaylight/fullstack-prisma-apollo/issues"
19 | },
20 | "homepage": "https://github.com/captDaylight/fullstack-prisma-apollo#readme",
21 | "dependencies": {
22 | "apollo-boost": "^0.1.20",
23 | "apollo-cache-inmemory": "^1.3.10",
24 | "apollo-client": "^2.4.6",
25 | "apollo-link-http": "^1.5.5",
26 | "bcryptjs": "^2.4.3",
27 | "dotenv": "^6.1.0",
28 | "express-session": "^1.15.6",
29 | "graphql": "^14.0.2",
30 | "graphql-yoga": "^1.16.7",
31 | "ms": "^2.1.1",
32 | "prisma-binding": "^2.1.6",
33 | "prop-types": "^15.6.2",
34 | "react": "^16.6.1",
35 | "react-apollo": "^2.2.4",
36 | "react-dom": "^16.6.1",
37 | "react-router-dom": "^4.3.1"
38 | },
39 | "devDependencies": {
40 | "concurrently": "^4.0.1",
41 | "eslint": "^5.9.0",
42 | "eslint-config-airbnb": "^17.1.0",
43 | "eslint-plugin-import": "^2.14.0",
44 | "eslint-plugin-jsx-a11y": "^6.1.2",
45 | "eslint-plugin-react": "^7.11.1",
46 | "parcel": "^1.10.3"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | const { GraphQLServer } = require('graphql-yoga');
2 | const { Prisma } = require('prisma-binding');
3 | const session = require('express-session');
4 | const dotenv = require('dotenv');
5 | const ms = require('ms');
6 | const Mutation = require('./resolvers/Mutation');
7 | const Query = require('./resolvers/Query');
8 |
9 | const isProduction = process.env.NODE_ENV === 'production';
10 |
11 | if (!isProduction) {
12 | dotenv.config();
13 | }
14 |
15 | const resolvers = {
16 | Mutation,
17 | Query,
18 | };
19 |
20 | const server = new GraphQLServer({
21 | typeDefs: 'src/schema.graphql',
22 | resolvers,
23 | context: req => ({
24 | ...req,
25 | db: new Prisma({
26 | typeDefs: 'src/generated/prisma.graphql',
27 | endpoint: process.env.DB_URL,
28 | secret: process.env.APP_SECRET,
29 | }),
30 | }),
31 | });
32 |
33 | server.express.use(session({
34 | name: 'pid',
35 | secret: 'some-random-secret-here',
36 | resave: false,
37 | saveUninitialized: false,
38 | cookie: {
39 | httpOnly: true,
40 | secure: isProduction,
41 | maxAge: ms('1d'),
42 | },
43 | }));
44 |
45 | server.start({
46 | cors: {
47 | credentials: true,
48 | origin: 'http://localhost:1234',
49 | },
50 | }, () => console.log('server is running on localhost:4000'));
51 |
--------------------------------------------------------------------------------
/src/resolvers/Mutation.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require('bcrypt');
2 |
3 | async function login(parent, args, context) {
4 | const user = await context.db.query.user({ where: { email: args.email } });
5 | if (!user) throw new Error('wrong_credentials');
6 |
7 | const isValidPass = await bcrypt.compare(args.password, user.password);
8 | if (!isValidPass) throw new Error('wrong_credentials');
9 |
10 | context.request.session.userId = user.id;
11 |
12 | return user;
13 | }
14 |
15 | async function signup(parent, args, context, info) {
16 | const password = await bcrypt.hash(args.password, 10);
17 | const user = await context.db.mutation.createUser({ data: { ...args, password } }, info);
18 |
19 | context.request.session.userId = user.id;
20 |
21 | return user;
22 | }
23 |
24 | function logout(parent, args, context) {
25 | if (!context.request.session) throw new Error('auth_error');
26 |
27 | context.request.session.destroy();
28 |
29 | return { status: false };
30 | }
31 |
32 | module.exports = {
33 | login,
34 | signup,
35 | logout,
36 | };
37 |
--------------------------------------------------------------------------------
/src/resolvers/Query.js:
--------------------------------------------------------------------------------
1 | const isLoggedIn = (parent, args, { request }) => ({ status: typeof request.session.userId !== 'undefined' });
2 |
3 | async function user(parent, args, context, info) {
4 | const id = context.request.session.userId;
5 |
6 | if (typeof id === 'undefined') throw new Error('auth_error');
7 |
8 | return context.db.query.user({
9 | where: { id },
10 | }, info);
11 | }
12 |
13 | module.exports = {
14 | isLoggedIn,
15 | user,
16 | };
17 |
--------------------------------------------------------------------------------
/src/schema.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | isLoggedIn: AuthStatus!
3 | user: User
4 | }
5 |
6 | type Mutation {
7 | login(email: String!, password: String!): User
8 | signup(name: String!, email: String!, password: String!): User
9 | logout: AuthStatus!
10 | }
11 |
12 | type User {
13 | id: ID!
14 | name: String!
15 | email: String!
16 | }
17 |
18 | type AuthStatus {
19 | status: Boolean!
20 | }
21 |
--------------------------------------------------------------------------------