Merge branch 'waiting-list' into feat/neoleap-integration

This commit is contained in:
Abdalhamid Alhamad
2025-05-21 09:59:18 +03:00
13 changed files with 140 additions and 132 deletions

View File

@ -18,7 +18,6 @@ import {
setJuniorPasswordRequestDto, setJuniorPasswordRequestDto,
SetPasscodeRequestDto, SetPasscodeRequestDto,
VerifyLoginOtpRequestDto, VerifyLoginOtpRequestDto,
VerifyOtpRequestDto,
VerifyUserRequestDto, VerifyUserRequestDto,
} from '../dtos/request'; } from '../dtos/request';
import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response'; import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response';
@ -72,22 +71,22 @@ export class AuthController {
await this.authService.setPasscode(sub, passcode); await this.authService.setPasscode(sub, passcode);
} }
@Post('register/set-phone/otp') // @Post('register/set-phone/otp')
@UseGuards(AccessTokenGuard) // @UseGuards(AccessTokenGuard)
async setPhoneNumber( // async setPhoneNumber(
@AuthenticatedUser() { sub }: IJwtPayload, // @AuthenticatedUser() { sub }: IJwtPayload,
@Body() setPhoneNumberDto: CreateUnverifiedUserRequestDto, // @Body() setPhoneNumberDto: CreateUnverifiedUserRequestDto,
) { // ) {
const phoneNumber = await this.authService.setPhoneNumber(sub, setPhoneNumberDto); // const phoneNumber = await this.authService.setPhoneNumber(sub, setPhoneNumberDto);
return ResponseFactory.data(new SendRegisterOtpResponseDto(phoneNumber)); // return ResponseFactory.data(new SendRegisterOtpResponseDto(phoneNumber));
} // }
@Post('register/set-phone/verify') // @Post('register/set-phone/verify')
@HttpCode(HttpStatus.NO_CONTENT) // @HttpCode(HttpStatus.NO_CONTENT)
@UseGuards(AccessTokenGuard) // @UseGuards(AccessTokenGuard)
async verifyPhoneNumber(@AuthenticatedUser() { sub }: IJwtPayload, @Body() { otp }: VerifyOtpRequestDto) { // async verifyPhoneNumber(@AuthenticatedUser() { sub }: IJwtPayload, @Body() { otp }: VerifyOtpRequestDto) {
await this.authService.verifyPhoneNumber(sub, otp); // await this.authService.verifyPhoneNumber(sub, otp);
} // }
@Post('biometric/enable') @Post('biometric/enable')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)

View File

@ -1,19 +1,14 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Matches } from 'class-validator'; import { IsEmail } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n'; import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
export class CreateUnverifiedUserRequestDto { export class CreateUnverifiedUserRequestDto {
@ApiProperty({ example: '+962' }) @ApiProperty({ example: 'test@test.com' })
@Matches(COUNTRY_CODE_REGEX, { @IsEmail(
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }), {},
}) {
countryCode: string = '+966'; message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }),
},
@ApiProperty({ example: '787259134' }) )
@IsValidPhoneNumber({ email!: string;
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
} }

View File

