Skip to content

Commit

Permalink
Add hasura integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
Jaikant committed Dec 2, 2022
1 parent dc36eb8 commit 9bc3138
Show file tree
Hide file tree
Showing 18 changed files with 346 additions and 165 deletions.
28 changes: 28 additions & 0 deletions src/admin-user/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
Controller,
Post,
Body,
Req,
} from '@nestjs/common';
import { Request } from 'express';
import { AdminService } from './admin.service';
import { SignUpDto, LoginDto } from './admin.dto';
import { API_VERSION } from '../version';

@Controller(`${API_VERSION}`)
export class AdminController {
constructor(private readonly adminService: AdminService) {}

@Post("signUp")
signUp(@Body() signUpData: SignUpDto) {
return this.adminService.signUp(signUpData);
}

@Post("login")
login(
@Req() req,
@Body() loginData: LoginDto
) {
return this.adminService.login(loginData);
}
}
23 changes: 23 additions & 0 deletions src/admin-user/admin.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { PartialType } from '@nestjs/mapped-types';

export class LoginDto {
@ApiProperty()
@IsString()
email: string;

@ApiProperty()
@IsString()
password: string;
}

export class SignUpDto extends PartialType(LoginDto) {
@ApiProperty()
@IsString()
email: string;

@ApiProperty()
@IsString()
password: string;
}
11 changes: 11 additions & 0 deletions src/admin-user/admin.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { AdminService } from './admin.service';
import { AdminController } from './admin.controller';
import { AuthModule } from 'src/auth/auth.module';

@Module({
imports: [AuthModule],
controllers: [AdminController],
providers: [AdminService],
})
export class AdminModule {}
110 changes: 110 additions & 0 deletions src/admin-user/admin.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
BadRequestException,
HttpException,
HttpStatus,
Injectable,
Logger,
} from '@nestjs/common';
import { SignUpDto, LoginDto } from './admin.dto';
import { GraphQLClient } from "graphql-request";
import * as bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import { JWTService } from 'src/auth/jwt.service';
import { GRAPHQL_URL, HASURA_ADMIN_SECRET } from 'src/config.constants';
import { registerUser, getUserByEmail } from 'src/graphql-api';

@Injectable()
export class AdminService {

adminClient;
// Initialize logger
//TODO: Move this to module level
logger: Logger = new Logger('UserLogger');


constructor(
private jwtService: JWTService
) {
this.adminClient = new GraphQLClient(GRAPHQL_URL, {
headers: { "x-hasura-admin-secret": HASURA_ADMIN_SECRET},
})
}

async signUp(signUpDto: SignUpDto) {
// We need to check if user is already registered
// We skip that for the sake of time

// We insert the user using a mutation
// Note that we salt and hash the password using bcrypt
const { email, password } = signUpDto;
const id = uuidv4();
try {
const { insert_User_one } = await this.adminClient.request(
registerUser,
{
user: {
id,
email,
password: await bcrypt.hash(password, 10),
},
},
);

const { id: userId } = insert_User_one;

return this.jwtService.generateHasuraJWT({
id: userId,
defaultRole: 'user',
allowedRoles: ['user'],
otherClaims: {
'X-Hasura-User-Id': userId,
},
});
} catch (e) {
this.logger.error(e);
throw new BadRequestException('Cannot create user!');
}
}

async login(loginDto: LoginDto) {
const { email, password } = loginDto;
let foundUser = null;

try {
foundUser = await this.adminClient.request(
getUserByEmail,
{
email,
},
);
} catch (e) {
this.logger.error(e);
throw new BadRequestException('Cannot create user!');
}

let { User } = foundUser;
// Since we filtered on a non-primary key we got an array back
User = User[0];

if (!User) {
return 401;
}

// Check if password matches the hashed version
const passwordMatch = await bcrypt.compare(password, User.password);

if (passwordMatch) {
return this.jwtService.generateHasuraJWT({
id: User.id,
defaultRole: 'user',
allowedRoles: ['user'],
otherClaims: {
'X-Hasura-User-Id': User.id,
},
});
} else {
throw new HttpException( 'Not allowed or no user found',
HttpStatus.FORBIDDEN);
}
}
}
6 changes: 5 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UserModule } from './user/user.module';
import { AuthModule } from './auth/auth.module';
import { AdminModule } from './admin-user/admin.module';

