npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@slonum/common

v4.6.0

Published

backend kit

Downloads

34

Readme

Краткое руководство

Ключевые компоненты

  • Модуль авторизации

Подключение:

import globals, { ACCESS_SECRET } from './config/global.config';
import { JwtModule } from '@slonum/common';

@Module({
  imports: [JwtModule.register({ ACCESS_SECRET: globals()[ACCESS_SECRET]})],
})
export class AppModule {}

Использование:

@Auth()
@Get('me')
getCurrentUser() {
  // user-info
}

@Auth('ADMIN')
@Get('secret')
getSecretData() {
  // секретные данные
}
  • Модуль Rmq. Подключение и регистрация сервисов

Подключение:

// app.module.ts

// Сначала импортируем в AppModule.
// Это необходимо, чтобы сработала инъекция ConfigService в экспортируемый RmqService
// Делать для этого ничего не нужно, инъекция произойдет сама
import { RmqModule } from '@slonum/common';

@Module({
  imports: [RmqModule],
})
export class AppModule {}

// main.ts

const rmqService: RmqService = app.get(RmqService);
app.connectMicroservice(rmqService.getRmqOptions('название очереди'));
await app.startAllMicroservices();

Регистрация сервиса:

import { RmqModule } from '@slonum/common';

@Module({
  imports: [RmqModule.register({ service: 'NAME_SERVICE', queue: 'queue' })],
})
export class AppModule {}

В этом случае также экспортируется RmqService для подключения очереди в main.ts. Также можно перенастроить дефолтные опции

export interface SimplifiedRmqOptions {
  /**
   * Название токена сервиса для DI
   */
  service: string;
  /**
   * Название очереди
   */
  queue: string;

  extras?: RmqOptions;
}
  • Куки авторизации

env для дева

CORS_ORIGIN_CONFIG=".slonum.ru$
localhost"

env для прода

CORS_ORIGIN_CONFIG=".slonum.ru$"

Устанавливаем cookie-parser

npm i cookie-parser

Подключаем его

// main.ts
import * as cookieParser from 'cookie-parser';

// Настраиваем cors
const configService: ConfigService = app.get(ConfigService);
  const origin: RegExp[] = configService
    .get<string>('CORS_ORIGIN_CONFIG')
    ?.split('\n')
    .map((item: string): RegExp => new RegExp(item));
  if (!origin) logger.warn('Не удалось прочитать CORS_ORIGIN_CONFIG');
  app.enableCors({ origin, credentials: true });
// Подключаем куки в свагере
const config = new DocumentBuilder()
    .setTitle('Authentication')
    .setDescription('Here we can find all API methods of Authentication')
    .setVersion(configService.get<string>('npm_package_version'))
    .addCookieAuth()
    .build();

// Подключаем парсер к сервису
app.use(cookieParser())

Установка куки

import { Response } from 'express';
import { setTokenCookies } from '@slonum/common';

@Post('register-participant')
async registerParticipant(@Res() res: Response, @Body() registerParticipantDto: RegisterParticipantDto): ResponseDto {
  const responseDto: ResponseDto = await this.participantService.registerParticipant(registerParticipantDto);
  setTokenCookies(res, response.tokens);
  return response; 
}

При необходимости третьим аргументом можно передать CookieOptions

setTokenCookies(res, tokens, { ...cookieOptions })

Настройка кук для локалхоста

setTokenCookies(res, tokens, { domain: null })

Содержимое библиотеки

Types

Interfaces

Dtos

Enums

Utils

Passport Strategies

Guards

  • AtGuard
  • RolesGuard

Pipes

  • ValidationPipe

Middlewares

Message Services

BaseMessageService

Базовый класс сервисов сообщений

Auth

UserInfo

Loggers

Exception Filters

Decorators

Types

AuthData

Данные сущности User в сервисе slonum-auth

export type AuthData = {
  login?: string;
  email?: string;
  password?: string;
  vkId?: number;
  googleId?: string;
  metadata?: AuthMetaData;
};

AuthMetaData

export type AuthMetaData = {
  ipAddress?: string;
  userAgent?: string;
};

JwtPayload

Данные access_token

import { IRole } from '../interfaces/role.interface';

export type JwtPayload = {
  id: number;
  email: string;
  vkId: number;
  emailConfirmed: boolean;
  googleId: string;
  roles: IRole[];
};

