(FEAT): Deployed! Need to work on the CI/CD!

This commit is contained in:
Hayden Hargreaves 2025-12-28 22:20:39 -07:00
parent 6bbd58b471
commit f027a16b8c
16 changed files with 154 additions and 76 deletions

View File

@ -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"]

57
Dockerfile.old Normal file
View File

@ -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"]

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

2
web/.gitignore vendored
View File

@ -22,3 +22,5 @@ dist-ssr
*.njsproj
*.sln
*.sw?
.env

View File

@ -75,8 +75,11 @@ export default function IngredientItem({ classes, ingredient, onChange, removeIn
</button>
<div
tabIndex={-1}
onPointerDown={(e) => 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"
>
<DragIconSmall />
</div>

View File

@ -41,7 +41,13 @@ export default function IngredientSection({ section, onChange, removeIngredientS
>
<DeleteIconSmall />
</button>
<div className="p-0 cursor-pointer" onPointerDown={(e) => controls.start(e)}>
<div
className="p-0 cursor-pointer touch-none"
onPointerDown={(e) => {
e.preventDefault();
controls.start(e);
}}
>
<DragIconSmall />
</div>
</div>

View File

@ -52,7 +52,13 @@ export default function InstructionElement({ instruction, index, allowDelete, on
</div>
<div className="flex flex-col items-center">
<div className="p-2 pr-0 cursor-grab" onPointerDown={e => controls.start(e)}>
<div
className="p-2 pr-0 cursor-grab touch-none"
onPointerDown={e => {
e.preventDefault();
controls.start(e);
}}
>
<DragIconSmall />
</div>

View File

@ -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

View File

@ -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<string | ApiError> {
const response = await axios.get<GetGoogleAuthUrlResponse>("http://localhost:3000/v2/api/auth/login");
const response = await axios.get<GetGoogleAuthUrlResponse>(`${BACKEND_URL}/v2/api/auth/login`);
if (response.status !== 200) {
const err: ApiError = {
@ -18,7 +21,7 @@ export async function GetGoogleAuthUrl(): Promise<string | ApiError> {
}
export async function Logout(): Promise<void> {
const response = await axios.get<LogoutResponse>("http://localhost:3000/v2/api/auth/logout");
const response = await axios.get<LogoutResponse>(`${BACKEND_URL}/v2/api/auth/logout`);
// This should never happen
if (response.status !== 204)

View File

@ -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<Engagement | ApiError> {
const response = await axios.post<EngagementViewRecipeResponse>(`http://localhost:3000/v2/api/engagement/view/${recipeId}`);
const response = await axios.post<EngagementViewRecipeResponse>(`${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<Engagement
}
export async function EngagementShareRecipe(recipeId: number): Promise<Engagement | ApiError> {
const response = await axios.post<EngagementShareRecipeResponse>(`http://localhost:3000/v2/api/engagement/share/${recipeId}`);
const response = await axios.post<EngagementShareRecipeResponse>(`${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<Engagemen
}
export async function EngagementFavoriteRecipe(recipeId: number): Promise<Engagement | ApiError> {
const response = await axios.post<EngagementFavoriteRecipeResponse>(`http://localhost:3000/v2/api/engagement/favorite/${recipeId}`);
const response = await axios.post<EngagementFavoriteRecipeResponse>(`${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<Engage
}
export async function EngagementMakeRecipe(recipeId: number): Promise<Engagement | ApiError> {
const response = await axios.post<EngagementMakeRecipeResponse>(`http://localhost:3000/v2/api/engagement/make/${recipeId}`);
const response = await axios.post<EngagementMakeRecipeResponse>(`${BACKEND_URL}/v2/api/engagement/make/${recipeId}`);
if (response.status !== 200 || response.data.engagement === undefined) {
const err: ApiError = {

View File

@ -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<Recipe | ApiError> {
const response = await axios.get<GetRecipeOfTheWeekResponse>("http://localhost:3000/v2/api/recipe/of-the-week");
const response = await axios.get<GetRecipeOfTheWeekResponse>(`${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<Recipe | ApiError> {
}
export async function GetRecipe(id: number): Promise<Recipe | ApiError> {
const response = await axios.get<GetRecipeResponse>(`http://localhost:3000/v2/api/recipe/${id}`);
const response = await axios.get<GetRecipeResponse>(`${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<Recipe | ApiError> {
}
export async function SearchRecipes(filters: SearchFilters): Promise<Recipe[] | ApiError> {
const response = await axios.post<SearchRecipesResponse>("http://localhost:3000/v2/api/recipe/search", filters);
const response = await axios.post<SearchRecipesResponse>(`${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<Recipe[] |
}
export async function CreateRecipe(data: CreateRecipeRequest): Promise<Recipe | ApiError> {
const response = await axios.post<CreateRecipeResponse>("http://localhost:3000/v2/api/recipe", data);
const response = await axios.post<CreateRecipeResponse>(`${BACKEND_URL}/v2/api/recipe`, data);
if (response.status !== 200 || response.data.recipe === undefined) {
const err: ApiError = {

View File

@ -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<User | ApiError> {
const response = await axios.get<GetUserResponse>(`http://localhost:3000/v2/api/user/${id}`);
const response = await axios.get<GetUserResponse>(`${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<User | ApiError> {
}
export async function GetAuthenticatedUser(): Promise<User | ApiError> {
const response = await axios.get<GetAuthenticateUserResponse>("http://localhost:3000/v2/api/user");
const response = await axios.get<GetAuthenticateUserResponse>(`${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<User | ApiError> {
}
export async function GetAuthenticatedUserRecipes(): Promise<Recipe[] | ApiError> {
const response = await axios.get<GetAuthenticateUserRecipesResponse>("http://localhost:3000/v2/api/user/recipes");
const response = await axios.get<GetAuthenticateUserRecipesResponse>(`${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<Recipe[] | ApiError
}
export async function GetAuthenticatedUserFavorites(): Promise<Recipe[] | ApiError> {
const response = await axios.get<GetAuthenticateUserFavoritesResponse>("http://localhost:3000/v2/api/user/favorites");
const response = await axios.get<GetAuthenticateUserFavoritesResponse>(`${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<Recipe[] | ApiErr
}
export async function GetAuthenticatedUserEngagement(): Promise<Engagement[] | ApiError> {
const response = await axios.get<GetAuthenticateUserEngagementResponse>("http://localhost:3000/v2/api/user/engagement");
const response = await axios.get<GetAuthenticateUserEngagementResponse>(`${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<Engagement[] | A
}
export async function GetAuthenticatedUserMadeRecipes(): Promise<Recipe[] | ApiError> {
const response = await axios.get<GetAuthenticateUserMadeRecipesResponse>("http://localhost:3000/v2/api/user/recipes/made");
const response = await axios.get<GetAuthenticateUserMadeRecipesResponse>(`${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<Recipe[] | ApiE
}
export async function GetAuthenticateUserViewedRecipes(): Promise<Recipe[] | ApiError> {
const response = await axios.get<GetAuthenticateUserViewedRecipesResponse>("http://localhost:3000/v2/api/user/recipes/viewed");
const response = await axios.get<GetAuthenticateUserViewedRecipesResponse>(`${BACKEND_URL}/v2/api/user/recipes/viewed`);
if (response.data.status !== 200 || response.data.recipes === undefined) {
const err: ApiError = {

15
web/src/services/util.ts Normal file
View File

@ -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 ""
}
}