-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* added token signing and veryfing, context verification, added userids with uuid and userid in token conformant to jwt spec, fixed some api definitions * fixed signup not generating token * implements login * added rules and permissions, fixed project structure, added proper api methods * added data seeding for tests * fixes resolver to return author.name by author.id * test fixed reslover maping * exports context * adds Authorisation check for create * fixes Post shield * tests fixed * Update backend/src/tests/tests.test.js Co-authored-by: Robert Schäfer <git@roschaefer.de> * Update backend/src/datasources/userApi.js Co-authored-by: Robert Schäfer <git@roschaefer.de> Co-authored-by: nasden <nasdenkov@gmail.com> Co-authored-by: Robert Schäfer <git@roschaefer.de>
- Loading branch information
1 parent
6def6cd
commit f01584a
Showing
12 changed files
with
932 additions
and
171 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
const jwt = require('jsonwebtoken'); | ||
|
||
module.exports = ({ req }) => { | ||
let token = req.headers.authorization || ''; | ||
token = token.replace('Bearer ', ''); | ||
try { | ||
const decodedToken = jwt.verify(token, process.env.JWT_SECRET); | ||
return { token: decodedToken } | ||
} | ||
catch (e) { | ||
return {} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
const { RESTDataSource } = require('apollo-datasource-rest'); | ||
const jwt = require('jsonwebtoken'); | ||
|
||
class AuthAPI extends RESTDataSource { | ||
|
||
constructor() { | ||
super(); | ||
} | ||
|
||
async createToken(userId) { | ||
return jwt.sign({ uId: userId }, process.env.JWT_SECRET, { algorithm: 'HS256', expiresIn: '1h' }); | ||
} | ||
|
||
} | ||
|
||
module.exports = AuthAPI; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,36 @@ | ||
const { RESTDataSource } = require('apollo-datasource-rest'); | ||
const { v4: uuidv4 } = require('uuid'); | ||
const bcrypt = require('bcrypt'); | ||
|
||
class UsersAPI extends RESTDataSource { | ||
constructor() { | ||
super(); | ||
this.users = [ | ||
{ | ||
name: 'Peter', | ||
posts: [], | ||
}, | ||
{ | ||
name: 'Max', | ||
posts: [], | ||
}, | ||
]; | ||
this.users = []; | ||
} | ||
|
||
async getUser(name) { | ||
return this.users.find(user => user.name === name); | ||
async getUserById(id) { | ||
return this.users.find(user => user.id == id); | ||
} | ||
|
||
async getUsers() { | ||
return this.users; | ||
} | ||
|
||
async getUserByEmail(email) { | ||
return this.users.find(user => user.email === email); | ||
} | ||
|
||
async createUser(name, email, password) { | ||
let obj = {id: uuidv4(), name: name, email: email, posts: [], password: await this.hashPassword(password)}; | ||
this.users.push(obj); | ||
return obj; | ||
} | ||
|
||
async hashPassword(password) { | ||
const saltRounds = parseInt(process.env.SALT_ROUNDS); | ||
return bcrypt.hash(password, saltRounds); | ||
} | ||
|
||
} | ||
|
||
module.exports = UsersAPI; | ||
module.exports = UsersAPI; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,41 @@ | ||
const { ApolloServer } = require('apollo-server'); | ||
const typeDefs = require('./typeDefs'); | ||
const resolvers = require('./resolvers'); | ||
const permissions = require('./security/permissions'); | ||
const UsersAPI = require('./datasources/userApi'); | ||
const PostsAPI = require('./datasources/postApi'); | ||
const AuthAPI = require('./datasources/authenticationApi'); | ||
require('dotenv').config(); | ||
const { applyMiddleware } = require('graphql-middleware'); | ||
const { makeExecutableSchema } = require('graphql-tools'); | ||
|
||
|
||
// DO NOT initialize the endpoint inside the dataSources function! | ||
// See https://github.com/apollographql/apollo-server/issues/3150 | ||
// Alternatively, set schema.polling.enable to false. (Haven't tested if this works tho) | ||
const usersApi = new UsersAPI(); | ||
const postsApi = new PostsAPI(); | ||
const authApi = new AuthAPI(); | ||
|
||
const context = require('./context'); | ||
|
||
const schema = applyMiddleware( | ||
makeExecutableSchema({ | ||
typeDefs, | ||
resolvers, | ||
}), | ||
permissions, | ||
); | ||
|
||
const server = new ApolloServer({ | ||
typeDefs, | ||
resolvers, | ||
schema, | ||
context: ({req}) => context({req}), | ||
dataSources: () => { | ||
return { | ||
usersApi: usersApi, | ||
postsApi: postsApi | ||
postsApi: postsApi, | ||
authApi: authApi, | ||
} | ||
} | ||
}); | ||
|
||
|
||
server.listen().then(({url}) => { | ||
console.log(`Server ready at ${url}`); | ||
}) | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
const { isAuthenticated, isPostUpvoted, isPostWithTitlePresent, passwordIsValid, passwordIsTooShort, emailIsTaken, canSeeEmail} = require("./rules"); | ||
const { shield, and, not, deny, allow, chain} = require('graphql-shield'); | ||
const { UserInputError } = require('apollo-server'); | ||
|
||
const permissions = shield({ | ||
Query: { | ||
"*": allow, | ||
}, | ||
User: { | ||
"*": deny, | ||
name: allow, | ||
posts: allow, | ||
email: and(isAuthenticated, canSeeEmail), | ||
}, | ||
Post: { | ||
"*": allow, | ||
}, | ||
Mutation: { | ||
"*": deny, | ||
signup: and( | ||
not(emailIsTaken, new UserInputError("A user with this email already exists.")), | ||
not(passwordIsTooShort, new UserInputError("The password must be at least 8 characters long.")) | ||
), | ||
login: chain( | ||
// This may look strange, but if an already authenticated user authenticates (logs in) a second time, she'll have two tokens! This is not good practice. | ||
// There are two options: either implement a cache layer for the tokens, or just disallow authenticated users to login a second time. | ||
not(isAuthenticated, new Error("Already logged in. Redirect to home page.")), | ||
not(passwordIsTooShort, new UserInputError("The password must be at least 8 characters long.")), | ||
passwordIsValid, | ||
), | ||
write: and( | ||
isAuthenticated, | ||
not(isPostWithTitlePresent, new UserInputError("Post with this title already exists.")) | ||
), | ||
upvote: and( | ||
isAuthenticated, | ||
isPostWithTitlePresent, | ||
not(isPostUpvoted, new UserInputError("You've already upvoted this post")) | ||
) | ||
} | ||
}); | ||
|
||
module.exports = permissions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
const { rule } = require('graphql-shield'); | ||
const bcrypt = require('bcrypt'); | ||
|
||
|
||
const isAuthenticated = rule({ cache: 'contextual' })( | ||
async (parent, args, { token, dataSources }) => { | ||
const user = await dataSources.usersApi.getUserById(token.uId); | ||
return user !== undefined; | ||
} | ||
); | ||
|
||
const canSeeEmail = rule({ cache: 'strict' })( | ||
async (parent, args, { token }) => { | ||
return token.uId === parent.id; | ||
} | ||
); | ||
|
||
const emailIsTaken = rule({ cache: 'strict' })( | ||
async(parent, args, { token, dataSources }) => { | ||
const user = await dataSources.usersApi.getUserByEmail(args.email); | ||
return user !== undefined; | ||
} | ||
); | ||
|
||
const passwordIsTooShort = rule({ cache: 'strict' })( | ||
async(parent, args, { token, dataSources }) => { | ||
return args.password.length < 8; | ||
} | ||
); | ||
|
||
const passwordIsValid = rule({ cache: 'strict' })( | ||
async (parent, args, { dataSources }) => { | ||
const user = await dataSources.usersApi.getUserByEmail(args.email); | ||
return user !== undefined && bcrypt.compareSync(args.password, user.password); | ||
} | ||
); | ||
|
||
const isPostWithTitlePresent = rule({ cache: 'strict'})( | ||
async (parent, args, { dataSources }) => { | ||
let title; | ||
if (args.post === undefined) { | ||
title = args.title; | ||
} | ||
else { | ||
title = args.post.title | ||
} | ||
|
||
const post = await dataSources.postsApi.getPost(title); | ||
return post !== undefined; | ||
} | ||
); | ||
|
||
const isPostUpvoted = rule({ cache: 'strict'})( | ||
async (parent, args, { dataSources, token }) => { | ||
const title = args.title; | ||
const post = await dataSources.postsApi.getPost(title); | ||
return post.upvoters.includes(token.uId); | ||
} | ||
); | ||
|
||
exports.isAuthenticated = isAuthenticated; | ||
exports.isPostUpvoted = isPostUpvoted; | ||
exports.isPostWithTitlePresent = isPostWithTitlePresent; | ||
exports.passwordIsValid = passwordIsValid; | ||
exports.passwordIsTooShort = passwordIsTooShort; | ||
exports.emailIsTaken = emailIsTaken; | ||
exports.canSeeEmail = canSeeEmail; |
Oops, something went wrong.