Skip to content

Commit

Permalink
feat(server): introduce user friendly server errors (#7111)
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo authored Jun 17, 2024
1 parent 5307a55 commit 54fc119
Show file tree
Hide file tree
Showing 65 changed files with 3,169 additions and 923 deletions.
8 changes: 7 additions & 1 deletion packages/backend/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@
"ts-node/esm/transpile-only.mjs",
"--es-module-specifier-resolution=node"
],
"watchMode": {
"ignoreChanges": [
"**/*.gen.*"
]
},
"files": [
"tests/**/*.spec.ts",
"tests/**/*.e2e.ts"
Expand Down Expand Up @@ -160,7 +165,8 @@
],
"ignore": [
"**/__tests__/**",
"**/dist/**"
"**/dist/**",
"*.gen.*"
],
"env": {
"TS_NODE_TRANSPILE_ONLY": true,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ConfigModule,
mergeConfigOverride,
} from './fundamentals/config';
import { ErrorModule } from './fundamentals/error';
import { EventModule } from './fundamentals/event';
import { GqlModule } from './fundamentals/graphql';
import { HelpersModule } from './fundamentals/helpers';
Expand All @@ -52,6 +53,7 @@ export const FunctionalityModules = [
MailModule,
StorageProviderModule,
HelpersModule,
ErrorModule,
];

