Nest JS Graphql с Apollo Federation Gateway для микросервисов #часть-3


Создание Graphql сервиса (микросервис User Auth API) 🚀 🚀 🚀

Вы можете ознакомиться с частью 1/2 этого блога здесь, В этом примере мы построим первый graphql сервис Auth User Service.

Для понимания всей архитектуры нам необходимо иметь

  • шлюз Nestjs сервис (шлюз-сервис)
  • сервис пользователя graphql (Микросервис-1)
  • graphql Home manager сервис (Микросервис-2)

И мы увидим, как мы можем получить пользователя и назначенный дом для пользователя из одной единственной конечной точки api, мы увидим, как шлюз apolo объединяет данные и собирает их вместе в одном api запросе.

Построим систему авторизации пользователей на Graphql

  • мы создаем первый сервис graphql на основе схемы
  • мы будем использовать nestjs/typeorm для сохранения данных в базе данных.

Вот как выглядит наша зависимость

  "dependencies": {
    "@apollo/federation": "^0.36.1",
    "@nestjs/apollo": "^10.0.11",
    "@nestjs/common": "^8.0.0",
    "@nestjs/config": "^2.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/graphql": "^10.0.11",
    "@nestjs/jwt": "^8.0.0",
    "@nestjs/passport": "^8.2.1",
    "@nestjs/platform-express": "^8.0.0",
    "@nestjs/typeorm": "^8.0.3",
    "@sendgrid/mail": "^7.6.2",
    "apollo-server-core": "^3.7.0",
    "apollo-server-express": "^2.25.2",
    "bcrypt": "^5.0.1",
    "class-validator": "^0.13.2",
    "crypto": "^1.0.1",
    "dcrypt": "0.0.2",
    "debug": "^4.3.4",
    "dotenv": "^16.0.0",
    "graphql": "^15.8.0",
    "graphql-tool": "^1.0.0",
    "graphql-tools": "^8.2.8",
    "joi": "^14.3.1",
    "moment": "^2.29.3",
    "nodemailer": "^6.7.4",
    "passport": "^0.5.2",
    "passport-jwt": "^4.0.0",
    "pg": "^8.7.3",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0",
    "ts-morph": "^14.0.0",
    "typeorm": "^0.3.6",
    "url-join-ts": "^1.0.5",
    "winston": "^3.7.2"
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Мы строим базовую систему аутентификации пользователей, используя nestjs/graphql
Наш модуль app будет содержать все модули системы

import { MiddlewareConsumer, Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { UsersModule } from './users/users.module';
import { AuthModule } from './auth/auth.module';
import { join } from 'path';
import { ConfigModule } from './config/config.module';
import { LoggerModule } from './logger/logger.module';
import { DbModule } from './db/db.module';
import { UserEntity } from './users/entity/users.entity';
import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo';
import { GraphQLError, GraphQLFormattedError } from 'graphql';

@Module({
  imports: [
    DbModule.forRoot({
      entities: [UserEntity],
    }),
    GraphQLModule.forRoot({
      typePaths: ['./**/*.graphql'],
      driver: ApolloFederationDriver,
      context: ({ req }: any) => ({ req }),
      formatError: (error: GraphQLError) => {
        const graphQLFormattedError: GraphQLFormattedError = {
          message: error?.extensions?.exception?.response?.message || error?.message,
        };
        return graphQLFormattedError;
      },
      definitions: {
        path: join(process.cwd(), 'src/graphql.classes.ts'),
        outputAs: 'class',
      },
    }),
    UsersModule,
    AuthModule,
    ConfigModule,
    LoggerModule,
  ],
})
export class AppModule {}
Вход в полноэкранный режим Выйти из полноэкранного режима

Мы используем postgres для хранения данных, связанных с пользователем Entity, и эта сущность typeOrm содержит все связанные с пользователем фиэли, такие как

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  BaseEntity,
  ManyToOne,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity('users')
export class UserEntity extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  public id!: string;

  @Column({ type: 'varchar', length: 255, select: true, unique: true })
  public email!: string;

  @Column({ type: 'varchar', length: 500 })
  public password!: string;

  @Column({ type: 'varchar', length: 255, select: true, unique: true })
  public username!: string;

  @Column({ type: 'jsonb', default: [] })
  public permissions!: string[];

  @Column({ type: 'varchar', length: 255, select: true })
  public lowercaseUsername!: string;

  @Column({ type: 'varchar', length: 255, select: true })
  public lowercaseEmail!: string;

  @Column({ type: 'jsonb', default: null })
  public passwordReset!: any;

  @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP', select: true })
  public created_at!: Date;

  @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP', select: true })
  public updated_at!: Date;
}
Войти в полноэкранный режим Выход из полноэкранного режима

Это приложение имеет модуль авторизации пользователя и модуль пользователя, и оба модуля используют схему nestjs Первый подход для nestjs graphql
Определение graphql выглядит простым для простой системы авторизации

type Query {
  login(user: LoginUserInput!): LoginResult!
  refreshToken: String!
}

type LoginResult {
  user: User!
  token: String!
}

input LoginUserInput {
  username: String
  email: String
  password: String!
}
Вход в полноэкранный режим Выход из полноэкранного режима

Для аутентификации мы используем паспортную стратегию для проверки пользователя с помощью имени пользователя/пароля, В этом модуле аутентификации у нас есть два различных запроса, один из которых login, а другой refreshToken, оба могут быть определены в resolver.

когда мы пишем nestjs graphql, мы пишем

  • резольверы (имеющие @Query и @Mutations)
  • сервисы (использующие typeORM)
  • схема graphql
  • основной модуль

Auth resolver здесь будет общаться с auth сервисом

import { Resolver, Args, Query, Context } from '@nestjs/graphql';
import { LoginUserInput, LoginResult } from '../graphql.classes';
import { AuthService } from './auth.service';
import { AuthenticationError } from 'apollo-server-core';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { UseGuards } from '@nestjs/common';
import { UserEntity } from '../users/entity/users.entity';

@Resolver('Auth')
export class AuthResolver {
  constructor(private authService: AuthService) {}

  @Query('login')
  async login(@Args('user') user: LoginUserInput): Promise<LoginResult> {
    try {
      const result = await this.authService.validateUserByPassword(user);

      if (result) return result;
      throw new AuthenticationError('Could not log-in with the provided credentials');
    } catch (err) {
      throw err;
    }
  }

  // There is no username guard here because if the person has the token, they can be any user
  @Query('refreshToken')
  @UseGuards(JwtAuthGuard)
  async refreshToken(@Context('req') request: any): Promise<string> {
    const user: UserEntity = request.user;
    if (!user) throw new AuthenticationError('Could not log-in with the provided credentials');
    const result = await this.authService.createJwt(user);
    if (result) return result.token;
    throw new AuthenticationError('Could not log-in with the provided credentials');
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наш Auth-сервис проверит пользователя и позволит ему войти в систему с генерацией токена

import { Injectable, forwardRef, Inject, ConsoleLogger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import { JwtPayload } from './interfaces/jwt-payload.interface';
import { LoginUserInput, User, LoginResult } from '../graphql.classes';
import { ConfigService } from '../config/config.service';
import { resolve } from 'path';
import { Console } from 'console';
import { UserEntity } from '../users/entity/users.entity';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => UsersService))
    private usersService: UsersService,
    private jwtService: JwtService,
    private configService: ConfigService
  ) {}

  /**
   * Checks if a user's password is valid
   *
   * @param {LoginUserInput} loginAttempt Include username or email. If both are provided only
   * username will be used. Password must be provided.
   * @returns {(Promise<LoginResult | undefined>)} returns the User and token if successful, undefined if not
   * @memberof AuthService
   */
  async validateUserByPassword(loginAttempt: LoginUserInput): Promise<LoginResult | undefined> {
    console.log(loginAttempt);

    // This will be used for the initial login
    let userToAttempt: UserEntity | undefined;
    if (loginAttempt.email) {
      userToAttempt = await this.usersService.findOneByEmail(loginAttempt.email);
    }

    // Check the supplied password against the hash stored for this email address
    let isMatch = false;
    try {
      isMatch = await this.comparePassword(loginAttempt.password, userToAttempt.password);
    } catch (error) {
      console.log(error);
      return undefined;
    }

    if (isMatch) {
      // If there is a successful match, generate a JWT for the user
      const token = this.createJwt(userToAttempt!).token;
      const result: any = {
        user: userToAttempt!,
        token,
      };
      userToAttempt.updated_at = new Date();
      userToAttempt.save();
      return result;
    }
    return null;
  }

  private async comparePassword(enteredPassword, dbPassword) {
    const match = await bcrypt.compare(enteredPassword, dbPassword);
    return match;
  }

  /**
   * Verifies that the JWT payload associated with a JWT is valid by making sure the user exists and is enabled
   *
   * @param {JwtPayload} payload
   * @returns {(Promise<UserDocument | undefined>)} returns undefined if there is no user or the account is not enabled
   * @memberof AuthService
   */
  async validateJwtPayload(payload: JwtPayload): Promise<UserEntity | undefined> {
    // This will be used when the user has already logged in and has a JWT
    const user = await this.usersService.findOneByUsername(payload.username);

    // Ensure the user exists and their account isn't disabled
    if (user) {
      user.updated_at = new Date();
      user.save();
      return user;
    }

    return undefined;
  }

  /**
   * Creates a JwtPayload for the given User
   *
   * @param {User} user
   * @returns {{ data: JwtPayload; token: string }} The data contains the email, username, and expiration of the
   * token depending on the environment variable. Expiration could be undefined if there is none set. token is the
   * token created by signing the data.
   * @memberof AuthService
   */
  createJwt(user: UserEntity): { data: JwtPayload; token: string } {
    const expiresIn = this.configService.get().auth.expireIn as number;
    let expiration: Date | undefined;
    if (expiresIn) {
      expiration = new Date();
      expiration.setTime(expiration.getTime() + expiresIn * 1000);
    }
    const data: JwtPayload = {
      userId: user.id,
      username: user.username,
      permissions: user.permissions,
      expiration,
    };

    const jwt = this.jwtService.sign(data);

    return {
      data,
      token: jwt,
    };
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у нас есть еще один модуль под названием user Module, в котором также есть

  • схема graohql пользователя
  • resolver
  • сервисы
  • сущность
  • модуль пользователя

рассмотрим схему graphql для сущности пользователь

scalar Date

type Query {
  users: [User!]!
  user(id: ID!): User!
  forgotPassword(email: String): Boolean
}

type Mutation {
  createUser(createUserInput: CreateUserInput): User!
  updateUser(fieldsToUpdate: UpdateUserInput!, username: String): User!
  addAdminPermission(username: String!): User!
  removeAdminPermission(username: String!): User!
  resetPassword(username: String!, code: String!, password: String!): User!
}

type User @key(fields: "id") {
  id: ID!
  username: String!
  email: String!
  permissions: [String!]!
  created_at: Date!
  updated_at: Date!
}

input CreateUserInput {
  username: String!
  email: String!
  password: String!
}

input UpdateUserInput {
  username: String
  email: String
  password: UpdatePasswordInput
  enabled: Boolean
}

input UpdatePasswordInput {
  oldPassword: String!
  newPassword: String!
}
Вход в полноэкранный режим Выход из полноэкранного режима

Эти запросы и мутации имеют дело с операциями пользователя, такими как CRUD-операции

  • создание нового пользователя
  • обновление пользователя
  • назначить права администратора
  • удаление разрешений
  • сброс пароля

Поэтому для управления всем этим наш сервис будет иметь всю логику для сущности User

import { BadRequestException, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver, Context, ResolveReference } from '@nestjs/graphql';
import { UsersService } from './users.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CreateUserInput, User, UpdateUserInput } from '../graphql.classes';
import { UsernameEmailAdminGuard } from '../auth/guards/username-email-admin.guard';
import { AdminGuard } from '../auth/guards/admin.guard';
import { UserInputError, ValidationError } from 'apollo-server-core';
import { AdminAllowedArgs } from '../decorators/admin-allowed-args';
import { UserEntity } from './entity/users.entity';
import { Logger } from 'src/logger/logger';
import { UserSignup } from './dto/users.dto';
import { validate } from 'class-validator';

@Resolver('User')
export class UserResolver {
  constructor(private usersService: UsersService, private readonly logger: Logger) {}

  @Query('users')
  @UseGuards(JwtAuthGuard, AdminGuard)
  async users(): Promise<UserEntity[]> {
    return await this.usersService.getAllUsers();
  }

  // A NotFoundException is intentionally not sent so bots can't search for emails
  @Query('forgotPassword')
  async forgotPassword(@Args('email') email: string): Promise<boolean> {
    return await this.usersService.forgotPassword(email);
  }

  // What went wrong is intentionally not sent (wrong username or code or user not in reset status)
  @Mutation('resetPassword')
  async resetPassword(
    @Args('username') username: string,
    @Args('code') code: string,
    @Args('password') password: string
  ): Promise<UserEntity> {
    const user = await this.usersService.resetPassword(username, code, password);
    if (!user) throw new UserInputError('The password was not reset');
    return user;
  }

  @Mutation('createUser')
  async createUser(@Args('createUserInput') createUserInput: CreateUserInput): Promise<UserEntity> {
    let createdUser: UserEntity | null;
    try {
      const { email, username, password } = createUserInput;
      const userSignup = new UserSignup();
      userSignup.email = email;
      userSignup.username = username;
      userSignup.password = password;
      const errors = await validate(userSignup);

      if (errors.length > 0) {
        const errorsResponse: any = errors.map((val: any) => {
          return Object.values(val.constraints)[0] as string;
        });
        throw new BadRequestException(errorsResponse.join(','));
      }
      return await this.usersService.create(createUserInput);
    } catch (error) {
      throw new UserInputError(error.message);
    }
    return createdUser;
  }

  @Mutation('updateUser')
  @AdminAllowedArgs('username', 'fieldsToUpdate.username', 'fieldsToUpdate.email', 'fieldsToUpdate.enabled')
  @UseGuards(JwtAuthGuard, UsernameEmailAdminGuard)
  async updateUser(
    @Args('username') username: string,
    @Args('fieldsToUpdate') fieldsToUpdate: UpdateUserInput,
    @Context('req') request: any
  ): Promise<UserEntity> {
    let user: UserEntity | undefined;
    if (!username && request.user) username = request.user.username;
    try {
      user = await this.usersService.update(username, fieldsToUpdate);
    } catch (error) {
      throw new ValidationError(error.message);
    }
    if (!user) throw new UserInputError('The user does not exist');
    return user;
  }

  @Mutation('addAdminPermission')
  @UseGuards(JwtAuthGuard, AdminGuard)
  async addAdminPermission(@Args('username') username: string): Promise<UserEntity> {
    const user = await this.usersService.addPermission('admin', username);
    if (!user) throw new UserInputError('The user does not exist');
    return user;
  }

  @Mutation('removeAdminPermission')
  @UseGuards(JwtAuthGuard, AdminGuard)
  async removeAdminPermission(@Args('username') username: string): Promise<UserEntity> {
    const user = await this.usersService.removePermission('admin', username);
    if (!user) throw new UserInputError('The user does not exist');
    return user;
  }

  @ResolveReference()
  async resolveReference(reference: { __typename: string; id: string }) {
    this.logger.http('ResolveReference :: user');
    return await this.usersService.findOneByUserId(reference.id);
  }

  @Query('user')
  @UseGuards(JwtAuthGuard, AdminGuard)
  user(@Args('id') id: string) {
    return this.usersService.findOneByUserId(id);
  }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь, поскольку мы хотим, чтобы это было частью apollo federation sub graphql, мы можем использовать graphqlModule с драйвером федерации ApolloFederationDriver.

GraphQLModule.forRoot({
  typePaths: ['./**/*.graphql'],
  driver: ApolloFederationDriver,
  context: ({ req }: any) => ({ req }),
  formatError: (error: GraphQLError) => {
    const graphQLFormattedError: GraphQLFormattedError = {
      message: error?.extensions?.exception?.response?.message || error?.message,
    };
    return graphQLFormattedError;
  },
  definitions: {
    path: join(process.cwd(), 'src/graphql.classes.ts'),
    outputAs: 'class',
  },
});
Войдите в полноэкранный режим Выйти из полноэкранного режима

Это рабочий пример, и вместо того, чтобы рассказывать весь код здесь, мы будем работать над видеосессией по этому вопросу, где я расскажу все об этой службе авторизации пользователей, используя схемный подход nestjs/graphql.
Когда мы выполним npm run start:dev, он сможет разместить наш сервис авторизации на PORT 3000, и мы сможем видеть все запросы и мутации от сервиса авторизации.

Мы можем подключить этот сервис к шлюзу graphql, теперь осталось построить еще один сервис и создать простой пример составления графиков вместе.

Заключение

Это было лишь краткое введение в то, что такое apollo graphql federation gateway, давайте узнаем больше об этом в нашем следующем блоге, где мы проверим фактическую реализацию кода graphql Gateway в nestjs.
Давайте посмотрим на часть 3
https://tkssharma.com/nestjs-with-apollo-federation-for-microservices-part-4

Ссылки

  • https://www.apollographql.com/docs/federation/
  • https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2
  • https://github.com/apollographql/supergraph-demo-fed2
  • https://www.apollographql.com/docs/federation/federation-spec/
  • https://docs.nestjs.com/graphql/migration-guide
  • https://docs.nestjs.com/graphql/federation

Оцените статью
Procodings.ru
Добавить комментарий