Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.git
.gitignore

# Local secrets
.env
.env.local
.env.*
!.env.example
credentials.json
*-creds.json

# Build/test artifacts
bauer
bauer-api
bauer-output/
dist/
*.log
bauer-doc-suggestions.json
bauer-log.json

# Local tooling files
.vscode/
.DS_Store
15 changes: 0 additions & 15 deletions .env

This file was deleted.

15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Google service account credentials
GOOGLE_TYPE=service_account
GOOGLE_PROJECT_ID=your-project-id
GOOGLE_PRIVATE_KEY_ID=your-private-key-id
GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nREPLACE_ME\n-----END PRIVATE KEY-----\n"
GOOGLE_CLIENT_EMAIL=service-account@your-project-id.iam.gserviceaccount.com
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_AUTH_URI=https://accounts.google.com/o/oauth2/auth
GOOGLE_TOKEN_URI=https://oauth2.googleapis.com/token
GOOGLE_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oauth2/v1/certs
GOOGLE_CLIENT_X509_CERT_URL=https://www.googleapis.com/robot/v1/metadata/x509/service-account%40your-project-id.iam.gserviceaccount.com
GOOGLE_UNIVERSE_DOMAIN=googleapis.com

# API basic auth shared secret (used as password)
API_SECRET=replace-with-a-long-random-secret
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ output.json
bauer-doc-suggestions.json
bauer-log.json
*-creds.json
credentials.json
*.log
/.github/copilot-instructions.md
.env.local-e
.env
.env.local
.env.*
!.env.example
# BAU output
bauer-output/
.DS_Store
Expand Down
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM golang:1.24-alpine AS builder

WORKDIR /src

RUN apk add --no-cache ca-certificates git

COPY go.mod go.sum ./
RUN go mod download

COPY cmd ./cmd
COPY internal ./internal

RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/bauer-api ./cmd/app

FROM alpine:3.22

RUN apk add --no-cache ca-certificates

WORKDIR /app
COPY --from=builder /out/bauer-api /app/bauer-api

EXPOSE 8090

