Создание 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