Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add grpc for synchronous communication between services #7

Merged
merged 5 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
apps/web/presets/** linguist-vendored
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
name: CI

on:
push:
branches:
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Release

on:
push:
branches:
- main

jobs:
release:
permissions:
contents: write
issues: write
pull-requests: write
runs-on: ubuntu-22.04
strategy:
matrix:
node-version: [20]
steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'

- name: Install dependencies
run: pnpm add -g semantic-release @semantic-release/git @semantic-release/github

- name: Release
run: pnpm exec semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Henshi is a webapp based on microservices architecture, it uses Vue.js as its pr
- [PostgreSQL](https://postgresql.org) as its SQL database
- [Redis](https://redis.io) as cache and key-value database
- [Nginx](https://nginx.org) as a reverse proxy
- [gRPC](https://grpc.io) as an synchronous method of communication between the services (🚧 **In progress**)
- [gRPC](https://grpc.io) as a synchronous method of communication between the services
- [RabbitMQ](https://rabbitmq.com) as an asynchronous method of communication between the services (🚧 **In progress**)
- [Docker](https://docker.com) as its container management tool
- [Kubernetes](https://kubernetes.io) as its container orchestration tool (🚧 **In progress**)
Expand All @@ -31,6 +31,7 @@ flowchart LR
FE(fa:fa-twitter Frontend)
LB(Reverse proxy)
A(Auth API)
A-R[(Redis)]
U(Users API)
U-P[(PostgreSQL)]
N(Notification API)
Expand All @@ -48,7 +49,7 @@ flowchart LR
RMQ <-.->|AMQP| AS
subgraph AS [Auth service]
direction LR
A
A --> A-R
end
subgraph US [Users service]
direction LR
Expand Down
4 changes: 3 additions & 1 deletion apps/auth-service/nest-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
"deleteOutDir": true,
"assets": ["**/*.proto"],
"watchAssets": true
}
}
2 changes: 2 additions & 0 deletions apps/auth-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@grpc/grpc-js": "^1.10.8",
"@grpc/proto-loader": "^0.7.13",
"@henshi/types": "workspace:*",
"@nestjs/cache-manager": "^2.2.2",
"@nestjs/common": "^10.3.8",
Expand Down
15 changes: 15 additions & 0 deletions apps/auth-service/src/auth/auth.grpc.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Controller } from '@nestjs/common';
import { AuthServiceController, AuthServiceControllerMethods, JwtUserOrUndefined, MeRequest } from '@henshi/types';
import { AuthService } from './auth.service';

@Controller()
@AuthServiceControllerMethods()
export class AuthGrpcController implements AuthServiceController {
constructor(private readonly authService: AuthService) {}

me({ jwt }: MeRequest): JwtUserOrUndefined {
if (!jwt) return { user: undefined };

return this.authService.getTokenPayload(jwt);
}
}
45 changes: 30 additions & 15 deletions apps/auth-service/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { AccessTokenStrategy } from './strategies/accessToken.strategy';
import { RefreshTokenStrategy } from './strategies/refreshToken.strategy';
import { UserModule } from '../users/users.module';
import { UsersService } from '../users/users.service';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ConfigService } from '@nestjs/config';
import { USERS_PACKAGE_NAME, USERS_SERVICE_NAME } from '@henshi/types';
import { AuthRestController } from './auth.rest.controller';
import { AuthGrpcController } from './auth.grpc.controller';
import { join } from 'node:path/win32';

