improvement: error handeling structure changed and unified all across the app. user no longer sees inappropriate messages.
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
Get,
|
||||
Patch,
|
||||
Put,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import {
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
import { AuthService } from './auth.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AppException, ErrorCode } from '../../common/errors';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { CreateOrganizationDto } from './dto/create-organization.dto';
|
||||
@@ -322,7 +322,7 @@ export class AuthController {
|
||||
const refreshToken = req?.cookies?.refreshToken;
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new UnauthorizedException('Refresh token not found');
|
||||
throw new AppException(ErrorCode.AUTH_REFRESH_TOKEN_NOT_FOUND, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const result = await this.authService.refreshToken(
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// backend/src/modules/auth/auth.service.ts
|
||||
import {
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
HttpStatus,
|
||||
HttpException,
|
||||
InternalServerErrorException
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
@@ -33,6 +31,7 @@ import {
|
||||
} from './dto/forgot-password.dto';
|
||||
import { normalizeIranMobile } from '../../common/utils/mobile.util';
|
||||
import { sessionExpiresAtFromNow } from '../../common/jwt-duration';
|
||||
import { AppException, ErrorCode } from '../../common/errors';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
const FORGOT_PASSWORD_PURPOSE = 'forgot_password';
|
||||
@@ -242,7 +241,7 @@ export class AuthService {
|
||||
const email = registerDto.email.trim().toLowerCase();
|
||||
|
||||
if (!RegisterDto.isValidMobile(registerDto.mobile)) {
|
||||
throw new BadRequestException('Please enter a valid mobile number');
|
||||
throw new AppException(ErrorCode.AUTH_REGISTRATION_MOBILE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const mobile = normalizeIranMobile(registerDto.mobile);
|
||||
@@ -256,9 +255,9 @@ export class AuthService {
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser.email === email) {
|
||||
throw new ConflictException('User already exists. Please login and create a new organization from your account.');
|
||||
throw new AppException(ErrorCode.AUTH_REGISTRATION_EMAIL_EXISTS, HttpStatus.CONFLICT);
|
||||
}
|
||||
throw new ConflictException('This mobile number is already registered.');
|
||||
throw new AppException(ErrorCode.AUTH_REGISTRATION_MOBILE_EXISTS, HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
// 2. Hash password
|
||||
@@ -315,7 +314,7 @@ export class AuthService {
|
||||
const validatedUser = await this.validateUser(email, password);
|
||||
|
||||
if (!validatedUser) {
|
||||
throw new UnauthorizedException('Auto-login failed');
|
||||
throw new AppException(ErrorCode.AUTH_REGISTRATION_FAILED, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
return this.login({ email, password } as any, validatedUser);
|
||||
@@ -332,13 +331,11 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!owner) {
|
||||
throw new UnauthorizedException('User not found');
|
||||
throw new AppException(ErrorCode.AUTH_USER_NOT_FOUND, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
if (!currentOrganizationId) {
|
||||
throw new ForbiddenException(
|
||||
'Select an organization before creating a new one.',
|
||||
);
|
||||
throw new AppException(ErrorCode.AUTH_SELECT_ORG_FIRST, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const currentMembership = await this.prisma.membership.findUnique({
|
||||
@@ -352,9 +349,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!currentMembership?.isOwner) {
|
||||
throw new ForbiddenException(
|
||||
'Only owners of the current organization can create new organizations.',
|
||||
);
|
||||
throw new AppException(ErrorCode.AUTH_CREATE_ORG_OWNER_ONLY, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const organization = await this.prisma.$transaction(async (tx) => {
|
||||
@@ -417,7 +412,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('User not found');
|
||||
throw new AppException(ErrorCode.AUTH_USER_NOT_FOUND, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const { passwordHash, ...result } = user;
|
||||
@@ -485,7 +480,7 @@ export class AuthService {
|
||||
|
||||
// Ensure this is a refresh token
|
||||
if (payload.type !== 'refresh') {
|
||||
throw new UnauthorizedException('Invalid token type');
|
||||
throw new AppException(ErrorCode.AUTH_SESSION_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// Find session with this refresh token
|
||||
@@ -518,7 +513,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
throw new AppException(ErrorCode.AUTH_REFRESH_TOKEN_INVALID, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const organizationId = await this.resolveOrganizationIdForRefresh(
|
||||
@@ -574,10 +569,13 @@ export class AuthService {
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
|
||||
throw new UnauthorizedException('Invalid or expired refresh token');
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new UnauthorizedException('Refresh token failed');
|
||||
if (error instanceof Error && (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError')) {
|
||||
throw new AppException(ErrorCode.AUTH_REFRESH_TOKEN_INVALID, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
throw new AppException(ErrorCode.AUTH_REFRESH_TOKEN_INVALID, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,22 +599,22 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!user || !user.passwordHash) {
|
||||
throw new BadRequestException('User not found or invalid password method');
|
||||
throw new AppException(ErrorCode.AUTH_USER_NOT_FOUND, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (skipCurrentPassword) {
|
||||
const hasRecentReset = await this.hasRecentPasswordResetVerification(userId);
|
||||
if (!hasRecentReset) {
|
||||
throw new UnauthorizedException('Password reset verification expired. Please verify your mobile again.');
|
||||
throw new AppException(ErrorCode.AUTH_PASSWORD_RESET_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
} else {
|
||||
if (!oldPassword) {
|
||||
throw new BadRequestException('Current password is required');
|
||||
throw new AppException(ErrorCode.AUTH_PASSWORD_CURRENT_REQUIRED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(oldPassword, user.passwordHash);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('Current password is incorrect');
|
||||
throw new AppException(ErrorCode.AUTH_PASSWORD_INCORRECT, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,7 +634,7 @@ export class AuthService {
|
||||
message: 'Password changed successfully. Please login again.',
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof UnauthorizedException || error instanceof BadRequestException) {
|
||||
if (error instanceof HttpException) {
|
||||
throw error;
|
||||
}
|
||||
throw new InternalServerErrorException('Failed to change password');
|
||||
@@ -645,7 +643,7 @@ export class AuthService {
|
||||
|
||||
async sendForgotPasswordCode(dto: ForgotPasswordSendCodeDto) {
|
||||
if (!ForgotPasswordSendCodeDto.validateMobile(dto.mobile)) {
|
||||
throw new BadRequestException('Please enter a valid mobile number');
|
||||
throw new AppException(ErrorCode.AUTH_REGISTRATION_MOBILE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const mobile = normalizeIranMobile(dto.mobile);
|
||||
@@ -671,7 +669,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (recentCode) {
|
||||
throw new BadRequestException('Please wait before requesting another code');
|
||||
throw new AppException(ErrorCode.AUTH_VERIFICATION_RATE_LIMIT, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const code = this.generateVerificationCode();
|
||||
@@ -701,7 +699,7 @@ export class AuthService {
|
||||
|
||||
async verifyForgotPasswordCode(dto: ForgotPasswordVerifyDto) {
|
||||
if (!ForgotPasswordSendCodeDto.validateMobile(dto.mobile)) {
|
||||
throw new BadRequestException('Please enter a valid mobile number');
|
||||
throw new AppException(ErrorCode.AUTH_REGISTRATION_MOBILE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const mobile = normalizeIranMobile(dto.mobile);
|
||||
@@ -718,7 +716,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!verification || !this.isVerificationCodeValid(code, verification.codeHash)) {
|
||||
throw new UnauthorizedException('Invalid or expired verification code');
|
||||
throw new AppException(ErrorCode.AUTH_VERIFICATION_CODE_INVALID, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
await this.prisma.phoneVerificationCode.update({
|
||||
@@ -748,7 +746,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid or expired verification code');
|
||||
throw new AppException(ErrorCode.AUTH_VERIFICATION_CODE_INVALID, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const loginResult = await this.login(
|
||||
@@ -877,7 +875,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (payload.type !== 'access') {
|
||||
throw new UnauthorizedException('Invalid token type');
|
||||
throw new AppException(ErrorCode.AUTH_SESSION_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const session = await this.prisma.session.findFirst({
|
||||
@@ -909,7 +907,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
throw new UnauthorizedException('Session not found or expired');
|
||||
throw new AppException(ErrorCode.AUTH_SESSION_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const { passwordHash, ...user } = session.user;
|
||||
@@ -937,7 +935,7 @@ export class AuthService {
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
throw new AppException(ErrorCode.AUTH_SESSION_EXPIRED, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,10 +963,10 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new UnauthorizedException('Access denied to this organization');
|
||||
throw new AppException(ErrorCode.AUTH_ORG_ACCESS_DENIED, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if (!membership.isOwner && !membership.isActive) {
|
||||
throw new UnauthorizedException('Your invitation is still pending activation');
|
||||
throw new AppException(ErrorCode.AUTH_INVITATION_PENDING, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
// 2. Build payload WITH org context
|
||||
@@ -1147,7 +1145,7 @@ export class AuthService {
|
||||
const language = dto.language;
|
||||
|
||||
if (!SUPPORTED_USER_LANGUAGES.includes(language)) {
|
||||
throw new BadRequestException('Language must be one of: en, fa, nl');
|
||||
throw new AppException(ErrorCode.VALIDATION_LANGUAGE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.update({
|
||||
@@ -1231,7 +1229,7 @@ export class AuthService {
|
||||
|
||||
private async getOwnerMembership(userId: string, organizationId: string) {
|
||||
if (!organizationId) {
|
||||
throw new BadRequestException('Organization is not selected');
|
||||
throw new AppException(ErrorCode.AUTH_ORG_NOT_SELECTED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
@@ -1245,7 +1243,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new ForbiddenException('Only organization owners can manage participation');
|
||||
throw new AppException(ErrorCode.PERMISSION_OWNER_ONLY, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
return membership;
|
||||
@@ -1281,9 +1279,7 @@ export class AuthService {
|
||||
const orgType = getOrgTypeFromMembership(membership);
|
||||
|
||||
if (!hasActivePlan(membership)) {
|
||||
throw new ForbiddenException(
|
||||
'An active subscription is required to participate in treatments or tasks',
|
||||
);
|
||||
throw new AppException(ErrorCode.PERMISSION_PARTICIPATION_SUBSCRIPTION, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
if (dto.participate) {
|
||||
@@ -1321,7 +1317,7 @@ export class AuthService {
|
||||
async getMyWorkingHours(userId: string, organizationId: string) {
|
||||
const membership = await this.getOwnerMembership(userId, organizationId);
|
||||
if (getOrgTypeFromMembership(membership) !== 'CLINIC') {
|
||||
throw new BadRequestException('Working hours are only available for clinic organizations');
|
||||
throw new AppException(ErrorCode.PERMISSION_CLINIC_WORKING_HOURS, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return this.staffWorkingHoursService.getMyWorkingHours(userId, organizationId);
|
||||
}
|
||||
@@ -1333,12 +1329,10 @@ export class AuthService {
|
||||
) {
|
||||
const membership = await this.getOwnerMembership(userId, organizationId);
|
||||
if (getOrgTypeFromMembership(membership) !== 'CLINIC') {
|
||||
throw new BadRequestException('Working hours are only available for clinic organizations');
|
||||
throw new AppException(ErrorCode.PERMISSION_CLINIC_WORKING_HOURS, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (!participatesInTreatments(membership)) {
|
||||
throw new ForbiddenException(
|
||||
'Enable treatment participation before setting working hours',
|
||||
);
|
||||
throw new AppException(ErrorCode.PERMISSION_ENABLE_PARTICIPATION_FIRST, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return this.staffWorkingHoursService.upsertMyWorkingHours(userId, organizationId, dto);
|
||||
}
|
||||
@@ -1406,9 +1400,7 @@ export class AuthService {
|
||||
});
|
||||
|
||||
if (futureAppointment) {
|
||||
throw new ConflictException(
|
||||
'You cannot stop participating in treatments while you have future appointments assigned. Reassign or cancel those appointments first.',
|
||||
);
|
||||
throw new AppException(ErrorCode.CONFLICT_FUTURE_APPOINTMENTS, HttpStatus.CONFLICT);
|
||||
}
|
||||
} else {
|
||||
await assertLabOrganization(this.prisma, membership.organizationId);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IsOptional, IsString, MinLength } from 'class-validator';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export class ChangePasswordDto {
|
||||
@IsOptional()
|
||||
@@ -6,6 +7,6 @@ export class ChangePasswordDto {
|
||||
currentPassword?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MinLength(8, { message: ErrorCode.VALIDATION_PASSWORD_TOO_SHORT })
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { IsEmail, IsEnum, IsOptional, IsString } from 'class-validator';
|
||||
import { IsEmail, IsEnum, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export class CreateOrganizationDto {
|
||||
@IsString()
|
||||
@MinLength(2, { message: ErrorCode.VALIDATION_ORGANIZATION_NAME_REQUIRED })
|
||||
organizationName: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsEmail({}, { message: ErrorCode.VALIDATION_EMAIL_INVALID })
|
||||
organizationEmail: string;
|
||||
|
||||
@IsEnum(['CLINIC', 'LAB'])
|
||||
@IsEnum(['CLINIC', 'LAB'], { message: ErrorCode.VALIDATION_ORGANIZATION_TYPE_INVALID })
|
||||
organizationType: 'CLINIC' | 'LAB';
|
||||
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { IsString, Matches, Length } from 'class-validator';
|
||||
import { IsString, Matches, Length, MinLength } from 'class-validator';
|
||||
import { isValidIranMobile } from '../../../common/utils/mobile.util';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export class ForgotPasswordSendCodeDto {
|
||||
@IsString()
|
||||
@Matches(/^[\d+\s()-]+$/, { message: 'Mobile number format is invalid' })
|
||||
@Matches(/^[\d+\s()-]+$/, { message: ErrorCode.VALIDATION_MOBILE_INVALID })
|
||||
mobile: string;
|
||||
|
||||
static validateMobile(mobile: string): boolean {
|
||||
@@ -13,10 +14,10 @@ export class ForgotPasswordSendCodeDto {
|
||||
|
||||
export class ForgotPasswordVerifyDto {
|
||||
@IsString()
|
||||
@Matches(/^[\d+\s()-]+$/, { message: 'Mobile number format is invalid' })
|
||||
@Matches(/^[\d+\s()-]+$/, { message: ErrorCode.VALIDATION_MOBILE_INVALID })
|
||||
mobile: string;
|
||||
|
||||
@IsString()
|
||||
@Length(5, 6)
|
||||
@Length(5, 6, { message: ErrorCode.VALIDATION_FIELD_REQUIRED })
|
||||
code: string;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
// backend/src/modules/auth/dto/login.dto.ts
|
||||
import { IsBoolean, IsEmail, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsString, MinLength, IsOptional, IsBoolean } from 'class-validator';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({
|
||||
description: 'User email address',
|
||||
example: 'user@example.com',
|
||||
required: true,
|
||||
})
|
||||
@IsEmail({}, { message: 'Please provide a valid email address' })
|
||||
@IsEmail({}, { message: ErrorCode.VALIDATION_EMAIL_INVALID })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'User password (min 6 characters)',
|
||||
example: 'password123',
|
||||
required: true,
|
||||
minLength: 6,
|
||||
})
|
||||
@IsString()
|
||||
@MinLength(6, { message: 'Password must be at least 6 characters long' })
|
||||
@MinLength(6, { message: ErrorCode.VALIDATION_PASSWORD_TOO_SHORT })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Keep the user signed in for 30 days on this device',
|
||||
required: false,
|
||||
default: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { IsEmail, IsString, MinLength, IsEnum, Matches } from 'class-validator';
|
||||
import { isValidIranMobile } from '../../../common/utils/mobile.util';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export class RegisterDto {
|
||||
@IsEmail()
|
||||
@IsEmail({}, { message: ErrorCode.VALIDATION_EMAIL_INVALID })
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@Matches(/^[\d+\s()-]+$/, { message: 'Mobile number format is invalid' })
|
||||
@Matches(/^[\d+\s()-]+$/, { message: ErrorCode.VALIDATION_MOBILE_INVALID })
|
||||
mobile: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MinLength(8, { message: ErrorCode.VALIDATION_PASSWORD_TOO_SHORT })
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2, { message: ErrorCode.VALIDATION_NAME_TOO_SHORT })
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(2, { message: ErrorCode.VALIDATION_ORGANIZATION_NAME_REQUIRED })
|
||||
organizationName: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsEmail({}, { message: ErrorCode.VALIDATION_EMAIL_INVALID })
|
||||
organizationEmail: string;
|
||||
|
||||
@IsEnum(['CLINIC', 'LAB'])
|
||||
@IsEnum(['CLINIC', 'LAB'], { message: ErrorCode.VALIDATION_ORGANIZATION_TYPE_INVALID })
|
||||
organizationType: 'CLINIC' | 'LAB';
|
||||
|
||||
static isValidMobile(mobile: string): boolean {
|
||||
return isValidIranMobile(mobile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsIn, IsString } from 'class-validator';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export const SUPPORTED_USER_LANGUAGES = ['en', 'fa', 'nl'] as const;
|
||||
export type SupportedUserLanguage = (typeof SUPPORTED_USER_LANGUAGES)[number];
|
||||
@@ -8,7 +9,7 @@ export class UpdateLanguageDto {
|
||||
@ApiProperty({ enum: SUPPORTED_USER_LANGUAGES, example: 'en' })
|
||||
@IsString()
|
||||
@IsIn(SUPPORTED_USER_LANGUAGES, {
|
||||
message: 'Language must be one of: en, fa, nl',
|
||||
message: ErrorCode.VALIDATION_LANGUAGE_INVALID,
|
||||
})
|
||||
language: SupportedUserLanguage;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// backend/src/modules/auth/strategies/jwt.strategy.ts
|
||||
import { Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from '../../../../prisma/prisma.service';
|
||||
import { Request } from 'express';
|
||||
import { AppException, ErrorCode } from '../../../common/errors';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
@@ -27,7 +28,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
throw new AppException(ErrorCode.AUTH_UNAUTHORIZED, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
const { passwordHash, ...result } = user;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// backend/src/modules/auth/strategies/local.strategy.ts
|
||||
import { Strategy } from 'passport-local';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { AppException, ErrorCode } from '../../../common/errors';
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
@@ -13,8 +13,8 @@ export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
async validate(email: string, password: string): Promise<any> {
|
||||
const user = await this.authService.validateUser(email, password);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Invalid credentials');
|
||||
throw new AppException(ErrorCode.AUTH_INVALID_CREDENTIALS, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { IsEmail, IsEnum, IsString, MinLength } from 'class-validator';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export class AcceptOrganizationInviteDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MinLength(1, { message: ErrorCode.VALIDATION_TOKEN_REQUIRED })
|
||||
token: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MinLength(2, { message: ErrorCode.VALIDATION_ORGANIZATION_NAME_REQUIRED })
|
||||
organizationName: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsEmail({}, { message: ErrorCode.VALIDATION_EMAIL_INVALID })
|
||||
organizationEmail: string;
|
||||
|
||||
@IsEnum(['CLINIC', 'LAB'])
|
||||
@IsEnum(['CLINIC', 'LAB'], { message: ErrorCode.VALIDATION_ORGANIZATION_TYPE_INVALID })
|
||||
organizationType: 'CLINIC' | 'LAB';
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MinLength(2, { message: ErrorCode.VALIDATION_NAME_TOO_SHORT })
|
||||
ownerName: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MinLength(8, { message: ErrorCode.VALIDATION_PASSWORD_TOO_SHORT })
|
||||
password: string;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { IsString, MinLength } from 'class-validator';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export class AcceptStaffInviteDto {
|
||||
@IsString()
|
||||
@MinLength(1, { message: ErrorCode.VALIDATION_TOKEN_REQUIRED })
|
||||
token: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@MinLength(8, { message: ErrorCode.VALIDATION_PASSWORD_TOO_SHORT })
|
||||
password: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MinLength(1, { message: ErrorCode.VALIDATION_NAME_TOO_SHORT })
|
||||
name: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user