Code base

- Authentication support
- Env file example for easy and secure setup
- Dynamic GraphQL Module loading
- Database linking using Prisma
This commit is contained in:
Tanguy Herbron 2020-05-04 01:43:42 +02:00
parent e39484b960
commit 4e12b56e2a
12 changed files with 7475 additions and 0 deletions

10
.env.example Normal file
View File

@ -0,0 +1,10 @@
JWT_SECRET=
ENABLE_REGISTRATION=
IS_ATTACHED=true
INSTALLED_MODULES=
## Enable module list
# Almost mandatory, not including this module could break everything
AUTHENTICATION=
TEMENOS=

5
datamodel.prisma Normal file
View File

@ -0,0 +1,5 @@
type User {
id: ID! @unique
username: String! @unique
password: String!
}

30
docker-compose.yml Normal file
View File

@ -0,0 +1,30 @@
version: '3'
services:
prisma:
image: prismagraphql/prisma:1.23
restart: always
ports:
- "4466:4466"
environment:
PRISMA_CONFIG: |
port: 4466
# uncomment the next line and provide the env var PRISMA_MANAGEMENT_API_SECRET=my-secret to activate cluster security
# managementApiSecret: my-secret
databases:
default:
connector: mysql
host: mysql
port: 3306
user: root
password: prisma
migrations: true
rawAccess: true
mysql:
image: mysql:5.7
restart: always
environment:
MYSQL_ROOT_PASSWORD: prisma
volumes:
- mysql:/var/lib/mysql
volumes:
mysql:

6589
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "amaxa",
"version": "0.0.1",
"description": "Amaxa is the API mainframe used to centralize every GraphQL Module",
"main": "index.js",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js",
"deploy": "prisma deploy"
},
"repository": {
"type": "git",
"url": "git+https://github.com/The-School-of-Athens/Amaxa.git"
},
"author": "Tanguy Herbron",
"license": "MIT",
"bugs": {
"url": "https://github.com/The-School-of-Athens/Amaxa/issues"
},
"homepage": "https://github.com/The-School-of-Athens/Amaxa#readme",
"dependencies": {
"@graphql-modules/core": "^0.7.15",
"apacheconf": "0.0.5",
"apollo-server": "^2.2.6",
"bcryptjs": "^2.4.3",
"dotenv": "^8.2.0",
"graphql": "^14.0.2",
"jsonwebtoken": "^8.5.1",
"prisma": "^1.23.0",
"prisma-client-lib": "^1.21.1",
"temenos": "git+https://github.com/The-School-of-Athens/Temenos.git"
},
"devDependencies": {
"nodemon": "^1.18.7"
}
}

11
prisma.yml Normal file
View File

@ -0,0 +1,11 @@
endpoint: http://localhost:4466
datamodel: datamodel.prisma
generate:
- generator: javascript-client
output: ./src/generated/prisma-client/
hooks:
post-deploy:
- prisma generate

403
src/generated/prisma-client/index.d.ts vendored Normal file
View File

