Scope server with workspace (#157)
* Rename User to AuthUser to avoid naming conflict with user business entity * Prevent query by workspace in graphql * Make full user and workspace object available in graphql resolvers * Add Seed to create companies and people accross two workspace * Check workspace on all entities findMany, find, create, update)
This commit is contained in:
@ -8,8 +8,9 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Request, Response } from 'express';
|
||||
import { Response } from 'express';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { GoogleRequest } from './strategies/google.auth.strategy';
|
||||
|
||||
@Controller('auth/google')
|
||||
export class GoogleAuthController {
|
||||
@ -24,10 +25,8 @@ export class GoogleAuthController {
|
||||
|
||||
@Get('redirect')
|
||||
@UseGuards(AuthGuard('google'))
|
||||
async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {
|
||||
const user = await this.authService.upsertUser(
|
||||
req.user as { firstName: string; lastName: string; email: string },
|
||||
);
|
||||
async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) {
|
||||
const user = await this.authService.upsertUser(req.user);
|
||||
|
||||
if (!user) {
|
||||
throw new HttpException(
|
||||
|
||||
85
server/src/auth/guards/check-workspace-ownership.guard.ts
Normal file
85
server/src/auth/guards/check-workspace-ownership.guard.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { Request } from 'express';
|
||||
import { PrismaService } from 'src/database/prisma.service';
|
||||
|
||||
type OperationEntity = {
|
||||
operation?: string;
|
||||
entity?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CheckWorkspaceOwnership implements CanActivate {
|
||||
constructor(private prismaService: PrismaService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const gqlContext = GqlExecutionContext.create(context);
|
||||
const request = gqlContext.getContext().req;
|
||||
|
||||
const { operation, entity } = this.fetchOperationAndEntity(request);
|
||||
const variables = request.body.variables;
|
||||
const workspace = await request.workspace;
|
||||
|
||||
if (!entity || !operation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (operation === 'updateOne') {
|
||||
const object = await this.prismaService[entity].findUniqueOrThrow({
|
||||
where: { id: variables.id },
|
||||
});
|
||||
|
||||
if (!object) {
|
||||
throw new HttpException(
|
||||
{ reason: 'Record not found' },
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
if (object.workspaceId !== workspace.id) {
|
||||
throw new HttpException(
|
||||
{ reason: 'Record not found' },
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation === 'deleteMany') {
|
||||
// TODO: write this logic
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation === 'findMany') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operation === 'createOne') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private fetchOperationAndEntity(request: Request): OperationEntity {
|
||||
if (!request.body.operationName) {
|
||||
return { operation: undefined, entity: undefined };
|
||||
}
|
||||
|
||||
const regex =
|
||||
/(updateOne|deleteMany|createOne|findMany)(Person|Company|User)/i;
|
||||
const match = request.body.query.match(regex);
|
||||
if (match) {
|
||||
return {
|
||||
operation: match[1],
|
||||
entity: match[2].toLowerCase(),
|
||||
};
|
||||
}
|
||||
return { operation: undefined, entity: undefined };
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
@ -8,12 +10,15 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { Request } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from 'src/database/prisma.service';
|
||||
import { JwtPayload } from '../strategies/jwt.auth.strategy';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private configService: ConfigService,
|
||||
private prismaService: PrismaService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
@ -24,10 +29,34 @@ export class JwtAuthGuard implements CanActivate {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token, {
|
||||
const payload: JwtPayload = await this.jwtService.verifyAsync(token, {
|
||||
secret: this.configService.get('JWT_SECRET'),
|
||||
});
|
||||
request['user'] = payload;
|
||||
|
||||
const user = this.prismaService.user.findUniqueOrThrow({
|
||||
where: { id: payload.userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new HttpException(
|
||||
{ reason: 'User does not exist' },
|
||||
HttpStatus.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = this.prismaService.workspace.findUniqueOrThrow({
|
||||
where: { id: payload.workspaceId },
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new HttpException(
|
||||
{ reason: 'Workspace does not exist' },
|
||||
HttpStatus.FORBIDDEN,
|
||||
);
|
||||
}
|
||||
|
||||
request.user = user;
|
||||
request.workspace = workspace;
|
||||
} catch (exception) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
@ -8,6 +8,12 @@ import { RefreshTokenRepository } from 'src/entities/refresh-token/refresh-token
|
||||
import { v4 } from 'uuid';
|
||||
import { RefreshToken, User } from '@prisma/client';
|
||||
|
||||
export type UserPayload = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@ -18,11 +24,7 @@ export class AuthService {
|
||||
private refreshTokenRepository: RefreshTokenRepository,
|
||||
) {}
|
||||
|
||||
async upsertUser(rawUser: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
}) {
|
||||
async upsertUser(rawUser: UserPayload) {
|
||||
if (!rawUser.email) {
|
||||
throw new HttpException(
|
||||
{ reason: 'Email is missing' },
|
||||
|
||||
@ -3,6 +3,11 @@ import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Request } from 'express';
|
||||
|
||||
export type GoogleRequest = Request & {
|
||||
user: { firstName: string; lastName: string; email: string };
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
|
||||
|
||||
@ -24,7 +24,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload) {
|
||||
async validate(payload: JwtPayload): Promise<JwtPayload> {
|
||||
return { userId: payload.userId, workspaceId: payload.workspaceId };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user