diff --git a/.containerignore b/.containerignore index b5b7c6a..baa5ca5 100644 --- a/.containerignore +++ b/.containerignore @@ -1,4 +1,5 @@ **/target **/node_modules **/dist -**/.sqlite3 \ No newline at end of file +**/bin +**/*.sqlite3 diff --git a/.gitignore b/.gitignore index 063a7e7..9ad11ec 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,6 @@ dist/ *.7z *.bak +backend/backups - +config.toml diff --git a/BUILD.md b/BUILD.md index e5a686f..664a1e4 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,7 +2,7 @@ The build is fairly simple, and I intend to keep it that way. The project is split into two main parts: the backend and the frontend. -Making all of these parts work properly under linux/macOS is fairly simple, but Windows is untested territory. You can always contact [Billy G™®©](https://support.microsoft.com) for help. (If there is demand for Windows support, I will make it work) +Making all of these parts work properly under linux/macOS is fairly simple, but Windows will require [WSL](https://learn.microsoft.com/en-us/windows/wsl/). When working with these tools, keep in mind that make is sensitive to working directory. If you're in the wrong directory, make will not work as expected. Always make sure you're in the right directory when running make commands. diff --git a/Justfile b/Justfile index 966fac9..eadfabd 100644 --- a/Justfile +++ b/Justfile @@ -28,4 +28,9 @@ clean: remove-podman-containers rm -rf frontend/node_modules rm -f ttime-server.tar.gz cd backend && make clean - @echo "Cleaned up!" \ No newline at end of file + @echo "Cleaned up!" + +# Cleans up everything related to podman, not just the project. Make sure you understand what this means. +[confirm] +podman-clean: + podman system reset --force \ No newline at end of file diff --git a/backend/Makefile b/backend/Makefile index a5b55df..2bd6895 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -19,6 +19,9 @@ build: run: build ./bin/server +watch: build + watchexec -w . -r make run + # Clean target clean: $(GOCLEAN) @@ -26,8 +29,8 @@ clean: rm -f db.sqlite3 # Test target -test: - $(GOTEST) ./... +test: db.sqlite3 + $(GOTEST) ./... -count=1 -v # Get dependencies target deps: @@ -46,9 +49,19 @@ migrate: sqlite3 $(DB_FILE) < $$file; \ done +# Target added primarily for CI/CD to ensure that the database is created before running tests +db.sqlite3: + make migrate + +backup: + mkdir -p backups + sqlite3 $(DB_FILE) .dump | gzip -9 > ./backups/BACKUP_$(DB_FILE)_$(shell date +"%Y-%m-%d_%H:%M:%S").sql.gz + # Restore with: + # gzip -cd BACKUP_FILE.sql.gz | sqlite3 $(DB_FILE) + # Format fmt: $(GOCMD) fmt ./... - + # Default target -default: build \ No newline at end of file +default: build diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 5ccb016..ea8d968 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "ttime/internal/config" "ttime/internal/database" _ "github.com/mattn/go-sqlite3" @@ -12,15 +13,18 @@ import ( // The button state as represented in memory type ButtonState struct { - pressCount int + PressCount int `json:"pressCount"` } // This is what a handler with a receiver looks like // Keep in mind that concurrent state access is not (usually) safe -// And will in pracice be guarded by a mutex +// And will in practice be guarded by a mutex func (b *ButtonState) pressHandler(w http.ResponseWriter, r *http.Request) { - b.pressCount++ - response, err := json.Marshal(b.pressCount) + if r.Method == "POST" { + b.PressCount++ + } + + response, err := json.Marshal(b) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -37,21 +41,33 @@ func handler(w http.ResponseWriter, r *http.Request) { } func main() { - database.DbConnect() - b := &ButtonState{pressCount: 0} + conf, err := config.ReadConfigFromFile("config.toml") + if err != nil { + conf = config.NewConfig() + conf.WriteConfigToFile("config.toml") + } + + // Pretty print the current config + str, _ := json.MarshalIndent(conf, "", " ") + fmt.Println(string(str)) + + database.DbConnect(conf.DbPath) + b := &ButtonState{PressCount: 0} // Mounting the handlers fs := http.FileServer(http.Dir("static")) http.Handle("/", fs) http.HandleFunc("/hello", handler) - http.HandleFunc("/button", b.pressHandler) + http.HandleFunc("/api/button", b.pressHandler) - // Start the server on port 8080 - println("Currently listening on http://localhost:8080") - println("Visit http://localhost:8080/hello to see the hello handler in action") - println("Visit http://localhost:8080/button to see the button handler in action") + // Construct a server URL + server_url := fmt.Sprintf(":%d", conf.Port) + + println("Server running on port", conf.Port) + println("Visit http://localhost" + server_url) println("Press Ctrl+C to stop the server") - err := http.ListenAndServe(":8080", nil) + + err = http.ListenAndServe(server_url, nil) if err != nil { panic(err) } diff --git a/backend/go.mod b/backend/go.mod index 34d4ebf..8c06f45 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -5,4 +5,5 @@ go 1.21.1 require ( github.com/jmoiron/sqlx v1.3.5 github.com/mattn/go-sqlite3 v1.14.22 + github.com/BurntSushi/toml v1.3.2 ) diff --git a/backend/go.sum b/backend/go.sum index ee85dd9..afab06f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..5bebc34 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,55 @@ +package config + +import ( + "os" + + "github.com/BurntSushi/toml" +) + +// Config is the configuration for the application +// It defines how the TOML file should be structured +type Config struct { + // The port to listen on + Port int `toml:"port"` + // The path to the SQLite database + DbPath string `toml:"db_path"` + // The username to use for the database + DbUser string `toml:"db_user"` + // The password to use for the database + DbPass string `toml:"db_pass"` + // The name of the database + DbName string `toml:"db_name"` +} + +// WriteConfigToFile writes a Config to a file +func (c *Config) WriteConfigToFile(filename string) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + + return toml.NewEncoder(f).Encode(c) +} + +// ReadConfigFromFile reads a Config from a file +func ReadConfigFromFile(filename string) (*Config, error) { + c := NewConfig() + _, err := toml.DecodeFile(filename, c) + if err != nil { + return nil, err + } + + return c, nil +} + +// NewConfig returns a new Config +func NewConfig() *Config { + return &Config{ + Port: 8080, + DbPath: "./db.sqlite3", + DbUser: "username", + DbPass: "password", + DbName: "ttime", + } +} diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go new file mode 100644 index 0000000..cb02a31 --- /dev/null +++ b/backend/internal/config/config_test.go @@ -0,0 +1,68 @@ +package config + +import ( + "os" + "testing" +) + +func TestNewConfig(t *testing.T) { + c := NewConfig() + if c.Port != 8080 { + t.Errorf("Expected port to be 8080, got %d", c.Port) + } + if c.DbPath != "./db.sqlite3" { + t.Errorf("Expected db path to be ./db.sqlite3, got %s", c.DbPath) + } + if c.DbUser != "username" { + t.Errorf("Expected db user to be username, got %s", c.DbUser) + } + if c.DbPass != "password" { + t.Errorf("Expected db pass to be password, got %s", c.DbPass) + } + if c.DbName != "ttime" { + t.Errorf("Expected db name to be ttime, got %s", c.DbName) + } +} + +func TestWriteConfig(t *testing.T) { + c := NewConfig() + err := c.WriteConfigToFile("test.toml") + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + // Remove the file after the test + _ = os.Remove("test.toml") +} + +func TestReadConfig(t *testing.T) { + c := NewConfig() + err := c.WriteConfigToFile("test.toml") + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + c2, err := ReadConfigFromFile("test.toml") + if err != nil { + t.Errorf("Expected no error, got %s", err) + } + + if c.Port != c2.Port { + t.Errorf("Expected port to be %d, got %d", c.Port, c2.Port) + } + if c.DbPath != c2.DbPath { + t.Errorf("Expected db path to be %s, got %s", c.DbPath, c2.DbPath) + } + if c.DbUser != c2.DbUser { + t.Errorf("Expected db user to be %s, got %s", c.DbUser, c2.DbUser) + } + if c.DbPass != c2.DbPass { + t.Errorf("Expected db pass to be %s, got %s", c.DbPass, c2.DbPass) + } + if c.DbName != c2.DbName { + t.Errorf("Expected db name to be %s, got %s", c.DbName, c2.DbName) + } + + // Remove the file after the test + _ = os.Remove("test.toml") +} diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index d99efde..c14ba97 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -1,32 +1,100 @@ package database import ( + "embed" + "log" "os" + "path/filepath" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" ) -func DbConnect() *sqlx.DB { - // Check for the environment variable - dbpath := os.Getenv("SQLITE_DB_PATH") +// This struct is a wrapper type that holds the database connection +// Internally DB holds a connection pool, so it's safe for concurrent use +type Db struct { + *sqlx.DB +} - // Default to something reasonable - if dbpath == "" { - dbpath = "./db.sqlite3" - } +//go:embed migrations +var scripts embed.FS +const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" +const projectInsert = "INSERT INTO projects (name, description, user_id) SELECT ?, ?, id FROM users WHERE username = ?" + +// DbConnect connects to the database +func DbConnect(dbpath string) *Db { // Open the database - // db, err := sqlx.Connect("sqlite3", ":memory:") db, err := sqlx.Connect("sqlite3", dbpath) if err != nil { panic(err) } + // Ping forces the connection to be established err = db.Ping() if err != nil { panic(err) } - return db + return &Db{db} +} + +// AddUser adds a user to the database +func (d *Db) AddUser(username string, password string) error { + _, err := d.Exec(userInsert, username, password) + return err +} + +// Removes a user from the database +func (d *Db) RemoveUser(username string) error { + _, err := d.Exec("DELETE FROM users WHERE username = ?", username) + return err +} + +func (d *Db) GetUserId(username string) (int, error) { + var id int + err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) + return id, err +} + +// Creates a new project in the database, associated with a user +func (d *Db) AddProject(name string, description string, username string) error { + _, err := d.Exec(projectInsert, name, description, username) + return err +} + +// Reads a directory of migration files and applies them to the database. +// This will eventually be used on an embedded directory +func (d *Db) Migrate(dirname string) error { + // Read the embedded scripts directory + files, err := scripts.ReadDir("migrations") + if err != nil { + return err + } + + tr := d.MustBegin() + + // Iterate over each SQL file and execute it + for _, file := range files { + if file.IsDir() || filepath.Ext(file.Name()) != ".sql" { + continue + } + + // This is perhaps not the most elegant way to do this + sqlFile := filepath.Join("migrations", file.Name()) + sqlBytes, err := os.ReadFile(sqlFile) + if err != nil { + return err + } + + sqlQuery := string(sqlBytes) + _, err = tr.Exec(sqlQuery) + if err != nil { + return err + } + log.Println("Executed SQL file:", file.Name()) + } + + tr.Commit() + return nil } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index d7b26c9..62e47db 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -4,7 +4,71 @@ import ( "testing" ) +// Tests are not guaranteed to be sequential + +func setupState() (*Db, error) { + db := DbConnect(":memory:") + err := db.Migrate("../../migrations") + if err != nil { + return nil, err + } + return db, nil +} + func TestDbConnect(t *testing.T) { - db := DbConnect() + db := DbConnect(":memory:") _ = db } + +func TestDbAddUser(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + err = db.AddUser("test", "password") + if err != nil { + t.Error("AddUser failed:", err) + } +} + +func TestDbGetUserId(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + db.AddUser("test", "password") + + var id int + + id, err = db.GetUserId("test") + if err != nil { + t.Error("GetUserId failed:", err) + } + if id != 1 { + t.Error("GetUserId failed: expected 1, got", id) + } +} + +func TestDbAddProject(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddProject("test", "description", "test") + if err != nil { + t.Error("AddProject failed:", err) + } +} + +func TestDbRemoveUser(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.RemoveUser("test") + if err != nil { + t.Error("RemoveUser failed:", err) + } +} diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql new file mode 100644 index 0000000..7cb23fd --- /dev/null +++ b/backend/internal/database/migrations/0010_users.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL +); + +CREATE INDEX IF NOT EXISTS users_username_index ON users (username); +CREATE INDEX IF NOT EXISTS users_userId_index ON users (userId); \ No newline at end of file diff --git a/backend/internal/database/migrations/0020_projects.sql b/backend/internal/database/migrations/0020_projects.sql new file mode 100644 index 0000000..8592e75 --- /dev/null +++ b/backend/internal/database/migrations/0020_projects.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS projects ( + id INTEGER PRIMARY KEY, + projectId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL UNIQUE, + description TEXT NOT NULL, + user_id INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES users (id) +); + +CREATE INDEX IF NOT EXISTS projects_projectId_index ON projects (projectId); +CREATE INDEX IF NOT EXISTS projects_user_id_index ON projects (user_id); \ No newline at end of file diff --git a/backend/internal/database/migrations/0030_time_reports.sql b/backend/internal/database/migrations/0030_time_reports.sql new file mode 100644 index 0000000..e8f3ec1 --- /dev/null +++ b/backend/internal/database/migrations/0030_time_reports.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS time_reports ( + id INTEGER PRIMARY KEY, + reportId TEXT DEFAULT (HEX(RANDOMBLOB(6))) NOT NULL UNIQUE, + project_id INTEGER NOT NULL, + start DATETIME NOT NULL, + end DATETIME NOT NULL, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE +); + +CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end + BEFORE INSERT ON time_reports + FOR EACH ROW + BEGIN + SELECT + CASE + WHEN NEW.start >= NEW.end THEN + RAISE (ABORT, 'start must be before end') + END; + END; \ No newline at end of file diff --git a/backend/internal/database/migrations/0040_time_report_collections.sql b/backend/internal/database/migrations/0040_time_report_collections.sql new file mode 100644 index 0000000..be406ff --- /dev/null +++ b/backend/internal/database/migrations/0040_time_report_collections.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS report_collection ( + id INTEGER PRIMARY KEY, + owner_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + date DATE NOT NULL, + signed_by INTEGER, -- NULL if not signed + FOREIGN KEY (owner_id) REFERENCES users (id) + FOREIGN KEY (signed_by) REFERENCES users (id) +); \ No newline at end of file diff --git a/backend/internal/database/migrations/0050_user_roles.sql b/backend/internal/database/migrations/0050_user_roles.sql new file mode 100644 index 0000000..32e03dc --- /dev/null +++ b/backend/internal/database/migrations/0050_user_roles.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS user_roles ( + user_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + role STRING NOT NULL, -- 'admin' or 'member' + FOREIGN KEY (user_id) REFERENCES users (id) + FOREIGN KEY (project_id) REFERENCES projects (id) +); \ No newline at end of file diff --git a/backend/internal/database/migrations/README.md b/backend/internal/database/migrations/README.md new file mode 100644 index 0000000..18b5e34 --- /dev/null +++ b/backend/internal/database/migrations/README.md @@ -0,0 +1,14 @@ +# Database migrations + +This directory contains all the database migrations for the backend. + +[!WARNING] +Keep in mind that these migrations are **not yet stable**. + +## Running migrations + +In the root of the backend directory, run: + +```bash +make migrate +``` diff --git a/backend/migrations/0010.sql b/backend/migrations/0010.sql deleted file mode 100644 index 11be51c..0000000 --- a/backend/migrations/0010.sql +++ /dev/null @@ -1,7 +0,0 @@ -PRAGMA foreign_keys = ON; - -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - username VARCHAR(255) NOT NULL, - password VARCHAR(255) NOT NULL -); diff --git a/container/Containerfile b/container/Containerfile index ba442de..23d177c 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -4,7 +4,7 @@ # make it available in the public directory. FROM docker.io/node:alpine as client WORKDIR /build -ADD frontend /build +ADD frontend ./ RUN npm install RUN npm run build @@ -13,31 +13,45 @@ FROM docker.io/golang:alpine as go RUN apk add gcompat RUN apk add gcc RUN apk add musl-dev -ADD backend /build +RUN apk add make +RUN apk add sqlite WORKDIR /build +ADD backend/go.mod backend/go.sum ./ # Get the dependencies RUN go mod download +# Add the source code +ADD backend . + +RUN make migrate + # RUN go build -o server RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./cmd/ + +# Strip the binary for a smaller image RUN strip ./server # The final stage for building a minimal image FROM docker.io/alpine:latest as runner +RUN adduser -D nonroot +RUN addgroup nonroot nonroot WORKDIR /app # Copy the frontend SPA build into public -COPY --from=client /build/dist /app/static +COPY --from=client /build/dist static # Copy the server binary -COPY --from=go /build/server /app/server +COPY --from=go /build/server server -# Copy the migration scripts -COPY --from=go /build/migrations /app/migrations +# Copy the database +COPY --from=go /build/db.sqlite3 db.sqlite3 # Expose port 8080 EXPOSE 8080 +# Set the user to nonroot +USER nonroot:nonroot + # Run the server CMD ["./server"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index 5bb483c..58e7e3e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -3,10 +3,30 @@ This is a React + TypeScript + Vite project. It is a single-page application (SPA) that communicates with the backend via a REST API. Most of this looks very complex, but it's mostly just a lot of boilerplate due to the ugodly amount of abstraction that comes with TypeScript and React, and just modern web development in general. +**Keep in mind** that the tooling for the frontend is configured to be extra strict (see the [eslint config](.eslintrc.cjs)). This is to make it hard to write bad code, by encouraging actual typing and not just using [any](https://www.typescriptlang.org/docs/handbook/basic-types.html#any) everywhere. This can be a bit of a struggle to work with at first, and I might have to backpedal on this if it becomes too much of a hassle. (Those familiar with Java will be more confused by the lack of types than the presence of them.) + ## Build Instructions -Our build tool, npm, is the only dependency required to build this project. It handles fetching the dependencies and placing them in the famously huge `node_modules` directory, also known as the heaviest object in the known universe. It also handles the build process, which is just a matter of running `npm run build`. +Our build tool, npm, is the only dependency required to build this project. It handles fetching the dependencies and placing them in the famously huge `node_modules` directory, also known as the heaviest object in the known universe. -The build target results in a `dist` directory, which contains the static assets that can be served by a web server. This is the directory that will be served by the backend. Think of this like compiling a program, the .exe/elf/binary is the `dist` directory. +### Development + +For a development session, in the `frontend` directory, run: + +```sh +npm install +npm run dev +``` For development, you use `npm run dev` to start a development server. This server will automatically rebuild the project when you make changes to the source code, and it will also automatically reload the page in your browser. The issue is that the development server will run without the backend, **so you will need to run the backend separately during development** if you want any meaningful data to be displayed. + +### Release + +The release build is usually automated by the CI/CD pipeline, but you can also build it manually. In the `frontend` directory, run: + +```sh +npm install +npm run build +``` + +The build target results in a `dist` directory, which contains the static assets that can be served by a web server. This is the directory that will be served by the backend. Think of this like compiling a program, the .exe/elf/binary is the `dist` directory. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9d045e1..20e5f1f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,9 @@ -import { useState } from "react"; import reactLogo from "./assets/react.svg"; import viteLogo from "/vite.svg"; import "./App.css"; +import { CountButton } from "./Components/CountButton"; function App(): JSX.Element { - const [count, setCount] = useState(0); - return ( <>
@@ -18,13 +16,7 @@ function App(): JSX.Element {

Vite + React

- +

Edit src/App.tsx and save to test HMR

diff --git a/frontend/src/Components/CountButton.tsx b/frontend/src/Components/CountButton.tsx new file mode 100644 index 0000000..a6f1b30 --- /dev/null +++ b/frontend/src/Components/CountButton.tsx @@ -0,0 +1,38 @@ +import { useState, useEffect } from "react"; + +// Interface for the response from the server +// This should eventually reside in a dedicated file +interface CountResponse { + pressCount: number; +} + +// Some constants for the button +const BUTTON_ENDPOINT = "/api/button"; + +// A simple button that counts how many times it's been pressed +export function CountButton(): JSX.Element { + const [count, setCount] = useState(NaN); + + // useEffect with a [] dependency array runs only once + useEffect(() => { + async function getCount(): Promise { + const response = await fetch(BUTTON_ENDPOINT); + const data = (await response.json()) as CountResponse; + setCount(data.pressCount); + } + void getCount(); + }, []); + + // This is what runs on every button click + function press(): void { + async function pressPost(): Promise { + const response = await fetch(BUTTON_ENDPOINT, { method: "POST" }); + const data = (await response.json()) as CountResponse; + setCount(data.pressCount); + } + void pressPost(); + } + + // Return some JSX with the button and associated handler + return ; +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index d366e8c..f8ec812 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,4 +4,19 @@ import react from "@vitejs/plugin-react-swc"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + // build: { + // outDir: '../server/public' // Override default outDir('dist') + // }, + server: { + port: 3000, + open: true, + proxy: { + "/api": { + target: "http://localhost:8080/api", + changeOrigin: true, + secure: false, + rewrite: (path): string => path.replace(/^\/api/, ""), + }, + }, + }, });