@ -0,0 +1,403 @@
// Code generated by Prisma (prisma@1.34.10). DO NOT EDIT.
// Please don't change this file manually but run `prisma generate` to update it.
// For more information, please read the docs: https://www.prisma.io/docs/prisma-client/
import { DocumentNode } from "graphql";
import {
makePrismaClientClass,
BaseClientOptions,
Model
} from "prisma-client-lib";
import { typeDefs } from "./prisma-schema";
export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> &
U[keyof U];
export type Maybe<T> = T | undefined | null;
export interface Exists {
user: (where?: UserWhereInput) => Promise<boolean>;
}
export interface Node {}
export type FragmentableArray<T> = Promise<Array<T>> & Fragmentable;
export interface Fragmentable {
$fragment<T>(fragment: string | DocumentNode): Promise<T>;
}
export interface Prisma {
$exists: Exists;
$graphql: <T = any>(
query: string,
variables?: { [key: string]: any }
) => Promise<T>;
/**
* Queries
*/
user: (where: UserWhereUniqueInput) => UserNullablePromise;
users: (args?: {
where?: UserWhereInput;
orderBy?: UserOrderByInput;
skip?: Int;
after?: String;
before?: String;
first?: Int;
last?: Int;
}) => FragmentableArray<User>;
usersConnection: (args?: {
where?: UserWhereInput;
orderBy?: UserOrderByInput;
skip?: Int;
after?: String;
before?: String;
first?: Int;
last?: Int;
}) => UserConnectionPromise;
node: (args: { id: ID_Output }) => Node;
/**
* Mutations
*/
createUser: (data: UserCreateInput) => UserPromise;
updateUser: (args: {
data: UserUpdateInput;
where: UserWhereUniqueInput;
}) => UserPromise;
updateManyUsers: (args: {
data: UserUpdateManyMutationInput;
where?: UserWhereInput;
}) => BatchPayloadPromise;
upsertUser: (args: {
where: UserWhereUniqueInput;
create: UserCreateInput;
update: UserUpdateInput;
}) => UserPromise;
deleteUser: (where: UserWhereUniqueInput) => UserPromise;
deleteManyUsers: (where?: UserWhereInput) => BatchPayloadPromise;
/**
* Subscriptions
*/
$subscribe: Subscription;
}
export interface Subscription {
user: (
where?: UserSubscriptionWhereInput
) => UserSubscriptionPayloadSubscription;
}
export interface ClientConstructor<T> {
new (options?: BaseClientOptions): T;
}
/**
* Types
*/
export type UserOrderByInput =
| "id_ASC"
| "id_DESC"
| "username_ASC"
| "username_DESC"
| "password_ASC"
| "password_DESC";
export type MutationType = "CREATED" | "UPDATED" | "DELETED";
export interface UserCreateInput {
username: String;
password: String;
}
export interface UserUpdateInput {
username?: Maybe<String>;
password?: Maybe<String>;
}
export interface UserUpdateManyMutationInput {
username?: Maybe<String>;
password?: Maybe<String>;
}
export interface UserWhereInput {
id?: Maybe<ID_Input>;
id_not?: Maybe<ID_Input>;
id_in?: Maybe<ID_Input[] | ID_Input>;
id_not_in?: Maybe<ID_Input[] | ID_Input>;
id_lt?: Maybe<ID_Input>;
id_lte?: Maybe<ID_Input>;
id_gt?: Maybe<ID_Input>;
id_gte?: Maybe<ID_Input>;
id_contains?: Maybe<ID_Input>;
id_not_contains?: Maybe<ID_Input>;
id_starts_with?: Maybe<ID_Input>;
id_not_starts_with?: Maybe<ID_Input>;
id_ends_with?: Maybe<ID_Input>;
id_not_ends_with?: Maybe<ID_Input>;
username?: Maybe<String>;
username_not?: Maybe<String>;
username_in?: Maybe<String[] | String>;
username_not_in?: Maybe<String[] | String>;
username_lt?: Maybe<String>;
username_lte?: Maybe<String>;
username_gt?: Maybe<String>;
username_gte?: Maybe<String>;
username_contains?: Maybe<String>;
username_not_contains?: Maybe<String>;
username_starts_with?: Maybe<String>;
username_not_starts_with?: Maybe<String>;
username_ends_with?: Maybe<String>;
username_not_ends_with?: Maybe<String>;
password?: Maybe<String>;
password_not?: Maybe<String>;
password_in?: Maybe<String[] | String>;
password_not_in?: Maybe<String[] | String>;
password_lt?: Maybe<String>;
password_lte?: Maybe<String>;
password_gt?: Maybe<String>;
password_gte?: Maybe<String>;
password_contains?: Maybe<String>;
password_not_contains?: Maybe<String>;
password_starts_with?: Maybe<String>;
password_not_starts_with?: Maybe<String>;
password_ends_with?: Maybe<String>;
password_not_ends_with?: Maybe<String>;
AND?: Maybe<UserWhereInput[] | UserWhereInput>;
OR?: Maybe<UserWhereInput[] | UserWhereInput>;
NOT?: Maybe<UserWhereInput[] | UserWhereInput>;
}
export interface UserSubscriptionWhereInput {
mutation_in?: Maybe<MutationType[] | MutationType>;
updatedFields_contains?: Maybe<String>;
updatedFields_contains_every?: Maybe<String[] | String>;
updatedFields_contains_some?: Maybe<String[] | String>;
node?: Maybe<UserWhereInput>;
AND?: Maybe<UserSubscriptionWhereInput[] | UserSubscriptionWhereInput>;
OR?: Maybe<UserSubscriptionWhereInput[] | UserSubscriptionWhereInput>;
NOT?: Maybe<UserSubscriptionWhereInput[] | UserSubscriptionWhereInput>;
}
export type UserWhereUniqueInput = AtLeastOne<{
id: Maybe<ID_Input>;
username?: Maybe<String>;
}>;
export interface NodeNode {
id: ID_Output;
}
export interface AggregateUser {
count: Int;
}
export interface AggregateUserPromise
extends Promise<AggregateUser>,
Fragmentable {
count: () => Promise<Int>;
}
export interface AggregateUserSubscription
extends Promise<AsyncIterator<AggregateUser>>,
Fragmentable {
count: () => Promise<AsyncIterator<Int>>;
}
export interface BatchPayload {
count: Long;
}
export interface BatchPayloadPromise
extends Promise<BatchPayload>,
Fragmentable {
count: () => Promise<Long>;
}
export interface BatchPayloadSubscription
extends Promise<AsyncIterator<BatchPayload>>,
Fragmentable {
count: () => Promise<AsyncIterator<Long>>;
}
export interface UserPreviousValues {
id: ID_Output;
username: String;
password: String;
}
export interface UserPreviousValuesPromise
extends Promise<UserPreviousValues>,
Fragmentable {
id: () => Promise<ID_Output>;
username: () => Promise<String>;
password: () => Promise<String>;
}
export interface UserPreviousValuesSubscription
extends Promise<AsyncIterator<UserPreviousValues>>,
Fragmentable {
id: () => Promise<AsyncIterator<ID_Output>>;
username: () => Promise<AsyncIterator<String>>;
password: () => Promise<AsyncIterator<String>>;
}
export interface UserEdge {
node: User;
cursor: String;
}
export interface UserEdgePromise extends Promise<UserEdge>, Fragmentable {
node: <T = UserPromise>() => T;
cursor: () => Promise<String>;
}
export interface UserEdgeSubscription
extends Promise<AsyncIterator<UserEdge>>,
Fragmentable {
node: <T = UserSubscription>() => T;
cursor: () => Promise<AsyncIterator<String>>;
}
export interface UserSubscriptionPayload {
mutation: MutationType;
node: User;
updatedFields: String[];
previousValues: UserPreviousValues;
}
export interface UserSubscriptionPayloadPromise
extends Promise<UserSubscriptionPayload>,
Fragmentable {
mutation: () => Promise<MutationType>;
node: <T = UserPromise>() => T;
updatedFields: () => Promise<String[]>;
previousValues: <T = UserPreviousValuesPromise>() => T;
}
export interface UserSubscriptionPayloadSubscription
extends Promise<AsyncIterator<UserSubscriptionPayload>>,
Fragmentable {
mutation: () => Promise<AsyncIterator<MutationType>>;
node: <T = UserSubscription>() => T;
updatedFields: () => Promise<AsyncIterator<String[]>>;
previousValues: <T = UserPreviousValuesSubscription>() => T;
}
export interface User {
id: ID_Output;
username: String;
password: String;
}
export interface UserPromise extends Promise<User>, Fragmentable {
id: () => Promise<ID_Output>;
username: () => Promise<String>;
password: () => Promise<String>;
}
export interface UserSubscription
extends Promise<AsyncIterator<User>>,
Fragmentable {
id: () => Promise<AsyncIterator<ID_Output>>;
username: () => Promise<AsyncIterator<String>>;
password: () => Promise<AsyncIterator<String>>;
}
export interface UserNullablePromise
extends Promise<User | null>,
Fragmentable {
id: () => Promise<ID_Output>;
username: () => Promise<String>;
password: () => Promise<String>;
}
export interface UserConnection {
pageInfo: PageInfo;
edges: UserEdge[];
}
export interface UserConnectionPromise
extends Promise<UserConnection>,
Fragmentable {
pageInfo: <T = PageInfoPromise>() => T;
edges: <T = FragmentableArray<UserEdge>>() => T;
aggregate: <T = AggregateUserPromise>() => T;
}
export interface UserConnectionSubscription
extends Promise<AsyncIterator<UserConnection>>,
Fragmentable {
pageInfo: <T = PageInfoSubscription>() => T;
edges: <T = Promise<AsyncIterator<UserEdgeSubscription>>>() => T;
aggregate: <T = AggregateUserSubscription>() => T;
}
export interface PageInfo {
hasNextPage: Boolean;
hasPreviousPage: Boolean;
startCursor?: String;
endCursor?: String;
}
export interface PageInfoPromise extends Promise<PageInfo>, Fragmentable {
hasNextPage: () => Promise<Boolean>;
hasPreviousPage: () => Promise<Boolean>;
startCursor: () => Promise<String>;
endCursor: () => Promise<String>;
}
export interface PageInfoSubscription
extends Promise<AsyncIterator<PageInfo>>,
Fragmentable {
hasNextPage: () => Promise<AsyncIterator<Boolean>>;
hasPreviousPage: () => Promise<AsyncIterator<Boolean>>;
startCursor: () => Promise<AsyncIterator<String>>;
endCursor: () => Promise<AsyncIterator<String>>;
}
/*
The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
*/
export type String = string;
export type Long = string;
/*
The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
*/
export type ID_Input = string | number;
export type ID_Output = string;
/*
The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
*/
export type Int = number;
/*
The `Boolean` scalar type represents `true` or `false`.
*/
export type Boolean = boolean;
/**
* Model Metadata
*/
export const models: Model[] = [
{
name: "User",
embedded: false
}
];
/**
* Type Defs
*/
export const prisma: Prisma;