ENTRYPOINT ["/app/bauer-api"]
29 changes: 17 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,16 @@ N.B. You need to install [Copilot CLI](https://docs.github.com/en/copilot/how-to
## Configuration
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add docker instruction to the README?


1. Install [Copilot CLI](https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli)
2. Create `credentials.json` file and copy the structure from the [default file](https://github.com/muhammadbassiony/Bauer/blob/main/credentials.json)
3. Get credentials from Google Cloud service or Bitwarden (internally)
4. Fill up `credentials.json` with Google Cloud credentials (see [Generating Google Cloud credentials](https://developers.google.com/workspace/guides/create-credentials)).
5. Share copy document with service account
2. Copy `.env.example` to `.env`
3. Fill `.env` with Google Cloud credentials (see [Generating Google Cloud credentials](https://developers.google.com/workspace/guides/create-credentials))
4. Set `API_SECRET` in `.env` for API basic auth
5. Share copy document with the service account from `GOOGLE_CLIENT_EMAIL`

If you already have `credentials.json`, migrate it with:

```bash
python3 scripts/migrate_credentials_to_env.py --input credentials.json --output .env
```

## Usage

Expand All @@ -43,7 +49,7 @@ N.B. You need to install [Copilot CLI](https://docs.github.com/en/copilot/how-to
4. Run Bauer

```bash
bauer --doc-id <your-document-id> --credentials ./credentials.json
bauer --doc-id <your-document-id>
```

6. Optional parameters
Expand All @@ -61,22 +67,20 @@ bauer --doc-id <your-document-id> --credentials ./credentials.json
#### Basic run

```bash
bauer --doc-id <your-document-id> --credentials ./credentials.json
bauer --doc-id <your-document-id>
```

#### Dry run (test without executing changes)

```bash
bauer --doc-id <your-document-id> \
--credentials ./credentials.json \
--dry-run
```

#### Custom chunk size and output directory

```bash
bauer --doc-id <your-document-id> \
--credentials ./credentials.json \
--chunk-size 5 \
--output-dir ./results
```
Expand All @@ -85,22 +89,19 @@ bauer --doc-id <your-document-id> \

```bash
bauer --doc-id <your-document-id> \
--credentials ./credentials.json \
--model "claude-sonnet-4.5"
```

#### Run on a different repository
```bash
bauer --doc-id <your-document-id> \
--credentials ./credentials.json \
--target-repo ../my-other-repo
```

### Page refresh

```bash
bauer --doc-id <your-document-id> \
--credentials ./credentials.json \
--page-refresh
```

Expand All @@ -117,6 +118,9 @@ task build-api
./bauer-api --config config.json
```

The API requires HTTP basic auth for all endpoints except `/api/v1/health`.
Use username `bauer` and password from `API_SECRET`.

### Endpoints

#### POST /api/v1/job
Expand Down Expand Up @@ -147,6 +151,7 @@ Example:

```bash
curl -X POST http://localhost:8090/api/v1/job \
-u "bauer:${API_SECRET}" \
-H 'Content-Type: application/json' \
-d '{"doc_id":"<google-doc-id>","chunk_size":2,"page_refresh":false}'
```
Expand All @@ -171,7 +176,7 @@ curl http://localhost:8090/api/v1/health

## Steps

1. Modify the [Taskfile](./Taskfile.yml) with your document ID and credentials path for convenience
1. Modify the [Taskfile](./Taskfile.yml) with your document ID for convenience
2. Run the project with task

```
Expand Down
3 changes: 1 addition & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@ tasks:
run:
desc: Run the bau tool locally with specified document ID
vars:
CREDS: "bau-test-creds.json"
DOC_ID: "1WJ-N_Xkkx4r_6knxW7h200oIDyi4mVMzgh1xYt5xaU0"
OUTPUT_DIR: "bauer-output"
MODEL: "gpt-5-mini-high"
cmds:
- go run cmd/bauer/main.go --doc-id "{{.DOC_ID}}" --credentials "{{.CREDS}}" --output-dir "{{.OUTPUT_DIR}}" --model "{{.MODEL}}"
- go run cmd/bauer/main.go --doc-id "{{.DOC_ID}}" --output-dir "{{.OUTPUT_DIR}}" --model "{{.MODEL}}"
# requires:
# vars: [DOC_ID]

Expand Down
33 changes: 33 additions & 0 deletions cmd/app/core/middleware/basic_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package middleware

import (
"crypto/subtle"
"net/http"
)

const apiUser = "bauer"

func APIBasicAuth(next http.Handler, secret string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be easier to add middlewares to handlers that need it than using an if statement?

if r.URL.Path == "/api/v1/health" {
next.ServeHTTP(w, r)
return
}

username, password, ok := r.BasicAuth()
if !ok || !secureEquals(username, apiUser) || !secureEquals(password, secret) {
w.Header().Set("WWW-Authenticate", `Basic realm="bauer-api"`)
http.Error(w, "unauthorized", http.StatusUnauthorized)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for the same of consistency, we can use types.UnAuthorized function.

return
}

next.ServeHTTP(w, r)
})
}

func secureEquals(a, b string) bool {
if len(a) != len(b) {
return false
}
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}
21 changes: 8 additions & 13 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,14 @@ func run() error {
slog.Info("startup", "status", "initializing API")
defer slog.Info("shutdown complete")

orchestrator := orchestrator.NewOrchestrator()
orch := orchestrator.NewOrchestrator()
cfg, err := types.LoadConfig()
if err != nil {
slog.Error("failed to load config", "error", err.Error())
return err
}

if cfg.TargetRepo != "" {
// Convert credentials path to absolute before changing directory
absCredsPath, err := filepath.Abs(cfg.CredentialsPath)
if err != nil {
return fmt.Errorf("failed to resolve credentials path: %w", err)
}
cfg.CredentialsPath = absCredsPath

// Convert output directory to absolute before changing directory
absOutputDir, err := filepath.Abs(cfg.BaseOutputDir)
if err != nil {
Expand All @@ -45,22 +38,24 @@ func run() error {
if err := os.Chdir(cfg.TargetRepo); err != nil {
return fmt.Errorf("failed to change to target repository %q: %w", cfg.TargetRepo, err)
}
cwd, _ := os.Getwd()
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
slog.Info("Working directory", "path", cwd)
}

rc := types.RouteConfig{
APIConfig: *cfg,
Orchestrator: orchestrator,
Orchestrator: orch,
}

mux := http.NewServeMux()
mux.HandleFunc("/api/v1/job", v1.JobPost(rc))
mux.HandleFunc("/api/v1/health", v1.GetHealth)
handler := middleware.RequestTrace(middleware.APIBasicAuth(mux, cfg.APISecret))
slog.Info("starting server", "address", ":8090")
err = http.ListenAndServe(":8090", middleware.RequestTrace(mux))

if err != nil {
if err := http.ListenAndServe(":8090", handler); err != nil {
slog.Error("server error", "error", err.Error())
slog.Info("shutdown complete with errors")
return err
Expand Down
Loading