Skip to content

Commit b5db232

Browse files
authored
Merge pull request #1 from recomendapp/feat/migrate-microservice
Feat/migrate microservice
2 parents aae4da8 + b51c2f6 commit b5db232

File tree

102 files changed

+7172
-5694
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

102 files changed

+7172
-5694
lines changed

.env.template

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
WEB_APP_URL=
2+
# MICROSERVICES
3+
AUTH_GRPC_URL=
24
# SUPABASE
35
SUPABASE_URL=
46
SUPABASE_ANON_KEY=

.github/workflows/build-and-deploy.yml

Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,57 +5,114 @@ on:
55
branches: ['main']
66

77
jobs:
8-
build-deploy:
8+
# =================================================================================
9+
# JOB 1: Detect changed services and prepare variables
10+
# =================================================================================
11+
detect-changes:
912
runs-on: ubuntu-latest
13+
outputs:
14+
services_to_build: ${{ steps.filter.outputs.changes }}
15+
version: ${{ steps.prep.outputs.VERSION }}
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 2
1021

22+
- name: Prepare version tag
23+
id: prep
24+
run: |
25+
VERSION=$(date +%Y%m%d%H%M%S)-${GITHUB_SHA::7}
26+
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
27+
28+
- name: Detect changed services
29+
id: filter
30+
uses: dorny/paths-filter@v3
31+
with:
32+
list-files: none
33+
filters: |
34+
all: &all
35+
- 'libs/shared/**'
36+
- '.github/workflows/**'
37+
- 'package.json'
38+
- 'pnpm-lock.yaml'
39+
gateway:
40+
- 'apps/gateway/**'
41+
- *all
42+
auth:
43+
- 'apps/auth/**'
44+
- *all
45+
46+
# =================================================================================
47+
# JOB 2: Build and push Docker images (in parallel via matrix)
48+
# =================================================================================
49+
build-and-push:
50+
needs: detect-changes
51+
if: needs.detect-changes.outputs.services_to_build != '[]'
52+
runs-on: ubuntu-latest
1153
permissions:
12-
contents: write
1354
packages: write
14-
55+
strategy:
56+
fail-fast: false
57+
matrix:
58+
service: ${{ fromJson(needs.detect-changes.outputs.services_to_build) }}
1559
steps:
16-
- uses: actions/checkout@v4
60+
- name: Checkout code
61+
uses: actions/checkout@v4
1762

18-
- uses: docker/login-action@v3
63+
- name: Login to GitHub Container Registry
64+
uses: docker/login-action@v3
1965
with:
2066
registry: ghcr.io
2167
username: ${{ github.actor }}
2268
password: ${{ secrets.GITHUB_TOKEN }}
2369