View File

@ -0,0 +1,17 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var prisma_lib_1 = require("prisma-client-lib");
var typeDefs = require("./prisma-schema").typeDefs;
var models = [
{
name: "User",
embedded: false
}
];
exports.Prisma = prisma_lib_1.makePrismaClientClass({
typeDefs,
models,
endpoint: `http://localhost:4466`
});
exports.prisma = new exports.Prisma();

View File

@ -0,0 +1,172 @@
module.exports = {
typeDefs: // Code generated by Prisma (prisma@1.34.10). DO NOT EDIT.
// Please don't change this file manually but run `prisma generate` to update it.
// For more information, please read the docs: https://www.prisma.io/docs/prisma-client/
/* GraphQL */ `type AggregateUser {
count: Int!
}
type BatchPayload {
count: Long!
}
scalar Long
type Mutation {
createUser(data: UserCreateInput!): User!
updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
updateManyUsers(data: UserUpdateManyMutationInput!, where: UserWhereInput): BatchPayload!
upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User!
deleteUser(where: UserWhereUniqueInput!): User
deleteManyUsers(where: UserWhereInput): BatchPayload!
}
enum MutationType {
CREATED
UPDATED
DELETED
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
user(where: UserWhereUniqueInput!): User
users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
node(id: ID!): Node
}
type Subscription {
user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
}
type User {
id: ID!
username: String!
password: String!
}
type UserConnection {
pageInfo: PageInfo!
edges: [UserEdge]!
aggregate: AggregateUser!
}
input UserCreateInput {
username: String!
password: String!
}
type UserEdge {
node: User!
cursor: String!
}
enum UserOrderByInput {
id_ASC
id_DESC
username_ASC
username_DESC
password_ASC
password_DESC
}
type UserPreviousValues {
id: ID!
username: String!
password: String!
}
type UserSubscriptionPayload {
mutation: MutationType!
node: User
updatedFields: [String!]
previousValues: UserPreviousValues
}
input UserSubscriptionWhereInput {
mutation_in: [MutationType!]
updatedFields_contains: String
updatedFields_contains_every: [String!]
updatedFields_contains_some: [String!]
node: UserWhereInput
AND: [UserSubscriptionWhereInput!]
OR: [UserSubscriptionWhereInput!]
NOT: [UserSubscriptionWhereInput!]
}
input UserUpdateInput {
username: String
password: String
}
input UserUpdateManyMutationInput {
username: String
password: String
}
input UserWhereInput {
id: ID
id_not: ID
id_in: [ID!]
id_not_in: [ID!]
id_lt: ID
id_lte: ID
id_gt: ID
id_gte: ID
id_contains: ID
id_not_contains: ID
id_starts_with: ID
id_not_starts_with: ID
id_ends_with: ID
id_not_ends_with: ID
username: String
username_not: String
username_in: [String!]
username_not_in: [String!]
username_lt: String
username_lte: String
username_gt: String
username_gte: String
username_contains: String
username_not_contains: String
username_starts_with: String
username_not_starts_with: String
username_ends_with: String
username_not_ends_with: String
password: String
password_not: String
password_in: [String!]
password_not_in: [String!]
password_lt: String
password_lte: String
password_gt: String
password_gte: String
password_contains: String
password_not_contains: String
password_starts_with: String
password_not_starts_with: String
password_ends_with: String
password_not_ends_with: String
AND: [UserWhereInput!]
OR: [UserWhereInput!]
NOT: [UserWhereInput!]
}
input UserWhereUniqueInput {
id: ID
username: String
}
`
}