JwtPayloadRT

Данные refresh_token

export type JwtPayloadRT = {
  id: number;
  userId: number;
  userAgent: string;
  ipAddress: string;
}

Name

export type Name = {
  firstName: string;
  lastName?: string;
};

Tokens

export type Tokens = {
  accessToken: string;
  refreshToken: string;
};

Interfaces

IProfile

Базовый интерфейс профиля. От него наследуются интерфейсы профилей родителя и ребёнка. Его поля содержаться в обоих наследуемых интерфейсах

export interface IProfile {
  firstName?: string;
  lastName?: string;
  fullName?: string;
  city?: string;
  avatarLink?: string;
  registrationSource: RegistrationSource;
}

IChildProfile

export interface IChildProfile extends IProfile {
  login: string;
  password: string;
  birthDate?: Date | string;
  parentProfileId?: number;
  parentProfile: IParentProfile;
}

IParentProfile

export interface IParentProfile extends IProfile {
  email: string;
  children?: IChildProfile[];
}

IRefreshToken

export interface IRefreshToken {
  id: number;
  userId: number;
  user: IUser;
  userAgent: string;
  ipAddress: string;
}

IRequest

export interface IRequest {
  err?: any;
  user: JwtPayload;
  info: any;
  context: any;
  status: any;
}

IRole

export interface IRole {
  id: number;
  value: RoleEnum;
  description: string;
}

IRpcException

В таком виде могут приходить исключения из сервисов при обращении к ним через rabbit.

export interface IRpcException {
  response: { statusCode: number; message: string; error: string };
  name: string;
  message: string;
  status: number;
  error?: IRpcException;
}

IUser

Интерфейс сущности User в slonum-auth

export interface IUser {
  id: number;
  login?: string;
  email?: string;
  vkId?: number;
  roles?: IRole[];
  emailConfirmed: boolean;
  googleId?: string;
  passwordHash?: string;
  refreshTokens?: IRefreshToken[];
}

Dtos

RegisterDto

export class RegisterDto {
  @ApiProperty({ description: 'Email родителя', example: '[email protected]', required: false })
  @IsEmail({}, { message: 'Неверно указан email' })
  @IsOptional()
  parentEmail?: string;

  @ApiProperty({ description: 'Пароль пользователя', example: 'password123' })
  @IsString({ message: 'Должно быть строкой' })
  @Length(MIN_PASSWORD_LENGTH, undefined, { message: `Минимальная длина - ${MIN_PASSWORD_LENGTH}` })
  password: string;

  @ApiProperty({ description: 'Фамилия, имя родителя', example: 'Иванов Иван', required: false })
  @IsString()
  @IsOptional()
  parentFullName?: string;

  @ApiProperty({ description: 'Город', example: 'Москва', required: false })
  @IsString()
  @IsOptional()
  city?: string;

  @ApiProperty({
    description: 'Мероприятие, через которое происходит регистрация. По умолчанию будет главная страница',
    example: RegistrationSource.OLYMPIAD,
    required: false,
  })
  @IsEnum(RegistrationSource)
  @IsOptional()
  registrationSource?: RegistrationSource;

  @ApiProperty({ type: ChildDto, description: 'Данные ребёнка', required: false })
  @IsOptional()
  childDto?: ChildDto;

  metaData: AuthMetaData;
}

RegisterResponseDto

export class RegisterResponseDto {
  @ApiProperty({ description: 'id пользователя', example: 1, type: 'number ' })
  userId?: number;

  @ApiProperty({ description: 'Токены пользователя', example: { accessToken: 'accessToken', refreshToken: 'refreshToken' } })
  tokens: Tokens;

  @ApiProperty({ description: 'Данные для входа в аккаунт ребёнка', type: LoginDto, required: false })
  childLoginDto?: LoginDto;
}

ChildDto

export class ChildDto {
  @ApiProperty({ description: 'Фамилия, имя ребенка', example: 'Иванова Анна', required: false })
  @IsOptional()
  @IsString()
  childFullName?: string;

  @ApiProperty({ description: 'Дата рождения ребенка', example: '2000-01-01T00:00:00.000Z', required: false })
  @Type(() => Date)
  @IsDate()
  @IsOptional()
  birthDate?: Date;

  @ApiProperty({ description: 'Город', example: 'Москва', required: false })
  @IsString()
  @IsOptional()
  city?: string;
}

