From d64f3a08dc4c28c95260175d6faa4bcd8e3706c8 Mon Sep 17 00:00:00 2001 From: adescoteaux1 Date: Fri, 29 Aug 2025 18:22:41 -0400 Subject: [PATCH 1/4] backend + db init + docs --- .env | 1 + .github/workflows/backend.yml | 45 +++ .github/workflows/frontend.yml | 57 +++ CONTRIBUTING.md | 18 + backend/.env | 11 + backend/.vscode/extensions.json | 3 + backend/.vscode/settings.json | 24 ++ backend/Dockerfile | 16 + backend/README.md | 50 ++- backend/cmd/main.go | 58 +++ backend/env.sample | 11 + backend/go.mod | 34 ++ backend/go.sum | 78 ++++ backend/internal/config/application.go | 7 + backend/internal/config/config.go | 7 + backend/internal/config/db.go | 15 + backend/internal/config/supabase.go | 7 + backend/internal/errs/http.go | 70 ++++ backend/internal/service/server.go | 70 ++++ backend/internal/storage/postgres/storage.go | 43 +++ backend/internal/storage/storage.go | 24 ++ backend/internal/supabase/.gitignore | 8 + backend/internal/supabase/config.toml | 335 ++++++++++++++++++ .../migrations/20250829211902_db_init.sql | 11 + backend/internal/supabase/seed.sql | 4 + docker-compose.yml | 42 +++ docs/schema.md | 44 +++ 27 files changed, 1092 insertions(+), 1 deletion(-) create mode 100644 .env create mode 100644 .github/workflows/backend.yml create mode 100644 .github/workflows/frontend.yml create mode 100644 backend/.env create mode 100644 backend/.vscode/extensions.json create mode 100644 backend/.vscode/settings.json create mode 100644 backend/Dockerfile create mode 100644 backend/cmd/main.go create mode 100644 backend/env.sample create mode 100644 backend/go.mod create mode 100644 backend/go.sum create mode 100644 backend/internal/config/application.go create mode 100644 backend/internal/config/config.go create mode 100644 backend/internal/config/db.go create mode 100644 backend/internal/config/supabase.go create mode 100644 backend/internal/errs/http.go create mode 100644 backend/internal/service/server.go create mode 100644 backend/internal/storage/postgres/storage.go create mode 100644 backend/internal/storage/storage.go create mode 100644 backend/internal/supabase/.gitignore create mode 100644 backend/internal/supabase/config.toml create mode 100644 backend/internal/supabase/migrations/20250829211902_db_init.sql create mode 100644 backend/internal/supabase/seed.sql create mode 100644 docker-compose.yml create mode 100644 docs/schema.md diff --git a/.env b/.env new file mode 100644 index 00000000..f96ed383 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NEXT_PUBLIC_API_BASE_URL=http://localhost:8080 diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 00000000..4592dbf6 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,45 @@ +name: backend-lint +on: + push: + branches: + - "**" + paths: + - "backend/**" + - ".github/workflows/backend.yaml" + +permissions: + contents: read + checks: write + +jobs: + backend-lint: + name: backend-lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: latest + working-directory: ./backend/ + args: --timeout=5m + + backend-tests: + name: Run backend-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.22" + + - name: Test Backend + run: | + cd backend + go test ./... diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml new file mode 100644 index 00000000..33057fb6 --- /dev/null +++ b/.github/workflows/frontend.yml @@ -0,0 +1,57 @@ +name: frontend +on: + push: + branches: + - "main" + paths: + - "frontend/**" + - ".github/workflows/frontend.yaml" + pull_request: + branches: + - "main" + paths: + - "frontend/**" + - ".github/workflows/frontend.yaml" + +jobs: + lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: "22" + + - name: Install dependencies + run: npm install + + - name: Lint + run: npm run lint + + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: "22" + + - name: Install dependencies + run: npm install + + - name: Build + run: npm run build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 854139a3..0ece0a28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1 +1,19 @@ # Contributing + +## Overview + +To submit changes, create a pull request and fill out the provided template. **Make sure that the pull request title is descriptive.** + +## Backend PRs + +If you're writing backend code, please include a screenshot of your endpoint(s) working (Postman, Insomnia, etc.). Please also include the URL of your endpoint and any parameters you passed in - this is super helpful for other developers looking to use your endpoint. + +## Frontend PRs + +If you're writing frontend code, please include a screenshot or screen recording of your new/updated pages. + +## Review process + +Once submitted, your pull request will require at least one of the tech leads' approval. Additionally, your code will be automatically linted and built using GitHub Actions. This is super important to keep our main branch clean and well-tested. Once the CI checks pass and an approval is given, you're free to merge your PR! + +If your pull request is reviewed and not approved, the reviewer will leave some suggestions for changes. (Sometimes we will approve and still leave suggestions). Once you have made your changes, make sure to re-request a review and/or message one of the TLs on Slack to ensure another review. diff --git a/backend/.env b/backend/.env new file mode 100644 index 00000000..29b9759b --- /dev/null +++ b/backend/.env @@ -0,0 +1,11 @@ +DB_USER=postgres.zvxatxmclijgdkvxdmbc +DB_PASSWORD=dixva5-gemrut-dAcpuz +DB_HOST=aws-1-us-east-1.pooler.supabase.com +DB_PORT=5432 +DB_NAME=postgres + +PORT=8080 + +SUPABASE_URL=https://zvxatxmclijgdkvxdmbc.supabase.co +SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp2eGF0eG1jbGlqZ2RrdnhkbWJjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY0MTM2NDEsImV4cCI6MjA3MTk4OTY0MX0.EHymLjsYov9pankPWHbyrEdwPcz__yf2SMBvMbhP3Eo +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp2eGF0eG1jbGlqZ2RrdnhkbWJjIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NjQxMzY0MSwiZXhwIjoyMDcxOTg5NjQxfQ.0F_u8r1upt-fA4pKUrXCNY7_CfNSZuK2BQXTTBM4jeE \ No newline at end of file diff --git a/backend/.vscode/extensions.json b/backend/.vscode/extensions.json new file mode 100644 index 00000000..74baffcc --- /dev/null +++ b/backend/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["denoland.vscode-deno"] +} diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json new file mode 100644 index 00000000..af62c23f --- /dev/null +++ b/backend/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "deno.enablePaths": [ + "supabase/functions" + ], + "deno.lint": true, + "deno.unstable": [ + "bare-node-builtins", + "byonm", + "sloppy-imports", + "unsafe-proto", + "webgpu", + "broadcast-channel", + "worker-options", + "cron", + "kv", + "ffi", + "fs", + "http", + "net" + ], + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + } +} diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..84dfa53d --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.23-alpine + + +# Set the working directory (this is /backend in Arenius) +WORKDIR /app + +# Copy only go.mod and go.sum first (to cache dependencies) +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the entire project +COPY . . + +WORKDIR /app/cmd + +CMD ["go", "run", "main.go"] \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 75cac094..244945ac 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1 +1,49 @@ -# Backend README +# Special Standard Backend + +## Getting Started + +The backend is written in [Golang](https://go.dev/learn/) and handles code dependencies with Go modules managed within go.mod and go.sum. +In order to run the backend, the most straightforward way is to navigate to backend/cmd and execute **go run main.go** or +**go build -o main . && ./main**. + +Alternatively, we will use **Docker** to build and run isolated containers to ensure that environment dependencies and runtimes are consistent across our machines. + +### Steps to use Docker + +Install [Docker Desktop](https://docs.docker.com/get-started/get-docker/) (or just [Docker Engine](https://docs.docker.com/engine/install/)). + +In a terminal of your choice: + +```bash +cd /specialstandard +docker compose up --build --watch +``` + +This will compose a cluster consisting of the backend and frontend containers. +Docker Watch is utilized for hot/live reloading to make development easier. The Docker Engine +watches for changes within the backend and frontend, syncs file changes from +the host to the respective container, and then restarts the respective container. + +To end development, in the terminal press ctrl+c / cmd+c OR in another terminal execute: + +```bash +docker compose down +``` + +Open with your browser to see the result. Requests *should* be logged in your terminal. + +## Postman + +For further development and testing, install [Postman](https://www.postman.com/downloads/), which simplifies making network requests. + +## Learn More + +- [Go Modules](https://faun.pub/understanding-go-mod-and-go-sum-5fd7ec9bcc34) - article about go.mod and go.sum. +- [Tour of Go](https://go.dev/tour/welcome/1) - guided tour of Golang. +- [Go Video](https://youtu.be/8uiZC0l4Ajw?si=YJq6z9nqTN-B-c8c) - fantastic build up of data structures and syntax to write a complicated API. +- [Fiber Framework](https://docs.gofiber.io/) - web framework, similar to Express, SpringBoot, Flask, FastAPI, etc. +- [Docker Engine](https://docs.docker.com/engine/) - documentation for docker building and running docker containers. +- [pgx](https://pkg.go.dev/github.com/jackc/pgx) - driver and toolkit for PostgreSQL which can be used to interact with Supabase. +- [Supabase](https://supabase.com/docs) - database hosting service, with Auth service alongside. + +This tech stack is totally flexible--suggestions are welcome! diff --git a/backend/cmd/main.go b/backend/cmd/main.go new file mode 100644 index 00000000..d5f08aa1 --- /dev/null +++ b/backend/cmd/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "log" + "log/slog" + "os" + "os/signal" + "specialstandard/internal/config" + "specialstandard/internal/service" + "syscall" + + "github.com/joho/godotenv" + "github.com/sethvargo/go-envconfig" +) + +func main() { + err := godotenv.Load("../.env") + if err != nil { + log.Fatalf("Error loading .env file: %v", err) + return + } + + var config config.Config + if err := envconfig.Process(context.Background(), &config); err != nil { + log.Fatalln("Error processing .env file: ", err) + } + + app := service.InitApp(config) + + // Pushing the closing of the database connection onto a + // stack of statements to be executed when this function returns. + + // **Uncomment after repo connection is actually made** + // defer app.Repo.Close() + + port := config.Application.Port + // Listen for connections with a goroutine. + go func() { + if err := app.Server.Listen(":" + port); err != nil { + log.Fatalf("Failed to start server: %v", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + // Wait for the termination signal: + <-quit + + // Then shutdown server gracefully. + slog.Info("Shutting down server") + if err := app.Server.Shutdown(); err != nil { + slog.Error("failed to shutdown server", "error", err) + } + + slog.Info("Server shutdown") +} diff --git a/backend/env.sample b/backend/env.sample new file mode 100644 index 00000000..a921ddcb --- /dev/null +++ b/backend/env.sample @@ -0,0 +1,11 @@ +DB_USER="" +DB_PASSWORD="" +DB_HOST="" +DB_PORT="" +DB_NAME="" + +PORT="8080" + +SUPABASE_URL="" +SUPABASE_ANON_KEY="" +SUPABASE_SERVICE_ROLE_KEY="" \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 00000000..32e35788 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,34 @@ +module specialstandard + +go 1.23.1 + +require ( + github.com/goccy/go-json v0.10.5 + github.com/gofiber/fiber/v2 v2.52.9 + github.com/google/uuid v1.6.0 + github.com/jackc/pgx/v5 v5.7.5 + github.com/lib/pq v1.10.9 + github.com/pkg/errors v0.9.1 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sethvargo/go-envconfig v1.3.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 00000000..cf7ab94a --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,78 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= +github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U= +github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/supabase-community/functions-go v0.0.0-20220927045802-22373e6cb51d/go.mod h1:nnIju6x3+OZSojtGQCQzu0h3kv4HdIZk+UWCnNxtSak= +github.com/supabase-community/gotrue-go v1.2.0/go.mod h1:86DXBiAUNcbCfgbeOPEh0PQxScLfowUbYgakETSFQOw= +github.com/supabase-community/postgrest-go v0.0.11/go.mod h1:cw6LfzMyK42AOSBA1bQ/HZ381trIJyuui2GWhraW7Cc= +github.com/supabase-community/storage-go v0.7.0/go.mod h1:oBKcJf5rcUXy3Uj9eS5wR6mvpwbmvkjOtAA+4tGcdvQ= +github.com/supabase-community/supabase-go v0.0.4/go.mod h1:SSHsXoOlc+sq8XeXaf0D3gE2pwrq5bcUfzm0+08u/o8= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/config/application.go b/backend/internal/config/application.go new file mode 100644 index 00000000..d6c11cd2 --- /dev/null +++ b/backend/internal/config/application.go @@ -0,0 +1,7 @@ +package config + +type Application struct { + Port string `env:"PORT, default=8080"` + Environment string `env:"ENVIRONMENT, default=development"` + AllowedOrigins string `env:"ALLOWED_ORIGINS, default=http://localhost:3000"` +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 00000000..4b44117b --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,7 @@ +package config + +type Config struct { + Application Application + DB DB + Supabase Supabase +} diff --git a/backend/internal/config/db.go b/backend/internal/config/db.go new file mode 100644 index 00000000..1a6e54b2 --- /dev/null +++ b/backend/internal/config/db.go @@ -0,0 +1,15 @@ +package config + +import "fmt" + +type DB struct { + Host string `env:"DB_HOST, required"` // the database host to connect to. + Port string `env:"DB_PORT, required"` // the database port to connect to. + User string `env:"DB_USER, required"` // the user to connect to the database with. + Password string `env:"DB_PASSWORD, required"` // the password to connect to the database with. + Name string `env:"DB_NAME, required"` // the name of the database to connect to. +} + +func (db *DB) Connection() string { + return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=require", db.Host, db.User, db.Password, db.Name, db.Port) +} diff --git a/backend/internal/config/supabase.go b/backend/internal/config/supabase.go new file mode 100644 index 00000000..944b78c4 --- /dev/null +++ b/backend/internal/config/supabase.go @@ -0,0 +1,7 @@ +package config + +type Supabase struct { + URL string `env:"SUPABASE_URL, required"` + AnonKey string `env:"SUPABASE_ANON_KEY, required"` + ServiceRoleKey string `env:"SUPABASE_SERVICE_ROLE_KEY, required"` +} diff --git a/backend/internal/errs/http.go b/backend/internal/errs/http.go new file mode 100644 index 00000000..b4a12a52 --- /dev/null +++ b/backend/internal/errs/http.go @@ -0,0 +1,70 @@ +package errs + +import ( + "errors" + "fmt" + "log/slog" + "net/http" + + "github.com/gofiber/fiber/v2" +) + +type HTTPError struct { + Code int `json:"code"` + Message any `json:"message"` +} + +func (e HTTPError) Error() string { + return fmt.Sprintf("http error: %d %v", e.Code, e.Message) +} + +func NewHTTPError(code int, err error) HTTPError { + return HTTPError{ + Code: code, + Message: err.Error(), + } +} + +func BadRequest(msg string) HTTPError { + return NewHTTPError(http.StatusBadRequest, errors.New(msg)) +} + +func Unauthorized() HTTPError { + return NewHTTPError(http.StatusUnauthorized, errors.New("unauthorized")) +} + +func NotFound(title string, withKey string, withValue any) HTTPError { + return NewHTTPError(http.StatusNotFound, fmt.Errorf("%s with %s='%v' not found", title, withKey, withValue)) +} + +func Conflict(title string, withKey string, withValue any) HTTPError { + return NewHTTPError(http.StatusConflict, fmt.Errorf("conflict: %s with %s='%s' already exists", title, withKey, withValue)) +} + +func InvalidRequestData(errors map[string]string) HTTPError { + return HTTPError{ + Code: http.StatusUnprocessableEntity, + Message: errors, + } +} + +func InvalidJSON() HTTPError { + return NewHTTPError(http.StatusBadRequest, errors.New("invalid json")) +} + +func InternalServerError() HTTPError { + return NewHTTPError(http.StatusInternalServerError, errors.New("internal server error")) +} + +func ErrorHandler(c *fiber.Ctx, err error) error { + var httpErr HTTPError + if castedErr, ok := err.(HTTPError); ok { + httpErr = castedErr + } else { + httpErr = InternalServerError() + } + + slog.Error("HTTP API error", "err", err.Error(), "method", c.Method(), "path", c.Path()) + + return c.Status(httpErr.Code).JSON(httpErr) +} diff --git a/backend/internal/service/server.go b/backend/internal/service/server.go new file mode 100644 index 00000000..7723ecd8 --- /dev/null +++ b/backend/internal/service/server.go @@ -0,0 +1,70 @@ +package service + +import ( + "specialstandard/internal/config" + "specialstandard/internal/errs" + "specialstandard/internal/storage" + "specialstandard/internal/storage/postgres" + + "context" + "net/http" + + go_json "github.com/goccy/go-json" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/compress" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/favicon" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +type App struct { + Server *fiber.App + Repo *storage.Repository +} + +// Initialize the App union type containing a fiber app, a repository, and a climatiq client. +func InitApp(config config.Config) *App { + ctx := context.Background() + repo := postgres.NewRepository(ctx, config.DB) + + app := SetupApp(config, repo) + + return &App{ + Server: app, + Repo: repo, + } +} + +// Setup the fiber app with the specified configuration, database, and climatiq client. +func SetupApp(config config.Config, repo *storage.Repository) *fiber.App { + app := fiber.New(fiber.Config{ + JSONEncoder: go_json.Marshal, + JSONDecoder: go_json.Unmarshal, + ErrorHandler: errs.ErrorHandler, + }) + + app.Use(recover.New()) + app.Use(favicon.New()) + app.Use(compress.New(compress.Config{ + Level: compress.LevelBestSpeed, + })) + + // Use logging middleware + app.Use(logger.New()) + + // Use CORS middleware to configure CORS and handle preflight/OPTIONS requests. + app.Use(cors.New(cors.Config{ + AllowOrigins: "http://localhost:3000,http://localhost:8080", + AllowMethods: "GET,POST,PUT,PATCH,DELETE,OPTIONS", // Using these methods. + AllowHeaders: "Origin, Content-Type, Accept, Authorization", + AllowCredentials: true, // Allow cookies + ExposeHeaders: "Content-Length, X-Request-ID", + })) + + app.Get("/health", func(c *fiber.Ctx) error { + return c.SendStatus(http.StatusOK) + }) + + return app +} diff --git a/backend/internal/storage/postgres/storage.go b/backend/internal/storage/postgres/storage.go new file mode 100644 index 00000000..9599a0dd --- /dev/null +++ b/backend/internal/storage/postgres/storage.go @@ -0,0 +1,43 @@ +package postgres + +import ( + "specialstandard/internal/config" + "specialstandard/internal/storage" + + "context" + "log" + + "github.com/jackc/pgx/v5/pgxpool" +) + +// Establishes a sustained connection to the PostgreSQL database using pooling. +func ConnectDatabase(ctx context.Context, config config.DB) (*pgxpool.Pool, error) { + dbConfig, err := pgxpool.ParseConfig(config.Connection()) + // dbConfig, err := pgxpool.ParseConfig("postgresql://postgres:postgres@host.docker.internal:54322/postgres") + if err != nil { + log.Fatalf("Failed to connect to the database: %v", err) + return nil, err + } + + conn, err := pgxpool.NewWithConfig(ctx, dbConfig) + if err != nil { + return nil, err + } + + err = conn.Ping(ctx) + if err != nil { + return nil, err + } + + log.Print("Connected to database!") + return conn, nil +} + +func NewRepository(ctx context.Context, config config.DB) *storage.Repository { + db, err := ConnectDatabase(ctx, config) + if err != nil { + log.Fatalf("Failed to connect to the database: %v", err) + } + + return storage.NewRepository(db) +} diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go new file mode 100644 index 00000000..2c1b8afd --- /dev/null +++ b/backend/internal/storage/storage.go @@ -0,0 +1,24 @@ +package storage + +import ( + "github.com/jackc/pgx/v5/pgxpool" +) + +type Repository struct { + db *pgxpool.Pool +} + +func (r *Repository) Close() error { + r.db.Close() + return nil +} + +func (r *Repository) GetDB() *pgxpool.Pool { + return r.db +} + +func NewRepository(db *pgxpool.Pool) *Repository { + return &Repository{ + db: db, + } +} diff --git a/backend/internal/supabase/.gitignore b/backend/internal/supabase/.gitignore new file mode 100644 index 00000000..ad9264f0 --- /dev/null +++ b/backend/internal/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/backend/internal/supabase/config.toml b/backend/internal/supabase/config.toml new file mode 100644 index 00000000..e7253590 --- /dev/null +++ b/backend/internal/supabase/config.toml @@ -0,0 +1,335 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "backend" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +[realtime] +enabled = true +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = true +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = true +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = true +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +[edge_runtime] +enabled = true +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/backend/internal/supabase/migrations/20250829211902_db_init.sql b/backend/internal/supabase/migrations/20250829211902_db_init.sql new file mode 100644 index 00000000..d68dedda --- /dev/null +++ b/backend/internal/supabase/migrations/20250829211902_db_init.sql @@ -0,0 +1,11 @@ +-- initial migration to set up sample table +CREATE TABLE IF NOT EXISTS sessions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + therapist_id UUID NOT NULL , + session_date DATE NOT NULL, + start_time TIME, + end_time TIME, + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); \ No newline at end of file diff --git a/backend/internal/supabase/seed.sql b/backend/internal/supabase/seed.sql new file mode 100644 index 00000000..5de739ec --- /dev/null +++ b/backend/internal/supabase/seed.sql @@ -0,0 +1,4 @@ +INSERT INTO sessions (therapist_id, session_date, start_time, end_time, notes) +VALUES + (uuid_generate_v4(), '2023-09-01', '10:00:00', '11:00:00', 'Initial consultation'), + (uuid_generate_v4(), '2023-09-02', '14:00:00', '15:00:00', 'Follow-up session'); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..43e15243 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + ports: + - "3000:3000" + depends_on: + - backend + command: npm run dev + develop: + watch: + - action: sync+restart + path: ./frontend + target: /app + networks: + - app-network + container_name: frontend + + backend: + build: + context: ./backend + ports: + - "8080:8080" + - "10000:10000" + env_file: + - ./backend/.env + command: go run main.go + develop: + watch: + - action: sync+restart + path: ./backend + target: /app + networks: + - app-network + container_name: backend + +networks: + app-network: + driver: bridge diff --git a/docs/schema.md b/docs/schema.md new file mode 100644 index 00000000..bb48d1d4 --- /dev/null +++ b/docs/schema.md @@ -0,0 +1,44 @@ +# + +We are using a Supabase-hosted Postgres database. Our schema is relational and we handle changes by making migration scripts so that we have a changelog of the database schema history. + +The following pathway allows you to make and test schema changes locally via migration script without affecting the shared database until you're ready. More information on local development best practices can be found in the [Supabase docs](https://supabase.com/docs/guides/cli/local-development). **Note that running the DB locally requires Docker to be installed and running.** + +0. Install the Supabase CLI by following directions [here](https://supabase.com/docs/guides/local-development/cli/getting-started?queryGroups=access-method&access-method=postgres&queryGroups=platform&platform=macos). +1. Create a new feature branch off of main to make sure you are up-to-date with any recently-added migration scripts. +2. `cd` into `/backend/internal` +3. Create a new migration script by running `supabase migration new migration-name`. Your migration-name should be concise but descriptive of what's going on! + - Ex. `supabase migration new update_track_add_url` if adding a URL column to the track table. +4. Add your SQL to the auto-generated script file in `/migrations` +5. Add or update the `seed.sql` data to see populated values for your change if relevant + +6. With Docker running, run `supabase start` + - This will take some time on the first run, because the CLI needs to download the Docker images to your local machine. The CLI includes the entire Supabase toolset, and a few additional images that are useful for local development +7. Run `supabase db reset` to apply your changes locally. This might also take some time. If there are any syntax errors with your migration script or `seed.sql` file, they'll be caught here. +8. If applying the db changes goes smoothly, go to to see a local version of the Supabase dashboard, where your sample data will be visible. Feel free to add/update data to test out your new schema and any constraints. + - Anything you do in this local database won't impact our shared instance +9. If you want to test new API functionality against the locally-running database, you can point the server + at this database by going to `specialstandard/backend/internal/storage/postgres/storage.go` and modifying the first line of the ConnectDatabase method to + + ```go + `dbConfig, err := pgxpool.ParseConfig("postgresql://postgres:postgres@127.0.0.1:54322/postgres")` + ``` + + > If your backend is Dockerized, you need to use the following instead: + + ```go + `dbConfig, err := pgxpool.ParseConfig("postgresql://postgres:postgres@host.docker.internal:54322/postgres")` + ``` + + - This is a connection string for the local DB + - Make sure to switch this back to what it was when you make your PR! +10. When done, run `supabase stop` to stop the local instance of the DB. + +Only After script is approved/merged: + +0. If this is your first time pushing to our shared database, you might need to link the supabase-cli to our specific project. Do this via `supabase link --project-ref [PROJECT-REF-VALUE]` + - Our specific project ref can be found in the Supabase UI (look at the string in the URL following `/project/` or slack a TL if you're stuck) + - It will also prompt you for a DB password - slack a TL to get this + - It'll also prompt you to log in to Supabase +1. Run the actual script(s) against the supabase db using `supabase db push` + - This will run the script and add a log to the migrations table. From e2297ab28863c1d6b60ce70c17e8708f20903326 Mon Sep 17 00:00:00 2001 From: adescoteaux1 Date: Fri, 29 Aug 2025 19:33:46 -0400 Subject: [PATCH 2/4] example endpoint and supabase setup --- backend/internal/models/session.go | 18 +++++++++ .../service/handler/session/get_contact.go | 14 +++++++ .../service/handler/session/handler.go | 13 +++++++ backend/internal/service/server.go | 7 ++++ .../storage/postgres/schema/sessions.go | 37 +++++++++++++++++++ backend/internal/storage/storage.go | 14 ++++++- 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 backend/internal/models/session.go create mode 100644 backend/internal/service/handler/session/get_contact.go create mode 100644 backend/internal/service/handler/session/handler.go create mode 100644 backend/internal/storage/postgres/schema/sessions.go diff --git a/backend/internal/models/session.go b/backend/internal/models/session.go new file mode 100644 index 00000000..ddee2fa8 --- /dev/null +++ b/backend/internal/models/session.go @@ -0,0 +1,18 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Session struct { + ID uuid.UUID `json:"id"` + TherapistID uuid.UUID `json:"therapist_id"` + SessionDate time.Time `json:"session_date"` + StartTime *string `json:"start_time"` + EndTime *string `json:"end_time"` + Notes *string `json:"notes"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` +} diff --git a/backend/internal/service/handler/session/get_contact.go b/backend/internal/service/handler/session/get_contact.go new file mode 100644 index 00000000..137d5886 --- /dev/null +++ b/backend/internal/service/handler/session/get_contact.go @@ -0,0 +1,14 @@ +package session + +import ( + "github.com/gofiber/fiber/v2" +) + +func (h *Handler) GetSessions(c *fiber.Ctx) error { + sessions, err := h.sessionRepository.GetSessions(c.Context()) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK).JSON(sessions) +} diff --git a/backend/internal/service/handler/session/handler.go b/backend/internal/service/handler/session/handler.go new file mode 100644 index 00000000..450a3ccf --- /dev/null +++ b/backend/internal/service/handler/session/handler.go @@ -0,0 +1,13 @@ +package session + +import "specialstandard/internal/storage" + +type Handler struct { + sessionRepository storage.SessionRepository +} + +func NewHandler(sessionRepository storage.SessionRepository) *Handler { + return &Handler{ + sessionRepository, + } +} diff --git a/backend/internal/service/server.go b/backend/internal/service/server.go index 7723ecd8..1baa83a4 100644 --- a/backend/internal/service/server.go +++ b/backend/internal/service/server.go @@ -3,6 +3,7 @@ package service import ( "specialstandard/internal/config" "specialstandard/internal/errs" + "specialstandard/internal/service/handler/session" "specialstandard/internal/storage" "specialstandard/internal/storage/postgres" @@ -66,5 +67,11 @@ func SetupApp(config config.Config, repo *storage.Repository) *fiber.App { return c.SendStatus(http.StatusOK) }) + // Setup routes + sessionHandler := session.NewHandler(repo.Session) + app.Route("/sessions", func(r fiber.Router) { + r.Get("/", sessionHandler.GetSessions) + }) + return app } diff --git a/backend/internal/storage/postgres/schema/sessions.go b/backend/internal/storage/postgres/schema/sessions.go new file mode 100644 index 00000000..f716cb2e --- /dev/null +++ b/backend/internal/storage/postgres/schema/sessions.go @@ -0,0 +1,37 @@ +package schema + +import ( + "context" + "specialstandard/internal/models" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type SessionRepository struct { + db *pgxpool.Pool +} + +func (r *SessionRepository) GetSessions(ctx context.Context) ([]models.Session, error) { + query := ` + SELECT id, therapist_id, session_date, start_time, end_time, notes, created_at, updated_at + FROM sessions` + + rows, err := r.db.Query(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + sessions, err := pgx.CollectRows(rows, pgx.RowToStructByName[models.Session]) + if err != nil { + return nil, err + } + return sessions, nil +} + +func NewSessionRepository(db *pgxpool.Pool) *SessionRepository { + return &SessionRepository{ + db, + } +} diff --git a/backend/internal/storage/storage.go b/backend/internal/storage/storage.go index 2c1b8afd..5d9e082a 100644 --- a/backend/internal/storage/storage.go +++ b/backend/internal/storage/storage.go @@ -1,11 +1,20 @@ package storage import ( + "context" + "specialstandard/internal/models" + "specialstandard/internal/storage/postgres/schema" + "github.com/jackc/pgx/v5/pgxpool" ) +type SessionRepository interface { + GetSessions(ctx context.Context) ([]models.Session, error) +} + type Repository struct { - db *pgxpool.Pool + db *pgxpool.Pool + Session SessionRepository } func (r *Repository) Close() error { @@ -19,6 +28,7 @@ func (r *Repository) GetDB() *pgxpool.Pool { func NewRepository(db *pgxpool.Pool) *Repository { return &Repository{ - db: db, + db: db, + Session: schema.NewSessionRepository(db), } } From 1c38a11d67a2499d54340c0db79c66348dae0e8a Mon Sep 17 00:00:00 2001 From: adescoteaux1 Date: Fri, 29 Aug 2025 20:45:13 -0400 Subject: [PATCH 3/4] git ignore --- .gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7feb1638 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.task/ + +.idea/ + +.env +node_modules +.vscode/ + +.DS_STORE + +.next/ \ No newline at end of file From fb6580d5235a0e16e885baaeba288817fce03741 Mon Sep 17 00:00:00 2001 From: adescoteaux1 Date: Fri, 29 Aug 2025 20:46:34 -0400 Subject: [PATCH 4/4] env fix --- .env | 1 - backend/.env | 11 ----------- 2 files changed, 12 deletions(-) delete mode 100644 .env delete mode 100644 backend/.env diff --git a/.env b/.env deleted file mode 100644 index f96ed383..00000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -NEXT_PUBLIC_API_BASE_URL=http://localhost:8080 diff --git a/backend/.env b/backend/.env deleted file mode 100644 index 29b9759b..00000000 --- a/backend/.env +++ /dev/null @@ -1,11 +0,0 @@ -DB_USER=postgres.zvxatxmclijgdkvxdmbc -DB_PASSWORD=dixva5-gemrut-dAcpuz -DB_HOST=aws-1-us-east-1.pooler.supabase.com -DB_PORT=5432 -DB_NAME=postgres - -PORT=8080 - -SUPABASE_URL=https://zvxatxmclijgdkvxdmbc.supabase.co -SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp2eGF0eG1jbGlqZ2RrdnhkbWJjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY0MTM2NDEsImV4cCI6MjA3MTk4OTY0MX0.EHymLjsYov9pankPWHbyrEdwPcz__yf2SMBvMbhP3Eo -SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inp2eGF0eG1jbGlqZ2RrdnhkbWJjIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NjQxMzY0MSwiZXhwIjoyMDcxOTg5NjQxfQ.0F_u8r1upt-fA4pKUrXCNY7_CfNSZuK2BQXTTBM4jeE \ No newline at end of file