15
src/index.js Normal file
View File

@ -0,0 +1,15 @@
require("dotenv").config();
const { ApolloServer } = require('apollo-server');
const modules = require("./modules/module-loader");
const {schema, context} = modules;
const server = new ApolloServer({
schema,
context
});
server
.listen({
port: 8383
})
.then(info => console.log(`Server started on http://localhost:${info.port}`));

View File

@ -0,0 +1,109 @@
const { GraphQLModule } = require("@graphql-modules/core");
const gql = require("graphql-tag");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { prisma } = require('../../generated/prisma-client/index');
const getUser = token => {
try {
if(token) {
return jwt.verify(token, process.env.JWT_SECRET);
}
return null;
} catch(err) {
return null;
}
}
module.exports = new GraphQLModule({
name: "AUTHENTICATION_MODULE",
typeDefs: gql`
type User {
id: ID!
username: String!
}
type Query {
currentUser: User!
}
type Mutation {
register(username: String!, password: String!): User!
login(username: String!, password: String!): LoginResponse!
}
type LoginResponse {
token: String
user: User
}
`,
resolvers: {
Query: {
currentUser: (parent, args, {user, prisma}) => {
// Check the authentication
if(!user) {
throw new Error("Not authenticated");
}
return prisma.user({id: user.id});
}
},
Mutation: {
register: async(parent, {username, password}, ctx, info) => {
if(process.env.ENABLE_REGISTRATION === "true") {
const hashedPassword = await bcrypt.hash(password, 10);
const user = await ctx.prisma.createUser({
username,
password: hashedPassword
});
return user;
}
throw new Error("Registration disabled");
},
login: async(parent, {username, password}, ctx, info) => {
console.log(ctx);
const user = await ctx.prisma.user({username});
if(!user) {
throw new Error("Invalid login");
}
const passwordMatch = await bcrypt.compare(password, user.password);
if(!passwordMatch) {
throw new Error("Invalid login");
}
const token = jwt.sign (
{
id: user.id,
username: user.email
},
process.env.JWT_SECRET,
{
expiresIn: "30d", // So that the token expires in 30 days
}
)
return {
token,
user
}
}
}
},
context: ({ req }) => {
const tokenWithBearer = req.headers.authorization || '';
const token = tokenWithBearer.split(' ')[1];
const user = getUser(token);
return {
user,
prisma
}
}
})

