When starting a backend project, two of the biggest concerns will usually be the right structure of
the project and authentication. If you could skip thinking and planning about these two, starting a
new backend project can be much easier.
Internally, we use GraphQL-Modules and
accounts-js to help us with those two decisions,
GraphQL-Modules helps us solve our architectural problems in modular, schema-first approaches with
the power of GraphQL and accounts-js helps us create our authentication solutions by providing a
simple API together with client and server libraries that saves us a lot of the groundwork around
authentication.
If you haven’t heard about accounts-js before, it is a
set of libraries to provide a full-stack authentication and accounts-management solutions for
Javascript.
It is really customizable; so you can write any plugins for your own authentication methods or use
the already existing email-password or the Facebook and Twitter OAuth integration packages.
accounts-js has connector libraries for MongoDB and Redis, but you can write your own database
handler by implementing a simple interface.
accounts-js provides a ready to use GraphQL API if you install their GraphQL library, and we are
happy to announce that the GraphQL library is now internally built using GraphQL-Modules !
It doesn’t affect people who are not using GraphQL Modules, but it helps the maintainers of
accounts-js and simplifies the integration for GraphQL-Modules-based projects.
How to Implement Server-Side Using accounts-js, GraphQL Modules and Apollo Server
First install required dependencies from npm or yarn:
yarn add mongodb @accounts/server @accounts/password @accounts/database-manager @accounts/mongo @accounts/graphql-api @graphql-modules/core apollo-server graphql-import-node
Let’s assume that we’re using MongoDB as our database, password-based authentication and
ApolloServer:
import "graphql-import-node" ;
import { ApolloServer } from "apollo-server" ;
import { MongoClient } from "mongodb" ;
import { DatabaseManager } from "@accounts/database-manager" ;
import { AccountsModule } from "@accounts/graphql-api" ;
import MongoDBInterface from "@accounts/mongo" ;
import { AccountsPassword } from "@accounts/password" ;
import { AccountsServer } from "@accounts/server" ;
import { resolvers } from "./resolvers" ;
import * as typeDefs from "./typeDefs.graphql" ;
const PORT = process.env. MONGO_URI || 4000 ;
const MONGO_URI = process.env. MONGO_URI || "mongodb://localhost:27017/myDb" ;
const TOKEN_SECRET = process.env. TOKEN_SECRET || "myTokenSecret" ;
async function main () {
const mongoClient = await MongoClient. connect ( MONGO_URI , {
useNewUrlParser: true ,
native_parser: true ,
});
const db = mongoClient. db ();
const userStorage = new MongoDBInterface (db, {
convertUserIdToMongoObjectId: false ,
});
// Create database manager (create user, find users, sessions etc) for accounts-js
const accountsDb = new DatabaseManager ({
sessionStorage: userStorage,
userStorage,
});
// Create accounts server that holds a lower level of all accounts operations
const accountsServer = new AccountsServer (
{
db: accountsDb,
tokenSecret: TOKEN_SECRET ,
},
{
password: new AccountsPassword (),
},
);
const { schema } = new GraphQLModule ({
typeDefs,
resolvers,
imports: [AccountsModule. forRoot ({ accountsServer })],
providers: [
{
provide: Db,
useValue: db, // Use MongoDB's instance inside DI
},
],
});
const apolloServer = new ApolloServer ({
schema,
context : ( session ) => session,
introspection: true ,
});
const { url } = await apolloServer. listen ( PORT );
console. log ( `Server listening: ${ url }` );
}
main ();
And we can extend User type with custom fields in our schema, and add a mutation which is restricted
to authenticated clients.
type Query {
allPosts : [ Post ]
}
type Mutation {
addPost ( title : String , content : String ): ID @auth
}
type User {
posts : [ Post ]
}
type Post {
id : ID
title : String
content : String
author : User
}
Finally, let’s define some resolvers for it:
export const resolvers = {
User: {
posts ({ _id }, args , { injector }) {
const db = injector. get (Db);
const Posts = db. collection ( "posts" );
return Posts. find ({ userId: _id }). toArray ();
},
},
Post: {
id : ({ _id }) => _id,
author ({ userId }, args , { injector }) {
const accountsServer = injector. get (AccountsServer);
return accountsServer. findUserById (userId);
},
},
Query: {
allPosts ( root , args , { injector }) {
const db = injector. get (Db);
const Posts = db. collection ( "posts" );
return Posts. find (). toArray ();
},
},
Mutation: {
addPost (
root ,
{ title , content },
{ injector , userId } : ModuleContext < AccountsContext >,
) {
const db = injector. get (Db);
const Posts = db. collection ( "posts" );
const { insertedId } = Posts. insertOne ({ title, content, userId });
return insertedId;
},
},
};
When you print the whole app’s schema, you would see something like above:
type TwoFactorSecretKey {
ascii : String
base32 : String
hex : String
qr_code_ascii : String
qr_code_hex : String
qr_code_base32 : String
google_auth_qr : String
otpauth_url : String
}
input TwoFactorSecretKeyInput {
ascii : String
base32 : String
hex : String
qr_code_ascii : String
qr_code_hex : String
qr_code_base32 : String
google_auth_qr : String
otpauth_url : String
}
input CreateUserInput {
username : String
email : String
password : String
}
type Query {
twoFactorSecret : TwoFactorSecretKey
getUser : User
allPosts : [ Post ]
}
type Mutation {
createUser ( user : CreateUserInput ! ): ID
verifyEmail ( token : String ! ): Boolean
resetPassword ( token : String ! , newPassword : String ! ): Boolean
sendVerificationEmail ( email : String ! ): Boolean
sendResetPasswordEmail ( email : String ! ): Boolean
changePassword ( oldPassword : String ! , newPassword : String ! ): Boolean
twoFactorSet ( secret : TwoFactorSecretKeyInput ! , code : String ! ): Boolean
twoFactorUnset ( code : String ! ): Boolean
impersonate ( accessToken : String ! , username : String ! ): ImpersonateReturn
refreshTokens ( accessToken : String ! , refreshToken : String ! ): LoginResult
logout : Boolean
authenticate (
serviceName : String !
params : AuthenticateParamsInput !
): LoginResult
addPost ( title : String , content : String ): Post
}
type Tokens {
refreshToken : String
accessToken : String
}
type LoginResult {
sessionId : String
tokens : Tokens
}
type ImpersonateReturn {
authorized : Boolean
tokens : Tokens
user : User
}
type EmailRecord {
address : String
verified : Boolean
}
type User {
id : ID !
emails : [ EmailRecord ! ]
username : String
posts : [ Post ]
}
input UserInput {
id : ID
email : String
username : String
}
input AuthenticateParamsInput {
access_token : String
access_token_secret : String
provider : String
password : String
user : UserInput
code : String
}
type Post {
id : ID
title : String
content : String
author : User
}
How to Implement Client-Side Using accounts-js, React and Apollo-Client
Now we can create a simple frontend app by using Apollo-Client and accounts-js client for this
backend app. The example below shows some example code that works on these two.
import React, { Component } from "react" ;
import { render } from "react-dom" ;
import ApolloClient from "apollo-boost" ;
import gql from "graphql-tag" ;
import { ApolloProvider, Mutation, Query } from "react-apollo" ;
import { AccountsClient } from "@accounts/client" ;
import { AccountsClientPassword } from "@accounts/client-password" ;
import GraphQLClient from "@accounts/graphql-client" ;
const apolloClient = new ApolloClient ({
async request ( operation ) {
const tokens = await accountsClient. getTokens ();
if (tokens) {
operation. setContext ({
headers: {
"accounts-access-token" : tokens.accessToken,
},
});
}
},
uri: "http://localhost:4000/graphql" ,
});
const accountsGraphQL = new GraphQLClient ({ graphQLClient: apolloClient });
const accountsClient = new AccountsClient ({}, accountsGraphQL);
const accountsPassword = new AccountsClientPassword (accountsClient);
const ALL_POSTS_QUERY = gql `
query AllPosts {
allPosts {
id
title
content
author {
username
}
}
}
` ;
const ADD_POST_MUTATION = gql `
mutation AddPost($title: String, $content: String) {
addPost(title: $title, content: $content)
}
` ;
class App extends Component {
state = {
credentials: {
username: "" ,
password: "" ,
},
newPost: {
title: "" ,
content: "" ,
},
user: null ,
};
componentDidMount () {
return this . updateUserState ();
}
async updateUserState () {
const tokens = await accountsClient. refreshSession ();
if (tokens) {
const user = await accountsGraphQL. getUser ();
await this . setState ({ user });
}
}
renderAllPosts () {
return (
< Query query = { ALL_POSTS_QUERY }>
{({ data , loading , error }) => {
if (loading) {
return < p >Loading...</ p >;
}
if (error) {
return < p >Error: {error}</ p >;
}
return data.allPosts. map (( post : any ) => (
< li >
< p >{post.title}</ p >
< p >{post.content}</ p >
< p >Author: {post.author.username}</ p >
</ li >
));
}}
</ Query >
);
}
renderLoginRegister () {
return (
< fieldset >
< legend >Login - Register</ legend >
< form >
< p >
< label >
Username:
< input
value = { this .state.credentials.username}
onChange = {( e ) =>
this . setState ({
credentials: {
... this .state.credentials,
username: e.target.value,
},
})
}
/>
</ label >
</ p >
< p >
< label >
Password:
< input
value = { this .state.credentials.password}
onChange = {( e ) =>
this . setState ({
credentials: {
... this .state.credentials,
password: e.target.value,
},
})
}
/>
</ label >
</ p >
< p >
< button
onClick = {( e ) => {
e. preventDefault ();
accountsPassword
. login ({
password: this .state.credentials.password,
user: {
username: this .state.credentials.username,
},
})
. then (() => this . updateUserState ());
}}
>
Login
</ button >
</ p >
< p >
< button
onClick = {( e ) => {
e. preventDefault ();
accountsPassword
. createUser ({
password: this .state.credentials.password,
username: this .state.credentials.username,
})
. then (() => {
alert ( "Please login with your new credentials" );
this . setState ({
credentials: {
username: "" ,
password: "" ,
},
});
});
}}
>
Register
</ button >
</ p >
</ form >
</ fieldset >
);
}
renderAddPost () {
return (
< Mutation mutation = { ADD_POST_MUTATION }>
{( addPost ) => (
< fieldset >
< legend >Add Post</ legend >
< form >
< p >
< label >
Title:
< input
value = { this .state.newPost.title}
onChange = {( e ) => {
this . setState ({
newPost: {
... this .state.newPost,
title: e.target.value,
},
});
}}
/>
</ label >
</ p >
< p >
< label >
Content:
< input
value = { this .state.newPost.content}
onChange = {( e ) => {
this . setState ({
newPost: {
... this .state.newPost,
content: e.target.value,
},
});
}}
/>
</ label >
</ p >
< p >
< input
type = "submit"
onClick = {( e ) => {
e. preventDefault ();
addPost ({
variables: {
title: this .state.newPost.title,
content: this .state.newPost.content,
},
});
}}
/>
</ p >
</ form >
</ fieldset >
)}
</ Mutation >
);
}
render () {
return (
< div >
< h2 >All Posts</ h2 >
{ this . renderAllPosts ()}
{ this .state.user ? this . renderAddPost () : this . renderLoginRegister ()}
</ div >
);
}
}
render (
< ApolloProvider client = {apolloClient}>
< App />
</ ApolloProvider >,
document. getElementById ( "root" ),
);
As you can see from the example, it can be really easy to create an application that has
authentication in modular and future-proof approach.
You can learn more about accounts-js from the
docs of this great library for more
features such as Two-Factor Authentication and Facebook and Twitter integration using OAuth.
Also, you can learn more about GraphQL-Modules on the website and see
how you can add GraphQL Modules features into your system in a gradual and selective way.
If you want strict types based on GraphQL Schema, for each module, GraphQL Code Generator has
built-in support for GraphQL-Modules based projects.
See the docs for more
details.
You can check out our example about this integration
https://github.com/ardatan/graphql-modules-accountsjs-boilerplate .
All Posts about GraphQL Modules