[NestJS 带你飞!] DAY30 - 实战演练 (中)

API 设计

前面有提到这次实作的系统共有两大资源,分别是:使用者 (user) 与 待办事项 (todo),事实上,以 API 的角度来看会多一个资源,那就是登入与注册的 身分验证 (Authentication)

身分验证

运用 Authentication 技巧来实作身分验证。

提醒:Authentication 的技巧可以参考 DAY23 - Authentication (上)DAY24 - Authentication (下)

Guards

我们可以先把本地策略与 JWT 所用到的 Guard 进行包装,在 src/core/guards 下新增 jwt-auth.guard.tslocal-auth.guard.ts

$ nest generate guard core/guards/jwt-auth
$ nest generate guard core/guards/local-auth

调整 JwtAuthGuard 的内容:

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

调整 LocalAuthGuard 的内容:

import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

建立 index.ts 做汇出管理:

export { JwtAuthGuard } from './jwt-auth.guard';
export { LocalAuthGuard } from './local-auth.guard';

提醒:Guard 的功能可以参考 DAY13 - Guard

使用者模组

我们需要建立 UserModuleUserService 来提供我们取得使用者资讯的操作:

$ nest generate module features/user
$ nest generate service features/user

建立完成後,先不急着更动它们,回想一下我们前面在处理使用者密码的时候,特别设计了一个方法来达成加密,我们先将其建立起来,在 src/core/utils 资料夹下 common.utility.ts

import { randomBytes, pbkdf2Sync } from 'crypto';

export class CommonUtility {
  public static encryptBySalt(
    input: string,
    salt = randomBytes(16).toString('hex'),
  ) {
    const hash = pbkdf2Sync(input, salt, 1000, 64, 'sha256').toString('hex');
    return { hash, salt };
  }
}

调整 UserModule 的内容,将 UserService 汇出并引入 MongooseModule 来建立 UserModel,进而操作 MongoDB 中使用者的 Collection,而需要带入的 Definition 为 UserDefinition

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { UserDefinition } from '../../common/models/user.model';

import { UserService } from './user.service';

@Module({
  imports: [MongooseModule.forFeature([UserDefinition])],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

我们先将注册使用者时会使用到的 CreateUserDto 建立起来,在 src/features/user/dto 资料夹下新增 create-user.dto.ts

import { IsEmail, IsEnum, MaxLength, MinLength } from 'class-validator';

import {
  USER_PASSWORD_MAX_LEN,
  USER_PASSWORD_MIN_LEN,
  USER_USERNAME_MAX_LEN,
  USER_USERNAME_MIN_LEN,
} from '../../../common/constants/user.const';
import { Role } from '../../../common/enums/role.enum';

export class CreateUserDto {
  @MinLength(USER_USERNAME_MIN_LEN)
  @MaxLength(USER_USERNAME_MAX_LEN)
  public readonly username: string;

  @MinLength(USER_PASSWORD_MIN_LEN)
  @MaxLength(USER_PASSWORD_MAX_LEN)
  public readonly password: string;

  @IsEmail()
  public readonly email: string;

  @IsEnum(Role)
  public readonly role: Role;
}

修改 UserService 的内容,透过 @InjectModel 装饰器指定 USER_MODEL_TOKEN 来注入 UserModel

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { USER_MODEL_TOKEN, UserDocument } from '../../common/models/user.model';

@Injectable()
export class UserService {
  constructor(
    @InjectModel(USER_MODEL_TOKEN)
    private readonly userModel: Model<UserDocument>,
  ) {}
}

我们这套系统的注册 API 主要是用来注册预设使用者,并透过该预设使用者来新增其他使用者,所以注册 API 只在没有任何使用者的情况下才能使用,故 UserService 不仅需要设计 createUser 方法来建立使用者,还需提供 hasUser 方法来确认是否有使用者资料在资料库中,又因为登入需要进行身分核对,所以还需要设计 findUser 来取得对应的使用者资料:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilterQuery, Model } from 'mongoose';

import { CommonUtility } from '../../core/utils';

import { USER_MODEL_TOKEN, UserDocument } from '../../common/models/user.model';

import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UserService {
  constructor(
    @InjectModel(USER_MODEL_TOKEN)
    private readonly userModel: Model<UserDocument>,
  ) {}

  public async createUser(user: CreateUserDto) {
    const { username, email, role } = user;
    const password = CommonUtility.encryptBySalt(user.password);
    const document = await this.userModel.create({
      username,
      password,
      email,
      role,
    });
    return document?.toJSON();
  }

  public async findUser(filter: FilterQuery<UserDocument>, select?: any) {
    const query = this.userModel.findOne(filter).select(select);
    const document = await query.exec();
    return document?.toJSON();
  }

  public async hasUser() {
    const count = await this.userModel.estimatedDocumentCount().exec();
    return count > 0;
  }
}

建立 index.ts 来做汇出管理:

export { UserModule } from './user.module';
export { UserService } from './user.service';
export { CreateUserDto } from './dto/create-user.dto';

验证模组

有了 UserService 让我们可以操作使用者资料之後,就要来实作验证的部分了,需要建立相关元件:

$ nest generate module features/auth
$ nest generate service features/auth
$ nest generate controller features/auth

还记得前面有提过的 Passport 吗?当验证程序通过之後,会将部分资料放入请求物件中,让後续的操作可以使用该资料,我们称它为 载体 (Payload),通常会将部分使用者资讯放入载体中,所以我们要在 src/features/auth/interfaces 资料夹下新增 payload.interface.ts 来将我们的载体定义好介面,主要包含的资料为 idusernamerole

import { Role } from '../../../common/enums/role.enum';

export interface UserPayload {
  id: string;
  username: string;
  role: Role;
}

我们还可以设计装饰器来取得载体内容,在 src/features/auth/decorators 资料夹下新增 payload.decorator.ts

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, context: ExecutionContext) => {
    const request: Express.Request = context.switchToHttp().getRequest();
    return request.user;
  },
);