View File

@ -0,0 +1,78 @@
const { GraphQLModule } = require("@graphql-modules/core");
const fs = require("fs");
const jwt = require("jsonwebtoken");
const prisma = require("../generated/prisma-client/index");
const MODULES_DIRECTORY = "./src/modules/"; // Could be an env variable
const getUser = token => {
try {
if(token) {
return jwt.verify(token, process.env.JWT_SECRET);
}
return null;
} catch(err) {
return null;
}
}
module.exports = new GraphQLModule({
imports: load_modules()
});
function add_authorization_context(new_module) {
new_module._options.context = ({req}) => {
const tokenWithBearer = req.headers.authorization || '';
const token = tokenWithBearer.split(' ')[1];
const user = getUser(token);
return {
user,
prisma
}
};
return new_module;
}
function is_module_enabled(name) {
return process.env[name.toUpperCase()] && process.env[name.toUpperCase()] === "true";
}
function load_modules() {
let modules = [];
let files_list = fs.readdirSync(MODULES_DIRECTORY, {withFileTypes: true});
for(file of files_list) {
if(file.isDirectory() && is_module_enabled(file.name)) {
let new_module = require("./" + file.name);
if(new_module._options.context === undefined) {
new_module = add_authorization_context(new_module);
}
modules.push(new_module);
}
}
let installed_modules = process.env.INSTALLED_MODULES.split(',');
for(module_name of installed_modules) {
if(is_module_enabled(module_name)) {
let new_module = require(module_name.toLowerCase());
if(new_module._options.context === undefined) {
new_module = add_authorization_context(new_module);
}
modules.push(new_module);
}
}
console.log("Loading " + modules.length + " modules");
for(module of modules) {
console.log("\t - " + module.name);
}
return modules;
}