feat(server): introduce user friendly server errors (#7111)

This commit is contained in:
liuyi
2024-06-17 11:30:58 +08:00
committed by GitHub
parent 5307a55f8a
commit 54fc1197ad
65 changed files with 3170 additions and 924 deletions

View File

@@ -1,7 +1,6 @@
import { randomUUID } from 'node:crypto';
import {
BadRequestException,
Body,
Controller,
Get,
@@ -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';
@@ -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) {
@@ -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();
}
}
@@ -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({
@@ -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);
@@ -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, {

View File

@@ -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) {
@@ -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;

View File

@@ -1,4 +1,3 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import {
Args,
Field,
@@ -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';
@@ -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(
@@ -102,7 +112,7 @@ export class AuthResolver {
);
if (!valid) {
throw new ForbiddenException('Invalid token');
throw new InvalidEmailToken();
}
await this.auth.changePassword(user.id, newPassword);
@@ -124,7 +134,7 @@ export class AuthResolver {
});
if (!valid) {
throw new ForbiddenException('Invalid token');
throw new InvalidEmailToken();
}
email = decodeURIComponent(email);
@@ -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(
@@ -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(
@@ -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);
@@ -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();
}
}
@@ -264,7 +276,7 @@ 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, {
@@ -272,7 +284,7 @@ export class AuthResolver {
});
if (!valid) {
throw new ForbiddenException('Invalid token');
throw new InvalidEmailToken();
}
const { emailVerifiedAt } = await this.auth.setEmailVerified(user.id);

View File

@@ -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';
@@ -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);
@@ -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(
@@ -142,7 +142,7 @@ export class AuthService implements OnApplicationBootstrap {
);
if (!passwordMatches) {
throw new NotAcceptableException('Invalid sign in credentials');
throw new WrongSignInCredentials();
}
return sessionUser(user);
@@ -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(),