LoginDto

export class LoginDto {
  @ApiProperty({ description: 'Логин пользователя', example: '[email protected]' })
  @IsString()
  login: string;

  @ApiProperty({ description: 'Пароль пользователя', example: 'password123' })
  @IsString()
  password: string;

  authMetaData?: AuthMetaData;

  @ApiResponseProperty({ type: Number, example: 1 })
  childId?: number;
}

UpdateProfileDto

export class AuthDto {
  @ApiProperty({ example: 'john_doe', description: 'Логин пользователя', required: false })
  @IsOptional()
  @IsString()
  login?: string;

  @ApiProperty({ example: 'old_password', description: 'Старый пароль пользователя', required: false })
  @IsOptional()
  @IsString()
  oldPassword?: string;

  @ApiProperty({ example: 'new_password', description: 'Новый пароль пользователя', required: false })
  @IsOptional()
  @IsString()
  newPassword?: string;

  @ApiProperty({ example: 'new_password', description: 'Подтверждение нового пароля', required: false })
  @IsOptional()
  @IsString()
  passwordConfirm?: string;

  @ApiProperty({ example: '[email protected]', description: 'Адрес электронной почты пользователя', required: false })
  @IsOptional()
  @IsEmail()
  email?: string;
}

export class ProfileDto {
  @ApiProperty({ example: 'Иван Иванов', description: 'Полное имя пользователя', required: false })
  @IsOptional()
  @IsString()
  fullName?: string;

  @ApiProperty({ example: 'Нью-Йорк', description: 'Город проживания пользователя', required: false })
  @IsOptional()
  @IsString()
  city?: string;

  @ApiProperty({ example: '1990-01-01', description: 'Дата рождения пользователя', required: false })
  @IsOptional()
  @IsDate()
  @Type(() => Date)
  birthDate?: Date;

  @ApiProperty({ example: 'https://example.com/avatar.jpg', description: 'URL аватара пользователя', required: false })
  @IsOptional()
  @IsString()
  avatarUrl?: string;
}

export class UpdateProfileDto {
  @ApiProperty({ type: ProfileDto, description: 'Данные профиля', required: false })
  @IsOptional()
  profileDto?: ProfileDto;

  @ApiProperty({ type: AuthDto, description: 'Данные авторизации', required: false })
  @IsOptional()
  authDto?: AuthDto;

  @ApiProperty({ example: 1, description: 'ID ребенка. Нужно передать, если родитель редактирует профиль ребёнка', required: false })
  @IsOptional()
  childId?: number;
}

Enums

RegistrationSourceEnum

export enum RegistrationSource {
  MAIN = 'MAIN',
  BLOG = 'BLOG',
  DRAWING_COMPETITION = 'DRAWING_COMPETITION',
  ENGLISH_LANG = 'ENGLISH_LANG',
  FRACTION = 'FRACTION',
  LK = 'LK',
  OLYMPIAD = 'OLYMPIAD',
  VOCABULARY_WORDS = 'VOCABULARY_WORDS',
}

RoleEnum

export enum RoleEnum {
  ADMIN = 'ADMIN',
  PARENT = 'PARENT',
  CHILD = 'CHILD',
}

Utils

splitFullName

export function splitFullName(fullName: string): Name {
  const [firstName, lastName] = fullName.split(' ');
  return { firstName, lastName };
}

joinFullName

export function joinFullName(name: Name): string {
  return `${name.firstName} ${name.lastName}`;
}

setTokenCookies

export function setTokenCookies(res: Response, tokens: Tokens): void {
  res.cookie('access_token', tokens.accessToken, { httpOnly: true, sameSite: 'strict', secure: true });
  res.cookie('refresh_token', tokens.refreshToken);
}

removeTokenCookies

export function removeTokenCookies(res: Response): void {
  res.clearCookie('access_token');
  res.clearCookie('refresh_token');
}

Strategies

AtStrategy

AtStrategy импортируется при регистрации JwtModule

@Injectable()
export class AtStrategy extends PassportStrategy(Strategy, 'jwt') {
  constructor(@Inject(JWT_OPTIONS_TOKEN) { ACCESS_SECRET }: JwtModuleOptions) {
    super({
      jwtFromRequest: ExtractJwt.fromExtractors([ExtractJwt.fromAuthHeaderAsBearerToken(), AtStrategy.extractJwtFromCookies]),
      ignoreExpiration: false,
      secretOrKey: ACCESS_SECRET || 'ACCESS_SECRET',
    });
  }

