From f027a16b8c2652ff9949ddd9138578aa826c8ac7 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sun, 28 Dec 2025 22:20:39 -0700 Subject: [PATCH] (FEAT): Deployed! Need to work on the CI/CD! --- Dockerfile | 55 ++---------------- Dockerfile.old | 57 +++++++++++++++++++ internal/app/server/auth_handler_v2.go | 6 +- internal/app/server/cookies.go | 9 ++- internal/app/server/server.go | 2 +- internal/domain/server/server.go | 13 +++++ web/.gitignore | 2 + web/src/components/forms/IngredientItem.tsx | 7 ++- .../components/forms/IngredientSection.tsx | 8 ++- .../components/forms/InstructionElement.tsx | 8 ++- .../inputs/RecipeCreateFormInput.tsx | 2 +- web/src/services/AuthService.ts | 7 ++- web/src/services/EngagementService.ts | 11 ++-- web/src/services/RecipeService.ts | 11 ++-- web/src/services/UserService.ts | 17 +++--- web/src/services/util.ts | 15 +++++ 16 files changed, 154 insertions(+), 76 deletions(-) create mode 100644 Dockerfile.old create mode 100644 web/src/services/util.ts diff --git a/Dockerfile b/Dockerfile index 3e3b3f1..cbf0426 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,57 +1,14 @@ -# Fetch stage -FROM golang:latest AS fetch-stage - -COPY . /app +FROM golang:1.25-alpine WORKDIR /app -RUN go mod tidy +COPY go.mod go.sum ./ RUN go mod download -# Generate stage -FROM ghcr.io/a-h/templ:latest AS generate-stage +COPY . . -COPY --chown=65532:65532 . /app +RUN go build -o server ./cmd/web/main.go -WORKDIR /app +EXPOSE 8080 -RUN ["templ", "generate"] - -# Generate stage two - -FROM node:lts-alpine AS tailwind-build-stage - -COPY --from=generate-stage /app /app - -WORKDIR /app - -RUN npm install tailwindcss @tailwindcss/cli && npx @tailwindcss/cli -i ./web/static/css/main.css -o ./web/static/css/tailwind.css --minify -c ./tailwind.config.js - - -# Build stage -FROM golang:latest AS build-stage - -COPY --from=tailwind-build-stage /app /app - -WORKDIR /app - -RUN go mod tidy -RUN go mod download - -RUN CGO_ENABLED=0 GOOS=linux go build -o /entrypoint /app/cmd/web/main.go - -# Deploy. -FROM gcr.io/distroless/static-debian11 AS release-stage - -WORKDIR / - -COPY --from=build-stage /entrypoint /entrypoint - -COPY --from=build-stage /app/web/static /web/static - - -EXPOSE 3000 - -USER nonroot:nonroot - -ENTRYPOINT ["/entrypoint"] +CMD ["./server"] diff --git a/Dockerfile.old b/Dockerfile.old new file mode 100644 index 0000000..3e3b3f1 --- /dev/null +++ b/Dockerfile.old @@ -0,0 +1,57 @@ +# Fetch stage +FROM golang:latest AS fetch-stage + +COPY . /app + +WORKDIR /app + +RUN go mod tidy +RUN go mod download + +# Generate stage +FROM ghcr.io/a-h/templ:latest AS generate-stage + +COPY --chown=65532:65532 . /app + +WORKDIR /app + +RUN ["templ", "generate"] + +# Generate stage two + +FROM node:lts-alpine AS tailwind-build-stage + +COPY --from=generate-stage /app /app + +WORKDIR /app + +RUN npm install tailwindcss @tailwindcss/cli && npx @tailwindcss/cli -i ./web/static/css/main.css -o ./web/static/css/tailwind.css --minify -c ./tailwind.config.js + + +# Build stage +FROM golang:latest AS build-stage + +COPY --from=tailwind-build-stage /app /app + +WORKDIR /app + +RUN go mod tidy +RUN go mod download + +RUN CGO_ENABLED=0 GOOS=linux go build -o /entrypoint /app/cmd/web/main.go + +# Deploy. +FROM gcr.io/distroless/static-debian11 AS release-stage + +WORKDIR / + +COPY --from=build-stage /entrypoint /entrypoint + +COPY --from=build-stage /app/web/static /web/static + + +EXPOSE 3000 + +USER nonroot:nonroot + +ENTRYPOINT ["/entrypoint"] diff --git a/internal/app/server/auth_handler_v2.go b/internal/app/server/auth_handler_v2.go index e31cfaa..5ab70f3 100644 --- a/internal/app/server/auth_handler_v2.go +++ b/internal/app/server/auth_handler_v2.go @@ -29,11 +29,13 @@ func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) { code string = ctx.Query("code") ) + domain := s.deps.EnvironmentConfig.FrontendDomain + if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil { - url := fmt.Sprintf("http://localhost:5173/v2/web/login?error=%s", url.QueryEscape(err.Error())) + url := fmt.Sprintf("%s/v2/web/login?error=%s", domain, url.QueryEscape(err.Error())) ctx.Redirect(http.StatusSeeOther, url) } else { - url := "http://localhost:5173/v2/web/home" + url := fmt.Sprintf("%s/v2/web/home", domain) s.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7) ctx.Redirect(http.StatusSeeOther, url) } diff --git a/internal/app/server/cookies.go b/internal/app/server/cookies.go index 088b346..78ac8f6 100644 --- a/internal/app/server/cookies.go +++ b/internal/app/server/cookies.go @@ -1,6 +1,7 @@ package server import ( + "net/http" "time" "github.com/gin-gonic/gin" @@ -20,7 +21,7 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D path string = "/" httpOnly bool = false // NOTE: Should use false so React can see it! maxAge int - secure bool = false + secure bool = true domain string = "" ) @@ -35,9 +36,12 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D maxAge = int(time.Until(time.Now().Add(duration)).Seconds()) } + // TODO: This whole system is stupid now if s.deps.EnvironmentConfig.Environment == "prod" { secure = true - domain = s.deps.EnvironmentConfig.Domain + // domain = "potion.gophernest" + // domain = s.deps.EnvironmentConfig.Domain + domain = ".gophernest.net" } else if s.deps.EnvironmentConfig.Environment == "dev" { secure = false @@ -45,5 +49,6 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D domain = "localhost" } + ctx.SetSameSite(http.SameSiteNoneMode) ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly) } diff --git a/internal/app/server/server.go b/internal/app/server/server.go index f2822af..a2a7b52 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -43,7 +43,7 @@ func Init(port int) *Server { // Setup the CORS settings and active them // server.config.AllowAllOrigins = true - server.config.AllowOrigins = []string{"http://localhost:5173"} + server.config.AllowOrigins = []string{"http://localhost:5173", "https://potion.gophernest.net"} server.config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"} server.config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"} server.config.AllowCredentials = true diff --git a/internal/domain/server/server.go b/internal/domain/server/server.go index 66093a0..7afc797 100644 --- a/internal/domain/server/server.go +++ b/internal/domain/server/server.go @@ -23,6 +23,7 @@ type EnvironmentConfig struct { DatabaseUrl string Environment string Domain string + FrontendDomain string } // InjectedDependencies is a collection of dependencies that are injected into the application. They @@ -81,16 +82,25 @@ func LoadEnvironment() (*EnvironmentConfig, error) { } var domain string + var frontendDomain string if env == "dev" { domain = os.Getenv("DOMAIN_DEV") if domain == "" { return nil, fmt.Errorf("DOMAIN_DEV environment variable is required when ENVIRONMENT is 'dev'.") } + frontendDomain = os.Getenv("FRONTEND_DOMAIN_DEV") + if frontendDomain == "" { + return nil, fmt.Errorf("FRONTEND_DOMAIN_DEV environment variable is required when ENVIRONMENT is 'dev'.") + } } else if env == "prod" { domain = os.Getenv("DOMAIN_PROD") if domain == "" { return nil, fmt.Errorf("DOMAIN_PROD environment variable is required when ENVIRONMENT is 'prod'.") } + frontendDomain = os.Getenv("FRONTEND_DOMAIN_PROD") + if frontendDomain == "" { + return nil, fmt.Errorf("FRONTEND_DOMAIN_PROD environment variable is required when ENVIRONMENT is 'dev'.") + } } else { return nil, fmt.Errorf("ENVIRONMENT environment variable is required and must be 'dev' or 'prod'.") } @@ -117,8 +127,11 @@ func LoadEnvironment() (*EnvironmentConfig, error) { DatabaseUrl: dbUrl, Environment: env, Domain: domain, + FrontendDomain: frontendDomain, } + fmt.Printf("Environment Config: %+v\n", cfg) + return cfg, nil } diff --git a/web/.gitignore b/web/.gitignore index a547bf3..50c8dda 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.env diff --git a/web/src/components/forms/IngredientItem.tsx b/web/src/components/forms/IngredientItem.tsx index 768f955..a6174b7 100644 --- a/web/src/components/forms/IngredientItem.tsx +++ b/web/src/components/forms/IngredientItem.tsx @@ -75,8 +75,11 @@ export default function IngredientItem({ classes, ingredient, onChange, removeIn
controls.start(e)} - className="p-1 md:p-0 cursor-pointer" + onPointerDown={(e) => { + e.preventDefault(); + controls.start(e); + }} + className="p-1 md:p-0 cursor-pointer touch-none" >
diff --git a/web/src/components/forms/IngredientSection.tsx b/web/src/components/forms/IngredientSection.tsx index ebd52a1..1748a1a 100644 --- a/web/src/components/forms/IngredientSection.tsx +++ b/web/src/components/forms/IngredientSection.tsx @@ -41,7 +41,13 @@ export default function IngredientSection({ section, onChange, removeIngredientS > -
controls.start(e)}> +
{ + e.preventDefault(); + controls.start(e); + }} + >
diff --git a/web/src/components/forms/InstructionElement.tsx b/web/src/components/forms/InstructionElement.tsx index d7bddb1..79c8c0b 100644 --- a/web/src/components/forms/InstructionElement.tsx +++ b/web/src/components/forms/InstructionElement.tsx @@ -52,7 +52,13 @@ export default function InstructionElement({ instruction, index, allowDelete, on
-
controls.start(e)}> +
{ + e.preventDefault(); + controls.start(e); + }} + >
diff --git a/web/src/components/inputs/RecipeCreateFormInput.tsx b/web/src/components/inputs/RecipeCreateFormInput.tsx index c98e553..b8a69cd 100644 --- a/web/src/components/inputs/RecipeCreateFormInput.tsx +++ b/web/src/components/inputs/RecipeCreateFormInput.tsx @@ -1,4 +1,4 @@ -import { useEffect, type ChangeEvent, type Dispatch, type InputHTMLAttributes, type SetStateAction } from "react"; +import { type ChangeEvent, type Dispatch, type InputHTMLAttributes, type SetStateAction } from "react"; import type { CreateRecipeFormDirtyEntries } from "../../pages/Create"; interface RecipeCreateFormInputProps diff --git a/web/src/services/AuthService.ts b/web/src/services/AuthService.ts index f8b3cee..1adb4b3 100644 --- a/web/src/services/AuthService.ts +++ b/web/src/services/AuthService.ts @@ -1,10 +1,13 @@ import axios from "axios"; import type { GetGoogleAuthUrlResponse, LogoutResponse } from "../types/api/auth"; import type { ApiError } from "../types/api/error"; +import { GetBackendUrl } from "./util"; + +const BACKEND_URL = GetBackendUrl(); export async function GetGoogleAuthUrl(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/auth/login"); + const response = await axios.get(`${BACKEND_URL}/v2/api/auth/login`); if (response.status !== 200) { const err: ApiError = { @@ -18,7 +21,7 @@ export async function GetGoogleAuthUrl(): Promise { } export async function Logout(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/auth/logout"); + const response = await axios.get(`${BACKEND_URL}/v2/api/auth/logout`); // This should never happen if (response.status !== 204) diff --git a/web/src/services/EngagementService.ts b/web/src/services/EngagementService.ts index 382b4db..6677c20 100644 --- a/web/src/services/EngagementService.ts +++ b/web/src/services/EngagementService.ts @@ -2,9 +2,12 @@ import axios from "axios"; import type { ApiError } from "../types/api/error"; import type { Engagement } from "../types/engagement"; import type { EngagementFavoriteRecipeResponse, EngagementMakeRecipeResponse, EngagementShareRecipeResponse, EngagementViewRecipeResponse } from "../types/api/engagement"; +import { GetBackendUrl } from "./util"; + +const BACKEND_URL = GetBackendUrl(); export async function EngagementViewRecipe(recipeId: number): Promise { - const response = await axios.post(`http://localhost:3000/v2/api/engagement/view/${recipeId}`); + const response = await axios.post(`${BACKEND_URL}/v2/api/engagement/view/${recipeId}`); if (response.status !== 200 || response.data.engagement === undefined) { const err: ApiError = { @@ -18,7 +21,7 @@ export async function EngagementViewRecipe(recipeId: number): Promise { - const response = await axios.post(`http://localhost:3000/v2/api/engagement/share/${recipeId}`); + const response = await axios.post(`${BACKEND_URL}/v2/api/engagement/share/${recipeId}`); if (response.status !== 200 || response.data.engagement === undefined) { const err: ApiError = { @@ -32,7 +35,7 @@ export async function EngagementShareRecipe(recipeId: number): Promise { - const response = await axios.post(`http://localhost:3000/v2/api/engagement/favorite/${recipeId}`); + const response = await axios.post(`${BACKEND_URL}/v2/api/engagement/favorite/${recipeId}`); if (response.status !== 200 || response.data.engagement === undefined) { const err: ApiError = { @@ -46,7 +49,7 @@ export async function EngagementFavoriteRecipe(recipeId: number): Promise { - const response = await axios.post(`http://localhost:3000/v2/api/engagement/make/${recipeId}`); + const response = await axios.post(`${BACKEND_URL}/v2/api/engagement/make/${recipeId}`); if (response.status !== 200 || response.data.engagement === undefined) { const err: ApiError = { diff --git a/web/src/services/RecipeService.ts b/web/src/services/RecipeService.ts index 0ac614d..fd2a7c2 100644 --- a/web/src/services/RecipeService.ts +++ b/web/src/services/RecipeService.ts @@ -3,10 +3,13 @@ import type { CreateRecipeRequest, CreateRecipeResponse, GetRecipeOfTheWeekRespo import type { Recipe } from "../types/recipe"; import type { ApiError } from "../types/api/error"; import type { SearchFilters } from "../types/search"; +import { GetBackendUrl } from "./util"; + +const BACKEND_URL = GetBackendUrl(); export async function GetRecipeOfTheWeek(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/recipe/of-the-week"); + const response = await axios.get(`${BACKEND_URL}/v2/api/recipe/of-the-week`); if (response.status !== 200 || response.data.recipe === undefined) { const err: ApiError = { @@ -20,7 +23,7 @@ export async function GetRecipeOfTheWeek(): Promise { } export async function GetRecipe(id: number): Promise { - const response = await axios.get(`http://localhost:3000/v2/api/recipe/${id}`); + const response = await axios.get(`${BACKEND_URL}/v2/api/recipe/${id}`); if (response.status !== 200 || response.data.recipe === undefined) { const err: ApiError = { @@ -34,7 +37,7 @@ export async function GetRecipe(id: number): Promise { } export async function SearchRecipes(filters: SearchFilters): Promise { - const response = await axios.post("http://localhost:3000/v2/api/recipe/search", filters); + const response = await axios.post(`${BACKEND_URL}/v2/api/recipe/search`, filters); if (response.status !== 200 || response.data.recipes === undefined) { const err: ApiError = { @@ -48,7 +51,7 @@ export async function SearchRecipes(filters: SearchFilters): Promise { - const response = await axios.post("http://localhost:3000/v2/api/recipe", data); + const response = await axios.post(`${BACKEND_URL}/v2/api/recipe`, data); if (response.status !== 200 || response.data.recipe === undefined) { const err: ApiError = { diff --git a/web/src/services/UserService.ts b/web/src/services/UserService.ts index cd785a3..b9f8b63 100644 --- a/web/src/services/UserService.ts +++ b/web/src/services/UserService.ts @@ -4,9 +4,12 @@ import type { User } from "../types/user"; import type { GetAuthenticateUserEngagementResponse, GetAuthenticateUserFavoritesResponse, GetAuthenticateUserMadeRecipesResponse, GetAuthenticateUserRecipesResponse, GetAuthenticateUserResponse, GetAuthenticateUserViewedRecipesResponse, GetUserResponse } from "../types/api/user"; import type { Recipe } from "../types/recipe"; import type { Engagement } from "../types/engagement"; +import { GetBackendUrl } from "./util"; + +const BACKEND_URL = GetBackendUrl(); export async function GetUser(id: number): Promise { - const response = await axios.get(`http://localhost:3000/v2/api/user/${id}`); + const response = await axios.get(`${BACKEND_URL}/v2/api/user/${id}`); if (response.data.status !== 200 || response.data.user === undefined) { const err: ApiError = { @@ -20,7 +23,7 @@ export async function GetUser(id: number): Promise { } export async function GetAuthenticatedUser(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/user"); + const response = await axios.get(`${BACKEND_URL}/v2/api/user`); if (response.data.status !== 200 || response.data.user === undefined) { const err: ApiError = { @@ -34,7 +37,7 @@ export async function GetAuthenticatedUser(): Promise { } export async function GetAuthenticatedUserRecipes(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/user/recipes"); + const response = await axios.get(`${BACKEND_URL}/v2/api/user/recipes`); if (response.data.status !== 200 || response.data.recipes === undefined) { const err: ApiError = { @@ -48,7 +51,7 @@ export async function GetAuthenticatedUserRecipes(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/user/favorites"); + const response = await axios.get(`${BACKEND_URL}/v2/api/user/favorites`); if (response.data.status !== 200 || response.data.favorites === undefined) { const err: ApiError = { @@ -62,7 +65,7 @@ export async function GetAuthenticatedUserFavorites(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/user/engagement"); + const response = await axios.get(`${BACKEND_URL}/v2/api/user/engagement`); if (response.data.status !== 200 || response.data.engagement === undefined) { const err: ApiError = { @@ -76,7 +79,7 @@ export async function GetAuthenticatedUserEngagement(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/user/recipes/made"); + const response = await axios.get(`${BACKEND_URL}/v2/api/user/recipes/made`); if (response.data.status !== 200 || response.data.recipes === undefined) { const err: ApiError = { @@ -90,7 +93,7 @@ export async function GetAuthenticatedUserMadeRecipes(): Promise { - const response = await axios.get("http://localhost:3000/v2/api/user/recipes/viewed"); + const response = await axios.get(`${BACKEND_URL}/v2/api/user/recipes/viewed`); if (response.data.status !== 200 || response.data.recipes === undefined) { const err: ApiError = { diff --git a/web/src/services/util.ts b/web/src/services/util.ts new file mode 100644 index 0000000..e749dce --- /dev/null +++ b/web/src/services/util.ts @@ -0,0 +1,15 @@ +const ENV = import.meta.env; + +export function GetBackendUrl(): string { + const env = ENV.VITE_ENVIRONMENT as string; + if (!env) return ""; + + switch (env.toLowerCase()) { + case "dev": + return ENV.VITE_DOMAIN_DEV as string; + case "prod": + return ENV.VITE_DOMAIN_PROD as string; + default: + return "" + } +}