function filterOptionalModule(
Expand Down
24 changes: 15 additions & 9 deletions packages/backend/server/src/core/auth/controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { randomUUID } from 'node:crypto';

import {
BadRequestException,
Body,
Controller,
Get,
Expand All @@ -14,7 +13,16 @@ import {
} from '@nestjs/common';
import type { Request, Response } from 'express';

import { Config, Throttle, URLHelper } from '../../fundamentals';
import {
Config,
EarlyAccessRequired,
EmailTokenNotFound,
InternalServerError,
InvalidEmailToken,
SignUpForbidden,
Throttle,
URLHelper,
} from '../../fundamentals';
import { UserService } from '../user';
import { validators } from '../utils/validators';
import { CurrentUser } from './current-user';
Expand Down Expand Up @@ -55,9 +63,7 @@ export class AuthController {
validators.assertValidEmail(credential.email);
const canSignIn = await this.auth.canSignIn(credential.email);
if (!canSignIn) {
throw new BadRequestException(
`You don't have early access permission\nVisit https://community.affine.pro/c/insider-general/ for more information`
);
throw new EarlyAccessRequired();
}

if (credential.password) {
Expand All @@ -74,7 +80,7 @@ export class AuthController {
if (!user) {
const allowSignup = await this.config.runtime.fetch('auth/allowSignup');
if (!allowSignup) {
throw new BadRequestException('You are not allows to sign up.');
throw new SignUpForbidden();
}
}

Expand All @@ -84,7 +90,7 @@ export class AuthController {
);

if (result.rejected.length) {
throw new Error('Failed to send sign-in email.');
throw new InternalServerError('Failed to send sign-in email.');
}

res.status(HttpStatus.OK).send({
Expand Down Expand Up @@ -145,7 +151,7 @@ export class AuthController {
@Body() { email, token }: MagicLinkCredential
) {
if (!token || !email) {
throw new BadRequestException('Missing sign-in mail token');
throw new EmailTokenNotFound();
}

validators.assertValidEmail(email);
Expand All @@ -155,7 +161,7 @@ export class AuthController {
});

if (!valid) {
throw new BadRequestException('Invalid sign-in mail token');
throw new InvalidEmailToken();
}

const user = await this.user.fulfillUser(email, {
Expand Down
14 changes: 6 additions & 8 deletions packages/backend/server/src/core/auth/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ import type {
ExecutionContext,
OnModuleInit,
} from '@nestjs/common';
import {
Injectable,
SetMetadata,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { Injectable, SetMetadata, UseGuards } from '@nestjs/common';
import { ModuleRef, Reflector } from '@nestjs/core';

import { getRequestResponseFromContext } from '../../fundamentals';
import {
AuthenticationRequired,
getRequestResponseFromContext,
} from '../../fundamentals';
import { AuthService, parseAuthUserSeqNum } from './service';

function extractTokenFromHeader(authorization: string) {
Expand Down Expand Up @@ -84,7 +82,7 @@ export class AuthGuard implements CanActivate, OnModuleInit {
}

if (!req.user) {
throw new UnauthorizedException('You are not signed in.');
throw new AuthenticationRequired();
}

return true;
Expand Down
42 changes: 27 additions & 15 deletions packages/backend/server/src/core/auth/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import {
Args,
Field,
Expand All @@ -10,7 +9,18 @@ import {
Resolver,
} from '@nestjs/graphql';

import { Config, SkipThrottle, Throttle, URLHelper } from '../../fundamentals';
import {
ActionForbidden,
Config,
EmailAlreadyUsed,
EmailTokenNotFound,
EmailVerificationRequired,
InvalidEmailToken,
SameEmailProvided,
SkipThrottle,
Throttle,
URLHelper,
} from '../../fundamentals';
import { UserService } from '../user';
import { UserType } from '../user/types';
import { validators } from '../utils/validators';
Expand Down Expand Up @@ -62,7 +72,7 @@ export class AuthResolver {
@Parent() user: UserType
): Promise<ClientTokenType> {
if (user.id !== currentUser.id) {
throw new ForbiddenException('Invalid user');
throw new ActionForbidden();
}

const session = await this.auth.createUserSession(
Expand Down Expand Up @@ -102,7 +112,7 @@ export class AuthResolver {
);

if (!valid) {
throw new ForbiddenException('Invalid token');
throw new InvalidEmailToken();
}

await this.auth.changePassword(user.id, newPassword);
Expand All @@ -124,7 +134,7 @@ export class AuthResolver {
});

if (!valid) {
throw new ForbiddenException('Invalid token');
throw new InvalidEmailToken();
}

email = decodeURIComponent(email);
Expand All @@ -144,7 +154,7 @@ export class AuthResolver {
@Args('email', { nullable: true }) _email?: string
) {
if (!user.emailVerified) {
throw new ForbiddenException('Please verify your email first.');
throw new EmailVerificationRequired();
}

const token = await this.token.createToken(
Expand All @@ -166,7 +176,7 @@ export class AuthResolver {
@Args('email', { nullable: true }) _email?: string
) {
if (!user.emailVerified) {
throw new ForbiddenException('Please verify your email first.');
throw new EmailVerificationRequired();
}

const token = await this.token.createToken(
Expand Down Expand Up @@ -195,7 +205,7 @@ export class AuthResolver {
@Args('email', { nullable: true }) _email?: string
) {
if (!user.emailVerified) {
throw new ForbiddenException('Please verify your email first.');
throw new EmailVerificationRequired();
}

const token = await this.token.createToken(TokenType.ChangeEmail, user.id);
Expand All @@ -213,24 +223,26 @@ export class AuthResolver {
@Args('email') email: string,
@Args('callbackUrl') callbackUrl: string
) {
if (!token) {
throw new EmailTokenNotFound();
}

validators.assertValidEmail(email);
const valid = await this.token.verifyToken(TokenType.ChangeEmail, token, {
credential: user.id,
});

if (!valid) {
throw new ForbiddenException('Invalid token');
throw new InvalidEmailToken();
}

const hasRegistered = await this.user.findUserByEmail(email);

if (hasRegistered) {
if (hasRegistered.id !== user.id) {
throw new BadRequestException(`The email provided has been taken.`);
throw new EmailAlreadyUsed();
} else {
throw new BadRequestException(
`The email provided is the same as the current email.`
);
throw new SameEmailProvided();
}
}

Expand Down Expand Up @@ -264,15 +276,15 @@ export class AuthResolver {
@Args('token') token: string
) {
if (!token) {
throw new BadRequestException('Invalid token');
throw new EmailTokenNotFound();
}

const valid = await this.token.verifyToken(TokenType.VerifyEmail, token, {
credential: user.id,
});

if (!valid) {
throw new ForbiddenException('Invalid token');
throw new InvalidEmailToken();
}

const { emailVerifiedAt } = await this.auth.setEmailVerified(user.id);
Expand Down
41 changes: 14 additions & 27 deletions packages/backend/server/src/core/auth/service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import {
BadRequestException,
Injectable,
NotAcceptableException,
OnApplicationBootstrap,
} from '@nestjs/common';
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import type { User } from '@prisma/client';
import { PrismaClient } from '@prisma/client';
import type { CookieOptions, Request, Response } from 'express';
import { assign, omit } from 'lodash-es';

import { Config, CryptoHelper, MailService } from '../../fundamentals';
import {
Config,
CryptoHelper,
EmailAlreadyUsed,
MailService,
WrongSignInCredentials,
WrongSignInMethod,
} from '../../fundamentals';
import { FeatureManagementService } from '../features/management';
import { QuotaService } from '../quota/service';
import { QuotaType } from '../quota/types';
Expand Down Expand Up @@ -109,7 +111,7 @@ export class AuthService implements OnApplicationBootstrap {
const user = await this.user.findUserByEmail(email);

if (user) {
throw new BadRequestException('Email was taken');
throw new EmailAlreadyUsed();
}

const hashedPassword = await this.crypto.encryptPassword(password);
Expand All @@ -127,13 +129,11 @@ export class AuthService implements OnApplicationBootstrap {
const user = await this.user.findUserWithHashedPasswordByEmail(email);

if (!user) {
throw new NotAcceptableException('Invalid sign in credentials');
throw new WrongSignInCredentials();
}

if (!user.password) {
throw new NotAcceptableException(
'User Password is not set. Should login through email link.'
);
throw new WrongSignInMethod();
}

const passwordMatches = await this.crypto.verifyPassword(
Expand All @@ -142,7 +142,7 @@ export class AuthService implements OnApplicationBootstrap {
);

if (!passwordMatches) {
throw new NotAcceptableException('Invalid sign in credentials');
throw new WrongSignInCredentials();
}

return sessionUser(user);
Expand Down Expand Up @@ -382,27 +382,14 @@ export class AuthService implements OnApplicationBootstrap {
id: string,
newPassword: string
): Promise<Omit<User, 'password'>> {
const user = await this.user.findUserById(id);

if (!user) {
throw new BadRequestException('Invalid email');
}

const hashedPassword = await this.crypto.encryptPassword(newPassword);

return this.user.updateUser(user.id, { password: hashedPassword });
return this.user.updateUser(id, { password: hashedPassword });
}

async changeEmail(
id: string,
newEmail: string
): Promise<Omit<User, 'password'>> {
const user = await this.user.findUserById(id);

if (!user) {
throw new BadRequestException('Invalid email');
}

return this.user.updateUser(id, {
email: newEmail,
emailVerifiedAt: new Date(),
Expand Down
9 changes: 6 additions & 3 deletions packages/backend/server/src/core/common/admin-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import type {
ExecutionContext,
OnModuleInit,
} from '@nestjs/common';
import { Injectable, UnauthorizedException, UseGuards } from '@nestjs/common';
import { Injectable, UseGuards } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

import { getRequestResponseFromContext } from '../../fundamentals';
import {
ActionForbidden,
getRequestResponseFromContext,
} from '../../fundamentals';
import { FeatureManagementService } from '../features';

@Injectable()
Expand All @@ -27,7 +30,7 @@ export class AdminGuard implements CanActivate, OnModuleInit {
}

if (!allow) {
throw new UnauthorizedException('Your operation is not allowed.');
throw new ActionForbidden();
}

return true;
Expand Down
Loading

0 comments on commit 54fc119

Please sign in to comment.