  private static extractJwtFromCookies(req: Request): string | null {
    if (req.cookies && req.cookies.access_token) {
      return req.cookies.access_token;
    }
    return null;
  }

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

Middlewares

LoggerMiddleware

Применение:

@Module({})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}

Message Services

BaseMessageService

От него наследуются остальные сервисы сообщений

AuthService. Использоваться должен ТОЛЬКО в user-info. Из других сервисов запросы сюда поступать не должны

Регистрация.

async register(authData: AuthData, metaData: AuthMetaData, role: RoleEnum): Promise<RegisterResponseDto> {
    return this.authMessageService.send({ authData, metaData, role }, AuthMessagePatterns.REGISTER);
  }

Обновление User

async updateUser(updateUserDto: IUpdateUser): Promise<IUser> {
    return this.authMessageService.send(updateUserDto, AuthMessagePatterns.UPDATE_USER);
  }

Удаление User

async deleteUser(toDeleteId: number): Promise<number> {
    return this.authMessageService.send({ id: toDeleteId }, AuthMessagePatterns.DELETE);
  }

Подтвердить email(Неизвестно работает ли этот эндпоинт)

async sendConfirmationEmail(user: IUser): Promise<boolean> {
    return this.authMessageService.send(user, AuthMessagePatterns.SEND_CONFIRM_EMAIL);
  }

Выдать роль. Единственный эндпоинт, который, можно использовать с других сервисов, но, опять же, я не знаю работает ли он и зачем он у нас есть, так как роли у нас выдаются по http.

async provideUserRole(provideRoleDto: IProvideUserRole): Promise<IUser> {
    return this.authMessageService.send(provideRoleDto, AuthMessagePatterns.PROVIDE_ROLE);
  }

Поиск User по id

async findOneById(id: number): Promise<IUser | null> {
    return this.authMessageService.send({ id }, AuthMessagePatterns.FIND_ONE_BY_ID);
  }

ProfileService. Сервис для обращения к slonum-user-info

Регистрация. Создаёт профиль и User в auth. Можно регистрировать и родителя отдельно, и родителя с ребёнком. Если передан childFullName, значит регистрация происходит не через главную страницу, а через страницу мероприятия, следовательно также необходимо передать с какого мероприятия происходит регистрация

async register(createUserInfoDto: RegisterDto): Promise<RegisterResponseDto> {
    return this.profileMessageService.send(createUserInfoDto, ProfileMessagePatterns.REGISTER);
  }

Получение профиля по id

async getProfileById(id: number): Promise<IProfile> {
    return this.profileMessageService.send({ id }, ProfileMessagePatterns.GET_PROFILE_BY_ID);
  }

Получение нескольких профилей по массиву id

async getProfilesByIds(ids: number[]): Promise<IProfile[]> {
    return this.profileMessageService.send({ ids }, ProfileMessagePatterns.GET_PROFILES_BY_IDS);
  }

Обновление данных профиля и User. При обращении через rabbit схема запроса должна выглядеть следующим образом:

{
  "user": { ...JwtPayload },
  "updateProfileDto": { ...здесь_обычная_схема }
}
async updateProfile(user: JwtPayload, udpateProfileDto: UpdateProfileDto): Promise<IProfile> {
    return this.profileMessageService.send({ user, udpateProfileDto }, ProfileMessagePatterns.UPDATE_PROFILE);
  }

Проверяет принадлежит ли ребёнок родителю.

async checkParentByChild(parentId, childId): Promise<boolean> {
    return this.profileMessageService.send({ parentId, childId }, ProfileMessagePatterns.CHECK_PARENT_BY_CHILD);
  }

Импортирование:

import { ProfileModule } from '@slonum/common';

@Module({
  imports: [ProfileModule],
})
export class ParticipantModule {}

Применение:

import { ProfileService } from '@slonum/common';
import { RegisterDto } from '@slonum/common';

@Injectable()
export class ParticipantService {
  constructor(
    private readonly profileService: ProfileService, // Зарегистрированный сервис
  ) {}

  async registerParticipant(registerDto: RegisterDto, ...rest) {
    const registerResponseDto: RegisterResponseDto = await this.profileService.register(registerDto);
    // Ваш код
  }
}