@ -1,10 +1,32 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty, PickType } from '@nestjs/swagger';
import { IsNumberString, MaxLength, MinLength } from 'class-validator'; import { IsDateString, IsNotEmpty, IsNumberString, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n'; import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants'; import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
import { IsAbove18 } from '~/core/decorators/validations';
import { CreateUnverifiedUserRequestDto } from './create-unverified-user.request.dto'; import { CreateUnverifiedUserRequestDto } from './create-unverified-user.request.dto';
export class VerifyUserRequestDto extends CreateUnverifiedUserRequestDto { export class VerifyUserRequestDto extends PickType(CreateUnverifiedUserRequestDto, ['email']) {
@ApiProperty({ example: 'John' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.firstName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.firstName' }) })
firstName!: string;
@ApiProperty({ example: 'Doe' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.lastName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.lastName' }) })
lastName!: string;
@ApiProperty({ example: '2021-01-01' })
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
dateOfBirth!: Date;
@ApiProperty({ example: 'JO' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'customer.countryOfResidence' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'customer.countryOfResidence' }) })
@IsOptional()
countryOfResidence: string = 'SA';
@ApiProperty({ example: '111111' }) @ApiProperty({ example: '111111' })
@IsNumberString( @IsNumberString(
{ no_symbols: true }, { no_symbols: true },

View File

@ -2,9 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
export class SendRegisterOtpResponseDto { export class SendRegisterOtpResponseDto {
@ApiProperty() @ApiProperty()
phoneNumber!: string; email!: string;
constructor(phoneNumber: string) { constructor(email: string) {
this.phoneNumber = phoneNumber; this.email = email;
} }
} }

View File

@ -45,62 +45,45 @@ export class AuthService {
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
private readonly oauth2Service: Oauth2Service, private readonly oauth2Service: Oauth2Service,
) {} ) {}
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) { async sendRegisterOtp(body: CreateUnverifiedUserRequestDto) {
this.logger.log(`Sending OTP to ${countryCode + phoneNumber}`); this.logger.log(`Sending OTP to ${body.email}`);
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode }); const user = await this.userService.findOrCreateUser(body);
return this.otpService.generateAndSendOtp({ return this.otpService.generateAndSendOtp({
userId: user.id, userId: user.id,
recipient: user.countryCode + user.phoneNumber, recipient: user.email,
scope: OtpScope.VERIFY_PHONE, scope: OtpScope.VERIFY_EMAIL,
otpType: OtpType.SMS, otpType: OtpType.EMAIL,
}); });
} }
async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> { async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> {
this.logger.log(`Verifying user with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber}`); this.logger.log(`Verifying user with email ${verifyUserDto.email}`);
const user = await this.userService.findUserOrThrow({ phoneNumber: verifyUserDto.phoneNumber }); const user = await this.userService.findUserOrThrow({ email: verifyUserDto.email });
if (user.isProfileCompleted) { if (user.isEmailVerified) {
this.logger.error( this.logger.error(`User with email ${verifyUserDto.email} already verified`);
`User with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber} already verified`, throw new BadRequestException('USER.EMAIL_ALREADY_VERIFIED');
);
throw new BadRequestException('USER.PHONE_ALREADY_VERIFIED');
} }
const isOtpValid = await this.otpService.verifyOtp({ const isOtpValid = await this.otpService.verifyOtp({
userId: user.id, userId: user.id,
scope: OtpScope.VERIFY_PHONE, scope: OtpScope.VERIFY_EMAIL,
otpType: OtpType.SMS, otpType: OtpType.EMAIL,
value: verifyUserDto.otp, value: verifyUserDto.otp,
}); });
if (!isOtpValid) { if (!isOtpValid) {
this.logger.error( this.logger.error(`Invalid OTP for user with email ${verifyUserDto.email}`);
`Invalid OTP for user with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber}`,
);
throw new BadRequestException('OTP.INVALID_OTP'); throw new BadRequestException('OTP.INVALID_OTP');
} }
if (user.isPhoneVerified) { await this.userService.verifyUser(user.id, verifyUserDto);
this.logger.log(
`User with phone number ${
verifyUserDto.countryCode + verifyUserDto.phoneNumber
} already verified but did not complete registration process`,
);
const tokens = await this.generateAuthToken(user);
return [tokens, user];
}
await this.userService.verifyPhoneNumber(user.id);
await user.reload(); await user.reload();
const tokens = await this.generateAuthToken(user); const tokens = await this.generateAuthToken(user);
this.logger.log( this.logger.log(`User with email ${verifyUserDto.email} verified successfully`);
`User with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber} verified successfully`,
);
return [tokens, user]; return [tokens, user];
} }
@ -138,46 +121,46 @@ export class AuthService {
this.logger.log(`Passcode set successfully for user with id ${userId}`); this.logger.log(`Passcode set successfully for user with id ${userId}`);
} }
async setPhoneNumber(userId: string, { phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) { // async setPhoneNumber(userId: string, { phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userService.findUserOrThrow({ id: userId }); // const user = await this.userService.findUserOrThrow({ id: userId });
if (user.phoneNumber || user.countryCode) { // if (user.phoneNumber || user.countryCode) {
this.logger.error(`Phone number already set for user with id ${userId}`); // this.logger.error(`Phone number already set for user with id ${userId}`);
throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_SET'); // throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_SET');
} // }
const existingUser = await this.userService.findUser({ phoneNumber, countryCode }); // const existingUser = await this.userService.findUser({ phoneNumber, countryCode });
if (existingUser) { // if (existingUser) {
this.logger.error(`Phone number ${countryCode + phoneNumber} already taken`); // this.logger.error(`Phone number ${countryCode + phoneNumber} already taken`);
throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_TAKEN'); // throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_TAKEN');
} // }
await this.userService.setPhoneNumber(userId, phoneNumber, countryCode); // await this.userService.setPhoneNumber(userId, phoneNumber, countryCode);
return this.otpService.generateAndSendOtp({ // return this.otpService.generateAndSendOtp({
userId, // userId,
recipient: countryCode + phoneNumber, // recipient: countryCode + phoneNumber,
scope: OtpScope.VERIFY_PHONE, // scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS, // otpType: OtpType.SMS,
}); // });
} // }
async verifyPhoneNumber(userId: string, otp: string) { // async verifyPhoneNumber(userId: string, otp: string) {
const isOtpValid = await this.otpService.verifyOtp({ // const isOtpValid = await this.otpService.verifyOtp({
otpType: OtpType.SMS, // otpType: OtpType.SMS,
scope: OtpScope.VERIFY_PHONE, // scope: OtpScope.VERIFY_PHONE,
userId, // userId,
value: otp, // value: otp,
}); // });
if (!isOtpValid) { // if (!isOtpValid) {
this.logger.error(`Invalid OTP for user with id ${userId}`); // this.logger.error(`Invalid OTP for user with id ${userId}`);
throw new BadRequestException('OTP.INVALID_OTP'); // throw new BadRequestException('OTP.INVALID_OTP');
} // }
return this.userService.verifyPhoneNumber(userId); // return this.userService.verifyPhoneNumber(userId);
} // }
async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) { async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) {
this.logger.log(`Enabling biometric for user with id ${userId}`); this.logger.log(`Enabling biometric for user with id ${userId}`);
@ -330,7 +313,8 @@ export class AuthService {
} }
async sendLoginOtp({ email }: SendLoginOtpRequestDto) { async sendLoginOtp({ email }: SendLoginOtpRequestDto) {
const user = await this.userService.findOrCreateByEmail(email); const user = await this.userService.findUserOrThrow({ email });
this.logger.log(`Sending login OTP to ${email}`); this.logger.log(`Sending login OTP to ${email}`);
return this.otpService.generateAndSendOtp({ return this.otpService.generateAndSendOtp({
recipient: email, recipient: email,

View File

@ -1,5 +1,6 @@
export enum OtpScope { export enum OtpScope {
VERIFY_PHONE = 'VERIFY_PHONE', VERIFY_PHONE = 'VERIFY_PHONE',
VERIFY_EMAIL = 'VERIFY_EMAIL',
FORGET_PASSWORD = 'FORGET_PASSWORD', FORGET_PASSWORD = 'FORGET_PASSWORD',
LOGIN = 'LOGIN', LOGIN = 'LOGIN',
} }

View File

@ -29,17 +29,6 @@ export class CustomerRepository {
firstName: body.firstName, firstName: body.firstName,
lastName: body.lastName, lastName: body.lastName,
dateOfBirth: body.dateOfBirth, dateOfBirth: body.dateOfBirth,
gender: body.gender,
countryOfResidence: body.countryOfResidence,
nationalId: body.nationalId,
nationalIdExpiry: body.nationalIdExpiry,
sourceOfIncome: body.sourceOfIncome,
profession: body.profession,
professionType: body.professionType,
isPep: body.isPep,
profilePictureId: body.profilePictureId,
civilIdFrontId: body.civilIdFrontId,
civilIdBackId: body.civilIdBackId,
}), }),
); );
} }

