diff --git a/README.md b/README.md index 4b3c729..3782683 100644 --- a/README.md +++ b/README.md @@ -16,27 +16,28 @@ - Spike in chamber pressure. - Decrease in oxidizer pressure. - Decrease in oxidizer mass. -- [ ] Allow manual input for oxidizer shutoff time. +- [x] Allow manual input for test start and end. - [ ] Define test end when chamber pressure returns to normal. ### Data Handling -- [ ] Provide a way to download the filtered data. +- [ ] Provide a way to download the filtered data. (Needs review) - [ ] Display both unfiltered and filtered lines on the graph. -- [ ] Add a legend to the Plotly graph to toggle lines on/off. +- [x] Add a legend to the Plotly graph to toggle lines on/off. ### Graph Features -- [ ] Implement two data smoothing filters that do not distort the data. -- [ ] Add a button to enable full-screen mode for the graph. -- [ ] Display mass flow rate on the graph. +- [x] Implement two data smoothing filters that do not distort the data. +- [ ] Integrate the data smoothing filters into the graph. +- [x] Add a button to enable full-screen mode for the graph. +- [ ] Display mass flow rate on the graph. (Needs review) - [ ] (Optional) Plot fill mass and pressure over time. ### Website Features -- [ ] Integrate a home page into the application. +- [x] Integrate a home page into the application. - [ ] Implement a file organization system: - - [ ] Use a custom file extension or metadata to categorize files. + - [x] Use a custom file extension or metadata to categorize files. - [ ] Enable filtering of files for different website sections. ## Example `.env.toml` file @@ -54,6 +55,18 @@ in_production=false # Place GMail addresses that are allowed to use the platform here. whitelist = ["example@gmail.com", "example2@gmail.com"] + +# Development configuration. +[dev] +host = "http://localhost" +port = "8080" +allowedorigins = ["http://localhost:5173"] + +# Production configuration. +[prod] +host = "https://api.soarpipeline.com" +port = "8080" +allowedorigins = ["https://soarpipeline.com", "https://api.soarpipeline.com"] ``` ## Running the backend diff --git a/build/package/prod/backend.prod.dockerfile b/build/package/prod/backend.prod.dockerfile index 987060e..771dd0e 100644 --- a/build/package/prod/backend.prod.dockerfile +++ b/build/package/prod/backend.prod.dockerfile @@ -1,9 +1,11 @@ -FROM golang:latest AS build +# Use the latest Go image if root access is available and version flexibility is acceptable. +# Use a specific Go version for consistency and when rootless Docker environments require it. +FROM golang:1.23 AS build # Set the working directory in the container WORKDIR /app -# Copy the Go module files +# Copy the Go module files. Make sure go.sum exists. If not, or if there's an error, run `go mod tidy` locally. COPY go.mod go.sum ./ RUN go mod download @@ -22,6 +24,9 @@ RUN apk --no-cache add ca-certificates # Set the working directory in the container WORKDIR /root/ +# Copy the .env.toml file +COPY .env.toml . + # Copy the built Go application from the build stage COPY --from=build /app/soarpipeline . diff --git a/cmd/soarpipeline/soarpipeline.go b/cmd/soarpipeline/soarpipeline.go index e909579..a70512a 100644 --- a/cmd/soarpipeline/soarpipeline.go +++ b/cmd/soarpipeline/soarpipeline.go @@ -20,22 +20,35 @@ const ( readTimeout = 10 * time.Second writeTimeout = 15 * time.Second idleTimeout = 10 * time.Second -) -const ( - addr = ":8080" envTomlFile = ".env.toml" ) func initDependencyInjection() (*controllers.DependencyInjection, error) { var env models.EnvToml - if _, err := toml.DecodeFile(envTomlFile, &env); err != nil { return nil, err } + // Determine correct base URL based on environment + host := env.Dev.Host + port := env.Dev.Port + if env.InProduction { + host = env.Prod.Host + port = env.Prod.Port + } + + // For cleaner redirect URLs, omit port if it's the default for the scheme: + // - 443 for HTTPS (production) + // - 80 for HTTP (development) + portSuffix := ":" + port + useDefaultPort := (env.InProduction && (port == "443" || port == "8080")) || (!env.InProduction && port == "80") + if useDefaultPort { + portSuffix = "" + } + // This should match route for callback in the router - redirectURL := fmt.Sprintf("http://localhost%s/auth/google/callback", addr) + redirectURL := fmt.Sprintf("%s%s/auth/google/callback", host, portSuffix) oauthCfg := oauth2.Config{ RedirectURL: redirectURL, @@ -62,14 +75,17 @@ func main() { } i, err := initDependencyInjection() - if err != nil { panic(err) } + // Determine correct port to listen on + port := i.AppConfig.Port + addr := ":" + port + // Set up the router and middleware r := chi.NewRouter() - middlewares.UseCorsMiddleware(r, i.AppConfig.InProduction) + middlewares.UseCorsMiddleware(r, i.AppConfig.AllowedOrigins) // Subrouter for authentication r.Route("/auth", func(r chi.Router) { @@ -97,7 +113,8 @@ func main() { }) }) - fmt.Println("Server running on http://localhost" + addr) + fmt.Printf("Server listening on %s\n", addr) + fmt.Printf("Public-facing host is %s\n", i.AppConfig.Host) // Start the server server := &http.Server{ diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index 833d023..9abe5f8 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -4,13 +4,15 @@ services: context: . dockerfile: ./build/package/prod/frontend.prod.dockerfile ports: - - "80:80" + - "8081:80" depends_on: - backend + restart: unless-stopped backend: build: context: . dockerfile: ./build/package/prod/backend.prod.dockerfile ports: - - "8080:8080" + - "8082:8080" + restart: unless-stopped diff --git a/internal/middlewares/cors.go b/internal/middlewares/cors.go index 86df5d4..5d151e3 100644 --- a/internal/middlewares/cors.go +++ b/internal/middlewares/cors.go @@ -12,20 +12,9 @@ const ( preflightCacheMaxAge = 300 * time.Second ) -const ( - devCorsOrigin = "http://localhost:5173" - prodCorsOrigin = "https://soarpipeline.com" -) - -func UseCorsMiddleware(router chi.Router, inProduction bool) { - allowedOrigin := devCorsOrigin - - if inProduction { - allowedOrigin = prodCorsOrigin - } - +func UseCorsMiddleware(router chi.Router, allowedOrigins []string) { corsConfig := cors.New(cors.Options{ - AllowedOrigins: []string{allowedOrigin}, + AllowedOrigins: allowedOrigins, AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, ExposedHeaders: []string{"Link"}, diff --git a/internal/models/app_config.go b/internal/models/app_config.go index fe0b6d1..8b6e016 100644 --- a/internal/models/app_config.go +++ b/internal/models/app_config.go @@ -5,7 +5,10 @@ import ( ) type AppConfig struct { - InProduction bool - SigningKey []byte - Whitelist set.HashSet[string] + InProduction bool + SigningKey []byte + Whitelist set.HashSet[string] + Host string + Port string + AllowedOrigins []string } diff --git a/internal/models/env_toml.go b/internal/models/env_toml.go index 21636da..8f1eccd 100644 --- a/internal/models/env_toml.go +++ b/internal/models/env_toml.go @@ -2,12 +2,20 @@ package models import set "soarpipeline/pkg/set" +type HostConfig struct { + Host string `toml:"host"` + Port string `toml:"port"` + AllowedOrigins []string `toml:"allowedorigins"` +} + type EnvToml struct { - GoogleClientID string `toml:"google_client_id"` - GoogleClientSecret string `toml:"google_client_secret"` - SigningKey string `toml:"signing_key"` - InProduction bool `toml:"in_production"` - Whitelist []string `toml:"whitelist"` + GoogleClientID string `toml:"google_client_id"` + GoogleClientSecret string `toml:"google_client_secret"` + SigningKey string `toml:"signing_key"` + InProduction bool `toml:"in_production"` + Whitelist []string `toml:"whitelist"` + Dev HostConfig `toml:"dev"` + Prod HostConfig `toml:"prod"` } func (e *EnvToml) ToAppConfig() AppConfig { @@ -17,10 +25,19 @@ func (e *EnvToml) ToAppConfig() AppConfig { whitelistSet.Put(item) } + // Select the appropriate environment config + env := e.Dev + if e.InProduction { + env = e.Prod + } + config := AppConfig{ - InProduction: e.InProduction, - SigningKey: []byte(e.SigningKey), - Whitelist: whitelistSet, + InProduction: e.InProduction, + SigningKey: []byte(e.SigningKey), + Whitelist: whitelistSet, + Host: env.Host, + Port: env.Port, + AllowedOrigins: env.AllowedOrigins, } return config diff --git a/web/jest.setup.js b/web/jest.setup.js new file mode 100644 index 0000000..f06e92c --- /dev/null +++ b/web/jest.setup.js @@ -0,0 +1,7 @@ +// Polyfill import.meta for Jest environment +global.import = {}; +global.import.meta = { + env: { + VITE_BACKEND_HOST: "http://localhost:3000", + }, +}; diff --git a/web/src/__tests__/formatBytes.test.ts b/web/src/__tests__/formatBytes.test.ts index b9f03cf..6f0cfd3 100644 --- a/web/src/__tests__/formatBytes.test.ts +++ b/web/src/__tests__/formatBytes.test.ts @@ -1,4 +1,4 @@ -import { formatBytes } from "$lib/utils/usage"; +import { formatBytes } from "$lib/utils/formatBytes"; describe("formatBytes", () => { test("returns '0 Bytes' when input is 0", () => { diff --git a/web/src/lib/config/env.ts b/web/src/lib/config/env.ts new file mode 100644 index 0000000..9cece02 --- /dev/null +++ b/web/src/lib/config/env.ts @@ -0,0 +1,12 @@ +const getBackendHost = (): string => { + if ( + typeof import.meta !== "undefined" && + import.meta.env?.VITE_BACKEND_HOST + ) { + return import.meta.env.VITE_BACKEND_HOST; + } else { + return "http://localhost:8080"; + } +}; + +export const backendHost = getBackendHost(); diff --git a/web/src/lib/utils/constants.ts b/web/src/lib/utils/constants.ts index 33275d5..9beb305 100644 --- a/web/src/lib/utils/constants.ts +++ b/web/src/lib/utils/constants.ts @@ -1,20 +1,14 @@ -export const backendDevPort = 8080; +// Read from environment variable or fallback to localhost +import { backendHost } from "../config/env"; export const redirectUriParam = "redirect_uri"; + export const endpointMapping = Object.freeze({ - getGoogleLoginUrl: new URL( - `http://localhost:${backendDevPort}/auth/google/login`, - ), - getMeUrl: new URL(`http://localhost:${backendDevPort}/auth/me`), - postLogoutUrl: new URL(`http://localhost:${backendDevPort}/auth/logout`), - uploadStaticFireUrl: new URL( - `http://localhost:${backendDevPort}/api/staticfire/upload`, - ), - getStaticFireMetadataUrl: new URL( - `http://localhost:${backendDevPort}/api/staticfire/metadata`, - ), - postStaticFireColumnsUrl: new URL( - `http://localhost:${backendDevPort}/api/staticfire/columns`, - ), - getUsageURL: new URL(`http://localhost:${backendDevPort}/api/usage`), + getGoogleLoginUrl: new URL(`${backendHost}/auth/google/login`), + getMeUrl: new URL(`${backendHost}/auth/me`), + postLogoutUrl: new URL(`${backendHost}/auth/logout`), + uploadStaticFireUrl: new URL(`${backendHost}/api/staticfire/upload`), + getStaticFireMetadataUrl: new URL(`${backendHost}/api/staticfire/metadata`), + postStaticFireColumnsUrl: new URL(`${backendHost}/api/staticfire/columns`), + getUsageURL: new URL(`${backendHost}/api/usage`), }); diff --git a/web/src/lib/utils/formatBytes.ts b/web/src/lib/utils/formatBytes.ts new file mode 100644 index 0000000..cda03d1 --- /dev/null +++ b/web/src/lib/utils/formatBytes.ts @@ -0,0 +1,14 @@ +// Function to Format Bytes +export function formatBytes(bytes: number, decimals: number = 2): string { + if (bytes === 0) return "0 Bytes"; + + const sizes = ["Bytes", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + + if (i >= sizes.length) return `${bytes} Bytes`; + + const value = bytes / Math.pow(1024, i); + return i === 0 || value < 1 + ? `${Math.floor(value)} ${sizes[i]}` + : `${value.toFixed(decimals)} ${sizes[i]}`; +} diff --git a/web/src/lib/utils/usage.ts b/web/src/lib/utils/usage.ts index 759866d..f036f8b 100644 --- a/web/src/lib/utils/usage.ts +++ b/web/src/lib/utils/usage.ts @@ -25,18 +25,3 @@ export async function getStorageUsage(): Promise { return null; } } - -// Function to Format Bytes -export function formatBytes(bytes: number, decimals: number = 2): string { - if (bytes === 0) return "0 Bytes"; - - const sizes = ["Bytes", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - - if (i >= sizes.length) return `${bytes} Bytes`; - - const value = bytes / Math.pow(1024, i); - return i === 0 || value < 1 - ? `${Math.floor(value)} ${sizes[i]}` - : `${value.toFixed(decimals)} ${sizes[i]}`; -} diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/tsconfig.json b/web/tsconfig.json index 4344710..555bcfb 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -9,6 +9,9 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "bundler" - } + "moduleResolution": "bundler", + "module": "esnext", + "target": "es2020", + }, + "include": ["src"] }