调整一下 AuthModule 的配置,我们需要运用 PassportModuleJwtModule 来完成一个完整的登入与注册的身分验证机制,所以将它们引入并从 ConfigService 取得相关环境变数,另外,还需要引入 UserModule 让我们可以使用 UserService 来操作使用者资料:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';

import { UserModule } from '../user';

import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';

@Module({
  imports: [
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        const secret = config.get('secrets.jwt');
        return {
          secret,
          signOptions: {
            expiresIn: '3600s',
          },
        };
      },
    }),
    UserModule,
  ],
  providers: [AuthService],
  controllers: [AuthController],
})
export class AuthModule {}

接着,调整 AuthService 的内容,在里面设计 validateUser 来验证是否为合法的使用者,以及设计 generateJwt 来产生 JWT 让使用者可以透过该 token 存取资源:

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

import { CommonUtility } from '../../core/utils/common.utility';

import { UserPayload } from './interfaces/payload.interface';

import { UserService } from '../user';

@Injectable()
export class AuthService {
  constructor(
    private readonly userService: UserService,
    private readonly jwtService: JwtService,
  ) {}

  public async validateUser(username: string, password: string) {
    const user = await this.userService.findUser({ username });
    const { hash } = CommonUtility.encryptBySalt(
      password,
      user?.password?.salt,
    );
    if (!user || hash !== user?.password?.hash) {
      return null;
    }
    return user;
  }

  public generateJwt(payload: UserPayload) {
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

完成 AuthService 以後,我们要把相关的验证策略建立起来,这样才能完整走完 Passport 的验证机制,首先,我们建立 LocalStrategy 来处理登入时使用的验证策略,在 src/features/auth/strategies 资料夹下新增 local.strategy.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';

import { UserPayload } from '../interfaces/payload.interface';

import { AuthService } from '../auth.service';

@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super();
  }

  public async validate(username: string, password: string) {
    const user = await this.authService.validateUser(username, password);
    if (!user) {
      throw new UnauthorizedException();
    }
    const payload: UserPayload = {
      id: user._id,
      username: user.username,
      role: user.role,
    };
    return payload;
  }
}

有登入使用的验证策略之後,还需要设计登入期间的验证策略,也就是针对 JWT 的验证,在 src/features/auth/strategies 资料夹下新增 jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';

import { ExtractJwt, Strategy } from 'passport-jwt';

import { UserPayload } from '../interfaces/payload.interface';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get('secrets.jwt'),
    });
  }

  validate(payload: UserPayload) {
    return payload;
  }
}

设计好验证策略後,需要将它们添加到 AuthModule 下的 providers

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';