@Module({
imports: [
UserModule
AdminModule,
UserModule,
AuthModule
],
controllers: [AppController],
providers: [AppService],
Expand Down
19 changes: 19 additions & 0 deletions src/auth/auth.functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Response, NextFunction } from 'express';
import * as jwt from "jsonwebtoken"
import { GRAPHQL_JWT_SECRET } from './jwt.service'

export function verifyJWT(token: string) {
//https://stackoverflow.com/questions/43915379/i-need-to-replace-bearer-from-the-header-to-verify-the-token
var parts = token.split(' ');
if (parts.length === 2) {
var scheme = parts[0];
var credentials = parts[1];

if (/^Bearer$/i.test(scheme)) {
//verify token
let jwtPayload = jwt.verify(credentials, GRAPHQL_JWT_SECRET.key);
return jwtPayload;
}
}
return null;
}
29 changes: 29 additions & 0 deletions src/auth/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { verifyJWT } from './auth.functions';

@Injectable()
export class AuthGuard implements CanActivate {

canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return this.validateRequest(request);
}

validateRequest(request): boolean {
if (!request.headers.authorization) {
// return res.status(403).json({ error: 'No credentials sent!' });
//TODO: How to send proper error code?
return false;
}
let jwtPayload = verifyJWT(request.headers.authorization);
if (!jwtPayload) {
// return res.status(403).json({ error: 'No payload in jwt!' });
//TODO: How to send proper error code?
return false;
}
return true;
}
}
8 changes: 8 additions & 0 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { JWTService } from './jwt.service';

@Module({
providers: [ JWTService ],
exports: [ JWTService ]
})
export class AuthModule {}
37 changes: 37 additions & 0 deletions src/auth/jwt.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import * as jwt from "jsonwebtoken"
import { JWT_SECRET_TYPE, JWT_SECRET_KEY } from "../config.constants"

//TODO Move these to environment variables.
export const GRAPHQL_JWT_SECRET = {
type: JWT_SECRET_TYPE,
key: JWT_SECRET_KEY,
};

interface GenerateJWTParams {
id: string;
defaultRole: string;
allowedRoles: string[];
otherClaims?: Record<string, string | string[]>;
}

@Injectable()
export class JWTService {

JWT_CONFIG: jwt.SignOptions = {
algorithm: JWT_SECRET_TYPE,
expiresIn: "10h",
}

generateHasuraJWT(params: GenerateJWTParams): string {
const payload = {
sub: params.id,
"https://hasura.io/jwt/claims": {
"x-hasura-allowed-roles": params.allowedRoles,
"x-hasura-default-role": params.defaultRole,
...params.otherClaims,
}
}
return jwt.sign(payload, GRAPHQL_JWT_SECRET.key, this.JWT_CONFIG);
}
}
5 changes: 5 additions & 0 deletions src/config.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// These should go into the environment variables and then get imported using the nestjs config
export const GRAPHQL_URL = 'http://localhost:8080/v1/graphql';
export const JWT_SECRET_TYPE = "HS256";
export const JWT_SECRET_KEY = "This-is-a-generic-hs256-secret-key-and-it-should-be-changed";
export const HASURA_ADMIN_SECRET = "myadminsecretkey";
28 changes: 28 additions & 0 deletions src/graphql-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { gql } from 'graphql-request';

export const registerUser = gql`
mutation registerUser($user: User_insert_input!) {
insert_User_one(object: $user) {
id
}
}
`

export const getUserByEmail = gql`
query getUserByEmail($email: String!) {
User(where: { email: { _eq: $email } }) {
id
password
}
}
`

export const getUserName = gql`
query getUserName($id: String!) {
User(where: { id: { _eq: $id } }) {
id
name
email
}
}
`
8 changes: 0 additions & 8 deletions src/graphql-client/client.module.ts

This file was deleted.

14 changes: 0 additions & 14 deletions src/graphql-client/client.service.ts

This file was deleted.

Loading

0 comments on commit 9bc3138

Please sign in to comment.