From e3cbb2ff9617055355bb9bbfda1b4c32d7a5591a Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 01:40:15 -0500 Subject: [PATCH 01/12] Initial Changes --- infrastructure/prod/Dockerfile.app | 8 ++ infrastructure/prod/Dockerfile.server | 6 + infrastructure/prod/get-ecr-image-name.sh | 23 +++ infrastructure/prod/redeploy.sh | 19 ++- package.json | 6 + packages/api-client/index.ts | 6 + packages/api-v2/src/app.module.ts | 2 + packages/api-v2/src/main.ts | 2 + packages/api-v2/src/meta/meta.controller.ts | 13 ++ packages/api-v2/src/meta/meta.module.ts | 10 ++ packages/api-v2/src/meta/meta.service.ts | 17 +++ packages/common/src/api-response-types.ts | 9 ++ packages/common/src/types.ts | 9 ++ .../components/Header/GraduateHeaders.tsx | 62 +++++++- .../components/MetaInfo/MetaInfo.tsx | 132 ++++++++++++++++++ 15 files changed, 321 insertions(+), 3 deletions(-) create mode 100755 infrastructure/prod/get-ecr-image-name.sh create mode 100644 packages/api-v2/src/meta/meta.controller.ts create mode 100644 packages/api-v2/src/meta/meta.module.ts create mode 100644 packages/api-v2/src/meta/meta.service.ts create mode 100644 packages/frontend-v2/components/MetaInfo/MetaInfo.tsx diff --git a/infrastructure/prod/Dockerfile.app b/infrastructure/prod/Dockerfile.app index 48deb0d53..82e2c6a08 100644 --- a/infrastructure/prod/Dockerfile.app +++ b/infrastructure/prod/Dockerfile.app @@ -22,6 +22,14 @@ COPY packages/frontend-v2 packages/frontend-v2 COPY packages/api-client packages/api-client COPY packages/common packages/common +ARG COMMIT +ARG BUILD_TIMESTAMP +ARG COMMIT_MESSAGE + +ENV NEXT_PUBLIC_COMMIT_HASH $COMMIT +ENV NEXT_PUBLIC_COMMIT_MESSAGE $COMMIT_MESSAGE +ENV NEXT_PUBLIC_BUILD_TIMESTAMP $BUILD_TIMESTAMP + RUN yarn packages/api-client build RUN yarn packages/common build RUN yarn packages/frontend-v2 build diff --git a/infrastructure/prod/Dockerfile.server b/infrastructure/prod/Dockerfile.server index 325d54ffc..386a863cf 100644 --- a/infrastructure/prod/Dockerfile.server +++ b/infrastructure/prod/Dockerfile.server @@ -22,7 +22,13 @@ RUN yarn packages/api-v2 build FROM node:16-alpine AS runner WORKDIR /server +ARG COMMIT +ARG COMMIT_MESSAGE +ARG BUILD_TIMESTAMP + ENV NODE_ENV production +ENV COMMIT_HASH $COMMIT +ENV COMMIT_MESSAGE $COMMIT_MESSAGE RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nestjs diff --git a/infrastructure/prod/get-ecr-image-name.sh b/infrastructure/prod/get-ecr-image-name.sh new file mode 100755 index 000000000..a7be79ea1 --- /dev/null +++ b/infrastructure/prod/get-ecr-image-name.sh @@ -0,0 +1,23 @@ +if [[ ! " backend frontend " =~ " $1 " ]]; then + echo "Please provide ECR repo to use: backend or frontend" + exit 1 +fi + +CURRENT_HASH=$( git rev-parse HEAD ) + +if [[ $1 = "backend" ]]; then + REPO="graduatenu-rails" +fi + +if [[ $1 = "frontend" ]]; then + REPO="graduatenu-node" +fi + +AWS_DEFAULT_REGION="us-east-1" +AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + +ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com" + +ECR_IMAGE_NAME="${ECR_REGISTRY}/${REPO}:${CURRENT_HASH}" + +echo $ECR_IMAGE_NAME diff --git a/infrastructure/prod/redeploy.sh b/infrastructure/prod/redeploy.sh index e0932d44f..926168706 100755 --- a/infrastructure/prod/redeploy.sh +++ b/infrastructure/prod/redeploy.sh @@ -8,11 +8,26 @@ fi AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) AWS_DEFAULT_REGION="us-east-1" REPOS=( "graduatenu-rails" "graduatenu-node" ) -LATEST_HASH=$(git ls-remote https://github.com/sandboxnu/graduatenu.git main | awk '{ print $1 }') +CURRENT_HASH=$(git ls-remote https://github.com/sandboxnu/graduatenu.git main | awk '{ print $1 }') ECS_CLUSTER="$1-graduatenu" TASK_FAMILIES=( "${ECS_CLUSTER}-api" "${ECS_CLUSTER}-web" ) SERVICES=( "${ECS_CLUSTER}-api" "${ECS_CLUSTER}-web" ) +if [[ "frontend" = $2 ]]; then + echo "INFO: Deploying frontend only." + REPOS=("graduatenu-node") +fi + +if [[ "backend" = $2 ]]; then + echo "INFO: Deploying backend only." + REPOS=("graduatenu-rails") +fi + +if [[ "current" = $3 ]]; then + echo "INFO: Using current hash!" + CURRENT_HASH=$(git rev-parse HEAD) +fi + # Disable aws from sending stdout to less export AWS_PAGER="" @@ -21,7 +36,7 @@ echo "Redeploying services for cluster: ${ECS_CLUSTER} with last pushed image" for i in "${!REPOS[@]}"; do # Last pushed image should always be tagged with the latest commit hash on main - ECR_IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${REPOS[$i]}:${LATEST_HASH}" + ECR_IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${REPOS[$i]}:${CURRENT_HASH}" TASK_FAMILY="${TASK_FAMILIES[$i]}" SERVICE="${SERVICES[$i]}" # fetch template for task definition diff --git a/package.json b/package.json index b855887a1..449caa903 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,12 @@ "backend:docker:build": "docker compose -f infrastructure/develop/docker-compose.api.yml build", "backend:docker:run": "docker compose -f infrastructure/develop/docker-compose.api.yml up -d", "backend:docker:down": "docker compose -f infrastructure/develop/docker-compose.api.yml down", + "frontend:build": "docker build --build-arg=\"COMMIT=$(git rev-parse HEAD)\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend) -f ./infrastructure/prod/Dockerfile.app .", + "frontend:run": "docker run -p 4000:3000 $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend)", + "backend:build": "docker build --build-arg=\"COMMIT=$(git rev-parse HEAD)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh backend) -f ./infrastructure/prod/Dockerfile.server .", + "backend:run": "docker run -p 4001:3001 $(sh ./infrastructure/prod/get-ecr-image-name.sh backend)", + "backend:push": "docker push $(sh ./infrastructure/prod/get-ecr-image-name.sh backend)", + "ecr:docker:auth": "aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com", "lint": "eslint packages/ --ext .ts,.tsx .", "tsc": "yarn workspaces foreach -v --exclude . run tsc", "g:babel": "cd $INIT_CWD && babel", diff --git a/packages/api-client/index.ts b/packages/api-client/index.ts index bd3bb8ed8..239ac3088 100644 --- a/packages/api-client/index.ts +++ b/packages/api-client/index.ts @@ -21,6 +21,7 @@ import { ResetPasswordDto, courseToString, NUPathEnum, + GetMetaInfoResponse, } from "@graduate/common"; import { ClassConstructor, plainToInstance } from "class-transformer"; @@ -103,6 +104,11 @@ class APIClient { getSupportedMajors: (): Promise => this.req("GET", `/majors/supportedMajors`, GetSupportedMajorsResponse), }; + + meta = { + getInfo: (): Promise => + this.req("GET", "/meta/info", GetMetaInfoResponse), + }; } /** diff --git a/packages/api-v2/src/app.module.ts b/packages/api-v2/src/app.module.ts index 69b3118d4..6ca2109b1 100644 --- a/packages/api-v2/src/app.module.ts +++ b/packages/api-v2/src/app.module.ts @@ -10,6 +10,7 @@ import { LoggingInterceptor } from "./interceptors/logging.interceptor"; import { MajorModule } from "./major/major.module"; import { EmailModule } from "./email/email.module"; import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; +import { MetaModule } from "./meta/meta.module"; @Module({ imports: [ @@ -29,6 +30,7 @@ import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; PlanModule, MajorModule, EmailModule, + MetaModule, ], providers: [ { diff --git a/packages/api-v2/src/main.ts b/packages/api-v2/src/main.ts index a5f64f2a9..0e5f9f85e 100644 --- a/packages/api-v2/src/main.ts +++ b/packages/api-v2/src/main.ts @@ -22,6 +22,8 @@ async function bootstrap() { cors: { origin: "https://graduatenu-frontend-v2-git-christina-move-fro-b625a5-sandboxneu.vercel.app", + credentials: true, + allowedHeaders: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"], }, }); diff --git a/packages/api-v2/src/meta/meta.controller.ts b/packages/api-v2/src/meta/meta.controller.ts new file mode 100644 index 000000000..20fec45e3 --- /dev/null +++ b/packages/api-v2/src/meta/meta.controller.ts @@ -0,0 +1,13 @@ +import { Controller, Get } from "@nestjs/common"; +import { MetaService } from "./meta.service"; +import { type MetaInfo } from "@graduate/common"; + +@Controller("meta") +export class MetaController { + constructor(private readonly metaService: MetaService) {} + + @Get("/info") + getMetaInfo(): MetaInfo { + return this.metaService.getMetaInfo(); + } +} diff --git a/packages/api-v2/src/meta/meta.module.ts b/packages/api-v2/src/meta/meta.module.ts new file mode 100644 index 000000000..5d0433160 --- /dev/null +++ b/packages/api-v2/src/meta/meta.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { MetaService } from "./meta.service"; +import { MetaController } from "./meta.controller"; + +@Module({ + controllers: [MetaController], + providers: [MetaService], + exports: [MetaService], +}) +export class MetaModule {} diff --git a/packages/api-v2/src/meta/meta.service.ts b/packages/api-v2/src/meta/meta.service.ts new file mode 100644 index 000000000..7169d8822 --- /dev/null +++ b/packages/api-v2/src/meta/meta.service.ts @@ -0,0 +1,17 @@ +import { type MetaInfo } from "@graduate/common"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class MetaService { + getMetaInfo(): MetaInfo { + return { + commit: process.env.COMMIT_SHA ?? false, + commitMessage: process.env.COMMIT_MESSAGE ?? false, + build_timestamp: + process.env.BUILD_TIMESTAMP !== undefined + ? Number(process.env.BUILD_TIMESTAMP) + : false, + environment: process.env.NODE_ENV ?? false, + }; + } +} diff --git a/packages/common/src/api-response-types.ts b/packages/common/src/api-response-types.ts index 859146677..7cb2c1dee 100644 --- a/packages/common/src/api-response-types.ts +++ b/packages/common/src/api-response-types.ts @@ -3,6 +3,8 @@ import { Schedule2, SupportedMajors, ScheduleCourse2, + MetaInfo, + Maybe, } from "./types"; /** Types our API responds with. */ @@ -52,3 +54,10 @@ export class GetSupportedMajorsResponse { // { year => { majorName => {concentrations, minRequiredConcentrations} }} supportedMajors: SupportedMajors; } + +export class GetMetaInfoResponse implements MetaInfo { + commit: Maybe; + commitMessage: Maybe; + build_timestamp: Maybe; + environment: Maybe; +} diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 87dc173fa..983f5aa3d 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -483,3 +483,12 @@ export const Err = (err: E): Result => ({ err, type: ResultType.Err, }); + +export type Maybe = T | false; + +export interface MetaInfo { + commit: Maybe; + commitMessage: Maybe; + build_timestamp: Maybe; + environment: Maybe; +} diff --git a/packages/frontend-v2/components/Header/GraduateHeaders.tsx b/packages/frontend-v2/components/Header/GraduateHeaders.tsx index a5aef3d4f..af7ae8094 100644 --- a/packages/frontend-v2/components/Header/GraduateHeaders.tsx +++ b/packages/frontend-v2/components/Header/GraduateHeaders.tsx @@ -2,7 +2,16 @@ import { HeaderContainer } from "./HeaderContainer"; import { Logo } from "./Logo"; import { GraduateButtonLink } from "../Link"; import { UserDropdown } from "./UserDropdown"; -import { Flex, Icon, IconProps, Link as ChakraLink } from "@chakra-ui/react"; +import { + Flex, + Icon, + IconProps, + Link as ChakraLink, + Box, + Text, +} from "@chakra-ui/react"; +import { useState } from "react"; +import { MetaInfo } from "../MetaInfo/MetaInfo"; export const GraduatePreAuthHeader: React.FC = () => { return ( @@ -27,10 +36,61 @@ interface GraduateHeaderProps { } const GraduateHeader: React.FC = ({ rightContent }) => { + const [showDevInfo, setShowDevInfo] = useState(false); + return ( + + setShowDevInfo((state) => !state)} + transition="background 0.15s ease" + userSelect="none" + > + + + + {process.env.NODE_ENV === "development" && Dev} + + { + + + + } + Feedback diff --git a/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx b/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx new file mode 100644 index 000000000..667013cf2 --- /dev/null +++ b/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx @@ -0,0 +1,132 @@ +import { Link, Text, Tooltip } from "@chakra-ui/react"; +import { API } from "@graduate/api-client"; +import { Maybe } from "@graduate/common"; +import useSWR from "swr"; + +function timeDifference(current: number, previous: number): string { + const msPerMinute = 60 * 1000; + const msPerHour = msPerMinute * 60; + const msPerDay = msPerHour * 24; + const msPerMonth = msPerDay * 30; + const msPerYear = msPerDay * 365; + + const elapsed = current - previous; + + if (elapsed < msPerMinute) { + return Math.round(elapsed / 1000) + " seconds ago"; + } else if (elapsed < msPerHour) { + return Math.round(elapsed / msPerMinute) + " minutes ago"; + } else if (elapsed < msPerDay) { + return Math.round(elapsed / msPerHour) + " hours ago"; + } else if (elapsed < msPerMonth) { + return "~" + Math.round(elapsed / msPerDay) + " days ago"; + } else if (elapsed < msPerYear) { + return "~" + Math.round(elapsed / msPerMonth) + " months ago"; + } else { + return "~" + Math.round(elapsed / msPerYear) + " years ago"; + } +} + +function formatBuildTime(timestamp: number): string { + return new Date(timestamp).toLocaleDateString("en-US", { + weekday: "short", + year: "numeric", + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }); +} + +export const MetaInfo: React.FC = () => { + const { data, error } = useSWR("/meta/info", API.meta.getInfo); + + // When we update Next versions we can use an instrumentation.ts file to create this information + // const relativeRunTime = timeDifference(Date.now(), Number(process.env.NEXT_PUBLIC_NEXT_RUN_TIMESTAMP ?? 0) * 1000) + + return ( + <> + + Docker Build Info + + Frontend + + + + Backend + + {error && Error Reaching Backend} + {data !== undefined ? ( + + ) : ( + !error && Loading Backend Info... + )} + + ); +}; + +const CommitText: React.FC<{ + commitHash: Maybe; + commitMessage: Maybe; +}> = ({ commitHash, commitMessage }) => { + if (commitHash !== false) { + const shortHash = commitHash.slice(0, 7); + const commitLink = `https://github.com/sandboxnu/graduatenu/commit/${commitHash}`; + return ( + + Commit:{" "} + + + {`${ + commitMessage !== false ? commitMessage : "" + } (${shortHash})`} + + + + ); + } else { + return Commit: {""}; + } +}; + +const BuildTime: React.FC<{ buildTime: Maybe }> = ({ + buildTime, +}) => { + if (buildTime !== false) { + const numericTime = Number(buildTime) * 1000; + return ( + + Image Built: {timeDifference(Date.now(), numericTime)} + + ); + } else { + return Built: {""}; + } +}; + +export const MetaInfoSection: React.FC<{ + environment: Maybe; + commitHash: Maybe; + buildTime: Maybe | Maybe; + commitMessage: Maybe; +}> = ({ environment, commitHash, buildTime, commitMessage }) => { + return ( + <> + + Environment: {environment !== false ? environment : ""} + + + + + ); +}; From 8b841c46464a4273eef2c8d17f63c4db0c98c34e Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 03:01:25 -0500 Subject: [PATCH 02/12] Some more tweaks --- infrastructure/prod/Dockerfile.server | 7 +++++++ infrastructure/prod/entrypoint.server.sh | 3 ++- infrastructure/prod/redeploy.sh | 10 +++++++--- package.json | 5 +++-- packages/api-v2/src/auth/auth.controller.ts | 4 ++-- packages/api-v2/src/auth/auth.module.ts | 2 +- packages/api-v2/src/auth/auth.service.ts | 6 +++--- .../src/auth/interfaces/authenticated-request.ts | 2 +- packages/api-v2/src/email/email.module.ts | 4 ++-- packages/api-v2/src/email/email.service.ts | 2 +- .../emailConfirmation/emailConfirmation.controller.ts | 4 ++-- .../src/emailConfirmation/emailConfirmation.service.ts | 4 ++-- packages/api-v2/src/graduate-logger.ts | 2 +- packages/api-v2/src/guards/emailConfirmation.guard.ts | 4 ++-- packages/api-v2/src/main.ts | 4 ++-- packages/api-v2/src/meta/meta.service.ts | 2 +- 16 files changed, 39 insertions(+), 26 deletions(-) diff --git a/infrastructure/prod/Dockerfile.server b/infrastructure/prod/Dockerfile.server index 386a863cf..af4f55c8f 100644 --- a/infrastructure/prod/Dockerfile.server +++ b/infrastructure/prod/Dockerfile.server @@ -15,6 +15,13 @@ RUN yarn install > /dev/null COPY packages/api-v2 packages/api-v2 COPY packages/common packages/common +ARG COMMIT +ARG COMMIT_MESSAGE +ARG BUILD_TIMESTAMP + +ENV COMMIT_HASH $COMMIT +ENV COMMIT_MESSAGE $COMMIT_MESSAGE + # Build server and common dependency RUN yarn packages/common build RUN yarn packages/api-v2 build diff --git a/infrastructure/prod/entrypoint.server.sh b/infrastructure/prod/entrypoint.server.sh index a2d9d4b22..89cfc1362 100644 --- a/infrastructure/prod/entrypoint.server.sh +++ b/infrastructure/prod/entrypoint.server.sh @@ -1,6 +1,7 @@ #!/bin/sh cd packages/api-v2 -yarn typeorm migration:run +echo "Running on commit: $COMMIT_HASH" +# yarn typeorm migration:run exec "$@" diff --git a/infrastructure/prod/redeploy.sh b/infrastructure/prod/redeploy.sh index 926168706..c9b97692e 100755 --- a/infrastructure/prod/redeploy.sh +++ b/infrastructure/prod/redeploy.sh @@ -13,14 +13,16 @@ ECS_CLUSTER="$1-graduatenu" TASK_FAMILIES=( "${ECS_CLUSTER}-api" "${ECS_CLUSTER}-web" ) SERVICES=( "${ECS_CLUSTER}-api" "${ECS_CLUSTER}-web" ) +INDEXES=(0 1) + if [[ "frontend" = $2 ]]; then echo "INFO: Deploying frontend only." - REPOS=("graduatenu-node") + INDEXES=(1) fi if [[ "backend" = $2 ]]; then echo "INFO: Deploying backend only." - REPOS=("graduatenu-rails") + INDEXES=(0) fi if [[ "current" = $3 ]]; then @@ -34,7 +36,9 @@ export AWS_PAGER="" echo "Redeploying services for cluster: ${ECS_CLUSTER} with last pushed image" -for i in "${!REPOS[@]}"; do +for ii in "${!INDEXES[@]}"; do + echo "Deploying ${INDEXES[ii]}..." + i=${INDEXES[ii]} # Last pushed image should always be tagged with the latest commit hash on main ECR_IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${REPOS[$i]}:${CURRENT_HASH}" TASK_FAMILY="${TASK_FAMILIES[$i]}" diff --git a/package.json b/package.json index 449caa903..696b3ab92 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,10 @@ "backend:docker:build": "docker compose -f infrastructure/develop/docker-compose.api.yml build", "backend:docker:run": "docker compose -f infrastructure/develop/docker-compose.api.yml up -d", "backend:docker:down": "docker compose -f infrastructure/develop/docker-compose.api.yml down", - "frontend:build": "docker build --build-arg=\"COMMIT=$(git rev-parse HEAD)\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend) -f ./infrastructure/prod/Dockerfile.app .", + "frontend:build": "docker build --platform linux/amd64 --build-arg=\"COMMIT=$(git rev-parse HEAD)\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend) -f ./infrastructure/prod/Dockerfile.app .", "frontend:run": "docker run -p 4000:3000 $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend)", - "backend:build": "docker build --build-arg=\"COMMIT=$(git rev-parse HEAD)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh backend) -f ./infrastructure/prod/Dockerfile.server .", + "frontend:push": "docker push $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend)", + "backend:build": "docker build --platform linux/amd64 --build-arg=\"COMMIT=$(git rev-parse HEAD)\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh backend) -f ./infrastructure/prod/Dockerfile.server .", "backend:run": "docker run -p 4001:3001 $(sh ./infrastructure/prod/get-ecr-image-name.sh backend)", "backend:push": "docker push $(sh ./infrastructure/prod/get-ecr-image-name.sh backend)", "ecr:docker:auth": "aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com", diff --git a/packages/api-v2/src/auth/auth.controller.ts b/packages/api-v2/src/auth/auth.controller.ts index 4bdcdb161..e0c40d8d1 100644 --- a/packages/api-v2/src/auth/auth.controller.ts +++ b/packages/api-v2/src/auth/auth.controller.ts @@ -22,13 +22,13 @@ import { weakPasswordError, } from "@graduate/common"; import { Response } from "express"; -import EmailConfirmationService from "src/emailConfirmation/emailConfirmation.service"; +import EmailConfirmationService from "../../src/emailConfirmation/emailConfirmation.service"; import { EmailAlreadyExists, EmailNotConfirmed, NoSuchEmail, WeakPassword, -} from "src/student/student.errors"; +} from "../../src/student/student.errors"; import { BadToken, InvalidPayload, TokenExpiredError } from "./auth.errors"; import { Throttle } from "@nestjs/throttler"; diff --git a/packages/api-v2/src/auth/auth.module.ts b/packages/api-v2/src/auth/auth.module.ts index 74bfc207a..069b98758 100644 --- a/packages/api-v2/src/auth/auth.module.ts +++ b/packages/api-v2/src/auth/auth.module.ts @@ -5,7 +5,7 @@ import { StudentModule } from "../student/student.module"; import { AuthController } from "./auth.controller"; import { AuthService } from "./auth.service"; import { JwtStrategy } from "./jwt.strategy"; -import { EmailModule } from "src/email/email.module"; +import { EmailModule } from "../../src/email/email.module"; @Module({ imports: [ diff --git a/packages/api-v2/src/auth/auth.service.ts b/packages/api-v2/src/auth/auth.service.ts index e53113ed0..73d58dd6e 100644 --- a/packages/api-v2/src/auth/auth.service.ts +++ b/packages/api-v2/src/auth/auth.service.ts @@ -15,10 +15,10 @@ import { EmailNotConfirmed, NoSuchEmail, WeakPassword, -} from "src/student/student.errors"; +} from "../../src/student/student.errors"; import { ConfigService } from "@nestjs/config"; -import { EnvironmentVariables } from "src/environment-variables"; -import EmailService from "src/email/email.service"; +import { EnvironmentVariables } from "../../src/environment-variables"; +import EmailService from "../../src/email/email.service"; import { BadToken, InvalidPayload, TokenExpiredError } from "./auth.errors"; @Injectable() diff --git a/packages/api-v2/src/auth/interfaces/authenticated-request.ts b/packages/api-v2/src/auth/interfaces/authenticated-request.ts index 76b7c3b20..8c95bffde 100644 --- a/packages/api-v2/src/auth/interfaces/authenticated-request.ts +++ b/packages/api-v2/src/auth/interfaces/authenticated-request.ts @@ -1,4 +1,4 @@ -import { Student } from "src/student/entities/student.entity"; +import { Student } from "../../../src/student/entities/student.entity"; /** Represents an authenticated request using the JwtAuthGuard. */ export interface AuthenticatedRequest extends Request { diff --git a/packages/api-v2/src/email/email.module.ts b/packages/api-v2/src/email/email.module.ts index 29317a561..20d0fc188 100644 --- a/packages/api-v2/src/email/email.module.ts +++ b/packages/api-v2/src/email/email.module.ts @@ -3,8 +3,8 @@ import { ConfigModule } from "@nestjs/config"; import EmailService from "./email.service"; import { JwtModule } from "@nestjs/jwt"; import EmailConfirmationService from "../emailConfirmation/emailConfirmation.service"; -import { StudentModule } from "src/student/student.module"; -import { EmailConfirmationController } from "src/emailConfirmation/emailConfirmation.controller"; +import { StudentModule } from "../../src/student/student.module"; +import { EmailConfirmationController } from "../../src/emailConfirmation/emailConfirmation.controller"; @Module({ imports: [ diff --git a/packages/api-v2/src/email/email.service.ts b/packages/api-v2/src/email/email.service.ts index 25190cf54..fd98e8fc3 100644 --- a/packages/api-v2/src/email/email.service.ts +++ b/packages/api-v2/src/email/email.service.ts @@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { createTransport } from "nodemailer"; import Mail from "nodemailer/lib/mailer"; -import { EnvironmentVariables } from "src/environment-variables"; +import { EnvironmentVariables } from "../../src/environment-variables"; @Injectable() export default class EmailService { diff --git a/packages/api-v2/src/emailConfirmation/emailConfirmation.controller.ts b/packages/api-v2/src/emailConfirmation/emailConfirmation.controller.ts index 6486c7c2a..612e04de3 100644 --- a/packages/api-v2/src/emailConfirmation/emailConfirmation.controller.ts +++ b/packages/api-v2/src/emailConfirmation/emailConfirmation.controller.ts @@ -12,8 +12,8 @@ import { emailAlreadyConfirmed, unableToSendEmail, } from "@graduate/common"; -import { AuthenticatedRequest } from "src/auth/interfaces/authenticated-request"; -import { JwtAuthGuard } from "src/guards/jwt-auth.guard"; +import { AuthenticatedRequest } from "../../src/auth/interfaces/authenticated-request"; +import { JwtAuthGuard } from "../../src/guards/jwt-auth.guard"; import EmailConfirmationService from "./emailConfirmation.service"; import { EmailAlreadyConfirmed, diff --git a/packages/api-v2/src/emailConfirmation/emailConfirmation.service.ts b/packages/api-v2/src/emailConfirmation/emailConfirmation.service.ts index af0dec3c0..ddc978f9d 100644 --- a/packages/api-v2/src/emailConfirmation/emailConfirmation.service.ts +++ b/packages/api-v2/src/emailConfirmation/emailConfirmation.service.ts @@ -1,8 +1,8 @@ import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; -import { EnvironmentVariables } from "src/environment-variables"; -import { StudentService } from "src/student/student.service"; +import { EnvironmentVariables } from "../../src/environment-variables"; +import { StudentService } from "../../src/student/student.service"; import { UpdateResult } from "typeorm"; import EmailService from "../email/email.service"; import { diff --git a/packages/api-v2/src/graduate-logger.ts b/packages/api-v2/src/graduate-logger.ts index 4b548dad9..cc61cb089 100644 --- a/packages/api-v2/src/graduate-logger.ts +++ b/packages/api-v2/src/graduate-logger.ts @@ -1,5 +1,5 @@ import { ConsoleLogger, LogLevel } from "@nestjs/common"; -import { deepFilter } from "src/utils"; +import { deepFilter } from "../src/utils"; const DENYLIST = ["password", "passwordConfirm"]; diff --git a/packages/api-v2/src/guards/emailConfirmation.guard.ts b/packages/api-v2/src/guards/emailConfirmation.guard.ts index 18c81cbe7..7d59aba06 100644 --- a/packages/api-v2/src/guards/emailConfirmation.guard.ts +++ b/packages/api-v2/src/guards/emailConfirmation.guard.ts @@ -6,8 +6,8 @@ import { UnauthorizedException, Logger, } from "@nestjs/common"; -import { AuthenticatedRequest } from "src/auth/interfaces/authenticated-request"; -import { formatServiceCtx } from "src/utils"; +import { AuthenticatedRequest } from "../../src/auth/interfaces/authenticated-request"; +import { formatServiceCtx } from "../../src/utils"; @Injectable() export class EmailConfirmationGuard implements CanActivate { diff --git a/packages/api-v2/src/main.ts b/packages/api-v2/src/main.ts index 0e5f9f85e..8709695b6 100644 --- a/packages/api-v2/src/main.ts +++ b/packages/api-v2/src/main.ts @@ -6,7 +6,7 @@ import { } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { NestFactory, Reflector } from "@nestjs/core"; -import { GraduateLogger } from "src/graduate-logger"; +import { GraduateLogger } from "../src/graduate-logger"; import { AppModule } from "./app.module"; import { EnvironmentVariables } from "./environment-variables"; import * as cookieParser from "cookie-parser"; @@ -23,7 +23,7 @@ async function bootstrap() { origin: "https://graduatenu-frontend-v2-git-christina-move-fro-b625a5-sandboxneu.vercel.app", credentials: true, - allowedHeaders: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"], + methods: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"], }, }); diff --git a/packages/api-v2/src/meta/meta.service.ts b/packages/api-v2/src/meta/meta.service.ts index 7169d8822..b1b0bfe90 100644 --- a/packages/api-v2/src/meta/meta.service.ts +++ b/packages/api-v2/src/meta/meta.service.ts @@ -5,7 +5,7 @@ import { Injectable } from "@nestjs/common"; export class MetaService { getMetaInfo(): MetaInfo { return { - commit: process.env.COMMIT_SHA ?? false, + commit: process.env.COMMIT_HASH ?? false, commitMessage: process.env.COMMIT_MESSAGE ?? false, build_timestamp: process.env.BUILD_TIMESTAMP !== undefined From ea74c8cae70837f1c55cef45611c85d2e61af6d9 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 13:17:53 -0500 Subject: [PATCH 03/12] feat: build github workflow images with metadata --- .github/workflows/aws-cd.yml | 4 ++-- infrastructure/prod/Dockerfile.server | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/aws-cd.yml b/.github/workflows/aws-cd.yml index 9f992a6ac..52ca1107b 100644 --- a/.github/workflows/aws-cd.yml +++ b/.github/workflows/aws-cd.yml @@ -35,7 +35,7 @@ jobs: # Build a docker container and # push it to ECR so that it can # be deployed to ECS. - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f infrastructure/prod/Dockerfile.server . + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --build-arg=\"COMMIT=$IMAGE_TAG\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -f infrastructure/prod/Dockerfile.server . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT @@ -49,7 +49,7 @@ jobs: # Build a docker container and # push it to ECR so that it can # be deployed to ECS. - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f infrastructure/prod/Dockerfile.app . + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --build-arg=\"COMMIT=$IMAGE_TAG\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -f infrastructure/prod/Dockerfile.app . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT diff --git a/infrastructure/prod/Dockerfile.server b/infrastructure/prod/Dockerfile.server index af4f55c8f..2e77d6c50 100644 --- a/infrastructure/prod/Dockerfile.server +++ b/infrastructure/prod/Dockerfile.server @@ -15,13 +15,6 @@ RUN yarn install > /dev/null COPY packages/api-v2 packages/api-v2 COPY packages/common packages/common -ARG COMMIT -ARG COMMIT_MESSAGE -ARG BUILD_TIMESTAMP - -ENV COMMIT_HASH $COMMIT -ENV COMMIT_MESSAGE $COMMIT_MESSAGE - # Build server and common dependency RUN yarn packages/common build RUN yarn packages/api-v2 build @@ -36,6 +29,7 @@ ARG BUILD_TIMESTAMP ENV NODE_ENV production ENV COMMIT_HASH $COMMIT ENV COMMIT_MESSAGE $COMMIT_MESSAGE +ENV BUILD_TIMESTAMP $BUILD_TIMESTAMP RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nestjs From 936253f32929a359cbac4ec7208a98a5179c177d Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 13:18:37 -0500 Subject: [PATCH 04/12] testing: cors + cookie changes for vercel --- packages/api-v2/src/auth/auth.controller.ts | 9 +++++++++ packages/api-v2/src/main.ts | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/api-v2/src/auth/auth.controller.ts b/packages/api-v2/src/auth/auth.controller.ts index e0c40d8d1..eedde14fe 100644 --- a/packages/api-v2/src/auth/auth.controller.ts +++ b/packages/api-v2/src/auth/auth.controller.ts @@ -61,11 +61,14 @@ export class AuthController { const { accessToken } = student; const isSecure = process.env.NODE_ENV !== "development"; + const domain = + process.env.NODE_ENV === "production" ? "graduatenu.com" : "localhost"; // Store JWT token in a cookie response.cookie("auth_cookie", accessToken, { httpOnly: true, sameSite: "strict", secure: isSecure, + domain, }); if (process.env.NODE_ENV !== "testing") { await this.emailConfirmationService.sendVerificationLink( @@ -90,11 +93,14 @@ export class AuthController { const { accessToken } = student; const isSecure = process.env.NODE_ENV !== "development"; + const domain = + process.env.NODE_ENV === "production" ? "graduatenu.com" : "localhost"; // Store JWT token in a cookie response.cookie("auth_cookie", accessToken, { httpOnly: true, sameSite: "strict", secure: isSecure, + domain, }); return student; @@ -152,10 +158,13 @@ export class AuthController { @Res({ passthrough: true }) response: Response ): Promise { const isSecure = process.env.NODE_ENV !== "development"; + const domain = + process.env.NODE_ENV === "production" ? "graduatenu.com" : "localhost"; response.clearCookie("auth_cookie", { httpOnly: true, sameSite: "strict", secure: isSecure, + domain, }); } } diff --git a/packages/api-v2/src/main.ts b/packages/api-v2/src/main.ts index 8709695b6..d250908c3 100644 --- a/packages/api-v2/src/main.ts +++ b/packages/api-v2/src/main.ts @@ -20,8 +20,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule, { logger: graduateLogger, cors: { - origin: - "https://graduatenu-frontend-v2-git-christina-move-fro-b625a5-sandboxneu.vercel.app", + origin: "https://frontend-staging.graduatenu.com", credentials: true, methods: ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"], }, From 2733c07021fef5c3cadb8fbdd96ef8b534821c33 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 13:27:31 -0500 Subject: [PATCH 05/12] fix: add migrations back to the server entrypoint --- infrastructure/prod/entrypoint.server.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/prod/entrypoint.server.sh b/infrastructure/prod/entrypoint.server.sh index 89cfc1362..0b36af281 100644 --- a/infrastructure/prod/entrypoint.server.sh +++ b/infrastructure/prod/entrypoint.server.sh @@ -2,6 +2,6 @@ cd packages/api-v2 echo "Running on commit: $COMMIT_HASH" -# yarn typeorm migration:run +yarn typeorm migration:run exec "$@" From 46527137a58b9df5de1c6ff9a88e4c1303030ea1 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 17:31:35 -0500 Subject: [PATCH 06/12] feat: Improve deploy script CLI APIs --- .github/workflows/aws-cd.yml | 2 +- infrastructure/prod/get-ecr-image-name.sh | 28 +++++++---- infrastructure/prod/redeploy.sh | 61 +++++++++++++---------- package.json | 12 ++--- 4 files changed, 61 insertions(+), 42 deletions(-) diff --git a/.github/workflows/aws-cd.yml b/.github/workflows/aws-cd.yml index 52ca1107b..c6104d2a1 100644 --- a/.github/workflows/aws-cd.yml +++ b/.github/workflows/aws-cd.yml @@ -57,7 +57,7 @@ jobs: run: | # - create new revision for task definition with latest image. # - redeploy ECS services with the latest revision. - ./infrastructure/prod/redeploy.sh staging + ./infrastructure/prod/redeploy.sh staging latest-main both - name: Logout of Amazon ECR if: always() diff --git a/infrastructure/prod/get-ecr-image-name.sh b/infrastructure/prod/get-ecr-image-name.sh index a7be79ea1..ea4b1a901 100755 --- a/infrastructure/prod/get-ecr-image-name.sh +++ b/infrastructure/prod/get-ecr-image-name.sh @@ -1,23 +1,33 @@ -if [[ ! " backend frontend " =~ " $1 " ]]; then - echo "Please provide ECR repo to use: backend or frontend" +if [[ $# != 2 ]]; then + echo "Usage: get-ecr-image-name.sh " exit 1 fi -CURRENT_HASH=$( git rev-parse HEAD ) - -if [[ $1 = "backend" ]]; then - REPO="graduatenu-rails" +if [[ ${#1} = 40 ]]; then + ECR_IMAGE_COMMIT_HASH=$1 +elif [[ $1 = "latest-main" ]]; then + ECR_IMAGE_COMMIT_HASH=$(git ls-remote https://github.com/sandboxnu/graduatenu.git main | awk '{ print $1 }') +elif [[ $1 = "local-head" ]]; then + ECR_IMAGE_COMMIT_HASH=$(git rev-parse HEAD) +else + echo "ERROR: Invalid commit '$1'" + exit 1 fi -if [[ $1 = "frontend" ]]; then +if [[ $2 = "frontend" ]]; then REPO="graduatenu-node" +elif [[ $2 = "backend" ]]; then + REPO="graduatenu-rails" +else + echo "Please choose a service to deploy: 'frontend' or 'backend'" + exit 1 fi -AWS_DEFAULT_REGION="us-east-1" AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) +AWS_DEFAULT_REGION="us-east-1" ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com" -ECR_IMAGE_NAME="${ECR_REGISTRY}/${REPO}:${CURRENT_HASH}" +ECR_IMAGE_NAME="${ECR_REGISTRY}/${REPO}:${ECR_IMAGE_COMMIT_HASH}" echo $ECR_IMAGE_NAME diff --git a/infrastructure/prod/redeploy.sh b/infrastructure/prod/redeploy.sh index c9b97692e..1d375bab3 100755 --- a/infrastructure/prod/redeploy.sh +++ b/infrastructure/prod/redeploy.sh @@ -1,54 +1,63 @@ #!/bin/bash +if [[ $# != 3 ]]; then + echo "Usage: redeploy.sh " + exit 1 +fi + if [[ ! " prod staging " =~ " $1 " ]]; then echo "Please provide environment to use: prod or staging" exit 1 fi +if [[ ${#2} = 40 ]]; then + ECR_IMAGE_COMMIT_HASH=$2 +elif [[ $2 = "latest-main" ]]; then + ECR_IMAGE_COMMIT_HASH=$(git ls-remote https://github.com/sandboxnu/graduatenu.git main | awk '{ print $1 }') +elif [[ $2 = "local-head" ]]; then + ECR_IMAGE_COMMIT_HASH=$(git rev-parse HEAD) +else + echo "ERROR: Invalid commit '$2'" + exit 1 +fi + +if [[ $3 = "frontend" ]]; then + DEPLOY_INDEXES=(1) +elif [[ $3 = "backend" ]]; then + DEPLOY_INDEXES=(0) +elif [[ $3 = "both" ]]; then + DEPLOY_INDEXES=(0 1) +else + echo "Please choose a service to deploy: 'frontend', 'backend', or 'both'" + exit 1 +fi + +echo "Deploying $3 repo(s) to $1 with commit "$2" ($ECR_IMAGE_COMMIT_HASH)" + AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) AWS_DEFAULT_REGION="us-east-1" REPOS=( "graduatenu-rails" "graduatenu-node" ) -CURRENT_HASH=$(git ls-remote https://github.com/sandboxnu/graduatenu.git main | awk '{ print $1 }') ECS_CLUSTER="$1-graduatenu" TASK_FAMILIES=( "${ECS_CLUSTER}-api" "${ECS_CLUSTER}-web" ) SERVICES=( "${ECS_CLUSTER}-api" "${ECS_CLUSTER}-web" ) -INDEXES=(0 1) - -if [[ "frontend" = $2 ]]; then - echo "INFO: Deploying frontend only." - INDEXES=(1) -fi - -if [[ "backend" = $2 ]]; then - echo "INFO: Deploying backend only." - INDEXES=(0) -fi - -if [[ "current" = $3 ]]; then - echo "INFO: Using current hash!" - CURRENT_HASH=$(git rev-parse HEAD) -fi # Disable aws from sending stdout to less export AWS_PAGER="" -echo "Redeploying services for cluster: ${ECS_CLUSTER} with last pushed image" - - -for ii in "${!INDEXES[@]}"; do - echo "Deploying ${INDEXES[ii]}..." - i=${INDEXES[ii]} +for service_index in "${!DEPLOY_INDEXES[@]}"; do + echo "Deploying ${REPOS[DEPLOY_INDEXES[service_index]]}..." + i=${DEPLOY_INDEXES[service_index]} # Last pushed image should always be tagged with the latest commit hash on main - ECR_IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${REPOS[$i]}:${CURRENT_HASH}" + ECR_IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${REPOS[$i]}:${ECR_IMAGE_COMMIT_HASH}" TASK_FAMILY="${TASK_FAMILIES[$i]}" SERVICE="${SERVICES[$i]}" # fetch template for task definition TASK_DEFINITION=$(aws ecs describe-task-definition --task-definition "$TASK_FAMILY" --region "$AWS_DEFAULT_REGION") # update the template's image to use the latest ECR_IMAGE - NEW_TASK_DEFINTIION=$(echo $TASK_DEFINITION | jq --arg IMAGE "$ECR_IMAGE" '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') + NEW_TASK_DEFINITION=$(echo $TASK_DEFINITION | jq --arg IMAGE "$ECR_IMAGE" '.taskDefinition | .containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn) | del(.revision) | del(.status) | del(.requiresAttributes) | del(.compatibilities) | del(.registeredAt) | del(.registeredBy)') # register the new revision for the task definition - NEW_TASK_INFO=$(aws ecs register-task-definition --region "$AWS_DEFAULT_REGION" --cli-input-json "$NEW_TASK_DEFINTIION") + NEW_TASK_INFO=$(aws ecs register-task-definition --region "$AWS_DEFAULT_REGION" --cli-input-json "$NEW_TASK_DEFINITION") NEW_REVISION=$(echo $NEW_TASK_INFO | jq '.taskDefinition.revision') # update the service to replace tasks with the latest revision using the latest image aws ecs update-service --cluster ${ECS_CLUSTER} \ diff --git a/package.json b/package.json index 696b3ab92..f65242b5d 100644 --- a/package.json +++ b/package.json @@ -22,12 +22,12 @@ "backend:docker:build": "docker compose -f infrastructure/develop/docker-compose.api.yml build", "backend:docker:run": "docker compose -f infrastructure/develop/docker-compose.api.yml up -d", "backend:docker:down": "docker compose -f infrastructure/develop/docker-compose.api.yml down", - "frontend:build": "docker build --platform linux/amd64 --build-arg=\"COMMIT=$(git rev-parse HEAD)\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend) -f ./infrastructure/prod/Dockerfile.app .", - "frontend:run": "docker run -p 4000:3000 $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend)", - "frontend:push": "docker push $(sh ./infrastructure/prod/get-ecr-image-name.sh frontend)", - "backend:build": "docker build --platform linux/amd64 --build-arg=\"COMMIT=$(git rev-parse HEAD)\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh backend) -f ./infrastructure/prod/Dockerfile.server .", - "backend:run": "docker run -p 4001:3001 $(sh ./infrastructure/prod/get-ecr-image-name.sh backend)", - "backend:push": "docker push $(sh ./infrastructure/prod/get-ecr-image-name.sh backend)", + "frontend:build": "docker build --platform linux/amd64 --build-arg=\"COMMIT=$(git rev-parse HEAD)\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh local-head frontend) -f ./infrastructure/prod/Dockerfile.app .", + "frontend:run": "docker run -p 4000:3000 $(sh ./infrastructure/prod/get-ecr-image-name.sh local-head frontend)", + "frontend:push": "docker push $(sh ./infrastructure/prod/get-ecr-image-name.sh local-head frontend)", + "backend:build": "docker build --platform linux/amd64 --build-arg=\"COMMIT=$(git rev-parse HEAD)\" --build-arg=\"BUILD_TIMESTAMP=$(date +%s)\" --build-arg=\"COMMIT_MESSAGE=$(git --no-pager show -s --format=%s)\" -t $(sh ./infrastructure/prod/get-ecr-image-name.sh local-head backend) -f ./infrastructure/prod/Dockerfile.server .", + "backend:run": "docker run -p 4001:3001 $(sh ./infrastructure/prod/get-ecr-image-name.sh local-head backend)", + "backend:push": "docker push $(sh ./infrastructure/prod/get-ecr-image-name.sh local-head backend)", "ecr:docker:auth": "aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com", "lint": "eslint packages/ --ext .ts,.tsx .", "tsc": "yarn workspaces foreach -v --exclude . run tsc", From 1445f9533c4ec9a7324c721f09fc5ccec1542d8d Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 17:40:11 -0500 Subject: [PATCH 07/12] refactor: factor dev widget out of header --- .../components/Header/GraduateHeaders.tsx | 64 +------------------ .../components/MetaInfo/MetaInfo.tsx | 57 ++++++++++++++++- 2 files changed, 59 insertions(+), 62 deletions(-) diff --git a/packages/frontend-v2/components/Header/GraduateHeaders.tsx b/packages/frontend-v2/components/Header/GraduateHeaders.tsx index af7ae8094..c38f5ef2c 100644 --- a/packages/frontend-v2/components/Header/GraduateHeaders.tsx +++ b/packages/frontend-v2/components/Header/GraduateHeaders.tsx @@ -2,16 +2,8 @@ import { HeaderContainer } from "./HeaderContainer"; import { Logo } from "./Logo"; import { GraduateButtonLink } from "../Link"; import { UserDropdown } from "./UserDropdown"; -import { - Flex, - Icon, - IconProps, - Link as ChakraLink, - Box, - Text, -} from "@chakra-ui/react"; -import { useState } from "react"; -import { MetaInfo } from "../MetaInfo/MetaInfo"; +import { Flex, Icon, IconProps, Link as ChakraLink } from "@chakra-ui/react"; +import { MetaInfoWidget } from "../MetaInfo/MetaInfo"; export const GraduatePreAuthHeader: React.FC = () => { return ( @@ -36,61 +28,11 @@ interface GraduateHeaderProps { } const GraduateHeader: React.FC = ({ rightContent }) => { - const [showDevInfo, setShowDevInfo] = useState(false); - return ( - - setShowDevInfo((state) => !state)} - transition="background 0.15s ease" - userSelect="none" - > - - - - {process.env.NODE_ENV === "development" && Dev} - - { - - - - } - + Feedback diff --git a/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx b/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx index 667013cf2..341e4da7a 100644 --- a/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx +++ b/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx @@ -1,6 +1,7 @@ -import { Link, Text, Tooltip } from "@chakra-ui/react"; +import { Box, Flex, Icon, Link, Text, Tooltip } from "@chakra-ui/react"; import { API } from "@graduate/api-client"; import { Maybe } from "@graduate/common"; +import { useState } from "react"; import useSWR from "swr"; function timeDifference(current: number, previous: number): string { @@ -38,6 +39,60 @@ function formatBuildTime(timestamp: number): string { }); } +export const MetaInfoWidget: React.FC = () => { + const [showDevInfo, setShowDevInfo] = useState(false); + + return ( + + setShowDevInfo((state) => !state)} + transition="background 0.15s ease" + userSelect="none" + > + + + + {process.env.NODE_ENV === "development" && Dev} + + { + + + + } + + ); +}; + export const MetaInfo: React.FC = () => { const { data, error } = useSWR("/meta/info", API.meta.getInfo); From acba9d5db3e1ded693981374040e65364f821171 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 19:15:58 -0500 Subject: [PATCH 08/12] docs: added documentation for deploy scripts + component --- infrastructure/prod/README.md | 36 +++++++++++++++++++ infrastructure/prod/get-ecr-image-name.sh | 1 + infrastructure/prod/redeploy.sh | 2 ++ .../components/MetaInfo/MetaInfo.tsx | 27 ++++++++++---- 4 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 infrastructure/prod/README.md diff --git a/infrastructure/prod/README.md b/infrastructure/prod/README.md new file mode 100644 index 000000000..650ace411 --- /dev/null +++ b/infrastructure/prod/README.md @@ -0,0 +1,36 @@ +# How to do a Manual Deploy (to staging or prod). + +Note: All of the `yarn` scripts in this guide are in the root `package.json`. + +**This will deploy the current files as they are in your local repo**. They will be tagged with the local HEAD commit. + +If you don't want this, you'll need to run modified calls rather than the package.json scripts directly. + +## Prereqs: + +- Install Docker +- Install the AWS CLI +- Have an AWS Access Key ID and AWS Secret Access Key pair. +- Run `aws configure`, and input those keys along with us-east-1 as the region. Leave the Default Output Format as None. + +## Steps: + +1. Run `yarn frontend:build` and/or `yarn backend:build` depending on what you want to build. +2. Run `yarn frontend:run` and/or `yarn backend:run` to test that the image runs (the backend won't be able to connect to the database which is expected, and the frontend will not connect to the backend). +3. You may have to run `yarn ecr:docker:auth` to reauthenticate Docker with AWS ECR. (There is no harm in running this command unnecessarily, so if you're unsure you can just run it). +4. If the images run successfully, run `yarn frontend:push` and/or `yarn backend:push` to push the built images to AWS ECR. +5. Run `./infrastructure/prod/redeploy.sh local-head ` + + `` is either `prod` or `staging`. + `` is one of `frontend`, `backend`, or `both`. + +# How to Manually Redeploy a Previously Deployed Commit + +If you know that a commit already has an image pushed to ECR (ex. the commit is in main, or you already pushed the image using the above steps) and you want to deploy that particular image to staging or prod, you can use the redeploy.sh script. + +1. Find the commit's hash (looks like this: 1445f9533c4ec9a7324c721f09fc5ccec1542d8d). +2. Run `./infrastructure/prod/redeploy.sh ` + + `` is either `prod` or `staging`. + `` is the hash of the commit you just found. + `` is one of `frontend`, `backend`, or `both`. diff --git a/infrastructure/prod/get-ecr-image-name.sh b/infrastructure/prod/get-ecr-image-name.sh index ea4b1a901..ca5ff0eef 100755 --- a/infrastructure/prod/get-ecr-image-name.sh +++ b/infrastructure/prod/get-ecr-image-name.sh @@ -3,6 +3,7 @@ if [[ $# != 2 ]]; then exit 1 fi +# Accept a few different formats for the commit hash. if [[ ${#1} = 40 ]]; then ECR_IMAGE_COMMIT_HASH=$1 elif [[ $1 = "latest-main" ]]; then diff --git a/infrastructure/prod/redeploy.sh b/infrastructure/prod/redeploy.sh index 1d375bab3..e3c135af7 100755 --- a/infrastructure/prod/redeploy.sh +++ b/infrastructure/prod/redeploy.sh @@ -10,6 +10,7 @@ if [[ ! " prod staging " =~ " $1 " ]]; then exit 1 fi +# Accept a few different formats for the commit hash. if [[ ${#2} = 40 ]]; then ECR_IMAGE_COMMIT_HASH=$2 elif [[ $2 = "latest-main" ]]; then @@ -21,6 +22,7 @@ else exit 1 fi +# Only deploy the service specified. if [[ $3 = "frontend" ]]; then DEPLOY_INDEXES=(1) elif [[ $3 = "backend" ]]; then diff --git a/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx b/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx index 341e4da7a..1b726ab80 100644 --- a/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx +++ b/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx @@ -4,6 +4,12 @@ import { Maybe } from "@graduate/common"; import { useState } from "react"; import useSWR from "swr"; +// adapted from https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site +/** + * @param current The current time as a JS timestamp (milliseconds) + * @param previous The previous time as a JS timestamp (milliseconds) + * @returns Textual relative time difference + */ function timeDifference(current: number, previous: number): string { const msPerMinute = 60 * 1000; const msPerHour = msPerMinute * 60; @@ -14,20 +20,24 @@ function timeDifference(current: number, previous: number): string { const elapsed = current - previous; if (elapsed < msPerMinute) { - return Math.round(elapsed / 1000) + " seconds ago"; + return Math.round(elapsed / 1000) + " second(s) ago"; } else if (elapsed < msPerHour) { - return Math.round(elapsed / msPerMinute) + " minutes ago"; + return Math.round(elapsed / msPerMinute) + " minute(s) ago"; } else if (elapsed < msPerDay) { - return Math.round(elapsed / msPerHour) + " hours ago"; + return Math.round(elapsed / msPerHour) + " hour(s) ago"; } else if (elapsed < msPerMonth) { - return "~" + Math.round(elapsed / msPerDay) + " days ago"; + return "~" + Math.round(elapsed / msPerDay) + " day(s) ago"; } else if (elapsed < msPerYear) { - return "~" + Math.round(elapsed / msPerMonth) + " months ago"; + return "~" + Math.round(elapsed / msPerMonth) + " month(s) ago"; } else { - return "~" + Math.round(elapsed / msPerYear) + " years ago"; + return "~" + Math.round(elapsed / msPerYear) + " year(s) ago"; } } +/** + * @param timestamp A JS timestamp (milliseconds) + * @returns Formats the given timestamp as a string time + */ function formatBuildTime(timestamp: number): string { return new Date(timestamp).toLocaleDateString("en-US", { weekday: "short", @@ -39,6 +49,7 @@ function formatBuildTime(timestamp: number): string { }); } +/** A clickable dev widget for the header at the top of all pages. */ export const MetaInfoWidget: React.FC = () => { const [showDevInfo, setShowDevInfo] = useState(false); @@ -93,6 +104,7 @@ export const MetaInfoWidget: React.FC = () => { ); }; +/** The contents of the meta info widget which displays docker build info. */ export const MetaInfo: React.FC = () => { const { data, error } = useSWR("/meta/info", API.meta.getInfo); @@ -130,6 +142,7 @@ export const MetaInfo: React.FC = () => { ); }; +/** Display the given optional commit hash and message. */ const CommitText: React.FC<{ commitHash: Maybe; commitMessage: Maybe; @@ -154,6 +167,7 @@ const CommitText: React.FC<{ } }; +/** Displays the given optional build time timestamp. */ const BuildTime: React.FC<{ buildTime: Maybe }> = ({ buildTime, }) => { @@ -169,6 +183,7 @@ const BuildTime: React.FC<{ buildTime: Maybe }> = ({ } }; +/** A Docker meta info section. */ export const MetaInfoSection: React.FC<{ environment: Maybe; commitHash: Maybe; From cb2bf4c84ca08681b789ae71f9091c41dba41bed Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Fri, 24 Nov 2023 19:21:18 -0500 Subject: [PATCH 09/12] Tiny Styling Tweak --- packages/frontend-v2/components/MetaInfo/MetaInfo.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx b/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx index 1b726ab80..4dc9a498e 100644 --- a/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx +++ b/packages/frontend-v2/components/MetaInfo/MetaInfo.tsx @@ -96,6 +96,7 @@ export const MetaInfoWidget: React.FC = () => { background="white" borderRadius="md" width="300px" + zIndex={1} > From ea5ccad0c63eaac2390e5638594d933d7cd255d9 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sat, 25 Nov 2023 19:09:04 -0500 Subject: [PATCH 10/12] docs: one small note tweak --- infrastructure/prod/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/prod/README.md b/infrastructure/prod/README.md index 650ace411..248e309f8 100644 --- a/infrastructure/prod/README.md +++ b/infrastructure/prod/README.md @@ -11,7 +11,7 @@ If you don't want this, you'll need to run modified calls rather than the packag - Install Docker - Install the AWS CLI - Have an AWS Access Key ID and AWS Secret Access Key pair. -- Run `aws configure`, and input those keys along with us-east-1 as the region. Leave the Default Output Format as None. +- Run `aws configure`, and input those keys along with us-east-1 as the region. You can set the Default Output Format to json. ## Steps: From 16aae79f2f7a0b3b346a850cee18bd78f505d127 Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sun, 26 Nov 2023 16:30:16 -0500 Subject: [PATCH 11/12] Small script comment tweak --- infrastructure/prod/get-ecr-image-name.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/prod/get-ecr-image-name.sh b/infrastructure/prod/get-ecr-image-name.sh index ca5ff0eef..a8f946ff7 100755 --- a/infrastructure/prod/get-ecr-image-name.sh +++ b/infrastructure/prod/get-ecr-image-name.sh @@ -20,7 +20,7 @@ if [[ $2 = "frontend" ]]; then elif [[ $2 = "backend" ]]; then REPO="graduatenu-rails" else - echo "Please choose a service to deploy: 'frontend' or 'backend'" + echo "Please choose a service to create an image name for: 'frontend' or 'backend'" exit 1 fi From cc54f315a586f24d239029ce42fbf5efab9c59ca Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Sun, 26 Nov 2023 21:53:11 -0500 Subject: [PATCH 12/12] refactor: Make a COOKIE_DOMAIN constant --- packages/api-v2/src/auth/auth.controller.ts | 16 +++++++--------- packages/api-v2/src/constants.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 packages/api-v2/src/constants.ts diff --git a/packages/api-v2/src/auth/auth.controller.ts b/packages/api-v2/src/auth/auth.controller.ts index eedde14fe..56720b7ca 100644 --- a/packages/api-v2/src/auth/auth.controller.ts +++ b/packages/api-v2/src/auth/auth.controller.ts @@ -31,6 +31,7 @@ import { } from "../../src/student/student.errors"; import { BadToken, InvalidPayload, TokenExpiredError } from "./auth.errors"; import { Throttle } from "@nestjs/throttler"; +import { COOKIE_DOMAIN } from "../../src/constants"; @Controller("auth") export class AuthController { @@ -61,14 +62,13 @@ export class AuthController { const { accessToken } = student; const isSecure = process.env.NODE_ENV !== "development"; - const domain = - process.env.NODE_ENV === "production" ? "graduatenu.com" : "localhost"; + // Store JWT token in a cookie response.cookie("auth_cookie", accessToken, { httpOnly: true, sameSite: "strict", secure: isSecure, - domain, + domain: COOKIE_DOMAIN, }); if (process.env.NODE_ENV !== "testing") { await this.emailConfirmationService.sendVerificationLink( @@ -93,14 +93,13 @@ export class AuthController { const { accessToken } = student; const isSecure = process.env.NODE_ENV !== "development"; - const domain = - process.env.NODE_ENV === "production" ? "graduatenu.com" : "localhost"; + // Store JWT token in a cookie response.cookie("auth_cookie", accessToken, { httpOnly: true, sameSite: "strict", secure: isSecure, - domain, + domain: COOKIE_DOMAIN, }); return student; @@ -158,13 +157,12 @@ export class AuthController { @Res({ passthrough: true }) response: Response ): Promise { const isSecure = process.env.NODE_ENV !== "development"; - const domain = - process.env.NODE_ENV === "production" ? "graduatenu.com" : "localhost"; + response.clearCookie("auth_cookie", { httpOnly: true, sameSite: "strict", secure: isSecure, - domain, + domain: COOKIE_DOMAIN, }); } } diff --git a/packages/api-v2/src/constants.ts b/packages/api-v2/src/constants.ts new file mode 100644 index 000000000..4af358ff8 --- /dev/null +++ b/packages/api-v2/src/constants.ts @@ -0,0 +1,9 @@ +/** + * The root Domain on which all cookies should be set. (See: + * https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_where_cookies_are_sent) + * + * In production, this should be set to "graduatenu.com" which allows + * api.graduatenu.com to set cookies on every other *.graduatenu.com domain. + */ +export const COOKIE_DOMAIN = + process.env.NODE_ENV === "production" ? "graduatenu.com" : "localhost";