diff --git a/.github/workflows/api-contract-tests.yml b/.github/workflows/api-contract-tests.yml index e3c2b3c6..f2ae524f 100644 --- a/.github/workflows/api-contract-tests.yml +++ b/.github/workflows/api-contract-tests.yml @@ -42,7 +42,7 @@ jobs: sleep 15 - name: Print application logs - run: docker-compose logs + run: docker compose logs - name: Test connectivity run: curl ${API_URL} diff --git a/.github/workflows/backend-deploy-aws.yml b/.github/workflows/backend-deploy-aws.yml new file mode 100644 index 00000000..4c66aa87 --- /dev/null +++ b/.github/workflows/backend-deploy-aws.yml @@ -0,0 +1,44 @@ +name: Deploy Backend to AWS + +on: + workflow_dispatch: + +jobs: + build: + defaults: + run: + working-directory: ./backend + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + run: | + yarn docker:app:build + docker tag notes-app-be:latest ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-be:latest + docker push ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-be:latest + deploy: + needs: build + runs-on: ec2-runner-be + steps: + - name: Delete old be container + run: docker rm -f notes-app-be + - name: Delete old be image + run: docker image rm -f ${{ secrets.DOCKERHUB_USERNAME }}notes-app-fe + - name: Prune docker system + run: docker system prune -f + - name: Pull image from docker hub + run: docker pull ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-be:latest + - name: Run docker container + run: docker run -e DATABASE_URL=${{ secrets.PROD_DATABASE_URL }} -e JWT_SECRET=${{ secrets.JWT_SECRET }} -d -p 5000:5000 --name notes-app-be ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-be:latest \ No newline at end of file diff --git a/.github/workflows/backend-deploy.yml b/.github/workflows/backend-deploy.yml deleted file mode 100644 index e9368820..00000000 --- a/.github/workflows/backend-deploy.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Backend Production Deploy - -on: - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Deploy to production - uses: johnbeynon/render-deploy-action@v0.0.8 - with: - service-id: ${{ secrets.MY_RENDER_SERVICE_ID_BE }} - api-key: ${{ secrets.MY_RENDER_API_KEY }} diff --git a/.github/workflows/backend-docker-push.yml b/.github/workflows/backend-docker-push.yml deleted file mode 100644 index 3cd9525f..00000000 --- a/.github/workflows/backend-docker-push.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Docker Push Backend - -on: - push: - tags: - - "be/*.*" -defaults: - run: - working-directory: ./backend - -permissions: write-all - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=VERSION::$(echo ${GITHUB_REF/refs\/tags\//} | cut -d'/' -f2)" - - - name: Build and push Docker image - env: - IMAGE_TAG: ${{ steps.extract_version.outputs.VERSION }} - run: | - yarn docker:app:build - docker tag notes-app-be:latest ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-be:$IMAGE_TAG - docker push ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-be:$IMAGE_TAG diff --git a/.github/workflows/backend-e2e-tests.yml b/.github/workflows/backend-e2e-tests.yml index 720d235a..120a0a84 100644 --- a/.github/workflows/backend-e2e-tests.yml +++ b/.github/workflows/backend-e2e-tests.yml @@ -44,7 +44,7 @@ jobs: sleep 15 - name: Print application logs - run: docker-compose logs + run: docker compose logs - name: Test connectivity run: curl ${API_URL} diff --git a/.github/workflows/frontend-deploy-aws.yml b/.github/workflows/frontend-deploy-aws.yml new file mode 100644 index 00000000..1ee86907 --- /dev/null +++ b/.github/workflows/frontend-deploy-aws.yml @@ -0,0 +1,44 @@ +name: Deploy Frontend to AWS + +on: + workflow_dispatch: + +permissions: write-all + +jobs: + build: + defaults: + run: + working-directory: ./frontend + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + run: | + docker build --build-arg REACT_APP_API_BASE_URL=${{ secrets.REACT_APP_API_BASE_URL }} -t notes-app-fe . + docker tag notes-app-fe:latest ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-fe:latest + docker push ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-fe:latest + deploy: + needs: build + runs-on: ec2-runner-fe + steps: + - name: Delete old fe container + run: docker rm -f notes-app-fe + - name: Prune docker system + run: docker system prune -f + - name: Pull image from docker hub + run: docker pull ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-fe:latest + - name: Run docker container + run: docker run -e REACT_APP_API_BASE_URL=${{ secrets.REACT_APP_API_BASE_URL }} -d -p 3000:3000 --name notes-app-fe ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-fe:latest \ No newline at end of file diff --git a/.github/workflows/frontend-deploy.yml b/.github/workflows/frontend-deploy.yml deleted file mode 100644 index 1a658699..00000000 --- a/.github/workflows/frontend-deploy.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Frontend Production Deploy - -on: - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Deploy to production - uses: johnbeynon/render-deploy-action@v0.0.8 - with: - service-id: ${{ secrets.MY_RENDER_SERVICE_ID_FE }} - api-key: ${{ secrets.MY_RENDER_API_KEY }} diff --git a/.github/workflows/frontend-docker-push.yml b/.github/workflows/frontend-docker-push.yml deleted file mode 100644 index a7b916e1..00000000 --- a/.github/workflows/frontend-docker-push.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Docker Push Frontend - -on: - push: - tags: - - "fe/*.*" # Trigger on any tag creation -defaults: - run: - working-directory: ./frontend - -permissions: write-all - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=VERSION::$(echo ${GITHUB_REF/refs\/tags\//} | cut -d'/' -f2)" - - - name: Build and push Docker image - env: - IMAGE_TAG: ${{ steps.extract_version.outputs.VERSION }} - run: | - yarn docker:build - docker tag notes-app-fe:latest ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-fe:$IMAGE_TAG - docker push ${{ secrets.DOCKERHUB_USERNAME }}/notes-app-fe:$IMAGE_TAG diff --git a/.github/workflows/frontend-service-tests-deploy.yml b/.github/workflows/frontend-service-tests-deploy.yml deleted file mode 100644 index 71fd69b7..00000000 --- a/.github/workflows/frontend-service-tests-deploy.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Frontend Service Tests - Deploy to Production - -on: - push: - branches: [ "main" ] - paths: - - "frontend/**" - -jobs: - build: - runs-on: ubuntu-latest - env: - API_URL: http://localhost:5000 - steps: - - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Use Node.js 20.x - uses: actions/setup-node@v4 - with: - node-version: 20.x - - - name: Create .env file - run: | - cd ./backend - echo "JWT_SECRET=${{ secrets.JWT_SECRET }}" > .env - - - name: Start service in docker - run: | - cd ./backend - yarn docker:up - - - name: Wait for service to start - run: | - sleep 15 - - - name: Test connectivity - run: curl ${API_URL} - - - name: Install and start frontend - run: | - cd ./frontend - yarn - yarn build - yarn start & - - - name: Install Playwright - run: | - cd ./playwright - yarn - yarn playwright install chromium - - - name: Run Playwright Tests against chromium - run: | - cd ./playwright - yarn test:chromium - - - name: Stop service in docker - run: | - cd ./backend - yarn docker:down - - - name: Deploy FE to production - uses: johnbeynon/render-deploy-action@v0.0.8 - - with: - service-id: ${{ secrets.MY_RENDER_SERVICE_ID_FE }} - api-key: ${{ secrets.MY_RENDER_API_KEY }} diff --git a/backend/Dockerfile b/backend/Dockerfile index 215ab822..6b0e390f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,37 +1,40 @@ # Use an official Node.js runtime based on Alpine Linux as the base image -FROM node:20-alpine - -# Install bash -RUN apk add --no-cache bash +FROM node:20-alpine as builder # Set the working directory inside the container WORKDIR /usr/src/app -# Copy package.json and package-lock.json to the working directory -COPY package.json yarn.lock ./ +# Install dependencies required for the build +RUN apk add --no-cache bash openssl3 + +# Copy package.json, package-lock.json, and TypeScript configuration files +COPY package.json yarn.lock tsconfig.json ./ # Install the application dependencies RUN yarn -# Required for prisma -RUN apk add openssl3 +# Copy the application code and other necessary files into the container +COPY . ./ +COPY wait-for-it.sh wait-for-it.sh -# Copy the TypeScript configuration files -COPY tsconfig.json ./ +# Make the script executable and build the application +RUN chmod +x wait-for-it.sh && \ + npx prisma generate && \ + yarn build -# Copy the application code into the container -COPY . . +# Use a new, clean base image for the runtime +FROM node:20-alpine -COPY wait-for-it.sh wait-for-it.sh +WORKDIR /usr/src/app -# Make the script executable -RUN chmod +x wait-for-it.sh +# Install dependencies required for the build +RUN apk add --no-cache bash -# Primsa generate -RUN npx prisma generate +# Copy only the built artifacts and necessary files from the builder stage +COPY --from=builder /usr/src/app/ ./ -# Build TypeScript code -RUN yarn build +# Copy the wait-for-it.sh script and make it executable +RUN chmod +x wait-for-it.sh # Expose the port that the application will run on EXPOSE 5000 diff --git a/backend/package.json b/backend/package.json index 2ce66ae8..c14fac3d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -13,9 +13,9 @@ "build": "tsc", "docker:app:build": "docker build -t notes-app-be .", "docker:app:up": "docker run -p 5001:5001 notes-app-be", - "docker:up": "docker-compose up -d", - "docker:db:up": "docker-compose up postgres -d", - "docker:down": "docker-compose down", + "docker:up": "docker compose up -d", + "docker:db:up": "docker compose up postgres -d", + "docker:down": "docker compose down", "prisma": "prisma migrate dev --name init", "seed": "ts-node prisma/seed.ts", "lint": "eslint .", diff --git a/backend/src/index.ts b/backend/src/index.ts index aeb55d64..10c438a3 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -23,7 +23,7 @@ app.use('/', noteRoutes); app.use('/', userRoutes); app.use('/', loginRoutes); -app.get('/health', (req, res) => { +app.get('/api/health', (req, res) => { res.json({ status: 'ok' }); }); diff --git a/backend/src/routes/userRoutes.ts b/backend/src/routes/userRoutes.ts index 45ebffcd..ca2f7285 100644 --- a/backend/src/routes/userRoutes.ts +++ b/backend/src/routes/userRoutes.ts @@ -20,6 +20,23 @@ router.get('/api/users', authenticateToken, async (req, res) => { } }); +router.get('/api/user', authenticateToken, async (req, res) => { + // get id from decoded jwt token + const id = req.user.userId; + + try { + const user = await prisma.user.findFirst({ + where: { id }, + }); + // Remove password from the response + delete user.password; + res.json(user); + } catch (error) { + console.log('error', error); + res.status(500).send({ error: 'Oops, something went wrong' }); + } +}); + router.post('/api/users', async (req, res) => { const { email, password, username } = req.body; diff --git a/backend/tests/e2e/notes.spec.ts b/backend/tests/e2e/notes.spec.ts index 528f0994..19d4cf07 100644 --- a/backend/tests/e2e/notes.spec.ts +++ b/backend/tests/e2e/notes.spec.ts @@ -11,7 +11,7 @@ let createdID: number; let token: string; test('Health check', async () => { - const response = await request(BASE_URL).get('/health'); + const response = await request(BASE_URL).get('/api/health'); expect(response.status).toBe(200); }); diff --git a/backend/tests/e2e/users.spec.ts b/backend/tests/e2e/users.spec.ts index 1fdff6b7..c777ad74 100644 --- a/backend/tests/e2e/users.spec.ts +++ b/backend/tests/e2e/users.spec.ts @@ -7,6 +7,7 @@ config(); const BASE_URL = `${process.env.API_URL}`; const USERS_URL = `${BASE_URL}/api/users`; +const USER_URL = `${BASE_URL}/api/user`; const username = faker.internet.userName().toLowerCase(); const email = faker.internet.email(); @@ -41,6 +42,16 @@ describe('Authenticated Flows', () => { expect(getUsersResponse.body.length).toBeGreaterThan(0); }); + test('Get the user response', async () => { + const getUsersResponse = await request(USER_URL) + .get('/') + .set('Authorization', `Bearer ${token}`); + expect(getUsersResponse.status).toBe(200); + expect(getUsersResponse.body.username).toBe('Test User'); + expect(getUsersResponse.body.email).toBe('helloitsdave@hotmail.com'); + expect(getUsersResponse.body.password).toBeUndefined(); + }); + test("Username's should be unique", async () => { const response = await request(USERS_URL) .post('/') diff --git a/backend/tests/integration/notes.spec.ts b/backend/tests/integration/notes.spec.ts index 37ad95aa..83c75ad7 100644 --- a/backend/tests/integration/notes.spec.ts +++ b/backend/tests/integration/notes.spec.ts @@ -223,8 +223,8 @@ describe('Delete a note', () => { }); describe('Health check', () => { - test('GET /health', async ({}) => { - const response = await request(app).get('/health'); + test('GET /api/health', async ({}) => { + const response = await request(app).get('/api/health'); expect(response.status).toBe(200); expect(response.body).toStrictEqual({ status: 'ok' }); }); diff --git a/backend/tests/integration/users.spec.ts b/backend/tests/integration/users.spec.ts index 5a8f0c09..1f24be0a 100644 --- a/backend/tests/integration/users.spec.ts +++ b/backend/tests/integration/users.spec.ts @@ -56,6 +56,39 @@ describe('Get Users', () => { }); }); +describe('Get user', () => { + test('GET user info for existing user', async () => { + prisma.user.findFirst.mockResolvedValue({ + id: 'ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272', + username: 'Dave', + password: 'check', + email: 'testing@backend.com', + createdAt: new Date('2024-02-05T23:33:42.252Z'), + updatedAt: new Date('2024-02-05T23:33:42.252Z'), + }); + + const response = await request(app).get('/api/user'); + expect(response.status).toBe(200); + expect(response.body).toStrictEqual({ + id: 'ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272', + username: 'Dave', + email: 'testing@backend.com', + createdAt: '2024-02-05T23:33:42.252Z', + updatedAt: '2024-02-05T23:33:42.252Z', + }); + }); + test('Network Error', async ({}) => { + prisma.user.findFirst.mockImplementation(() => { + throw new Error('Test error'); + }); + const response = await request(app).get('/api/user'); + expect(response.status).toBe(500); + expect(response.body).toStrictEqual({ + error: 'Oops, something went wrong', + }); + }); +}); + describe('Create User', () => { test('POST with email, username and password', async ({}) => { prisma.user.create.mockResolvedValue({ @@ -137,7 +170,7 @@ describe('Create User', () => { describe('Delete User', () => { test('DELETE with id', async ({}) => { prisma.user.delete.mockResolvedValue({ - id: 'gcf89a7e-b941-4f17-bbe0-4e0c8b2cd272', + id: 'ccf89a7e-b941-4f17-bbe0-4e0c8b2cd272', username: 'Dave', password: 'check', email: null, diff --git a/frontend/src/App.css b/frontend/src/App.css index 28f0df29..71bd60e9 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -38,7 +38,8 @@ body::before { } .action-header { - width: 30%; + width: 40%; + flex-basis: auto; } } diff --git a/frontend/src/NoteApp.tsx b/frontend/src/NoteApp.tsx index ada3e011..59ac3832 100644 --- a/frontend/src/NoteApp.tsx +++ b/frontend/src/NoteApp.tsx @@ -4,8 +4,16 @@ import './App.css'; import NoteFormModal from './components/NoteFormModal'; import NoteGrid from './components/NoteGrid'; import Spinner from './components/Spinner'; +import UserModal from './components/UserModal'; import type NoteType from './types/note'; -import { postNote, patchNote, getNotes, removeNote } from './api/apiService'; +import type UserType from './types/user'; +import { + postNote, + patchNote, + getNotes, + removeNote, + getUser, +} from './api/apiService'; export interface LogoutProps { onLogout: () => void; @@ -17,9 +25,12 @@ const NoteApp: React.FC = ({ onLogout }) => { const [connectionIssue, setConnectionIssue] = useState(false); const [isDataLoading, setIsDataLoading] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); + const [isUserModalVisible, setIsUserModalVisible] = useState(false); + const [user, setUser] = useState(null); useEffect(() => { fetchNotes(); + getUserInfo(); }, []); const fetchNotes = async () => { @@ -76,6 +87,15 @@ const NoteApp: React.FC = ({ onLogout }) => { } }; + const getUserInfo = async () => { + try { + const response = await getUser(); + setUser(response.data); + } catch (error) { + console.error(error); + } + }; + const handleEdit = (note: NoteType) => { setSelectedNote(note); setIsModalVisible(true); @@ -84,6 +104,7 @@ const NoteApp: React.FC = ({ onLogout }) => { const handleCancel = () => { setSelectedNote(null); setIsModalVisible(false); + setIsUserModalVisible(false); }; return ( @@ -97,6 +118,14 @@ const NoteApp: React.FC = ({ onLogout }) => { > Add a note +