View File

@ -89,13 +89,13 @@ export class CustomerService {
} }
@Transactional() @Transactional()
async createGuardianCustomer(userId: string, body: CreateCustomerRequestDto) { async createGuardianCustomer(userId: string, body: Partial<CreateCustomerRequestDto>) {
this.logger.log(`Creating guardian customer for user ${userId}`); this.logger.log(`Creating guardian customer for user ${userId}`);
const existingCustomer = await this.customerRepository.findOne({ id: userId }); const existingCustomer = await this.customerRepository.findOne({ id: userId });
if (existingCustomer) { if (existingCustomer) {
this.logger.error(`Customer ${userId} already exists`); this.logger.error(`Customer ${userId} already exists`);
throw new BadRequestException('CUSTOMER.ALRADY_EXISTS'); throw new BadRequestException('CUSTOMER.ALREADY_EXISTS');
} }
// await this.validateCivilIdForCustomer(userId, body.civilIdFrontId, body.civilIdBackId); // await this.validateCivilIdForCustomer(userId, body.civilIdFrontId, body.civilIdBackId);

View File

@ -15,6 +15,7 @@
"USER": { "USER": {
"PHONE_ALREADY_VERIFIED": "تم التحقق من رقم الهاتف بالفعل.", "PHONE_ALREADY_VERIFIED": "تم التحقق من رقم الهاتف بالفعل.",
"EMAIL_ALREADY_VERIFIED": "تم التحقق من عنوان البريد الإلكتروني بالفعل.",
"EMAIL_ALREADY_SET": "تم تعيين عنوان البريد الإلكتروني بالفعل.", "EMAIL_ALREADY_SET": "تم تعيين عنوان البريد الإلكتروني بالفعل.",
"EMAIL_ALREADY_TAKEN": "عنوان البريد الإلكتروني مستخدم بالفعل. يرجى تجربة عنوان بريد إلكتروني آخر.", "EMAIL_ALREADY_TAKEN": "عنوان البريد الإلكتروني مستخدم بالفعل. يرجى تجربة عنوان بريد إلكتروني آخر.",
"PHONE_NUMBER_ALREADY_SET": "تم تعيين رقم الهاتف بالفعل.", "PHONE_NUMBER_ALREADY_SET": "تم تعيين رقم الهاتف بالفعل.",

View File

@ -15,6 +15,7 @@
"USER": { "USER": {
"PHONE_ALREADY_VERIFIED": "The phone number has already been verified.", "PHONE_ALREADY_VERIFIED": "The phone number has already been verified.",
"EMAIL_ALREADY_VERIFIED": "The email address has already been verified.",
"EMAIL_ALREADY_SET": "The email address has already been set.", "EMAIL_ALREADY_SET": "The email address has already been set.",
"EMAIL_ALREADY_TAKEN": "The email address is already in use. Please try another email address.", "EMAIL_ALREADY_TAKEN": "The email address is already in use. Please try another email address.",
"PHONE_NUMBER_ALREADY_SET": "The phone number has already been set.", "PHONE_NUMBER_ALREADY_SET": "The phone number has already been set.",

View File

@ -11,8 +11,7 @@ export class UserRepository {
createUnverifiedUser(data: Partial<User>) { createUnverifiedUser(data: Partial<User>) {
return this.userRepository.save( return this.userRepository.save(
this.userRepository.create({ this.userRepository.create({
phoneNumber: data.phoneNumber, email: data.email,
countryCode: data.countryCode,
roles: data.roles, roles: data.roles,
}), }),
); );

View File

@ -5,7 +5,8 @@ import moment from 'moment';
import { FindOptionsWhere } from 'typeorm'; import { FindOptionsWhere } from 'typeorm';
import { Transactional } from 'typeorm-transactional'; import { Transactional } from 'typeorm-transactional';
import { NotificationsService } from '~/common/modules/notification/services'; import { NotificationsService } from '~/common/modules/notification/services';
import { CreateUnverifiedUserRequestDto } from '../../auth/dtos/request'; import { CustomerService } from '~/customer/services';
import { CreateUnverifiedUserRequestDto, VerifyUserRequestDto } from '../../auth/dtos/request';
import { Roles } from '../../auth/enums'; import { Roles } from '../../auth/enums';
import { import {
CreateCheckerRequestDto, CreateCheckerRequestDto,
@ -29,6 +30,7 @@ export class UserService {
private readonly deviceService: DeviceService, private readonly deviceService: DeviceService,
private readonly userTokenService: UserTokenService, private readonly userTokenService: UserTokenService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
private customerService: CustomerService,
) {} ) {}
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) { findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
@ -51,9 +53,18 @@ export class UserService {
return this.userRepository.update(userId, { phoneNumber, countryCode }); return this.userRepository.update(userId, { phoneNumber, countryCode });
} }
verifyPhoneNumber(userId: string) { @Transactional()
this.logger.log(`Verifying phone number for user ${userId}`); async verifyUser(userId: string, body: VerifyUserRequestDto) {
return this.userRepository.update(userId, { isPhoneVerified: true }); this.logger.log(`Verifying user email with id ${userId}`);
await Promise.all([
this.customerService.createGuardianCustomer(userId, {
firstName: body.firstName,
lastName: body.lastName,
dateOfBirth: body.dateOfBirth,
countryOfResidence: body.countryOfResidence,
}),
this.userRepository.update(userId, { isEmailVerified: true }),
]);
} }
findUsers(filters: UserFiltersRequestDto) { findUsers(filters: UserFiltersRequestDto) {
@ -74,26 +85,27 @@ export class UserService {
return user; return user;
} }
async findOrCreateUser({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) { @Transactional()
this.logger.log(`Finding or creating user with phone number ${phoneNumber} and country code ${countryCode}`); async findOrCreateUser(body: CreateUnverifiedUserRequestDto) {
const user = await this.userRepository.findOne({ phoneNumber }); this.logger.log(`Finding or creating user with email ${body.email}`);
const user = await this.userRepository.findOne({ email: body.email });
if (!user) { if (!user) {
this.logger.log(`User with phone number ${phoneNumber} not found, creating new user`); this.logger.log(`User with email ${body.email} not found, creating new user`);
return this.userRepository.createUnverifiedUser({ phoneNumber, countryCode, roles: [Roles.GUARDIAN] }); return this.userRepository.createUnverifiedUser({ email: body.email, roles: [Roles.GUARDIAN] });
} }
if (user && user.roles.includes(Roles.GUARDIAN) && user.isProfileCompleted) { if (user && user.roles.includes(Roles.GUARDIAN) && user.isEmailVerified) {
this.logger.error(`User with phone number ${phoneNumber} already exists`); this.logger.error(`User with email ${body.email} already exists`);
throw new BadRequestException('USER.PHONE_NUMBER_ALREADY_EXISTS'); throw new BadRequestException('USER.EMAIL_ALREADY_TAKEN');
} }
if (user && user.roles.includes(Roles.JUNIOR)) { if (user && user.roles.includes(Roles.JUNIOR)) {
this.logger.error(`User with phone number ${phoneNumber} is an already registered junior`); this.logger.error(`User with email ${body.email} is an already registered junior`);
throw new BadRequestException('USER.JUNIOR_UPGRADE_NOT_SUPPORTED_YET'); throw new BadRequestException('USER.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
//TODO add role Guardian to the existing user and send OTP //TODO add role Guardian to the existing user and send OTP
} }
this.logger.log(`User with phone number ${phoneNumber} and country code ${countryCode} found successfully`); this.logger.log(`User with email ${body.email}`);
return user; return user;
} }

View File

@ -1,13 +1,18 @@
import { forwardRef, Module } from '@nestjs/common'; import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { NotificationModule } from '~/common/modules/notification/notification.module'; import { NotificationModule } from '~/common/modules/notification/notification.module';
import { CustomerModule } from '~/customer/customer.module';
import { AdminUserController, UserController } from './controllers'; import { AdminUserController, UserController } from './controllers';
import { Device, User, UserRegistrationToken } from './entities'; import { Device, User, UserRegistrationToken } from './entities';
import { DeviceRepository, UserRepository, UserTokenRepository } from './repositories'; import { DeviceRepository, UserRepository, UserTokenRepository } from './repositories';
import { DeviceService, UserService, UserTokenService } from './services'; import { DeviceService, UserService, UserTokenService } from './services';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([User, Device, UserRegistrationToken]), forwardRef(() => NotificationModule)], imports: [
TypeOrmModule.forFeature([User, Device, UserRegistrationToken]),
forwardRef(() => NotificationModule),
forwardRef(() => CustomerModule),
],
providers: [UserService, DeviceService, UserRepository, DeviceRepository, UserTokenRepository, UserTokenService], providers: [UserService, DeviceService, UserRepository, DeviceRepository, UserTokenRepository, UserTokenService],
exports: [UserService, DeviceService, UserTokenService], exports: [UserService, DeviceService, UserTokenService],
controllers: [UserController, AdminUserController], controllers: [UserController, AdminUserController],