24-
- name: Build image
70+
- name: Build and Push Docker image for ${{ matrix.service }}
2571
run: |
26-
VERSION=$(date +%Y%m%d%H%M%S)
27-
echo "VERSION=$VERSION" >> $GITHUB_ENV
72+
IMAGE_NAME="api-${{ matrix.service }}"
2873
docker build \
29-
-t ghcr.io/${{ github.repository_owner }}/api:$VERSION .
30-
docker tag ghcr.io/${{ github.repository_owner }}/api:$VERSION ghcr.io/${{ github.repository_owner }}/api:latest
31-
32-
- name: Push image
33-
run: |
34-
docker push ghcr.io/${{ github.repository_owner }}/api:$VERSION
35-
docker push ghcr.io/${{ github.repository_owner }}/api:latest
74+
-f apps/${{ matrix.service }}/Dockerfile \
75+
-t ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME}:${{ needs.detect-changes.outputs.version }} \
76+
-t ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME}:latest .
77+
docker push ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME} --all-tags
3678
79+
# =================================================================================
80+
# JOB 3: Update infrastructure repository (runs once)
81+
# =================================================================================
82+
update-infra:
83+
needs: [detect-changes, build-and-push]
84+
if: needs.detect-changes.outputs.services_to_build != '[]'
85+
runs-on: ubuntu-latest
86+
permissions:
87+
contents: write
88+
steps:
3789
- name: Clone infra repo
3890
run: |
3991
git clone https://ci-bot:${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository_owner }}/infra.git
4092
41-
- name: Update api deployment image tag
93+
- name: Update deployment image tags
4294
run: |
43-
cd infra/apps/services/api
44-
sed -i "s|image: ghcr.io.*/api.*|image: ghcr.io/${{ github.repository_owner }}/api:$VERSION|" deployment.yaml
95+
cd infra
96+
SERVICES_JSON='${{ needs.detect-changes.outputs.services_to_build }}'
97+
VERSION='${{ needs.detect-changes.outputs.version }}'
98+
99+
for service in $(echo $SERVICES_JSON | jq -r '.[]'); do
100+
IMAGE_NAME="api-$service"
101+
DEPLOYMENT_PATH="apps/services/api/$service/deployment.yaml"
102+
echo "Updating deployment for $service in $DEPLOYMENT_PATH"
103+
104+
sed -i "s|image: ghcr.io.*/${IMAGE_NAME}:.*|image: ghcr.io/${{ github.repository_owner }}/${IMAGE_NAME}:${VERSION}|" "$DEPLOYMENT_PATH"
105+
done
45106
46-
- name: Commit manifest change
107+
- name: Commit and Push Manifest Changes
47108
run: |
48109
cd infra
49110
git config user.name "ci-bot"
50-
git config user.email "ci-bot@github.com"
51-
52-
git remote set-url origin https://ci-bot:${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository_owner }}/infra.git
53-
111+
git config user.email "ci-bot@users.noreply.github.com"
54112
git add .
55-
git commit -m "deploy: api $VERSION" || echo "No changes to commit"
56-
git push origin main
57-
env:
58-
GIT_AUTHOR_NAME: ci-bot
59-
GIT_AUTHOR_EMAIL: ci-bot@github.com
60-
GIT_COMMITTER_NAME: ci-bot
61-
GIT_COMMITTER_EMAIL: ci-bot@github.com
113+
if git diff --staged --quiet; then
114+
echo "No changes to commit."
115+
else
116+
git commit -m "deploy(api): update image versions to ${{ needs.detect-changes.outputs.version }}"
117+
git push
118+
fi
Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
# 1. Build Stage
1+
# 1. Builder stage
22
FROM node:22-slim AS builder
3+
RUN apt-get update && apt-get install -y protobuf-compiler && rm -rf /var/lib/apt/lists/*
34
ENV PNPM_HOME="/pnpm"
45
ENV PATH="$PNPM_HOME:$PATH"
56
RUN corepack enable
67
WORKDIR /usr/src/app
78
COPY package.json pnpm-lock.yaml ./
89
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
910
COPY . .
10-
RUN pnpm run build
11+
RUN pnpm run build auth
1112

12-
# 2. Production Stage
13+
# 2. Production stage
1314
FROM node:22-slim AS production
1415
ENV PNPM_HOME="/pnpm"
1516
ENV PATH="$PNPM_HOME:$PATH"
1617
RUN corepack enable
1718
WORKDIR /usr/src/app
1819
COPY package.json pnpm-lock.yaml ./
1920
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
21+
22+
COPY libs/shared/src/protos ./libs/shared/src/protos
2023
COPY --from=builder /usr/src/app/dist ./dist
24+
2125
USER node
22-
EXPOSE 3000
23-
CMD ["node", "dist/main.js"]
26+
CMD ["node", "dist/apps/auth/src/main.js"]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { AuthController } from './auth.controller';
3+
import { AuthService } from './auth.service';
4+
import { ValidateTokenResponse } from '@app/shared/protos/__generated__';
5+
6+
describe('AuthController', () => {
7+
let authController: AuthController;
8+
let authService: AuthService;
9+
10+
const mockAuthService = {
11+
validateToken: jest.fn(),
12+
};
13+
14+
beforeEach(async () => {
15+
const module: TestingModule = await Test.createTestingModule({
16+
controllers: [AuthController],
17+
providers: [
18+
{
19+
provide: AuthService,
20+
useValue: mockAuthService,
21+
},
22+
],
23+
}).compile();
24+
25+
authController = module.get<AuthController>(AuthController);
26+
authService = module.get<AuthService>(AuthService);
27+
});
28+
29+
it('should be defined', () => {
30+
expect(authController).toBeDefined();
31+
});
32+
33+
describe('validateToken', () => {
34+
it('should return a user payload for a valid token', async () => {
35+
const token = 'valid-token';
36+
const expectedResponse: ValidateTokenResponse = {
37+
user: {
38+
id: 'user-id',
39+
email: 'test@example.com',
40+
exp: 1234567890,
41+
},
42+
};
43+
44+
mockAuthService.validateToken.mockResolvedValue(expectedResponse);
45+
46+
const result = await authController.validateToken({ token });
47+
48+
expect(result).toEqual(expectedResponse);
49+
expect(authService.validateToken).toHaveBeenCalledWith(token);
50+
});
51+
52+
it('should return an error for an invalid token', async () => {
53+
const token = 'invalid-token';
54+
const expectedResponse: ValidateTokenResponse = {
55+
error: 'Invalid token',
56+
};
57+
58+
mockAuthService.validateToken.mockResolvedValue(expectedResponse);
59+
60+
const result = await authController.validateToken({ token });
61+
62+
expect(result).toEqual(expectedResponse);
63+
expect(authService.validateToken).toHaveBeenCalledWith(token);
64+
});
65+
});
66+
});

apps/auth/src/auth.controller.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Controller } from '@nestjs/common';
2+
import { AuthService } from './auth.service';
3+
import {
4+
AuthServiceController,
5+
AuthServiceControllerMethods,
6+
ValidateTokenRequest,
7+
ValidateTokenResponse,
8+
} from '@app/shared/protos/__generated__';
9+
import { Observable } from 'rxjs';
10+
11+
@Controller()
12+
@AuthServiceControllerMethods()
13+
export class AuthController implements AuthServiceController {
14+
constructor(private readonly authService: AuthService) {}
15+
16+
validateToken(
17+
request: ValidateTokenRequest,
18+
):
19+
| ValidateTokenResponse
20+
| Promise<ValidateTokenResponse>
21+
| Observable<ValidateTokenResponse> {
22+
return this.authService.validateToken(request.token);
23+
}
24+
}

apps/auth/src/auth.module.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Module } from '@nestjs/common';
2+
import { AuthController } from './auth.controller';
3+
import { AuthService } from './auth.service';
4+
import { JwtModule } from '@nestjs/jwt';
5+
import { SharedModule } from '@app/shared';
6+
7+
@Module({
8+
imports: [
9+
SharedModule,
10+
JwtModule.register({
11+
secret: process.env.SUPABASE_JWT_SECRET,
12+
}),
13+
],
14+
controllers: [AuthController],
15+
providers: [AuthService],
16+
})
17+
export class AuthModule {}

apps/auth/src/auth.service.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { JwtService } from '@nestjs/jwt';
3+
import { ValidateTokenResponse } from '@app/shared/protos/__generated__';
4+
import { JwtPayload } from '@supabase/supabase-js';
5+
6+
@Injectable()
7+
export class AuthService {
8+
constructor(private readonly jwtService: JwtService) {}
9+
10+
validateToken = async (token: string): Promise<ValidateTokenResponse> => {
11+
try {
12+
const payload = await this.jwtService.verifyAsync<JwtPayload>(token);
13+
return {
14+
user: {
15+
id: payload.sub,
16+
email: payload.email,
17+
exp: payload.exp,
18+
},
19+
};
20+
} catch {
21+
return {
22+
error: 'Invalid token',
23+
};
24+
}
25+
};
26+
}

apps/auth/src/main.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { NestFactory } from '@nestjs/core';
2+
3+
import { SharedService } from '@app/shared';
4+
5+
import { AuthModule } from './auth.module';
6+
import { AUTH_PACKAGE_NAME } from '@app/shared/protos/__generated__';
7+
8+
async function bootstrap() {
9+
const app = await NestFactory.create(AuthModule);
10+
11+
const sharedService = app.get(SharedService);
12+
13+
app.connectMicroservice(
14+
sharedService.getGrpcOptions('auth', AUTH_PACKAGE_NAME),
15+
);
16+
17+
await app.startAllMicroservices();
18+
}
19+
bootstrap().catch((err) => {
20+
console.error('Error starting Auth microservice:', err);
21+
process.exit(1);
22+
});

apps/auth/test/app.e2e-spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { INestApplication } from '@nestjs/common';
3+
import { AuthModule } from './../src/auth.module';
4+
5+
describe('AuthController (e2e)', () => {
6+
let app: INestApplication;
7+
8+
beforeEach(async () => {
9+
const moduleFixture: TestingModule = await Test.createTestingModule({
10+
imports: [AuthModule],
11+
}).compile();
12+
13+
app = moduleFixture.createNestApplication();
14+
await app.init();
15+
});
16+
});

0 commit comments

Comments
 (0)