diff --git a/.env b/.env index 6e509032..c6b2cdba 100644 --- a/.env +++ b/.env @@ -28,9 +28,10 @@ NATS_URL=nats:4222 ## frontend INTERNAL_BACKEND_BASE_URL=https://backend.tarhche.com -NEXT_PUBLIC_FILES_BASE_URL=https://backend.tarhche.com -NEXT_PUBLIC_EXTERNAL_BACKEND_BASE_URL=https://tarhche.com +NEXT_PUBLIC_FILES_PROTOCOL=https +NEXT_PUBLIC_FILES_HOST=backend.tarhche.com +## frontend local # INTERNAL_BACKEND_BASE_URL=http://app -# NEXT_PUBLIC_FILES_BASE_URL=http://127.0.0.1:8000 -# NEXT_PUBLIC_EXTERNAL_BACKEND_BASE_URL=http://127.0.0.1:3000 +# NEXT_PUBLIC_FILES_PROTOCOL=http +# NEXT_PUBLIC_FILES_HOST=127.0.0.1 diff --git a/.github/actions/docker-build/action.yaml b/.github/actions/docker-build/action.yaml new file mode 100644 index 00000000..da1b6843 --- /dev/null +++ b/.github/actions/docker-build/action.yaml @@ -0,0 +1,74 @@ +name: 'Docker Build Action' +description: 'Builds a Docker image using a specified Dockerfile and context' + +inputs: + context: + description: 'The Docker build context (path to the directory containing the Dockerfile)' + required: true + default: '.' + dockerfile: + description: 'The path to the Dockerfile (relative to the context)' + required: true + default: 'Dockerfile' + target: + description: 'The build target' + required: false + image-name: + description: 'The name of the image to build' + required: true + push: + description: 'Determines if the built image should be pushed' + required: true + default: 'no' + container-registry: + description: 'container registry address (example: ghcr.io)' + required: false + container-registry-username: + description: 'container registry username' + required: false + container-registry-password: + description: 'container registry password' + required: false + +runs: + using: 'composite' + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + if: ${{ inputs.push == 'true' }} + with: + logout: false + registry: ${{ inputs.container-registry }} + username: ${{ inputs.container-registry-username }} + password: ${{ inputs.container-registry-password }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata (tags, labels) for Docker image + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ inputs.container-registry }}/${{ github.repository_owner }}/${{ inputs.image-name }} + tags: | + type=raw,value=latest,priority=200,enable={{is_default_branch}} + type=sha,enable=true,priority=100,prefix=,suffix=,format=short + + - name: Build image and push (optional) + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: ${{ inputs.push == 'true' }} + context: ${{ inputs.context }} + file: ${{ inputs.dockerfile }} + target: ${{ inputs.target }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/backend.yaml b/.github/workflows/backend.yaml index ea63340a..e452fbfa 100644 --- a/.github/workflows/backend.yaml +++ b/.github/workflows/backend.yaml @@ -1,8 +1,7 @@ name: Backend CI and CD + on: push: - branches: - - main paths: - .github/** - backend/** @@ -11,10 +10,17 @@ on: - .github/** - backend/** +defaults: + run: + working-directory: ./backend + env: REGISTRY: ghcr.io IMAGE_NAME: backend + EC2_SSH_ADDRESS: ${{ secrets.EC2_SSH_ADDRESS }} + EC2_SSH_ENDPOINT: ${{ secrets.EC2_SSH_USER }}@${{ secrets.EC2_SSH_ADDRESS }} + jobs: ci: runs-on: ubuntu-latest @@ -30,24 +36,22 @@ jobs: - name: Run unit tests run: | - cd ./backend && go test ./... -v -race -cover - - - name: Provide image name and version - run: | - IMAGE_ID=$(echo $REGISTRY/${{ github.repository_owner }}/$IMAGE_NAME | tr '[A-Z]' '[a-z]') - IMAGE_VERSION=${{ github.sha }} - echo "IMAGE_ID=$IMAGE_ID" >> "$GITHUB_ENV" - echo "IMAGE_VERSION=$IMAGE_VERSION" >> "$GITHUB_ENV" + go test ./... -v -race -cover - name: Build image - run: | - cd ./backend && docker build . --file Dockerfile --target production --tag $IMAGE_ID:$IMAGE_VERSION --tag $IMAGE_ID:latest - - cd: + uses: ./.github/actions/docker-build + with: + context: ./backend + dockerfile: ./backend/Dockerfile + image-name: ${{ env.IMAGE_NAME }} + target: production + push: false + container-registry: ${{ env.REGISTRY }} + + build-and-push-images: runs-on: ubuntu-latest - # This job will be invoked only on default branch - if: ${{ always() && format('refs/heads/{0}', github.event.repository.default_branch) == github.ref }} + if: ${{ format('refs/heads/{0}', github.event.repository.default_branch) == github.ref }} permissions: packages: write @@ -60,33 +64,82 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Provide image name and version - run: | - IMAGE_ID=$(echo $REGISTRY/${{ github.repository_owner }}/$IMAGE_NAME | tr '[A-Z]' '[a-z]') - IMAGE_VERSION=${{ github.sha }} - echo "IMAGE_ID=$IMAGE_ID" >> "$GITHUB_ENV" - echo "IMAGE_VERSION=$IMAGE_VERSION" >> "$GITHUB_ENV" + - name: Build and push image + uses: ./.github/actions/docker-build + with: + context: ./backend + dockerfile: ./backend/Dockerfile + image-name: ${{ env.IMAGE_NAME }} + target: production + push: true + container-registry: ${{ env.REGISTRY }} + container-registry-username: ${{ github.actor }} + container-registry-password: ${{ secrets.GITHUB_TOKEN }} + + deploy: + runs-on: ubuntu-latest - - name: Build image - run: | - cd ./backend && docker build . --file Dockerfile --target production --tag $IMAGE_ID:$IMAGE_VERSION --tag $IMAGE_ID:latest + if: ${{ format('refs/heads/{0}', github.event.repository.default_branch) == github.ref }} - - name: Log in to registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + needs: + - build-and-push-images - - name: Push image + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy services run: | - docker push $IMAGE_ID:$IMAGE_VERSION - docker push $IMAGE_ID:latest + # Setup ssh key + echo '${{ secrets.EC2_SSH_PRIVATE_KEY }}' > ~/ec2-key.pem + chmod 400 ~/ec2-key.pem - - uses: actions/setup-node@v3 - with: - node-version: "18" + mkdir -p ~/.ssh + ssh-keyscan -H $EC2_SSH_ADDRESS >> ~/.ssh/known_hosts - - name: deploy to production - env: - LIARA_TOKEN: ${{ secrets.LIARA_API_TOKEN }} - LIARA_BACKEND_APP_NAME: ${{secrets.LIARA_BACKEND_APP_NAME}} - run: | - npm i -g @liara/cli@7 - liara deploy --image $IMAGE_ID:$IMAGE_VERSION --platform=docker --port="80" --app="$LIARA_BACKEND_APP_NAME" --api-token="$LIARA_TOKEN" --detach + # Ensure remote directory exists + ssh -q -i ~/ec2-key.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $EC2_SSH_ENDPOINT > /dev/null 2>&1 << 'EOF' + sudo mkdir -p /tmp/deployment_backend + sudo chown ${{ secrets.EC2_SSH_USER }}:${{ secrets.EC2_SSH_USER }} /tmp/deployment_backend + + EOF + + # Copy files + scp -q -i ~/ec2-key.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -r ./compose.app.yaml $EC2_SSH_ENDPOINT:/tmp/deployment_backend/ > /dev/null 2>&1 + + # Connect and deploy services + ssh -q -i ~/ec2-key.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $EC2_SSH_ENDPOINT > /dev/null 2>&1 << 'EOF' + export MONGO_USERNAME='${{ secrets.MONGO_USERNAME }}' + export MONGO_PASSWORD='${{ secrets.MONGO_PASSWORD }}' + + export BACKEND_NATS_URL='${{ secrets.BACKEND_NATS_URL }}' + export BACKEND_PRIVATE_KEY='${{ secrets.BACKEND_PRIVATE_KEY }}' + + export BACKEND_MONGO_HOST='mongodb' + export BACKEND_MONGO_PORT='27017' + export BACKEND_MONGO_SCHEME='mongodb' + export BACKEND_MONGO_DATABASE_NAME='${{ secrets.BACKEND_MONGO_DATABASE_NAME }}' + export BACKEND_MONGO_USERNAME='${{ secrets.MONGO_USERNAME }}' + export BACKEND_MONGO_PASSWORD='${{ secrets.MONGO_PASSWORD }}' + + export BACKEND_MAIL_SMTP_PASSWORD='${{ secrets.BACKEND_MAIL_SMTP_PASSWORD }}' + export BACKEND_MAIL_SMTP_HOST='${{ secrets.BACKEND_MAIL_SMTP_HOST }}' + export BACKEND_MAIL_SMTP_FROM='${{ secrets.BACKEND_MAIL_SMTP_FROM }}' + export BACKEND_MAIL_SMTP_USERNAME='${{ secrets.BACKEND_MAIL_SMTP_USERNAME }}' + export BACKEND_MAIL_SMTP_PORT='${{ secrets.BACKEND_MAIL_SMTP_PORT }}' + + export BACKEND_S3_ENDPOINT='${{ secrets.BACKEND_S3_ENDPOINT }}' + export BACKEND_S3_SECRET_KEY='${{ secrets.BACKEND_S3_SECRET_KEY }}' + export BACKEND_S3_ACCESS_KEY='${{ secrets.BACKEND_S3_ACCESS_KEY }}' + export BACKEND_S3_USE_SSL='${{ secrets.BACKEND_S3_USE_SSL }}' + export BACKEND_S3_BUCKET_NAME='${{ secrets.BACKEND_S3_BUCKET_NAME }}' + + export APP_IMAGE='${{ secrets.APP_IMAGE }}' + + # Run Docker Compose + cd /tmp/deployment_backend/ + + docker compose -f compose.app.yaml --project-name app up --pull always --detach + + sudo rm -rf /tmp/deployment_backend + EOF diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index dfff995a..3899c86f 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -1,8 +1,7 @@ name: Frontend CI and CD + on: push: - branches: - - main paths: - .github/** - frontend/** @@ -11,10 +10,21 @@ on: - .github/** - frontend/** +defaults: + run: + working-directory: ./frontend + env: REGISTRY: ghcr.io IMAGE_NAME: frontend + EC2_SSH_ADDRESS: ${{ secrets.EC2_SSH_ADDRESS }} + EC2_SSH_ENDPOINT: ${{ secrets.EC2_SSH_USER }}@${{ secrets.EC2_SSH_ADDRESS }} + + INTERNAL_BACKEND_BASE_URL: ${{ secrets.INTERNAL_BACKEND_BASE_URL }} + NEXT_PUBLIC_FILES_PROTOCOL: https + NEXT_PUBLIC_FILES_HOST: ${{ secrets.NEXT_PUBLIC_FILES_HOST }} + jobs: ci: runs-on: ubuntu-latest @@ -23,26 +33,20 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - uses: actions/setup-node@v3 - with: - node-version: "18" - - - name: Provide image name and version - run: | - IMAGE_ID=$(echo $REGISTRY/${{ github.repository_owner }}/$IMAGE_NAME | tr '[A-Z]' '[a-z]') - IMAGE_VERSION=${{ github.sha }} - echo "IMAGE_ID=$IMAGE_ID" >> "$GITHUB_ENV" - echo "IMAGE_VERSION=$IMAGE_VERSION" >> "$GITHUB_ENV" - - name: Build image - run: | - cd ./frontend && docker build . --file Dockerfile --target production --tag $IMAGE_ID:$IMAGE_VERSION --tag $IMAGE_ID:latest - - cd: + uses: ./.github/actions/docker-build + with: + context: ./frontend + dockerfile: ./frontend/Dockerfile + image-name: ${{ env.IMAGE_NAME }} + target: production + push: false + container-registry: ${{ env.REGISTRY }} + + build-and-push-images: runs-on: ubuntu-latest - # This job will be invoked only on default branch - if: ${{ always() && format('refs/heads/{0}', github.event.repository.default_branch) == github.ref }} + if: ${{ format('refs/heads/{0}', github.event.repository.default_branch) == github.ref }} permissions: packages: write @@ -55,33 +59,59 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Provide image name and version - run: | - IMAGE_ID=$(echo $REGISTRY/${{ github.repository_owner }}/$IMAGE_NAME | tr '[A-Z]' '[a-z]') - IMAGE_VERSION=${{ github.sha }} - echo "IMAGE_ID=$IMAGE_ID" >> "$GITHUB_ENV" - echo "IMAGE_VERSION=$IMAGE_VERSION" >> "$GITHUB_ENV" + - name: Build and push image + uses: ./.github/actions/docker-build + with: + context: ./frontend + dockerfile: ./frontend/Dockerfile + image-name: ${{ env.IMAGE_NAME }} + target: production + push: true + container-registry: ${{ env.REGISTRY }} + container-registry-username: ${{ github.actor }} + container-registry-password: ${{ secrets.GITHUB_TOKEN }} + + deploy: + runs-on: ubuntu-latest - - name: Build image - run: | - cd ./frontend && docker build . --file Dockerfile --target production --tag $IMAGE_ID:$IMAGE_VERSION --tag $IMAGE_ID:latest + if: ${{ format('refs/heads/{0}', github.event.repository.default_branch) == github.ref }} + + needs: + - build-and-push-images - - name: Log in to registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + steps: + - name: Checkout code + uses: actions/checkout@v4 - - name: Push image + - name: Deploy services run: | - docker push $IMAGE_ID:$IMAGE_VERSION - docker push $IMAGE_ID:latest + # Setup ssh key + echo '${{ secrets.EC2_SSH_PRIVATE_KEY }}' > ~/ec2-key.pem + chmod 400 ~/ec2-key.pem - - uses: actions/setup-node@v3 - with: - node-version: "18" + mkdir -p ~/.ssh + ssh-keyscan -H $EC2_SSH_ADDRESS >> ~/.ssh/known_hosts - - name: deploy to production - env: - LIARA_TOKEN: ${{ secrets.LIARA_API_TOKEN }} - LIARA_FRONTEND_APP_NAME: ${{secrets.LIARA_FRONTEND_APP_NAME}} - run: | - npm i -g @liara/cli@7 - liara deploy --image $IMAGE_ID:$IMAGE_VERSION --platform=docker --port="3000" --app="$LIARA_FRONTEND_APP_NAME" --api-token="$LIARA_TOKEN" --detach + # Ensure remote directory exists + ssh -q -i ~/ec2-key.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $EC2_SSH_ENDPOINT > /dev/null 2>&1 << 'EOF' + sudo mkdir -p /tmp/deployment_frontend + sudo chown ${{ secrets.EC2_SSH_USER }}:${{ secrets.EC2_SSH_USER }} /tmp/deployment_frontend + EOF + + # Copy files + scp -q -i ~/ec2-key.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -r ./compose.frontend.yaml $EC2_SSH_ENDPOINT:/tmp/deployment_frontend/ > /dev/null 2>&1 + + # Connect and deploy services + ssh -q -i ~/ec2-key.pem -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $EC2_SSH_ENDPOINT > /dev/null 2>&1 << 'EOF' + export FRONTEND_IMAGE='${{ secrets.FRONTEND_IMAGE }}' + export INTERNAL_BACKEND_BASE_URL='${{ env.INTERNAL_BACKEND_BASE_URL }}' + export NEXT_PUBLIC_FILES_PROTOCOL='${{ env.NEXT_PUBLIC_FILES_PROTOCOL }}' + export NEXT_PUBLIC_FILES_HOST='${{ env.NEXT_PUBLIC_FILES_HOST }}' + + # Run Docker Compose + cd /tmp/deployment_frontend/ + + docker compose -f compose.frontend.yaml --project-name frontend up --pull always --detach + + sudo rm -rf /tmp/deployment_frontend + EOF diff --git a/.gitignore b/.gitignore index c899b176..6e512438 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -.idea .DS_Store +.vscode +.idea /private /private.pub +/tmp \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 070fa0bd..1a637777 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,4 @@ +.DS_Store .vscode .idea /tmp \ No newline at end of file diff --git a/backend/application/dashboard/file/deleteFile/usecase.go b/backend/application/dashboard/file/deleteFile/usecase.go index fe0bc054..976c1ad9 100644 --- a/backend/application/dashboard/file/deleteFile/usecase.go +++ b/backend/application/dashboard/file/deleteFile/usecase.go @@ -24,7 +24,7 @@ func (uc *UseCase) Execute(request Request) error { return err } - if err := uc.storage.Delete(context.Background(), file.Name); err != nil { + if err := uc.storage.Delete(context.Background(), file.StoredName); err != nil { return err } diff --git a/backend/application/dashboard/file/deleteFile/usecase_test.go b/backend/application/dashboard/file/deleteFile/usecase_test.go index c024e36f..63d8a88e 100644 --- a/backend/application/dashboard/file/deleteFile/usecase_test.go +++ b/backend/application/dashboard/file/deleteFile/usecase_test.go @@ -23,8 +23,9 @@ func TestUseCase_Execute(t *testing.T) { r = Request{FileUUID: "file-uuid"} f = file.File{ - UUID: r.FileUUID, - Name: "file-name", + UUID: r.FileUUID, + Name: "file-name", + StoredName: "store-name", } ) @@ -32,7 +33,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("Delete", r.FileUUID).Return(nil) defer filesRepository.AssertExpectations(t) - storage.On("Delete", context.Background(), f.Name).Once().Return(nil) + storage.On("Delete", context.Background(), f.StoredName).Once().Return(nil) defer storage.AssertExpectations(t) err := NewUseCase(&filesRepository, &storage).Execute(r) @@ -73,8 +74,9 @@ func TestUseCase_Execute(t *testing.T) { r = Request{FileUUID: "file-uuid"} f = file.File{ - UUID: r.FileUUID, - Name: "file-name", + UUID: r.FileUUID, + Name: "file-name", + StoredName: "store-name", } expectedErr = errors.New("error") @@ -83,7 +85,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("GetOne", r.FileUUID).Once().Return(f, nil) defer filesRepository.AssertExpectations(t) - storage.On("Delete", context.Background(), f.Name).Once().Return(expectedErr) + storage.On("Delete", context.Background(), f.StoredName).Once().Return(expectedErr) defer storage.AssertExpectations(t) err := NewUseCase(&filesRepository, &storage).Execute(r) @@ -103,8 +105,9 @@ func TestUseCase_Execute(t *testing.T) { r = Request{FileUUID: "file-uuid"} f = file.File{ - UUID: r.FileUUID, - Name: "file-name", + UUID: r.FileUUID, + Name: "file-name", + StoredName: "store-name", } expectedErr = errors.New("error") @@ -114,7 +117,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("Delete", r.FileUUID).Return(expectedErr) defer filesRepository.AssertExpectations(t) - storage.On("Delete", context.Background(), f.Name).Once().Return(nil) + storage.On("Delete", context.Background(), f.StoredName).Once().Return(nil) defer storage.AssertExpectations(t) err := NewUseCase(&filesRepository, &storage).Execute(r) diff --git a/backend/application/dashboard/file/deleteUserFile/usecase.go b/backend/application/dashboard/file/deleteUserFile/usecase.go index 9e401e56..c52f1461 100644 --- a/backend/application/dashboard/file/deleteUserFile/usecase.go +++ b/backend/application/dashboard/file/deleteUserFile/usecase.go @@ -24,7 +24,7 @@ func (uc *UseCase) Execute(request Request) error { return err } - if err := uc.storage.Delete(context.Background(), file.Name); err != nil { + if err := uc.storage.Delete(context.Background(), file.StoredName); err != nil { return err } diff --git a/backend/application/dashboard/file/deleteUserFile/usecase_test.go b/backend/application/dashboard/file/deleteUserFile/usecase_test.go index 07f222ae..a802dce2 100644 --- a/backend/application/dashboard/file/deleteUserFile/usecase_test.go +++ b/backend/application/dashboard/file/deleteUserFile/usecase_test.go @@ -26,9 +26,10 @@ func TestUseCase_Execute(t *testing.T) { } f = file.File{ - UUID: r.FileUUID, - Name: "file-name", - OwnerUUID: r.OwnerUUID, + UUID: r.FileUUID, + Name: "file-name", + StoredName: "store-name", + OwnerUUID: r.OwnerUUID, } ) @@ -36,7 +37,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("DeleteByOwnerUUID", r.OwnerUUID, r.FileUUID).Return(nil) defer filesRepository.AssertExpectations(t) - storage.On("Delete", context.Background(), f.Name).Once().Return(nil) + storage.On("Delete", context.Background(), f.StoredName).Once().Return(nil) defer storage.AssertExpectations(t) err := NewUseCase(&filesRepository, &storage).Execute(r) @@ -83,9 +84,10 @@ func TestUseCase_Execute(t *testing.T) { } f = file.File{ - UUID: r.FileUUID, - Name: "file-name", - OwnerUUID: r.OwnerUUID, + UUID: r.FileUUID, + Name: "file-name", + StoredName: "store-name", + OwnerUUID: r.OwnerUUID, } expectedErr = errors.New("error") @@ -94,7 +96,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("GetOneByOwnerUUID", r.OwnerUUID, r.FileUUID).Once().Return(f, nil) defer filesRepository.AssertExpectations(t) - storage.On("Delete", context.Background(), f.Name).Once().Return(expectedErr) + storage.On("Delete", context.Background(), f.StoredName).Once().Return(expectedErr) defer storage.AssertExpectations(t) err := NewUseCase(&filesRepository, &storage).Execute(r) @@ -117,9 +119,10 @@ func TestUseCase_Execute(t *testing.T) { } f = file.File{ - UUID: r.FileUUID, - Name: "file-name", - OwnerUUID: r.OwnerUUID, + UUID: r.FileUUID, + Name: "file-name", + StoredName: "store-name", + OwnerUUID: r.OwnerUUID, } expectedErr = errors.New("error") @@ -129,7 +132,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("DeleteByOwnerUUID", r.OwnerUUID, r.FileUUID).Return(expectedErr) defer filesRepository.AssertExpectations(t) - storage.On("Delete", context.Background(), f.Name).Once().Return(nil) + storage.On("Delete", context.Background(), f.StoredName).Once().Return(nil) defer storage.AssertExpectations(t) err := NewUseCase(&filesRepository, &storage).Execute(r) diff --git a/backend/application/dashboard/file/getFile/useCase.go b/backend/application/dashboard/file/getFile/useCase.go index 70c24533..bfb7177a 100644 --- a/backend/application/dashboard/file/getFile/useCase.go +++ b/backend/application/dashboard/file/getFile/useCase.go @@ -24,7 +24,7 @@ func (uc *UseCase) Execute(UUID string) (*Response, error) { return nil, err } - reader, err := uc.storage.Read(context.Background(), f.Name) + reader, err := uc.storage.Read(context.Background(), f.StoredName) if err != nil { return nil, err } diff --git a/backend/application/dashboard/file/getFile/useCase_test.go b/backend/application/dashboard/file/getFile/useCase_test.go index 53ff3dce..9b6be3b5 100644 --- a/backend/application/dashboard/file/getFile/useCase_test.go +++ b/backend/application/dashboard/file/getFile/useCase_test.go @@ -51,7 +51,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("GetOne", uuid).Once().Return(f, nil) defer filesRepository.AssertExpectations(t) - storage.On("Read", context.Background(), f.Name).Return(reader, nil) + storage.On("Read", context.Background(), f.UUID).Return(reader, nil) defer storage.AssertExpectations(t) response, err := NewUseCase(&filesRepository, &storage).Execute(uuid) @@ -101,7 +101,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("GetOne", uuid).Once().Return(f, nil) defer filesRepository.AssertExpectations(t) - storage.On("Read", context.Background(), f.Name).Return(nil, expectedErr) + storage.On("Read", context.Background(), f.UUID).Return(nil, expectedErr) defer storage.AssertExpectations(t) response, err := NewUseCase(&filesRepository, &storage).Execute(uuid) diff --git a/backend/application/dashboard/file/uploadFile/request.go b/backend/application/dashboard/file/uploadFile/request.go index 4697c049..e7d31fb4 100644 --- a/backend/application/dashboard/file/uploadFile/request.go +++ b/backend/application/dashboard/file/uploadFile/request.go @@ -2,7 +2,9 @@ package createfile import ( "io" + "path/filepath" + "github.com/gofrs/uuid/v5" "github.com/khanzadimahdi/testproject/domain" ) @@ -37,3 +39,17 @@ func (r *Request) Validate() domain.ValidationErrors { return validationErrors } + +func (r *Request) StoredName() (string, error) { + var filename string + + extension := filepath.Ext(r.Name) + + uuid, err := uuid.NewV7() + if err != nil { + return filename, err + } + filename = uuid.String() + extension + + return filename, nil +} diff --git a/backend/application/dashboard/file/uploadFile/usecase.go b/backend/application/dashboard/file/uploadFile/usecase.go index e029ad1c..3b8b86b7 100644 --- a/backend/application/dashboard/file/uploadFile/usecase.go +++ b/backend/application/dashboard/file/uploadFile/usecase.go @@ -32,15 +32,21 @@ func (uc *UseCase) Execute(request *Request) (*Response, error) { }, nil } - if err := uc.storage.Store(context.Background(), request.Name, request.FileReader, request.Size); err != nil { + storedName, err := request.StoredName() + if err != nil { + return nil, err + } + + if err := uc.storage.Store(context.Background(), storedName, request.FileReader, request.Size); err != nil { return nil, err } uuid, err := uc.filesRepository.Save(&file.File{ - Name: request.Name, - Size: request.Size, - OwnerUUID: request.OwnerUUID, - MimeType: request.MimeType, + Name: request.Name, + StoredName: storedName, + Size: request.Size, + OwnerUUID: request.OwnerUUID, + MimeType: request.MimeType, }) if err != nil { return nil, err diff --git a/backend/application/dashboard/file/uploadFile/usecase_test.go b/backend/application/dashboard/file/uploadFile/usecase_test.go index e574c456..60640bf1 100644 --- a/backend/application/dashboard/file/uploadFile/usecase_test.go +++ b/backend/application/dashboard/file/uploadFile/usecase_test.go @@ -3,10 +3,12 @@ package createfile import ( "context" "errors" + "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/khanzadimahdi/testproject/domain" "github.com/khanzadimahdi/testproject/domain/file" @@ -33,12 +35,7 @@ func TestUseCase_Execute(t *testing.T) { OwnerUUID: "owner-uuid", FileReader: strings.NewReader(fileContent), Size: int64(len(fileContent)), - } - - f = file.File{ - Name: r.Name, - Size: r.Size, - OwnerUUID: r.OwnerUUID, + MimeType: "application/octet-stream", } fileUUID = "test-file-uuid" @@ -51,10 +48,14 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) - storage.On("Store", context.Background(), r.Name, r.FileReader, r.Size).Once().Return(nil) + storage.On("Store", context.Background(), mock.Anything, r.FileReader, r.Size).Once().Return(nil) defer storage.AssertExpectations(t) - filesRepository.On("Save", &f).Once().Return(fileUUID, nil) + matchingFile := mock.MatchedBy(func(f *file.File) bool { + return f.Name == r.Name && f.Size == r.Size && f.OwnerUUID == r.OwnerUUID && f.MimeType == r.MimeType && filepath.Ext(f.Name) == filepath.Ext(f.StoredName) + }) + + filesRepository.On("Save", matchingFile).Once().Return(fileUUID, nil) defer filesRepository.AssertExpectations(t) response, err := NewUseCase(&filesRepository, &storage, &validator).Execute(&r) @@ -87,8 +88,8 @@ func TestUseCase_Execute(t *testing.T) { response, err := NewUseCase(&filesRepository, &storage, &validator).Execute(&r) - storage.AssertNotCalled(t, "Store") filesRepository.AssertNotCalled(t, "Save") + storage.AssertNotCalled(t, "Store") assert.NoError(t, err) assert.Equal(t, &expectedResponse, response) @@ -109,6 +110,7 @@ func TestUseCase_Execute(t *testing.T) { OwnerUUID: "owner-uuid", FileReader: strings.NewReader(fileContent), Size: int64(len(fileContent)), + MimeType: "application/octet-stream", } expectedErr = errors.New("storage error") @@ -117,7 +119,9 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) - storage.On("Store", context.Background(), r.Name, r.FileReader, r.Size).Once().Return(expectedErr) + storage.On("Store", context.Background(), mock.MatchedBy(func(storedName string) bool { + return filepath.Ext(r.Name) == filepath.Ext(storedName) && storedName != r.Name + }), r.FileReader, r.Size).Once().Return(expectedErr) defer storage.AssertExpectations(t) response, err := NewUseCase(&filesRepository, &storage, &validator).Execute(&r) @@ -143,12 +147,7 @@ func TestUseCase_Execute(t *testing.T) { OwnerUUID: "owner-uuid", FileReader: strings.NewReader(fileContent), Size: int64(len(fileContent)), - } - - f = file.File{ - Name: r.Name, - Size: r.Size, - OwnerUUID: r.OwnerUUID, + MimeType: "application/octet-stream", } expectedErr = errors.New("error") @@ -157,10 +156,16 @@ func TestUseCase_Execute(t *testing.T) { validator.On("Validate", &r).Once().Return(nil) defer validator.AssertExpectations(t) - storage.On("Store", context.Background(), r.Name, r.FileReader, r.Size).Once().Return(nil) + storage.On("Store", context.Background(), mock.MatchedBy(func(storedName string) bool { + return filepath.Ext(r.Name) == filepath.Ext(storedName) && storedName != r.Name + }), r.FileReader, r.Size).Once().Return(nil) defer storage.AssertExpectations(t) - filesRepository.On("Save", &f).Once().Return("", expectedErr) + matchingFile := mock.MatchedBy(func(f *file.File) bool { + return f.Name == r.Name && f.Size == r.Size && f.OwnerUUID == r.OwnerUUID && f.MimeType == r.MimeType && filepath.Ext(f.Name) == filepath.Ext(f.StoredName) + }) + + filesRepository.On("Save", matchingFile).Once().Return("", expectedErr) defer filesRepository.AssertExpectations(t) response, err := NewUseCase(&filesRepository, &storage, &validator).Execute(&r) diff --git a/backend/application/file/getFile/useCase.go b/backend/application/file/getFile/useCase.go index 70c24533..bfb7177a 100644 --- a/backend/application/file/getFile/useCase.go +++ b/backend/application/file/getFile/useCase.go @@ -24,7 +24,7 @@ func (uc *UseCase) Execute(UUID string) (*Response, error) { return nil, err } - reader, err := uc.storage.Read(context.Background(), f.Name) + reader, err := uc.storage.Read(context.Background(), f.StoredName) if err != nil { return nil, err } diff --git a/backend/application/file/getFile/useCase_test.go b/backend/application/file/getFile/useCase_test.go index 53ff3dce..9b6be3b5 100644 --- a/backend/application/file/getFile/useCase_test.go +++ b/backend/application/file/getFile/useCase_test.go @@ -51,7 +51,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("GetOne", uuid).Once().Return(f, nil) defer filesRepository.AssertExpectations(t) - storage.On("Read", context.Background(), f.Name).Return(reader, nil) + storage.On("Read", context.Background(), f.UUID).Return(reader, nil) defer storage.AssertExpectations(t) response, err := NewUseCase(&filesRepository, &storage).Execute(uuid) @@ -101,7 +101,7 @@ func TestUseCase_Execute(t *testing.T) { filesRepository.On("GetOne", uuid).Once().Return(f, nil) defer filesRepository.AssertExpectations(t) - storage.On("Read", context.Background(), f.Name).Return(nil, expectedErr) + storage.On("Read", context.Background(), f.UUID).Return(nil, expectedErr) defer storage.AssertExpectations(t) response, err := NewUseCase(&filesRepository, &storage).Execute(uuid) diff --git a/backend/compose.app.yaml b/backend/compose.app.yaml new file mode 100644 index 00000000..d871f12b --- /dev/null +++ b/backend/compose.app.yaml @@ -0,0 +1,53 @@ +services: + app: + image: ${APP_IMAGE} + networks: + - app + - mongodb + - nats + - docker + deploy: + mode: replicated + replicas: 2 + endpoint_mode: vip + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + resources: + limits: + cpus: '0.20' + memory: 150M + environment: + PRIVATE_KEY: ${BACKEND_PRIVATE_KEY} + S3_ENDPOINT: ${BACKEND_S3_ENDPOINT} + S3_ACCESS_KEY: ${BACKEND_S3_ACCESS_KEY} + S3_SECRET_KEY: ${BACKEND_S3_SECRET_KEY} + S3_BUCKET_NAME: ${BACKEND_S3_BUCKET_NAME} + S3_USE_SSL: ${BACKEND_S3_USE_SSL} + MONGO_SCHEME: ${BACKEND_MONGO_SCHEME} + MONGO_USERNAME: ${BACKEND_MONGO_USERNAME} + MONGO_PASSWORD: ${BACKEND_MONGO_PASSWORD} + MONGO_HOST: ${BACKEND_MONGO_HOST} + MONGO_PORT: ${BACKEND_MONGO_PORT} + MONGO_DATABASE_NAME: ${BACKEND_MONGO_DATABASE_NAME} + MAIL_SMTP_FROM: ${BACKEND_MAIL_SMTP_FROM} + MAIL_SMTP_USERNAME: ${BACKEND_MAIL_SMTP_USERNAME} + MAIL_SMTP_PASSWORD: ${BACKEND_MAIL_SMTP_PASSWORD} + MAIL_SMTP_HOST: ${BACKEND_MAIL_SMTP_HOST} + MAIL_SMTP_PORT: ${BACKEND_MAIL_SMTP_PORT} + NATS_URL: ${BACKEND_NATS_URL} + +networks: + app: + name: app + external: true + mongodb: + name: mongodb + external: true + nats: + name: nats + external: true + docker: + name: docker + external: true diff --git a/backend/domain/file/file.go b/backend/domain/file/file.go index 178830a2..9f243922 100644 --- a/backend/domain/file/file.go +++ b/backend/domain/file/file.go @@ -7,12 +7,13 @@ import ( ) type File struct { - UUID string - Name string - Size int64 - OwnerUUID string - MimeType string - CreatedAt time.Time + UUID string + Name string + StoredName string + Size int64 + OwnerUUID string + MimeType string + CreatedAt time.Time } type Repository interface { diff --git a/backend/infrastructure/repository/mongodb/files/model.go b/backend/infrastructure/repository/mongodb/files/model.go index 6fb90e40..8fe91622 100644 --- a/backend/infrastructure/repository/mongodb/files/model.go +++ b/backend/infrastructure/repository/mongodb/files/model.go @@ -5,12 +5,13 @@ import ( ) type FileBson struct { - UUID string `bson:"_id,omitempty"` - Name string `bson:"name"` - Size int64 `bson:"size"` - OwnerUUID string `bson:"owner_uuid"` - MimeType string `bson:"mimetype"` - CreatedAt time.Time `bson:"created_at,omitempty"` + UUID string `bson:"_id,omitempty"` + Name string `bson:"name"` + StoredName string `bson:"stored_name"` + Size int64 `bson:"size"` + OwnerUUID string `bson:"owner_uuid"` + MimeType string `bson:"mimetype"` + CreatedAt time.Time `bson:"created_at,omitempty"` } type SetWrapper struct { diff --git a/backend/infrastructure/repository/mongodb/files/repository.go b/backend/infrastructure/repository/mongodb/files/repository.go index ff2893cc..8fcc49d3 100644 --- a/backend/infrastructure/repository/mongodb/files/repository.go +++ b/backend/infrastructure/repository/mongodb/files/repository.go @@ -62,12 +62,13 @@ func (r *FilesRepository) GetAll(offset uint, limit uint) ([]file.File, error) { return nil, err } items = append(items, file.File{ - UUID: a.UUID, - Name: a.Name, - Size: a.Size, - OwnerUUID: a.OwnerUUID, - MimeType: a.MimeType, - CreatedAt: a.CreatedAt, + UUID: a.UUID, + Name: a.Name, + StoredName: a.StoredName, + Size: a.Size, + OwnerUUID: a.OwnerUUID, + MimeType: a.MimeType, + CreatedAt: a.CreatedAt, }) } @@ -91,12 +92,13 @@ func (r *FilesRepository) GetOne(UUID string) (file.File, error) { } return file.File{ - UUID: a.UUID, - Name: a.Name, - Size: a.Size, - OwnerUUID: a.OwnerUUID, - MimeType: a.MimeType, - CreatedAt: a.CreatedAt, + UUID: a.UUID, + Name: a.Name, + StoredName: a.StoredName, + Size: a.Size, + OwnerUUID: a.OwnerUUID, + MimeType: a.MimeType, + CreatedAt: a.CreatedAt, }, nil } @@ -113,12 +115,13 @@ func (r *FilesRepository) Save(a *file.File) (string, error) { } update := FileBson{ - UUID: a.UUID, - Name: a.Name, - Size: a.Size, - OwnerUUID: a.OwnerUUID, - MimeType: a.MimeType, - CreatedAt: time.Now(), + UUID: a.UUID, + Name: a.Name, + StoredName: a.StoredName, + Size: a.Size, + OwnerUUID: a.OwnerUUID, + MimeType: a.MimeType, + CreatedAt: time.Now(), } upsert := true @@ -187,12 +190,13 @@ func (r *FilesRepository) GetAllByOwnerUUID(ownerUUID string, offset uint, limit return nil, err } items = append(items, file.File{ - UUID: a.UUID, - Name: a.Name, - Size: a.Size, - OwnerUUID: a.OwnerUUID, - MimeType: a.MimeType, - CreatedAt: a.CreatedAt, + UUID: a.UUID, + Name: a.Name, + StoredName: a.StoredName, + Size: a.Size, + OwnerUUID: a.OwnerUUID, + MimeType: a.MimeType, + CreatedAt: a.CreatedAt, }) } @@ -221,12 +225,13 @@ func (r *FilesRepository) GetOneByOwnerUUID(ownerUUID string, UUID string) (file } return file.File{ - UUID: a.UUID, - Name: a.Name, - Size: a.Size, - OwnerUUID: a.OwnerUUID, - MimeType: a.MimeType, - CreatedAt: a.CreatedAt, + UUID: a.UUID, + Name: a.Name, + StoredName: a.StoredName, + Size: a.Size, + OwnerUUID: a.OwnerUUID, + MimeType: a.MimeType, + CreatedAt: a.CreatedAt, }, nil } diff --git a/backend/presentation/http/api/dashboard/file/deleteUser_test.go b/backend/presentation/http/api/dashboard/file/deleteUser_test.go index 43226c43..c626716e 100644 --- a/backend/presentation/http/api/dashboard/file/deleteUser_test.go +++ b/backend/presentation/http/api/dashboard/file/deleteUser_test.go @@ -38,8 +38,9 @@ func TestDeleteUserHandler(t *testing.T) { } f = file.File{ - UUID: r.FileUUID, - Name: "file-name", + UUID: r.FileUUID, + Name: "file-name", + StoredName: "stored-name", } ) @@ -50,7 +51,7 @@ func TestDeleteUserHandler(t *testing.T) { filesRepository.On("DeleteByOwnerUUID", r.OwnerUUID, r.FileUUID).Return(nil) defer filesRepository.AssertExpectations(t) - storage.On("Delete", context.Background(), f.Name).Once().Return(nil) + storage.On("Delete", context.Background(), f.StoredName).Once().Return(nil) defer storage.AssertExpectations(t) handler := NewDeleteUserHandler(deleteuserfile.NewUseCase(&filesRepository, &storage), &authorizer) diff --git a/backend/presentation/http/api/dashboard/file/delete_test.go b/backend/presentation/http/api/dashboard/file/delete_test.go index 10ba96f9..548bbeae 100644 --- a/backend/presentation/http/api/dashboard/file/delete_test.go +++ b/backend/presentation/http/api/dashboard/file/delete_test.go @@ -35,8 +35,9 @@ func TestDeleteHandler(t *testing.T) { r = deletefile.Request{FileUUID: "file-uuid"} f = file.File{ - UUID: r.FileUUID, - Name: "file-name", + UUID: r.FileUUID, + Name: "file-name", + StoredName: "stored-name", } ) @@ -47,7 +48,7 @@ func TestDeleteHandler(t *testing.T) { filesRepository.On("Delete", r.FileUUID).Return(nil) defer filesRepository.AssertExpectations(t) - storage.On("Delete", context.Background(), f.Name).Once().Return(nil) + storage.On("Delete", context.Background(), f.StoredName).Once().Return(nil) defer storage.AssertExpectations(t) handler := NewDeleteHandler(deletefile.NewUseCase(&filesRepository, &storage), &authorizer) diff --git a/backend/presentation/http/api/dashboard/file/show_test.go b/backend/presentation/http/api/dashboard/file/show_test.go index 27f2a2ae..77b1f3cc 100644 --- a/backend/presentation/http/api/dashboard/file/show_test.go +++ b/backend/presentation/http/api/dashboard/file/show_test.go @@ -36,8 +36,9 @@ func TestShowHandler(t *testing.T) { u = user.User{UUID: "auth-user-uuid"} f = file.File{ - UUID: "file-test-uuid", - Name: "file-test-name", + UUID: "file-test-uuid", + Name: "file-test-name", + StoredName: "stored-file-name", } ) @@ -50,7 +51,7 @@ func TestShowHandler(t *testing.T) { filesRepository.On("GetOne", f.UUID).Once().Return(f, nil) defer filesRepository.AssertExpectations(t) - storage.On("Read", context.Background(), f.Name).Once().Return(reader, nil) + storage.On("Read", context.Background(), f.StoredName).Once().Return(reader, nil) defer storage.AssertExpectations(t) useCase := getfile.NewUseCase(&filesRepository, &storage) @@ -153,8 +154,9 @@ func TestShowHandler(t *testing.T) { u = user.User{UUID: "auth-user-uuid"} file = file.File{ - UUID: "file-test-uuid", - Name: "file-test-name", + UUID: "file-test-uuid", + Name: "file-test-name", + StoredName: "stored-file-name", } ) @@ -167,7 +169,7 @@ func TestShowHandler(t *testing.T) { filesRepository.On("GetOne", file.UUID).Once().Return(file, nil) defer filesRepository.AssertExpectations(t) - storage.On("Read", context.Background(), file.Name).Once().Return(reader, errors.New("some error")) + storage.On("Read", context.Background(), file.StoredName).Once().Return(reader, errors.New("some error")) defer storage.AssertExpectations(t) useCase := getfile.NewUseCase(&filesRepository, &storage) diff --git a/backend/presentation/http/api/dashboard/file/upload_test.go b/backend/presentation/http/api/dashboard/file/upload_test.go index 3b99c54c..6cc334fb 100644 --- a/backend/presentation/http/api/dashboard/file/upload_test.go +++ b/backend/presentation/http/api/dashboard/file/upload_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -46,13 +47,6 @@ func TestUploadHandler(t *testing.T) { } fileUUID = "test-file-uuid" - - f = file.File{ - Name: r.Name, - Size: r.Size, - OwnerUUID: u.UUID, - MimeType: "application/octet-stream", - } ) var payload bytes.Buffer @@ -70,10 +64,14 @@ func TestUploadHandler(t *testing.T) { requestValidator.On("Validate", mock.Anything).Once().Return(nil) defer requestValidator.AssertExpectations(t) - storage.On("Store", context.Background(), r.Name, mock.Anything, r.Size).Once().Return(nil) + storage.On("Store", context.Background(), mock.Anything, mock.Anything, r.Size).Once().Return(nil) defer storage.AssertExpectations(t) - filesRepository.On("Save", &f).Once().Return(fileUUID, nil) + matchingFile := mock.MatchedBy(func(f *file.File) bool { + return f.Name == r.Name && f.Size == r.Size && f.OwnerUUID == u.UUID && f.MimeType == "application/octet-stream" && filepath.Ext(f.Name) == filepath.Ext(f.StoredName) + }) + + filesRepository.On("Save", matchingFile).Once().Return(fileUUID, nil) defer filesRepository.AssertExpectations(t) handler := NewUploadHandler(createfile.NewUseCase(&filesRepository, &storage, &requestValidator), &authorizer) @@ -85,6 +83,8 @@ func TestUploadHandler(t *testing.T) { handler.ServeHTTP(response, request) + filesRepository.AssertNotCalled(t, "Delete") + expectedBody, err := os.ReadFile("testdata/upload-files-response.json") assert.NoError(t, err) diff --git a/backend/presentation/http/api/file/show_test.go b/backend/presentation/http/api/file/show_test.go index b5440e15..e2ef6225 100644 --- a/backend/presentation/http/api/file/show_test.go +++ b/backend/presentation/http/api/file/show_test.go @@ -31,8 +31,9 @@ func TestShowHandler(t *testing.T) { ) f := file.File{ - UUID: "file-test-uuid", - Name: "file-test-name", + UUID: "file-test-uuid", + Name: "file-test-name", + StoredName: "stored-file-name", } fileData := []byte("this is the file payload") @@ -41,7 +42,7 @@ func TestShowHandler(t *testing.T) { filesRepository.On("GetOne", f.UUID).Once().Return(f, nil) defer filesRepository.AssertExpectations(t) - storage.On("Read", context.Background(), f.Name).Once().Return(reader, nil) + storage.On("Read", context.Background(), f.StoredName).Once().Return(reader, nil) defer storage.AssertExpectations(t) useCase := getfile.NewUseCase(&filesRepository, &storage) @@ -98,8 +99,9 @@ func TestShowHandler(t *testing.T) { ) file := file.File{ - UUID: "file-test-uuid", - Name: "file-test-name", + UUID: "file-test-uuid", + Name: "file-test-name", + StoredName: "stored-file-name", } fileData := "this is the file payload" @@ -108,7 +110,7 @@ func TestShowHandler(t *testing.T) { filesRepository.On("GetOne", file.UUID).Once().Return(file, nil) defer filesRepository.AssertExpectations(t) - storage.On("Read", context.Background(), file.Name).Once().Return(reader, errors.New("some error")) + storage.On("Read", context.Background(), file.StoredName).Once().Return(reader, errors.New("some error")) defer storage.AssertExpectations(t) useCase := getfile.NewUseCase(&filesRepository, &storage) diff --git a/backend/presentation/http/api/home/home.go b/backend/presentation/http/api/home/home.go index 4ffa8e89..c73047ad 100644 --- a/backend/presentation/http/api/home/home.go +++ b/backend/presentation/http/api/home/home.go @@ -2,6 +2,7 @@ package home import ( "encoding/json" + "log" "net/http" "github.com/khanzadimahdi/testproject/application/home" @@ -22,6 +23,7 @@ func (h *homeHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { switch { case err != nil: + log.Println(err) rw.WriteHeader(http.StatusInternalServerError) default: rw.Header().Add("Content-Type", "application/json") diff --git a/compose.yaml b/compose.yaml index be320dd4..0a8114be 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,6 +4,7 @@ x-app: &app context: ./backend dockerfile: Dockerfile target: develop + restart: unless-stopped depends_on: - mongodb - minio @@ -36,6 +37,7 @@ services: context: ./frontend dockerfile: Dockerfile target: develop + restart: unless-stopped depends_on: - app ports: @@ -43,9 +45,9 @@ services: volumes: - ./frontend:/opt/app environment: - NEXT_PUBLIC_EXTERNAL_BACKEND_BASE_URL: ${NEXT_PUBLIC_EXTERNAL_BACKEND_BASE_URL} INTERNAL_BACKEND_BASE_URL: ${INTERNAL_BACKEND_BASE_URL} - NEXT_PUBLIC_FILES_BASE_URL: ${NEXT_PUBLIC_FILES_BASE_URL} + NEXT_PUBLIC_FILES_PROTOCOL: ${NEXT_PUBLIC_FILES_PROTOCOL} + NEXT_PUBLIC_FILES_HOST: ${NEXT_PUBLIC_FILES_HOST} command: > sh -c "npm install; npm run dev -- --hostname \"0.0.0.0\" --port 3000" @@ -56,8 +58,6 @@ services: runner: <<: *app - volumes: - - podman-data:/tmp ports: - "8001:80" command: > @@ -65,15 +65,19 @@ services: mongodb: image: mongo - restart: always + restart: unless-stopped ports: - "27017:27017" + volumes: + - ./tmp/mongodb/db:/data/db + - ./tmp/mongodb/configdb:/data/configdb environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_USERNAME} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} mongodashboard: image: mongo-express + restart: unless-stopped depends_on: - mongodb ports: @@ -85,8 +89,9 @@ services: minio: image: minio/minio + restart: unless-stopped ports: - - "9000:9000" + - "9100:9000" - "9001:9001" environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY} @@ -104,28 +109,38 @@ services: nats: image: nats:2.10 - command: ["--jetstream", "-m", "8222", "-p", "4222"] # --config /etc/nats/nats-server.conf + command: ["--jetstream", "--http_port", "8222", "--port", "4222", "--store_dir", "/data"] ports: - "4222:4222" # client port - "6222:6222" # cluster post - "8222:8222" # HTTP monitoring port + volumes: + - ./tmp/nats:/data natsdashboard: image: mdawar/nats-dashboard ports: - "8082:80" - podman: - image: quay.io/podman/stable - volumes: - - podman-data:/tmp - security_opt: - - label=disable - devices: - - /dev/fuse - user: podman - command: > - podman system service --time=0 unix:///tmp/podman.sock + docker: + image: docker:27-dind + restart: unless-stopped + privileged: true + ports: + - "2375:2375" + environment: + DOCKER_TLS_CERTDIR: "" # disable certs -volumes: - podman-data: + portainer: + image: portainer/portainer-ce + restart: unless-stopped + depends_on: + - docker + ports: + - "9200:9000" + - "9443:9443" + command: > + --admin-password="$$2a$$12$$4xcOa82Ni5rjgQF.v.JWi.i71OyUm3fwmfWiumgJHIAPGU.uOw3qu" + # username: admin + # password: admin-password + # standalone docker API url: docker:2375 diff --git a/frontend/.env.local.example b/frontend/.env.local.example index 2ccb6b69..84a29ce9 100644 --- a/frontend/.env.local.example +++ b/frontend/.env.local.example @@ -1,3 +1,3 @@ INTERNAL_BACKEND_BASE_URL=https://backend.tarhche.com -NEXT_PUBLIC_FILES_BASE_URL=https://backend.tarhche.com -NEXT_PUBLIC_EXTERNAL_BACKEND_BASE_URL=https://tarhche.com +NEXT_PUBLIC_FILES_PROTOCOL=https +NEXT_PUBLIC_FILES_HOST=backend.tarhche.com diff --git a/frontend/compose.frontend.yaml b/frontend/compose.frontend.yaml new file mode 100644 index 00000000..396ab31f --- /dev/null +++ b/frontend/compose.frontend.yaml @@ -0,0 +1,30 @@ +services: + frontend: + image: ${FRONTEND_IMAGE} + networks: + - frontend + - app + deploy: + mode: replicated + replicas: 2 + endpoint_mode: vip + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + resources: + limits: + cpus: '0.20' + memory: 150M + environment: + INTERNAL_BACKEND_BASE_URL: ${INTERNAL_BACKEND_BASE_URL} + NEXT_PUBLIC_FILES_PROTOCOL: ${NEXT_PUBLIC_FILES_PROTOCOL} + NEXT_PUBLIC_FILES_HOST: ${NEXT_PUBLIC_FILES_HOST} + +networks: + frontend: + name: frontend + external: true + app: + name: app + external: true diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index ea52468c..6b353224 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -4,8 +4,8 @@ const nextConfig = { images: { remotePatterns: [ { - protocol: "https", - hostname: "backend.tarhche.com", + protocol: process.env.NEXT_PUBLIC_FILES_PROTOCOL, + hostname: process.env.NEXT_PUBLIC_FILES_HOST, }, ], }, diff --git a/frontend/src/constants/envs.ts b/frontend/src/constants/envs.ts index 4e9a130d..e408dd91 100644 --- a/frontend/src/constants/envs.ts +++ b/frontend/src/constants/envs.ts @@ -1,4 +1,2 @@ export const INTERNAL_BACKEND_URL = process.env.INTERNAL_BACKEND_BASE_URL; -export const EXTERNAL_BACKEND_URL = - process.env.NEXT_PUBLIC_EXTERNAL_BACKEND_BASE_URL; -export const FILES_PUBLIC_URL = `https://backend.tarhche.com/files`; +export const FILES_PUBLIC_URL = `${process.env.NEXT_PUBLIC_FILES_PROTOCOL}://${process.env.NEXT_PUBLIC_FILES_HOST}/files`; diff --git a/frontend/src/features/articles/components/article-editor/article-editor.css b/frontend/src/features/articles/components/article-editor/article-editor.css index 5298ca7a..4b5aedec 100644 --- a/frontend/src/features/articles/components/article-editor/article-editor.css +++ b/frontend/src/features/articles/components/article-editor/article-editor.css @@ -284,3 +284,8 @@ --ck-color-link-default: hsl(190, 100%, 75%); } + +:root[data-mantine-color-scheme="dark"] .ck-content pre { + color: var(--mantine-color-text); + background-color: var(--mantine-color-dark-6); +} diff --git a/frontend/src/features/articles/components/article-editor/editor-config.ts b/frontend/src/features/articles/components/article-editor/editor-config.ts index 55ea1375..ed3fa51c 100644 --- a/frontend/src/features/articles/components/article-editor/editor-config.ts +++ b/frontend/src/features/articles/components/article-editor/editor-config.ts @@ -377,4 +377,13 @@ export const editorConfig: EditorConfig = { "tableCellProperties", ], }, + codeBlock: { + languages: [ + {language: "plaintext", label: "Plain text"}, + {language: "javascript", label: "JavaScript"}, + {language: "python", label: "Python"}, + {language: "go", label: "Golang"}, + {language: "bash", label: "bash"}, + ], + }, };