@Module({
imports: [
ClientsModule.register([
{
name: 'USERS_SERVICE',
transport: Transport.TCP,
options: {
host: 'localhost',
port: 4010,
ClientsModule.registerAsync({
clients: [
{
name: USERS_SERVICE_NAME,
useFactory: (configService: ConfigService) => {
return {
transport: Transport.GRPC,
options: {
package: USERS_PACKAGE_NAME,
protoPath: join(
__dirname,
'../../node_modules/@henshi/types/src/lib/proto/users.proto',
),
url:
configService.get('microservices.users.host') +
':' +
configService.get('microservices.users.port'),
},
};
},
inject: [ConfigService],
},
},
]),
],
}),
JwtModule.register({}),
UserModule,
],
controllers: [AuthController],
providers: [AuthService, AccessTokenStrategy, RefreshTokenStrategy, UsersService],
controllers: [AuthRestController, AuthGrpcController],
providers: [AuthService, AccessTokenStrategy, RefreshTokenStrategy],
})
export class AuthModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import { AuthDto } from './dto/auth.dto';
import { AccessTokenGuard } from '../shared/guards/accessToken.guard';
import { RefreshTokenGuard } from '../shared/guards/refreshToken.guard';
import { accessTokenCookieOptions, refreshTokenCookieOptions } from './utils/cookieOptions';
import { MessagePattern } from '@nestjs/microservices';
import { SignUpDto, SignUpResponse } from '@henshi/types';

@Controller('/api/auth')
export class AuthController {
export class AuthRestController {
constructor(private readonly authService: AuthService) {}

@Post('signup')
Expand Down Expand Up @@ -87,11 +86,4 @@ export class AuthController {
async me(@Req() req: Request) {
return req.user;
}

@MessagePattern('me')
async isLoggedIn({ jwt }: { jwt?: string }) {
if (!jwt) return false;

return this.authService.getTokenPayload(jwt);
}
}
57 changes: 37 additions & 20 deletions apps/auth-service/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common';
import { BadRequestException, ForbiddenException, Inject, Injectable, OnModuleInit } from '@nestjs/common';
import * as argon2 from 'argon2';
import { AuthDto } from './dto/auth.dto';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { SignUpDto, User } from '@henshi/types';
import { SignUpDto, User, USERS_SERVICE_NAME, UsersServiceClient } from '@henshi/types';
import { ConfigService } from '@nestjs/config';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import * as crypto from 'node:crypto';
import { ClientGrpc } from '@nestjs/microservices';
import { firstValueFrom } from 'rxjs';
// import { emailTransporter } from '../mail/transport';
// import { getEmailConfirmationBody } from '../mail/body';

@Injectable()
export class AuthService {
export class AuthService implements OnModuleInit {
private usersService: UsersServiceClient;

constructor(
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
private readonly usersService: UsersService,
@Inject(USERS_SERVICE_NAME) private readonly client: ClientGrpc,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
) {}

async signUp(signUpDto: SignUpDto): Promise<User> {
const userExists = await this.usersService.findOne({
email: signUpDto.email,
});
onModuleInit() {
this.usersService = this.client.getService<UsersServiceClient>(USERS_SERVICE_NAME);
}

async signUp(signUpDto: SignUpDto) {
const { user: userExists } = await firstValueFrom(
this.usersService.findOne({
email: signUpDto.email,
}),
);

if (userExists) {
throw new BadRequestException('User already exists');
}

const hash = await this.hashData(signUpDto.password);

const newUser = await this.usersService.create({
...signUpDto,
password: hash,
});
const { user: newUser } = await firstValueFrom(
this.usersService.create({
...signUpDto,
password: hash,
}),
);

const tokens = await this.getTokens(newUser);

await this.updateRefreshToken(newUser.id, tokens.refreshToken);
Expand All @@ -43,7 +55,7 @@ export class AuthService {
}

async signIn(data: AuthDto) {
const user = await this.usersService.findOne({ email: data.email });
const { user } = await firstValueFrom(this.usersService.findOne({ email: data.email }));

if (!user) throw new BadRequestException('The credentials are invalid');

Expand All @@ -59,7 +71,7 @@ export class AuthService {
}

async logout(userId: string) {
return this.usersService.update(userId, { refreshToken: null });
return this.usersService.update({ id: userId, refreshToken: null });
}

hashData(data: string) {
Expand All @@ -68,9 +80,12 @@ export class AuthService {

async updateRefreshToken(userId: string, refreshToken: string) {
const hashedRefreshToken = await this.hashData(refreshToken);
await this.usersService.update(userId, {
refreshToken: hashedRefreshToken,
});
await firstValueFrom(
this.usersService.update({
id: userId,
refreshToken: hashedRefreshToken,
}),
);
}

async getTokens(user: User) {
Expand Down Expand Up @@ -114,7 +129,7 @@ export class AuthService {
}

async refreshTokens(userId: string, refreshToken: string) {
const user = await this.usersService.findOne({ id: userId });
const { user } = await firstValueFrom(this.usersService.findOne({ id: userId }));

if (!user || !user.refreshToken) throw new ForbiddenException('Access Denied');

Expand All @@ -133,9 +148,10 @@ export class AuthService {

async emailConfirmed(userId: string) {
await this.cacheManager.del('emailConfirmationToken:' + userId);
await this.usersService.update(userId, { emailConfirmed: true });
await firstValueFrom(this.usersService.update({ id: userId, emailConfirmed: true }));
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async sendEmailConfirmation(userId: string, userName: string, userEmail: string) {
const A_DAY_IN_SECONDS = 86_400;
const verificationToken = crypto.randomBytes(64).toString('hex');
Expand All @@ -144,6 +160,7 @@ export class AuthService {
ttl: A_DAY_IN_SECONDS,
} as any);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const url = `${this.configService.get('client.url')}/verify-account?token=${verificationToken}`;

// await emailTransporter.sendMail({
Expand Down
2 changes: 1 addition & 1 deletion apps/auth-service/src/configuration/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as process from 'process';
export default () => ({
env: process.env.NODE_ENV,
port: parseInt(process.env.PORT, 10) || 3000,
microservice: {
microservices: {
auth: {
host: process.env.AUTH_MICROSERVICE_HOST || 'localhost',
port: parseInt(process.env.AUTH_MICROSERVICE_PORT) || 4000,
Expand Down
9 changes: 6 additions & 3 deletions apps/auth-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Transport } from '@nestjs/microservices';
import { ConfigService } from '@nestjs/config';
import * as cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { join } from 'node:path/win32';
import { AUTH_PACKAGE_NAME } from '@henshi/types';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
Expand All @@ -17,10 +19,11 @@ async function bootstrap() {
app.use(helmet());

app.connectMicroservice({
transport: Transport.TCP,
transport: Transport.GRPC,
options: {
host: configService.get('microservices.auth.host'),
port: configService.get('microservice.auth.port'),
package: AUTH_PACKAGE_NAME,
protoPath: join(__dirname, '../node_modules/@henshi/types/src/lib/proto/auth.proto'),
url: configService.get('microservices.auth.host') + ':' + configService.get('microservices.auth.port'),
},
});

Expand Down
42 changes: 0 additions & 42 deletions apps/auth-service/src/users/dto/create-user.dto.ts

This file was deleted.

Loading