diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e5e013f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,37 @@ +name: Docker Build and Push + +on: + push: + branches: + - main + pull_request: + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Docker image + run: | + docker build . -t ghcr.io/zeuswpi/zess:pr-${{ github.sha }} + if [ "${{ github.event_name }}" == "push" ]; then + docker tag ghcr.io/zeuswpi/zess:pr-${{ github.sha }} ghcr.io/zeuswpi/zess:latest + fi + + - name: Push Docker image + if: github.event_name == 'push' + run: docker push --all-tags ghcr.io/zeuswpi/zess diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b9e1e68 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# Build backend +FROM golang:1.22.1-alpine3.19 as build_backend + +RUN apk add upx alpine-sdk + +WORKDIR / + +COPY vingo/go.sum go.sum + +COPY vingo/go.mod go.mod + +RUN go mod download + +COPY vingo/main.go . + +COPY vingo/database database + +COPY vingo/handlers handlers + +RUN CGO_ENABLED=1 go build -ldflags "-s -w" -v -tags musl vingo/. + +RUN upx --best --lzma vingo + + + +# Build frontend +FROM node:20.15.1-alpine3.20 as build_frontend + +WORKDIR / + +COPY vinvoor/package.json package.json + +COPY vinvoor/yarn.lock yarn.lock + +RUN yarn install + +COPY vinvoor/ . + +COPY vinvoor/production.env .env + +RUN yarn run build + + + +# End container +FROM alpine:3.19 + +WORKDIR / + +COPY --from=build_backend vingo . +COPY --from=build_frontend /dist public + +ENV DEVELOPMENT=false + +EXPOSE 4000 + +ENTRYPOINT ["./vingo"] diff --git a/README.md b/README.md index 58ef7a0..8b239ff 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,6 @@ Automatically run them by running `git config --local core.hooksPath .githooks/` ### Run -#### Easy & Quick - - Install Docker and Docker Compose - Run the script `./dev.sh` with optional flags: - `-b`: Show the output of the backend. @@ -50,6 +48,12 @@ Automatically run them by running `git config --local core.hooksPath .githooks/` The backend is accessible at `localhost:3000`, and the frontend at `localhost:5173`. Both the backend and the frontend support hot module reloading (HMR). -#### Manual +## Production + +- Install Docker +- Set the required env variables for vinvoor in `vinvoor/production.env` _before_ building (see the [README in ./vinvoor](vinvoor/README.md)) +- Build the image `docker build -t zess .` +- Set the required variables in a `.env` file for the backend (see the [README in ./vingo](vingo/README.md)) +- Run the image `docker run -v ${PWD}/.env:/.env zess` -- Each part has it's own `README.md` with instructions on how to run it. +The website is accessible on port 4000 diff --git a/dev.sh b/dev.sh index 68b1c8e..79c599f 100755 --- a/dev.sh +++ b/dev.sh @@ -23,7 +23,7 @@ done if [ "$clean" = true ]; then rm vingo/.env || true rm vinvoor/.env || true - docker-compose -f docker-compose.yml build + docker compose -f docker-compose.yml build fi @@ -37,7 +37,7 @@ if [ ! -f vinvoor/.env ]; then fi # Start the docker containers -docker-compose -f docker-compose.yml up -d +docker compose -f docker-compose.yml up -d echo "-------------------------------------" echo "Following logs..." @@ -45,11 +45,11 @@ echo "Press CTRL + C to stop all containers" echo "-------------------------------------" if [ "$backend" = true ] && [ "$frontend" = false ]; then - docker-compose -f docker-compose.yml logs -f zess-backend + docker compose -f docker-compose.yml logs -f zess-backend elif [ "$backend" = false ] && [ "$frontend" = true ]; then - docker-compose -f docker-compose.yml logs -f zess-frontend + docker compose -f docker-compose.yml logs -f zess-frontend else - docker-compose -f docker-compose.yml logs -f zess-backend zess-frontend + docker compose -f docker-compose.yml logs -f zess-backend zess-frontend fi -docker-compose -f docker-compose.yml down +docker compose -f docker-compose.yml down diff --git a/vingo/Dockerfile b/vingo/Dockerfile deleted file mode 100644 index df96819..0000000 --- a/vingo/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM golang:1.22.1-alpine3.19 as build - -RUN apk add upx alpine-sdk - -WORKDIR / - -COPY go.sum . - -COPY go.mod . - -RUN go mod download - -COPY main.go . - -COPY database database - -COPY handlers handlers - -RUN CGO_ENABLED=1 go build -ldflags "-s -w" -v -tags musl - -RUN upx --best --lzma vingo - -FROM alpine:3.19 - -WORKDIR / - -COPY --from=build vingo . - -ENTRYPOINT ["./vingo"] diff --git a/vingo/README.md b/vingo/README.md index b0357ea..cf1231e 100644 --- a/vingo/README.md +++ b/vingo/README.md @@ -7,10 +7,13 @@ Register a scan by posting `card_serial;scan_key` to the `/scans` endpoint. To register a card, click the "Start registering a new card" button in the cards view, after which the server will register the next scanned card for the user that initiated the request. Only 1 user can register a card at a time. -## How to run (for development) +## Environment variables -- install go -- install docker -- `docker run --name zess-postgres -e POSTGRES_PASSWORD=zess -d -p 5432:5432 postgres` -- `go run .` with the appropriate env vars set (see dev.env) -- profit +- CORS_ALLOW_ORIGINS (string) | Allowed CORS +- DEVELOPMENT (bool) | Whether the program is run in development mode +- ZAUTH_URL (string) | URL of zauth +- ZAUTH_CALLBACK_PATH (string) | Callback path after the zauth authentication (should go to the backend) +- ZAUTH_CLIENT_ID (string) | ID of the zauth client +- ZAUTH_CLIENT_SECRET (string) | Secret of the zauth client +- ZAUTH_REDIRECT_URI (string) | Redirect URI after the zauth authentication is complete in the backend (should go to the frontend) +- POSTGRES_CONNECTION_STRING (string) | Connection string for the database diff --git a/vingo/dev.env b/vingo/dev.env index aa7bc01..474eefb 100644 --- a/vingo/dev.env +++ b/vingo/dev.env @@ -1,5 +1,8 @@ CORS_ALLOW_ORIGINS="http://localhost:5173" +DEVELOPMENT=true +ZAUTH_URL="https://zauth.zeus.gent" +ZAUTH_CALLBACK_PATH="/api/auth/callback" ZAUTH_CLIENT_ID="tomtest" ZAUTH_CLIENT_SECRET="blargh" ZAUTH_REDIRECT_URI="http://localhost:5173" diff --git a/vingo/handlers/auth.go b/vingo/handlers/auth.go index 3b578ea..b49bb7e 100644 --- a/vingo/handlers/auth.go +++ b/vingo/handlers/auth.go @@ -9,18 +9,17 @@ import ( "github.com/gofiber/fiber/v2" ) -const ( - ZAUTH_URL = "https://zauth.zeus.gent" - CALLBACK_PATH = "/auth/callback" // TODO: hardcode ono -) - var ( + ZauthURL = "" + ZauthCallbackPath = "" ZauthClientId = "" ZauthClientSecret = "" ZauthRedirectUri = "" ) -func SetZauth(client_id string, client_secret string, redirect_uri string) { +func SetZauth(url string, callback_path string, client_id string, client_secret string, redirect_uri string) { + ZauthURL = url + ZauthCallbackPath = callback_path ZauthClientId = client_id ZauthClientSecret = client_secret ZauthRedirectUri = redirect_uri @@ -37,8 +36,8 @@ func Login(c *fiber.Ctx) error { sess.Set(ZAUTH_STATE, state.String()) sess.Save() - callback_url := c.BaseURL() + CALLBACK_PATH - return c.Status(200).Redirect(fmt.Sprintf("%s/oauth/authorize?client_id=%s&response_type=code&state=%s&redirect_uri=%s", ZAUTH_URL, ZauthClientId, state.String(), callback_url)) + callback_url := c.BaseURL() + ZauthCallbackPath + return c.Status(200).Redirect(fmt.Sprintf("%s/oauth/authorize?client_id=%s&response_type=code&state=%s&redirect_uri=%s", ZauthURL, ZauthClientId, state.String(), callback_url)) } func Logout(c *fiber.Ctx) error { @@ -87,12 +86,12 @@ func Callback(c *fiber.Ctx) error { defer fiber.ReleaseArgs(args) args.Set("grant_type", "authorization_code") args.Set("code", code) - args.Set("redirect_uri", c.BaseURL()+CALLBACK_PATH) + args.Set("redirect_uri", c.BaseURL()+ZauthCallbackPath) // Convert callback code into access token zauth_token := new(ZauthToken) status, _, errs := fiber. - Post(ZAUTH_URL+"/oauth/token"). + Post(ZauthURL+"/oauth/token"). BasicAuth(ZauthClientId, ZauthClientSecret). Form(args). Struct(zauth_token) @@ -107,7 +106,7 @@ func Callback(c *fiber.Ctx) error { // Get user info using access token zauth_user := new(ZauthUser) status, _, errs = fiber. - Get(ZAUTH_URL+"/current_user"). + Get(ZauthURL+"/current_user"). Set("Authorization", "Bearer "+zauth_token.AccessToken). Struct(zauth_user) diff --git a/vingo/main.go b/vingo/main.go index 052df98..4de4975 100644 --- a/vingo/main.go +++ b/vingo/main.go @@ -3,6 +3,7 @@ package main import ( "encoding/gob" "os" + "strconv" "vingo/database" "vingo/handlers" @@ -14,7 +15,10 @@ import ( _ "github.com/lib/pq" ) -var corsAllowOrigins = "" +var ( + corsAllowOrigins string + development bool +) func main() { gob.Register(database.User{}) @@ -25,16 +29,22 @@ func main() { db := database.Get() defer db.Close() - api := fiber.New(fiber.Config{}) + app := fiber.New(fiber.Config{}) + + if development { + app.Use(cors.New(cors.Config{ + AllowOrigins: corsAllowOrigins, + AllowHeaders: "Origin, Content-Type, Accept, Access-Control-Allow-Origin", + AllowCredentials: true, + })) + } else { + app.Static("/", "./public") + } - api.Use(cors.New(cors.Config{ - AllowOrigins: corsAllowOrigins, - AllowHeaders: "Origin, Content-Type, Accept, Access-Control-Allow-Origin", - AllowCredentials: true, - })) + api := app.Group("/api") - // Public routes { + // Public routes api.Post("/login", handlers.Login) api.Get("/auth/callback", handlers.Callback) @@ -42,6 +52,7 @@ func main() { api.Get("/recent_scans", handlers.PublicRecentScans) + // Protected routes authed := api.Group("", handlers.IsLoggedIn) { authed.Post("/logout", handlers.Logout) @@ -57,6 +68,7 @@ func main() { authed.Get("/settings", handlers.Settings{}.Get) authed.Patch("/settings", handlers.Settings{}.Update) + // Admin routes admin := authed.Group("/admin", handlers.IsAdmin) { admin.Get("/days", handlers.Days{}.All) @@ -66,7 +78,12 @@ func main() { } } - log.Println(api.Listen(":4000")) + // Catch-all route leading to the frontend + app.Get("*", func(c *fiber.Ctx) error { + return c.SendFile("./public/index.html") + }) + + log.Println(app.Listen(":4000")) } func setupFromEnv() { @@ -81,7 +98,28 @@ func setupFromEnv() { } corsAllowOrigins = cors_allow_origins + dev, dev_ok := os.LookupEnv("DEVELOPMENT") + if !dev_ok { + log.Fatal("DEVELOPMENT environment variable not set") + } + dev_value, dev_value_err := strconv.ParseBool(dev) + if dev_value_err != nil { + log.Fatal("DEVELOPMENT environment variable is not a valid boolean") + } + development = dev_value + // stuff for Zauth oauth flow + + zauth_url, url_ok := os.LookupEnv("ZAUTH_URL") + if !url_ok { + log.Fatal("ZAUTH_URL environment variable not set") + } + + zauth_callback_path, callback_ok := os.LookupEnv("ZAUTH_CALLBACK_PATH") + if !callback_ok { + log.Fatal("ZAUTH_CALLBACK_PATH environment variable not set") + } + zauth_client_id, id_ok := os.LookupEnv("ZAUTH_CLIENT_ID") if !id_ok { log.Fatal("ZAUTH_CLIENT_ID environment variable not set") @@ -97,7 +135,7 @@ func setupFromEnv() { log.Fatal("ZAUTH_REDIRECT_URI environment variable not set") } - handlers.SetZauth(zauth_client_id, zauth_client_secret, zauth_redirect_uri) + handlers.SetZauth(zauth_url, zauth_callback_path, zauth_client_id, zauth_client_secret, zauth_redirect_uri) // PSK that will authorize the scanner scan_key, key_ok := os.LookupEnv("SCAN_KEY") diff --git a/vingo/template.env b/vingo/template.env index 26af887..94f1cba 100644 --- a/vingo/template.env +++ b/vingo/template.env @@ -1,5 +1,8 @@ CORS_ALLOW_ORIGINS= +DEVELOPMENT +ZAUTH_URL= +ZAUTH_CALLBACK_PATH= ZAUTH_CLIENT_ID= ZAUTH_CLIENT_SECRET= ZAUTH_REDIRECT_URI= diff --git a/vinvoor/.dockerignore b/vinvoor/.dockerignore new file mode 100644 index 0000000..2a1871b --- /dev/null +++ b/vinvoor/.dockerignore @@ -0,0 +1,31 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.vite/ +bin/ +pkg/ +*.env + +test.js diff --git a/vinvoor/Dockerfile b/vinvoor/Dockerfile deleted file mode 100644 index e3c8a11..0000000 --- a/vinvoor/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:20.15.1-alpine3.20 as build-stage - -WORKDIR /app - -COPY package.json yarn.lock ./ - -RUN yarn install - -COPY ./ . - -RUN yarn run build - - -FROM nginx:alpine-slim as production-stage - -EXPOSE 3000 - -RUN mkdir /app - -COPY nginx.conf /etc/nginx/conf.d/default.conf - -COPY --from=build-stage /app/dist /app diff --git a/vinvoor/README.md b/vinvoor/README.md index b1d3a94..4cd19fe 100644 --- a/vinvoor/README.md +++ b/vinvoor/README.md @@ -3,15 +3,10 @@ Keeping track of scans is cool and all but you need a place to show them. That's what this does! -## How to run (for development) +## Environment variables -- Install nodejs 22.2.0 -- Install yarn `npm install yarn` -- Install dependencies `yarn install` -- Start the frontend `yarn run dev` -- Visit: http://localhost:5173 +For production the environment variables need to be set before building! +You can specify them in `production.env` which will be used by the Dockerfile -## How to run (for production) - -- Build the image `docker build -t vinvoor:latest .`. -- Run the image `docker run -p 80:3000 vinvoor:latest`. +- VITE_BACKEND_URL (string) | URL for the backend +- VITE_DEFAULT_THEME_MODE (string) | Default theme for the frontend diff --git a/vinvoor/dev.env b/vinvoor/dev.env index 05d0ef5..fad4423 100644 --- a/vinvoor/dev.env +++ b/vinvoor/dev.env @@ -1,4 +1,3 @@ -VITE_BASE_URL="http://localhost:4000" -VITE_API_URL="http://localhost:4000" +VITE_BACKEND_URL="http://localhost:4000/api" VITE_DEFAULT_THEME_MODE="light" diff --git a/vinvoor/nginx.conf b/vinvoor/nginx.conf deleted file mode 100644 index cdb8eaa..0000000 --- a/vinvoor/nginx.conf +++ /dev/null @@ -1,9 +0,0 @@ -server { - listen 3000; - - location / { - root /app; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } -} diff --git a/vinvoor/production.env b/vinvoor/production.env new file mode 100644 index 0000000..fad4423 --- /dev/null +++ b/vinvoor/production.env @@ -0,0 +1,3 @@ +VITE_BACKEND_URL="http://localhost:4000/api" + +VITE_DEFAULT_THEME_MODE="light" diff --git a/vinvoor/src/user/Login.tsx b/vinvoor/src/user/Login.tsx index d7a50f7..d8a48e9 100644 --- a/vinvoor/src/user/Login.tsx +++ b/vinvoor/src/user/Login.tsx @@ -2,12 +2,12 @@ import { Button, ButtonProps } from "@mui/material"; import { FC } from "react"; export const Login: FC = props => { - const baseUrl = import.meta.env.VITE_BASE_URL as string; + const url = import.meta.env.VITE_BACKEND_URL as string; const handleClick = () => { const form = document.createElement("form"); form.method = "POST"; - form.action = `${baseUrl}/login`; + form.action = `${url}/login`; document.body.appendChild(form); form.submit(); }; diff --git a/vinvoor/src/user/Logout.tsx b/vinvoor/src/user/Logout.tsx index e07abc5..19f178b 100644 --- a/vinvoor/src/user/Logout.tsx +++ b/vinvoor/src/user/Logout.tsx @@ -2,12 +2,12 @@ import { Button, ButtonProps } from "@mui/material"; import { FC } from "react"; export const Logout: FC = props => { - const apiUrl = import.meta.env.VITE_API_URL as string; + const url = import.meta.env.VITE_BACKEND_URL as string; const handleClick = () => { const form = document.createElement("form"); form.method = "POST"; - form.action = `${apiUrl}/logout`; + form.action = `${url}/logout`; document.body.appendChild(form); form.submit(); }; diff --git a/vinvoor/src/util/fetch.ts b/vinvoor/src/util/fetch.ts index d279fc2..dfe9fe3 100644 --- a/vinvoor/src/util/fetch.ts +++ b/vinvoor/src/util/fetch.ts @@ -1,6 +1,5 @@ const URLS: Record = { - BASE: import.meta.env.VITE_BASE_URL as string, - API: import.meta.env.VITE_API_URL as string, + API: import.meta.env.VITE_BACKEND_URL as string, }; export const getApi = ( diff --git a/vinvoor/template.env b/vinvoor/template.env index befa1c3..062d546 100644 --- a/vinvoor/template.env +++ b/vinvoor/template.env @@ -1,4 +1,3 @@ -VITE_BASE_URL= -VITE_API_URL= +VITE_BACKEND_URL= VITE_DEFAULT_THEME_MODE=