import { UserModule } from '../user';

import { LocalStrategy } from './strategies/local.strategy';
import { JwtStrategy } from './strategies/jwt.strategy';

import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';

@Module({
  imports: [
    PassportModule,
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        const secret = config.get('secrets.jwt');
        return {
          secret,
          signOptions: {
            expiresIn: '3600s',
          },
        };
      },
    }),
    UserModule,
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

最後,就是调整 AuthController 的内容,设计 signupsignin 来实作注册与登入的效果:

import {
  Body,
  Controller,
  ForbiddenException,
  Post,
  UseGuards,
} from '@nestjs/common';

import { LocalAuthGuard } from '../../core/guards';

import { CreateUserDto, UserService } from '../user';

import { User } from './decorators/payload.decorator';
import { UserPayload } from './interfaces/payload.interface';

import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly userService: UserService,
  ) {}

  @Post('/signup')
  async signup(@Body() dto: CreateUserDto) {
    const hasUser = await this.userService.hasUser();
    if (hasUser) {
      throw new ForbiddenException();
    }
    const user = await this.userService.createUser(dto);
    const { _id: id, username, role } = user;
    return this.authService.generateJwt({ id, username, role });
  }

  @UseGuards(LocalAuthGuard)
  @Post('/signin')
  signin(@User() user: UserPayload) {
    return this.authService.generateJwt(user);
  }
}

建立 index.ts 做汇出管理:

export { AuthModule } from './auth.module';
export { UserPayload } from './interfaces/payload.interface';
export { User } from './decorators/payload.decorator';

使用者

预计会设计以下几个使用者相关的 API:

  • [GET] /users:取得使用者列表。
  • [POST] /users:新增使用者。
  • [DELETE] /users/:id:删除特定使用者。
  • [PATCH] /users/:id:更新特定使用者。

由於有更新使用者的部分,所以还需要设计对应的 DTO,在 src/features/user/dto 资料夹下新增 update-user.dto.ts,并使用 PartialType 继承 CreateUserDto 的属性:

import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}

另外,取得使用者列表的部分应当给予单次取得的上限值以及要跳过几笔资料,故需要设计一个 SearchDto 来定义相关的参数,在 src/core/bases 资料夹下新增 search.dto.ts

import { IsOptional } from 'class-validator';

export class SearchDto {
  @IsOptional()
  skip?: number;

  @IsOptional()
  limit?: number;
}

建立 index.ts 做汇出管理:

export { SearchDto } from './search.dto';

注意:会将 SearchDto 放在 core/bases 资料夹下是为了让其他 API 有更多的扩充空间,让它们的 DTO 来继承。

可以透过设计 Pipe 来将输入进来的 limitskip 限制在合理的范围内以及预设值,在 src/core/pipes 资料夹下新增 search.pipe.ts

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class SearchPipe implements PipeTransform<Record<string, any>> {
  private readonly DEFAULT_LIMIT = 30;
  private readonly MAX_LIMIT = 50;
  private readonly DEFAULT_SKIP = 0;

  transform(value: Record<string, any>, metadata: ArgumentMetadata) {
    const { limit, skip } = value;
    value.limit = this.setLimit(parseInt(limit));
    value.skip = this.setSkip(parseInt(skip));
    return value;
  }

  private setLimit(limit: number): number {
    if (!limit) {
      return this.DEFAULT_LIMIT;
    }
    if (limit > this.MAX_LIMIT) {
      return this.MAX_LIMIT;
    }
    return limit;
  }

  private setSkip(skip: number): number {
    if (!skip) {
      return this.DEFAULT_SKIP;
    }
    return skip;
  }
}

建立 index.ts 做汇出管理:

export { SearchPipe } from './search.pipe';

接着,修改 UserService 的内容,新增 findUsers 来取得使用者列表、deleteUser 来删除使用者、 updateUser 来更新使用者以及用来检查是否有重复注册使用者的 existUser

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { FilterQuery, Model } from 'mongoose';

import { CommonUtility } from '../../core/utils';
import { SearchDto } from '../../core/bases/dto';

import { USER_MODEL_TOKEN, UserDocument } from '../../common/models/user.model';

