Skip to content
Permalink
Browse files

Merge pull request #4 from miii/feat/3/typegraphql

Implement TypeGraphQL, closes #3
  • Loading branch information...
miii committed May 6, 2019
2 parents 831be7c + dc9b619 commit f027f292319d602dec8cf3ade9f4d755e29d3b78
@@ -10,7 +10,6 @@
"lint": "eslint src/**/*"
},
"dependencies": {
"@graphql-modules/core": "^0.7.0",
"apollo-server-express": "^2.4.8",
"bcrypt": "^3.0.5",
"class-validator": "^0.9.1",
@@ -22,6 +21,7 @@
"mysql": "^2.16.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^2.6.3",
"type-graphql": "^0.17.3",
"typeorm": "^0.2.16"
},
"devDependencies": {
@@ -1,16 +1,22 @@
import { BaseEntity, Column, Entity, JoinTable, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';

import { AccessPermission } from './AccessPermission';
import { User } from './User';

@Entity()
@ObjectType()
export class AccessGroup extends BaseEntity {
@PrimaryGeneratedColumn()
@Field(type => ID)
public id!: number;

@Column()
@Field()
public name!: string;

@Column()
@Field()
public description!: string;

@ManyToMany(type => User, user => user.accessGroups)
@@ -1,4 +1,5 @@
import { BaseEntity, Column, Entity, ManyToMany, PrimaryGeneratedColumn } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
import { AccessGroup } from './AccessGroup';

@Entity()
@@ -1,19 +1,23 @@
import { Entity, BaseEntity, PrimaryGeneratedColumn, Column } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
import { Matches, MinLength, Validate } from 'class-validator';
import { encryptString, decryptString } from '@/tools/encryption';
import { ValueMustNotExist } from '../validators';
import { ValueMustNotExist, IsValidSSN } from '../validators';

@Entity()
@ObjectType()
export class Profile extends BaseEntity {
@PrimaryGeneratedColumn()
public id!: number;

@Column()
@MinLength(2)
@Field()
public firstname!: string;

@Column()
@MinLength(2)
@Field()
public lastname!: string;

@Column({
@@ -22,7 +26,7 @@ export class Profile extends BaseEntity {
from: (value): string => decryptString(value),
},
})
@Matches(/^[0-9]{12}$/, { message: 'Social security number must be in the format YYYYMMDDXXXX' })
@Validate(IsValidSSN, { message: 'Social security number must be in the format YYYYMMDDXXXX' })
@Validate(ValueMustNotExist, [() => Profile], {
message: 'Social security number is already registered on another user',
})
@@ -1,15 +1,18 @@
import bcrypt from 'bcrypt';
import { BaseEntity, Column, Entity, Index, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn, OneToOne, JoinColumn } from 'typeorm';
import { IsEmail, ValidateNested, Validate } from 'class-validator';
import { BaseEntity, Column, Entity, Index, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn, OneToOne, JoinColumn, ManyToOne } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
import { IsEmail, MinLength, ValidateNested, Validate } from 'class-validator';

import { AccessGroup } from './AccessGroup';
import { RefreshToken } from './RefreshToken';
import { Profile } from './Profile';
import { ValueMustNotExist } from '../validators';

@Entity()
@ObjectType()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
@Field(type => ID)
public id!: number;

@Column()
@@ -18,6 +21,7 @@ export class User extends BaseEntity {
@Validate(ValueMustNotExist, [() => User], {
message: 'Email is already registered on another user',
})
@Field()
public username!: string;

@Column({
@@ -27,15 +31,18 @@ export class User extends BaseEntity {
from: (value: string) => value,
},
})
@MinLength(4)
public password!: string;

@OneToOne(type => Profile)
@JoinColumn()
@ValidateNested()
@Field(type => Profile)
public profile!: Profile;

@ManyToMany(type => AccessGroup, accessGroup => accessGroup.users, { eager: true })
@JoinTable()
@Field(type => [AccessGroup])
public accessGroups!: AccessGroup[];

@OneToMany(type => RefreshToken, refreshToken => refreshToken.userId)
@@ -48,4 +55,26 @@ export class User extends BaseEntity {
public validatePassword(password: string) {
return bcrypt.compareSync(password, this.password);
}

/**
* Verify user permissions
* @param {string | string[]} permission Single permission or array of permissions
*/
public hasPermissions(permission: string[] | string) {
if (permission.length === 0)
return true;

// Force array conversion
const permissions = [...permission];

// Check if any permission is missing from the user's access groups
return !permissions.some(rp => (
// Permission only need to exist in one group
// Will return true if no group includes the permission
!this.accessGroups.some(g => (
// Find permission in access group
g.permissions.find(p => p.name === rp) !== undefined
))
));
}
}
@@ -1,5 +1,8 @@
import { ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';

/**
* Check if value already exist in database
*/
@ValidatorConstraint()
export class ValueMustNotExist implements ValidatorConstraintInterface {
public async validate(value: string, validationArguments: ValidationArguments) {
@@ -15,4 +18,33 @@ export class ValueMustNotExist implements ValidatorConstraintInterface {
return true;
});
}
}

/**
* Check if value is a valid social security number
*/
@ValidatorConstraint()
export class IsValidSSN implements ValidatorConstraintInterface {
public validate(value: string, validationArguments: ValidationArguments) {
// Verify YYYYMMDDXXXX format
if (!/^[0-9]{12}$/.test(value))
return false;

const rest = value
// Check YYMMDDXXX
.substring(2, value.length - 1)
// Split all characters
.split('')
// Apply the luhn algorithm
.reduce((tot, n, i) => {
const sum = (2 - (i % 2)) * parseInt(n, 10);
const add = String(sum).split('').reduce((t, s) => t + parseInt(s, 10), 0);
return tot + add;
}, 0);

// Calculate checksum
const checksum = 10 - (rest % 10);
// Validate checksum
return String(checksum) === value.substring(value.length - 1);
}
}
@@ -1,35 +1,50 @@
import express from 'express';
import path from 'path';
import dotenv from 'dotenv';
import { ApolloServer } from 'apollo-server-express';
import { ApolloServer, UserInputError, ApolloError } from 'apollo-server-express';

import db from './db';
import { AppModule } from '@/modules/app.module';
import { buildSchema, context } from './modules/modules';

// Load environment variables
dotenv.config({ path: path.resolve(__dirname, '../.env') });

// Create Apollo server
const server = new ApolloServer({
schema: AppModule.schema,
context: AppModule.context,
playground: true,
debug: false,
});
async function bootstrap() {
// Get GQL schema
const schema = await buildSchema();

// Create express server and connect apollo
const app = express();
server.applyMiddleware({ app });
// Format Apollo errors
const formatError = (error: any) => {
if (error.message === 'Argument Validation Error')
// eslint-disable-next-line no-param-reassign
error.extensions.code = 'ARGUMENT_VALIDATION_ERROR';

return error;
};

// Create Apollo server
const server = new ApolloServer({
schema,
context,
playground: true,
debug: false,
formatError,
});

// Create express server and connect apollo
const app = express();
server.applyMiddleware({ app });

// Start express server
await app.listen({ port: 4000 }, () => {
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
});
}

// Connect to database
db.connect()
.then(() => {
// Start express server
app.listen({ port: 4000 }, () => {
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`);
});
})
.catch(() => {
console.error('Failed to connect to database');
.then(() => bootstrap())
.catch((e) => {
console.error(e);
process.exit(1);
});

This file was deleted.

@@ -0,0 +1,17 @@
import { AuthChecker } from 'type-graphql';
import { ForbiddenError, AuthenticationError } from 'apollo-server-express';
import { User } from '@/db/entities/User';

export const authChecker: AuthChecker<any> = ({ root, args, context }, permissions) => {
const { user }: { user: User } = context;

// Verify logged in user
if (!user)
throw new AuthenticationError('Authentication required');

// Verify user permissions
if (!user.hasPermissions(permissions))
throw new ForbiddenError('You need further permissions to perform this action');

return true;
};
@@ -1,7 +1,7 @@
import { AuthenticationError } from 'apollo-server-express';

import { User } from '@entity/User';
import { AuthTokens } from './classes/AuthTokens';
import { AuthTokens } from './types/AuthTokens';

/**
* Module context

This file was deleted.

@@ -1,23 +1,17 @@
import { GraphQLModule } from '@graphql-modules/core';
import { IResolvers } from 'graphql-tools';
import { Resolver, Mutation, Arg } from 'type-graphql';
import { AuthenticationError } from 'apollo-server-express';

import { context } from './auth.context';
import typeDefs from './auth.gql';
import { userCredentials } from './mutations/userCredentials';
import { User } from '@/db/entities/User';
import { AuthTokens } from './types/AuthTokens';

/**
* Module resolvers
*/
const resolvers: IResolvers = {
Mutation: {
Authentication: () => ({
userCredentials,
}),
},
};
@Resolver()
export class AuthResolver {
@Mutation(returns => AuthTokens)
public async authWithCredentials(@Arg('username') username: string, @Arg('password') password: string) {
const user = await User.findOne({ username });
if (!user || !user.validatePassword(password))
throw new AuthenticationError('Wrong credentials');

export default new GraphQLModule({
typeDefs,
resolvers,
context,
});
return AuthTokens.create(user.id);
}
}

This file was deleted.

0 comments on commit f027f29

Please sign in to comment.
You can’t perform that action at this time.