Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add notifications when there is an error in the service #264

Merged
merged 8 commits into from
Aug 3, 2024
48 changes: 45 additions & 3 deletions backend/src/auth/jwt.guard.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { ExecutionContext, Injectable } from "@nestjs/common";
import {
ExecutionContext,
ForbiddenException,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { AuthGuard } from "@nestjs/passport";
import { IS_PUBLIC_PATH } from "src/utils/decorators/auth.decorator";
import * as jwt from "jsonwebtoken";
import { ConfigService } from "@nestjs/config";

@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {
constructor(private reflector: Reflector) {
constructor(
private reflector: Reflector,
private configService: ConfigService
) {
super();
}

Expand All @@ -17,6 +27,38 @@ export class JwtAuthGuard extends AuthGuard("jwt") {
if (isPublic) {
return true;
}
return super.canActivate(context);

try {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(" ")[1];
if (!token) {
throw new UnauthorizedException("Unauthorized", {
cause: new Error(),
description: "Token not found",
});
}

const secretKey = this.configService.get<string>("JWT_AUTH_SECRET");
const decoded = jwt.verify(token, secretKey);
request.user = decoded;
return super.canActivate(context);
wet6123 marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
if (e.name === "TokenExpiredError") {
throw new UnauthorizedException("Unauthorized", {
cause: new Error(),
description: "Token has expired.",
});
} else if (e.name === "JsonWebTokenError") {
throw new UnauthorizedException("Unauthorized", {
cause: new Error(),
description: "Invalid token",
});
} else {
throw new ForbiddenException("Forbidden", {
cause: new Error(),
description: "Access denied",
});
}
}
}
}
10 changes: 8 additions & 2 deletions backend/src/documents/documents.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export class DocumentsService {
throw new Error();
}
} catch (e) {
throw new UnauthorizedException("Invalid sharing token");
throw new UnauthorizedException("Unauthorized", {
cause: new Error(),
description: "Invalid sharing token",
});
}

let document: Document;
Expand All @@ -43,7 +46,10 @@ export class DocumentsService {
},
});
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description: "Document not found",
});
}

return {
Expand Down
20 changes: 16 additions & 4 deletions backend/src/files/files.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,17 @@ export class FilesService {
},
});
} catch (e) {
throw new UnauthorizedException();
throw new UnauthorizedException("Unauthorized", {
cause: new Error(),
description: "Client unauthorized.",
});
}

if (contentLength > 10_000_000) {
throw new UnprocessableEntityException();
throw new UnprocessableEntityException("Unprocessable Entity", {
cause: new Error(),
description: "Content length too long.",
});
}

const fileKey = `${workspace.slug}-${generateRandomKey()}.${contentType.split("/")[1]}`;
Expand All @@ -75,7 +81,10 @@ export class FilesService {
});
return getSignedUrl(this.s3Client, command, { expiresIn: 3600 });
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description: "File not found.",
});
}
}

Expand All @@ -92,7 +101,10 @@ export class FilesService {
case "pdf":
return this.exportToPdf(content, fileName);
default:
throw new BadRequestException("Invalid export type");
throw new BadRequestException("Bad request", {
cause: new Error(),
description: "Invalid export type",
});
}
}

Expand Down
6 changes: 5 additions & 1 deletion backend/src/intelligence/intelligence.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export class IntelligenceService {
};
const selectedPrompt = promptTemplates[feature];

if (!selectedPrompt) throw new NotFoundException();
if (!selectedPrompt)
throw new NotFoundException("Not found", {
cause: new Error(),
description: "Feature not found",
});

return selectedPrompt;
}
Expand Down
5 changes: 4 additions & 1 deletion backend/src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export class UsersService {
const { conflict } = await this.checkService.checkNameConflict(nickname);

if (conflict) {
throw new ConflictException();
throw new ConflictException("Conflict", {
cause: new Error(),
description: "The nickname conflicts.",
});
}

await this.prismaService.user.update({
Expand Down
28 changes: 22 additions & 6 deletions backend/src/workspace-documents/workspace-documents.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export class WorkspaceDocumentsService {
},
});
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description:
"The workspace does not exist, or the user lacks the appropriate permissions.",
});
}

return this.prismaService.document.create({
Expand All @@ -52,7 +56,11 @@ export class WorkspaceDocumentsService {
},
});
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description:
"The workspace does not exist, or the user lacks the appropriate permissions.",
});
}

const additionalOptions: Prisma.DocumentFindManyArgs = {};
Expand Down Expand Up @@ -111,14 +119,18 @@ export class WorkspaceDocumentsService {
workspaceId,
},
});

return this.prismaService.document.findUniqueOrThrow({
const document = await this.prismaService.document.findUniqueOrThrow({
where: {
id: documentId,
},
});
return document;
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description:
"The workspace or document does not exist, or the user lacks the appropriate permissions.",
});
}
}

Expand Down Expand Up @@ -146,7 +158,11 @@ export class WorkspaceDocumentsService {
},
});
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description:
"The workspace or document does not exist, or the user lacks the appropriate permissions.",
});
}

const token = generateRandomKey();
Expand Down
6 changes: 5 additions & 1 deletion backend/src/workspace-users/workspace-users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ export class WorkspaceUsersService {
},
});
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description:
"The workspace does not exist, or the user lacks the appropriate permissions.",
});
}

const additionalOptions: Prisma.UserFindManyArgs = {};
Expand Down
26 changes: 21 additions & 5 deletions backend/src/workspaces/workspaces.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export class WorkspacesService {
const { conflict } = await this.checkService.checkNameConflict(title);

if (conflict) {
throw new ConflictException();
throw new ConflictException("Conflict", {
cause: new Error(),
description: "Workspace title is already in use.",
});
}

const workspace = await this.prismaService.workspace.create({
Expand Down Expand Up @@ -63,7 +66,10 @@ export class WorkspacesService {

return foundWorkspace;
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description: "Workspace not found, or the user lacks the appropriate permissions.",
});
}
}

Expand Down Expand Up @@ -114,7 +120,11 @@ export class WorkspacesService {
},
});
} catch (e) {
throw new NotFoundException();
throw new NotFoundException("Not found", {
cause: new Error(),
description:
"Worksapce does not exist, or the user lacks the appropriate permissions.",
});
}

const token = generateRandomKey();
Expand Down Expand Up @@ -152,7 +162,10 @@ export class WorkspacesService {
throw new Error();
}
} catch (err) {
throw new UnauthorizedException("Invitation token is invalid or expired.");
throw new UnauthorizedException("Unauthorized", {
cause: new Error(),
description: "Invitation token is invalid or expired.",
});
}

try {
Expand All @@ -162,7 +175,10 @@ export class WorkspacesService {
},
});
} catch (e) {
throw new NotFoundException("The workspace is deleted.");
throw new NotFoundException("Not found", {
cause: new Error(),
description: "The workspace is deleted.",
});
}

const userWorkspace = await this.prismaService.userWorkspace.findFirst({
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/hooks/useErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { useSnackbar } from "notistack";
import { useCallback } from "react";

interface CustomError extends Error {
response?: {
data: {
error?: string;
message: string;
statusCode: number;
};
};
}

export function useErrorHandler() {
const { enqueueSnackbar } = useSnackbar();
const handleError = useCallback(
(error: Error) => {
enqueueSnackbar(error.message || "Something went wrong...", { variant: "error" });
(error: CustomError) => {
enqueueSnackbar(
error.response?.data.error || error.message || "Something went wrong...",
{ variant: "error" }
);
},
[enqueueSnackbar]
);
Expand Down