import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UserService {
  constructor(
    @InjectModel(USER_MODEL_TOKEN)
    private readonly userModel: Model<UserDocument>,
  ) {}

  public async createUser(user: CreateUserDto) {
    const { username, email, role } = user;
    const password = CommonUtility.encryptBySalt(user.password);
    const document = await this.userModel.create({
      username,
      password,
      email,
      role,
    });
    return document?.toJSON();
  }

  public async findUser(filter: FilterQuery<UserDocument>, select?: any) {
    const query = this.userModel.findOne(filter).select(select);
    const document = await query.exec();
    return document?.toJSON();
  }

  public async findUsers(search: SearchDto, select?: any) {
    const { skip, limit } = search;
    const query = this.userModel.find().select(select);
    const documents = await query.skip(skip).limit(limit).exec();
    return documents.map((document) => document?.toJSON());
  }

  public async deleteUser(userId: string) {
    const document = await this.userModel.findByIdAndRemove(userId).exec();
    if (!document) {
      return;
    }
    return {};
  }

  public async updateUser(userId: string, data: UpdateUserDto, select?: any) {
    const obj: Record<string, any> = { ...data };
    if (obj.password) {
      obj.password = CommonUtility.encryptBySalt(obj.password);
    }
    const query = this.userModel
      .findByIdAndUpdate(userId, obj, { new: true })
      .select(select);
    const document = await query.exec();
    return document?.toJSON();
  }

  public existUser(filter: FilterQuery<UserDocument>) {
    return this.userModel.exists(filter);
  }

  public async hasUser() {
    const count = await this.userModel.estimatedDocumentCount().exec();
    return count > 0;
  }
}

透过 CLI 产生 UserController

$ nest generate controller features/user

修改 UserController 的内容以符合我们的 API 需求:

import {
  Body,
  ConflictException,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  Param,
  Patch,
  Post,
  Query,
  UseGuards,
} from '@nestjs/common';

import { SearchPipe } from '../../core/pipes';
import { JwtAuthGuard } from '../../core/guards';
import { SearchDto } from '../../core/bases/dto';

import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

import { UserService } from './user.service';

@UseGuards(JwtAuthGuard)
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  async getUsers(@Query(SearchPipe) query: SearchDto) {
    return this.userService.findUsers(query, '-password');
  }

  @Post()
  async createUser(@Body() dto: CreateUserDto) {
    const { username, email } = dto;
    const exist = await this.userService.existUser({
      $or: [{ username }, { email }],
    });

    if (exist) {
      throw new ConflictException('username or email is already exist.');
    }

    const user = await this.userService.createUser(dto);
    const { password, ...result } = user;
    return result;
  }

  @Delete(':id')
  async deleteUser(@Param('id') id: string) {
    const response = await this.userService.deleteUser(id);
    if (!response) {
      throw new ForbiddenException();
    }
    return response;
  }

  @Patch(':id')
  async updateUser(@Param('id') id: string, @Body() dto: UpdateUserDto) {
    const user = await this.userService.updateUser(id, dto, '-password');
    if (!user) {
      throw new ForbiddenException();
    }
    return user;
  }
}

待办事项

透过 CLI 快速产生相关元件:

$ nest generate module features/todo
$ nest generate service features/todo
$ nest generate controller features/todo

预计会设计以下几个待办事项相关的 API:

  • [GET] /todos:取得待办事项列表。
  • [POST] /todos:新增待办事项。
  • [DELETE] /todos/:id:删除特定待办事项。
  • [PATCH] /todos/:id:更新特定待办事项。

由於有新增与更新待办事项的部分,所以还需要设计对应的 DTO,在 src/features/todo/dto 资料夹下新增 create-todo.dto.tsupdate-todo.dto.ts

import { IsOptional, MaxLength, MinLength } from 'class-validator';
import {
  TODO_DESCRIPTION_MAX_LEN,
  TODO_TITLE_MAX_LEN,
  TODO_TITLE_MIN_LEN,
} from '../../../common/constants/todo.const';

export class CreateTodoDto {
  @MinLength(TODO_TITLE_MIN_LEN)
  @MaxLength(TODO_TITLE_MAX_LEN)
  public readonly title: string;

  @IsOptional()
  @MaxLength(TODO_DESCRIPTION_MAX_LEN)
  public readonly description?: string;

  @IsOptional()
  public readonly completed?: boolean;
}
import { PartialType } from '@nestjs/mapped-types';