Loggers

RpcExceptionLogger

Логгер для ошибок в rabbit контроллерах Глобальное применение

// main.ts
app.useGlobalFilters(new RpcExceptionLogger());

Применение к контроллеру

@Controller()
@UseFilters(new RpcExceptionLogger())
export class ProfileRabbitController

Просто выводит логи ошибок

@Catch(RpcException)
export class RpcExceptionLogger implements RpcExceptionFilter<RpcException> {
  catch(exception: RpcException, host: ArgumentsHost) {
    logger.error(exception);
    return throwError(() => exception.getError());
  }
}

Exception Filters

HttpExceptionFilter

Выводит лог в консоль и формирует ответ с сервера

// main.ts
app.useGlobalFilters(new HttpExceptionFilter());
const logger = new CustomLoggerService();

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();
    const res: any = { ...exception };

    logger.warn(res.response?.message ?? res.message, 'Exception');

    response.status(status).json(res.response);
  }
}

RpcExceptionFilter

Глобальное применение

// main.ts
app.useGlobalFilters(new RpcExceptionFilter());

Применение к контроллеру

@Controller()
@UseFilters(new RpcExceptionFilter())
export class ProfileRabbitController
@Catch(RpcException)
export class RpcExceptionFilter implements ExceptionFilter {
  catch(exception: RpcException, host: ArgumentsHost) {
    let err = exception.getError() as IRpcException;
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    if (err.error) err = err.error;
    logger.error(err);
    if (!err.response) {
      return response.status(500).json({ statusCode: 500, error: 'Internal server error', message: 'Internal server error' });
    }

    response.status(err.response?.statusCode ?? err.status).json(err.response ?? err);
  }
}

Decorators

Auth

Содержит в себе все необходимые декораторы для настройки доступа к эндпоинту Параметр roles - Роли, требуемые для доступа к эндпоинту. Опционально

export function Auth(...roles: string[]) {
  return applyDecorators(SetMetadata(ROLES_KEY, roles), UseGuards(AtGuard, RolesGuard), ApiBearerAuth('jwt'));
}

Примеры использования:

@ApiOperation({ summary: 'Добавление ребёнка', description: 'Логин для ребёнка генерируется автоматически' })
  @Post('add-child')
  @ApiBody({ type: ChildDto })
  @ApiCreatedResponse({ type: LoginDto, description: 'Логин и пароль ребёнка' })
  @Auth('PARENT')
  async addChild(
    @GetJwtPayload('id') parentId: number,
    @Body() childDto: ChildDto,
    @MetaData() metaData: AuthMetaData,
  ): Promise<LoginDto> {
    return this.profileService.addChild(parentId, childDto, metaData);
  }
@ApiOperation({ summary: 'Получение данных о текущем пользователе', description: 'Данные получаются по id пользователя из токена' })
  @ApiResponse({ type: Profile })
  @Auth()
  @Get()
  async getCurrentUserById(@GetJwtPayload('id') id: number): Promise<IProfile> {
    return this.profileService.getCurrentUserById(id);
  }

GetJwtPayload

Параметр data — ключ JwtPayload Возвращает декодированный токен, если не передан data Возвращает значение data из токена, если передан

export const GetJwtPayload = createParamDecorator(
  (data: keyof JwtPayload | undefined, context: ExecutionContext): JwtPayload | JwtPayload[keyof JwtPayload] => {
    const user = context.switchToHttp().getRequest().user;
    if (!data) return user;
    return user[data];
  },
);

GetRtJwtPayload

Параметр data — ключ JwtPayloadRT Возвращает декодированный токен, если не передан data Возвращает значение data из токена, если передан

export const GetRtJwtPayload = createParamDecorator(
  (data: keyof JwtPayloadRT | undefined, context: ExecutionContext): JwtPayloadRT | JwtPayloadRT[keyof JwtPayloadRT] => {
    const user = context.switchToHttp().getRequest().user;
    if (!data) return user;
    return user[data];
  },
);

Metadata

Возвращает AuthMetaData

export const MetaData = createParamDecorator((data: unknown, ctx: ExecutionContext): AuthMetaData => {
  const req = ctx.switchToHttp().getRequest();
  return { ipAddress: req.ip, userAgent: req.headers['user-agent'] };
});