From 4957a8b18c85c081fe1ec2df7b551c423d18f495 Mon Sep 17 00:00:00 2001 From: Arkady Buryakov Date: Mon, 11 Aug 2025 17:17:01 +0300 Subject: [PATCH 1/5] chore: Fixed middleware test import --- api/middleware/middleware_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/middleware/middleware_test.go b/api/middleware/middleware_test.go index a425eb6..319b381 100644 --- a/api/middleware/middleware_test.go +++ b/api/middleware/middleware_test.go @@ -6,8 +6,8 @@ import ( "net/http/httptest" "os" "testing" - "time" + "github.com/dreamsofcode-io/authly/api/middleware" "github.com/stretchr/testify/assert" ) @@ -23,5 +23,4 @@ func TestLogging(t *testing.T) { testHandler.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) - assert.Greater(t, time.Since(req.Context().Value("startTime").(time.Time)), 0) } From a951bcacb0e85387c6135c38742035111ea00757 Mon Sep 17 00:00:00 2001 From: Arkady Buryakov Date: Mon, 11 Aug 2025 17:18:52 +0300 Subject: [PATCH 2/5] chore: Used type narrowing to suppress TS alerts on API response handling --- app/src/components/go-api-test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/components/go-api-test.tsx b/app/src/components/go-api-test.tsx index ad38ccd..c6c146c 100644 --- a/app/src/components/go-api-test.tsx +++ b/app/src/components/go-api-test.tsx @@ -17,10 +17,10 @@ export function GoAPITest() { const response = await fetch("/api/me") .then(x => x.json()) - .then(x => ({data: x})) - .catch(e => ({error: e})) + .then(x => ({ data: x })) + .catch(e => ({ error: e })) - if (response.error) { + if ('error' in response) { setApiResponse(`❌ Go API Error: ${response.error}`) } else { setApiResponse(`✅ Go API Success: ${JSON.stringify(response.data, null, 2)}`) From 1d342dca166a32f3b8ebc4d6e1d2597b2527abab Mon Sep 17 00:00:00 2001 From: Arkady Buryakov Date: Mon, 11 Aug 2025 17:19:26 +0300 Subject: [PATCH 3/5] fix: added missing migration --- app/src/db/migrations/0001_flawless_klaw.sql | 6 + app/src/db/migrations/meta/0001_snapshot.json | 356 ++++++++++++++++++ app/src/db/migrations/meta/_journal.json | 7 + 3 files changed, 369 insertions(+) create mode 100644 app/src/db/migrations/0001_flawless_klaw.sql create mode 100644 app/src/db/migrations/meta/0001_snapshot.json diff --git a/app/src/db/migrations/0001_flawless_klaw.sql b/app/src/db/migrations/0001_flawless_klaw.sql new file mode 100644 index 0000000..fa5ad24 --- /dev/null +++ b/app/src/db/migrations/0001_flawless_klaw.sql @@ -0,0 +1,6 @@ +CREATE TABLE "jwks" ( + "id" text PRIMARY KEY NOT NULL, + "public_key" text NOT NULL, + "private_key" text NOT NULL, + "created_at" timestamp NOT NULL +); diff --git a/app/src/db/migrations/meta/0001_snapshot.json b/app/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..a886f85 --- /dev/null +++ b/app/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,356 @@ +{ + "id": "e83a9242-df30-4a4d-93b0-3d523b36eea7", + "prevId": "4339308a-53a8-4a3d-aa2c-cc3c3f11efad", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/app/src/db/migrations/meta/_journal.json b/app/src/db/migrations/meta/_journal.json index adbcf20..bac6bc8 100644 --- a/app/src/db/migrations/meta/_journal.json +++ b/app/src/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1752165095860, "tag": "0000_nice_bishop", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1754908977054, + "tag": "0001_flawless_klaw", + "breakpoints": true } ] } \ No newline at end of file From d85642ddc6a9dcb5ccde2dcea41f947a6406bbe7 Mon Sep 17 00:00:00 2001 From: Arkady Buryakov Date: Mon, 11 Aug 2025 17:37:35 +0300 Subject: [PATCH 4/5] chore: Updated documentation to be more clear --- api/README.md | 2 +- app/README.md | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/api/README.md b/api/README.md index b0bd8a4..2f7fd2f 100644 --- a/api/README.md +++ b/api/README.md @@ -31,7 +31,7 @@ go mod download Start the server: ```bash -go run main.go +go run ./ ``` The API will start on port 8080 by default. diff --git a/app/README.md b/app/README.md index e215bc4..c723ae1 100644 --- a/app/README.md +++ b/app/README.md @@ -1,8 +1,28 @@ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +## Prerequisites + +- PostgreSQL database running +- DATABASE_URL configured in app/.env file + ``` + DATABASE_URL="postgresql://username:password@localhost:5432/database_name" + ``` + ## Getting Started -First, run the development server: +First, run the database migration: + +```bash +npm run db:migrate +# or +yarn db:migrate +# or +pnpm db:migrate +# or +bun run db:migrate +``` + +Then, run the development server: ```bash npm run dev From 96aa9529b6defd8b2a66489fd6e77007e290e5bd Mon Sep 17 00:00:00 2001 From: Arkady Buryakov Date: Mon, 11 Aug 2025 18:22:04 +0300 Subject: [PATCH 5/5] feat: Added docker compose with automated migrations and hot reloads --- README.md | 17 +++++++++++++++++ api/.air.toml | 44 ++++++++++++++++++++++++++++++++++++++++++++ api/Dockerfile | 20 ++++++++++++++++++++ api/auth/auth.go | 8 +++++++- app/Dockerfile | 22 ++++++++++++++++++++++ app/entrypoint.sh | 9 +++++++++ docker-compose.yml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 api/.air.toml create mode 100644 api/Dockerfile create mode 100644 app/Dockerfile create mode 100644 app/entrypoint.sh create mode 100644 docker-compose.yml diff --git a/README.md b/README.md index 6f62f2f..e0fa1f7 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,27 @@ A Go service that: ## Getting Started +### Option 1: Manual Setup + Check the individual README files in each project directory for specific setup instructions: - [Frontend Setup](/app/README.md) - [Backend Setup](/api/README.md) +### Option 2: Docker Compose + +Run all services with Docker Compose: + +```bash +docker-compose up -d +``` + +This will start: +- PostgreSQL database on port 5432 +- Next.js app on port 3000 (with automatic database migration) +- Go API on port 8080 + +Both app and API services support hot reload for development. + ## Video Tutorial This project accompanies a tutorial video by Dreams of Code explaining the implementation details and best practices for integrating Better-Auth with Go. \ No newline at end of file diff --git a/api/.air.toml b/api/.air.toml new file mode 100644 index 0000000..437281c --- /dev/null +++ b/api/.air.toml @@ -0,0 +1,44 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_root = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..c9c9a47 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.24.2-alpine + +WORKDIR /app + +# Install air for hot reload +RUN go install github.com/air-verse/air@latest + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +EXPOSE 8080 + +# Use air for hot reload +CMD ["air", "-c", ".air.toml"] \ No newline at end of file diff --git a/api/auth/auth.go b/api/auth/auth.go index e0574fa..30ae450 100644 --- a/api/auth/auth.go +++ b/api/auth/auth.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "net/http" + "os" "github.com/lestrrat-go/jwx/v3/jwk" "github.com/lestrrat-go/jwx/v3/jwt" @@ -20,7 +21,12 @@ var ( ) func UserFromRequest(r *http.Request) (User, error) { - keyset, err := jwk.Fetch(r.Context(), "http://localhost:3000/api/auth/jwks") + authAPIURL := os.Getenv("AUTH_API_URL") + if authAPIURL == "" { + authAPIURL = "http://localhost:3000/api/auth" + } + + keyset, err := jwk.Fetch(r.Context(), authAPIURL+"/jwks") if err != nil { return User{}, fmt.Errorf("fetch jwks: %w", err) } diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..c66ceff --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,22 @@ +FROM oven/bun:1-alpine + +WORKDIR /app + +# Install build dependencies for native modules +RUN apk add --no-cache python3 make g++ + +# Copy package files +COPY package.json bun.lockb* ./ + +# Install dependencies +RUN bun install + +# Copy source code +COPY . . + +# Make entrypoint executable +RUN chmod +x /app/entrypoint.sh + +EXPOSE 3000 + +CMD ["/app/entrypoint.sh"] \ No newline at end of file diff --git a/app/entrypoint.sh b/app/entrypoint.sh new file mode 100644 index 0000000..db0944e --- /dev/null +++ b/app/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +echo "Running database migration..." +bun run db:migrate + +echo "Starting development server..." +bun run dev + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b4e4cc6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +services: + postgres: + image: postgres:15 + environment: + POSTGRES_USER: authly_user + POSTGRES_PASSWORD: authly_password + POSTGRES_DB: authly_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U authly_user -d authly_db"] + interval: 5s + timeout: 5s + retries: 5 + + app: + build: ./app + ports: + - "3000:3000" + environment: + DATABASE_URL: "postgresql://authly_user:authly_password@postgres:5432/authly_db" + GO_API_URL: "http://api:8080" + volumes: + - ./app:/app + - /app/node_modules + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + + api: + build: ./api + ports: + - "8080:8080" + environment: + AUTH_API_URL: "http://app:3000/api/auth" + volumes: + - ./api:/app + restart: unless-stopped + +volumes: + postgres_data: +