import { CreateTodoDto } from './create-todo.dto';

export class UpdateTodoDto extends PartialType(CreateTodoDto) {}

TodoModule 中引入 MongooseModule 来建立 TodoModel,进而操作 MongoDB 中待办事项的 Collection,而需要带入的 Definition 为 TodoDefinition

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

import { TodoDefinition } from '../../common/models/todo.model';

import { TodoController } from './todo.controller';
import { TodoService } from './todo.service';

@Module({
  imports: [MongooseModule.forFeature([TodoDefinition])],
  controllers: [TodoController],
  providers: [TodoService],
})
export class TodoModule {}

接着,调整 TodoService 的内容,我们要设计 createTodo 新增待办事项、findTodos 取得待办事项列表、deleteTdod 删除特定待办事项、updateTodo 更新特定待办事项:

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { SearchDto } from '../../core/bases/dto';

import { TodoDocument, TODO_MODEL_TOKEN } from '../../common/models/todo.model';

import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';

@Injectable()
export class TodoService {
  constructor(
    @InjectModel(TODO_MODEL_TOKEN)
    private readonly todoModel: Model<TodoDocument>,
  ) {}

  public async createTodo(data: CreateTodoDto) {
    const todo = await this.todoModel.create(data);
    return todo?.toJSON();
  }

  public async findTodos(search: SearchDto, select?: any) {
    const { skip, limit } = search;
    const query = this.todoModel.find().select(select);
    const documents = await query.skip(skip).limit(limit).exec();
    return documents.map((document) => document?.toJSON());
  }

  public async deleteTodo(todoId: string) {
    const document = await this.todoModel.findByIdAndRemove(todoId).exec();
    if (!document) {
      return;
    }
    return {};
  }

  public async updateTodo(todoId: string, data: UpdateTodoDto, select?: any) {
    const query = this.todoModel
      .findByIdAndUpdate(todoId, data, { new: true })
      .select(select);
    const document = await query.exec();
    return document?.toJSON();
  }
}

最後,调整 TodoController 的内容以符合我们的 API 需求:

import {
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  Param,
  Patch,
  Post,
  Query,
  UseGuards,
} from '@nestjs/common';

import { JwtAuthGuard } from '../../core/guards';
import { SearchPipe } from '../../core/pipes';
import { SearchDto } from '../../core/bases/dto';

import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';

import { TodoService } from './todo.service';

@UseGuards(JwtAuthGuard)
@Controller('todos')
export class TodoController {
  constructor(private readonly todoService: TodoService) {}

  @Get()
  async getTodos(@Query(SearchPipe) query: SearchDto) {
    return this.todoService.findTodos(query);
  }

  @Post()
  async createTodo(@Body() dto: CreateTodoDto) {
    return this.todoService.createTodo(dto);
  }

  @Delete(':id')
  async deleteTodo(@Param('id') id: string) {
    const response = await this.todoService.deleteTodo(id);
    if (!response) {
      throw new ForbiddenException();
    }
    return response;
  }

  @Patch(':id')
  async updateTodo(@Param('id') id: string, @Body() dto: UpdateTodoDto) {
    const todo = await this.todoService.updateTodo(id, dto);
    if (!todo) {
      throw new ForbiddenException();
    }
    return todo;
  }
}

小结

今天我们把所有资源的 API 都实作完毕了,包括:authtodosusers。今天篇幅较长可能耗费了各位读者们许多精力,加油!只剩下角色授权验证的部分了,明天再冲刺一天吧!


<<:  29. IT铁人赛30天之後

>>:  [Lesson30] 结语

iris的middleware

middleware 在上篇文章介绍routing时有提到Party时有传入一个handler不知道...

Day11 Vue directives(v-on & v-bind)

到底什麽式Vue directives Vue directives简单来说就是一种可以挂在HTML...

Laravel 实战经验分享 - Day26 Dockerize 你的 Laravel 专案(下)

昨天在文章中提到如何 Dockerize 你的专案,而设定你自己的 Docker Image 就是一...

How to convert RAW to NTFS file system without losing data?

What should you do if the partition on your extern...

纪录工作大小事,来看他人成长

如何改善痛点问题,不只有在工作,也可以用於纪录自己的生活以及家庭。在每个 Sprint 结束都有 R...