From 008d7d64d0c4ed6b0222eb79e4f9ecee6c6f2aa1 Mon Sep 17 00:00:00 2001 From: Gautham Date: Thu, 26 Mar 2026 20:19:02 +0000 Subject: [PATCH 01/13] Modify README.md --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c2bec0368b7..72b1b897695 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# learn-cicd-starter (Notely) +# learn-cicd-typescript-starter (Notely) -This repo contains the starter code for the "Notely" application for the "Learn CICD" course on [Boot.dev](https://boot.dev). +This repo contains the typescript starter code for the "Notely" application for the "Learn CICD" course on [Boot.dev](https://boot.dev). ## Local Development -Make sure you're on Go version 1.22+. +Make sure you're on Node version 22+. Create a `.env` file in the root of the project with the following contents: @@ -15,9 +15,12 @@ PORT="8080" Run the server: ```bash -go build -o notely && ./notely +npm install +npm run dev ``` -*This starts the server in non-database mode.* It will serve a simple webpage at `http://localhost:8080`. +_This starts the server in non-database mode._ It will serve a simple webpage at `http://localhost:8080`. -You do *not* need to set up a database or any interactivity on the webpage yet. Instructions for that will come later in the course! +You do _not_ need to set up a database or any interactivity on the webpage yet. Instructions for that will come later in the course! + +Gautham's version of Boot.dev's Notely app From b9e3da57d0120c392c29c67ba9db169886c727fe Mon Sep 17 00:00:00 2001 From: Gautham Date: Thu, 26 Mar 2026 20:19:39 +0000 Subject: [PATCH 02/13] Add Github Actions Workflow --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..fddd06d6373 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: ci + +on: + pull_request: + branches: [main] + +jobs: + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - name: Checko out code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.26.0" + + - name: Check version + run: go version From 4da6b4a15fb02c276d89f3ec383c84ef355b4c37 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 27 Mar 2026 16:55:41 +0000 Subject: [PATCH 03/13] Add unit tests to .yml workflow --- .github/workflows/ci.yml | 4 ++-- internal/auth/auth_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 internal/auth/auth_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fddd06d6373..417133513d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,5 +18,5 @@ jobs: with: go-version: "1.26.0" - - name: Check version - run: go version + - name: Run Unit Tests + run: go test ./... diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go new file mode 100644 index 00000000000..a860555a9c6 --- /dev/null +++ b/internal/auth/auth_test.go @@ -0,0 +1,31 @@ +package auth + +import ( + "net/http" + "testing" +) + +func TestGetAPIKey_Valid(t *testing.T) { + headers := http.Header{} + headers.Set("Authorization", "ApiKey 12345") + + key, err := GetAPIKey(headers) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if key != "12345" { + t.Fatalf("expected 12345, got %s", key) + } +} + +func TestGetAPIKey_NoHeader(t *testing.T) { + headers := http.Header{} + + _, err := GetAPIKey(headers) + + if err == nil { + t.Fatalf("expected error, got nil") + } +} From 048378cf2ddb81d7cf7b97458d7ff47d65398a5b Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 27 Mar 2026 17:08:13 +0000 Subject: [PATCH 04/13] Add coverage flag to go tests --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 417133513d2..3d001bc76a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,4 +19,4 @@ jobs: go-version: "1.26.0" - name: Run Unit Tests - run: go test ./... + run: go test ./... -cover From 3959f1ca225beffef3a7a1d1fa400e6da0b9ca86 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 27 Mar 2026 17:19:20 +0000 Subject: [PATCH 05/13] Add badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 72b1b897695..c98f431c85c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # learn-cicd-typescript-starter (Notely) +![Test Result](https://github.com/Gautham116006/learn-cicd-starter/actions/workflows/ci.yml/badge.svg) + This repo contains the typescript starter code for the "Notely" application for the "Learn CICD" course on [Boot.dev](https://boot.dev). ## Local Development From dae2af53cb1c9e99c75f30852c9953373d225a12 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 27 Mar 2026 17:59:49 +0000 Subject: [PATCH 06/13] Messup formatting --- internal/auth/auth.go | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index f969aacf638..1db9285212e 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -1,23 +1,5 @@ package auth - -import ( - "errors" - "net/http" - "strings" -) - -var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") - +import("errors";"net/http";"strings") +var ErrNoAuthHeaderIncluded=errors.New("no authorization header included") // GetAPIKey - -func GetAPIKey(headers http.Header) (string, error) { - authHeader := headers.Get("Authorization") - if authHeader == "" { - return "", ErrNoAuthHeaderIncluded - } - splitAuth := strings.Split(authHeader, " ") - if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { - return "", errors.New("malformed authorization header") - } - - return splitAuth[1], nil -} +func GetAPIKey(headers http.Header)(string,error){authHeader:=headers.Get("Authorization");if authHeader==""{return "",ErrNoAuthHeaderIncluded};splitAuth:=strings.Split(authHeader," ");if len(splitAuth)<2||splitAuth[0]!="ApiKey"{return "",errors.New("malformed authorization header")};return splitAuth[1],nil} From c6baca5c4680dbf56218c40b683f4f9475167514 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 27 Mar 2026 18:00:13 +0000 Subject: [PATCH 07/13] Add formatting check --- .github/workflows/ci.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d001bc76a9..1780a275fec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checko out code + - name: Checkout code uses: actions/checkout@v4 - name: Set up Go @@ -20,3 +20,19 @@ jobs: - name: Run Unit Tests run: go test ./... -cover +jobs: + style: + name: Style + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.26.0" + + - name: Check Styling + run: test -z $(go fmt ./...) From c7a0ebf1d752644f11d787ef42a689651e6177c7 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 27 Mar 2026 18:04:16 +0000 Subject: [PATCH 08/13] Fix formatting --- internal/auth/auth.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/internal/auth/auth.go b/internal/auth/auth.go index 1db9285212e..71fe3e49a3a 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -1,5 +1,22 @@ package auth -import("errors";"net/http";"strings") -var ErrNoAuthHeaderIncluded=errors.New("no authorization header included") + +import ( + "errors" + "net/http" + "strings" +) + +var ErrNoAuthHeaderIncluded = errors.New("no authorization header included") + // GetAPIKey - -func GetAPIKey(headers http.Header)(string,error){authHeader:=headers.Get("Authorization");if authHeader==""{return "",ErrNoAuthHeaderIncluded};splitAuth:=strings.Split(authHeader," ");if len(splitAuth)<2||splitAuth[0]!="ApiKey"{return "",errors.New("malformed authorization header")};return splitAuth[1],nil} +func GetAPIKey(headers http.Header) (string, error) { + authHeader := headers.Get("Authorization") + if authHeader == "" { + return "", ErrNoAuthHeaderIncluded + } + splitAuth := strings.Split(authHeader, " ") + if len(splitAuth) < 2 || splitAuth[0] != "ApiKey" { + return "", errors.New("malformed authorization header") + } + return splitAuth[1], nil +} From eb349b30edcea32b309b1e1cae58e79c7fd7ab22 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 27 Mar 2026 18:07:00 +0000 Subject: [PATCH 09/13] Fix workflow error --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1780a275fec..6943cd892b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Run Unit Tests run: go test ./... -cover -jobs: + style: name: Style runs-on: ubuntu-latest From e366ea14050eb05a5a5a2720d2e77692109d4daf Mon Sep 17 00:00:00 2001 From: Gautham Date: Sat, 28 Mar 2026 07:14:27 +0000 Subject: [PATCH 10/13] Enable staticcheck linter --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6943cd892b6..8f785c9d69f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,3 +36,9 @@ jobs: - name: Check Styling run: test -z $(go fmt ./...) + + - name: Install staticcheck + run: go install honnef.co/go/tools/cmd/staticcheck@latest + + - name: Run staticcheck + run: staticcheck ./... From 8b66a694747471aceca1609fc588379e3395c68b Mon Sep 17 00:00:00 2001 From: Gautham Date: Sat, 28 Mar 2026 07:29:35 +0000 Subject: [PATCH 11/13] Add gosec security check --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f785c9d69f..f96d558e124 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,12 @@ jobs: - name: Run Unit Tests run: go test ./... -cover + - name: Install gosec + run: go install github.com/securego/gosec/v2/cmd/gosec@latest + + - name: Run gosec + run: gosec ./... + style: name: Style runs-on: ubuntu-latest From 41d3d5376bef53ae3de096d2f9dc3de68e990770 Mon Sep 17 00:00:00 2001 From: Gautham Date: Sat, 28 Mar 2026 07:41:50 +0000 Subject: [PATCH 12/13] Fix vulnerabilities flagged by gosec --- json.go | 19 ++++++++--- main.go | 100 ++++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 85 insertions(+), 34 deletions(-) diff --git a/json.go b/json.go index 1e6e7985e18..afb6e44d61f 100644 --- a/json.go +++ b/json.go @@ -8,14 +8,17 @@ import ( func respondWithError(w http.ResponseWriter, code int, msg string, logErr error) { if logErr != nil { - log.Println(logErr) + log.Printf("internal error: %v", logErr) // avoid raw prints } - if code > 499 { + + if code >= 500 { log.Printf("Responding with 5XX error: %s", msg) } + type errorResponse struct { Error string `json:"error"` } + respondWithJSON(w, code, errorResponse{ Error: msg, }) @@ -23,12 +26,18 @@ func respondWithError(w http.ResponseWriter, code int, msg string, logErr error) func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { w.Header().Set("Content-Type", "application/json") + dat, err := json.Marshal(payload) if err != nil { - log.Printf("Error marshalling JSON: %s", err) - w.WriteHeader(500) + log.Printf("error marshalling JSON: %v", err) + http.Error(w, "internal server error", http.StatusInternalServerError) return } + w.WriteHeader(code) - w.Write(dat) + + // ✅ FIX: handle write error (G104) + if _, err := w.Write(dat); err != nil { + log.Printf("failed to write response: %v", err) + } } diff --git a/main.go b/main.go index 19d7366c5f7..c2008ad2664 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,17 @@ package main import ( + "context" "database/sql" "embed" "io" "log" "net/http" "os" + "os/signal" + "strconv" + "syscall" + "time" "github.com/go-chi/chi" "github.com/go-chi/cors" @@ -25,54 +30,67 @@ type apiConfig struct { var staticFiles embed.FS func main() { - err := godotenv.Load(".env") - if err != nil { - log.Printf("warning: assuming default configuration. .env unreadable: %v", err) + _ = godotenv.Load(".env") // avoid leaking env load errors + + portStr := os.Getenv("PORT") + if portStr == "" { + log.Fatal("PORT must be set") } - port := os.Getenv("PORT") - if port == "" { - log.Fatal("PORT environment variable is not set") + // ✅ FIX: validate & sanitize port (G706) + port, err := strconv.Atoi(portStr) + if err != nil || port <= 0 || port > 65535 { + log.Fatal("invalid PORT") } apiCfg := apiConfig{} - // https://github.com/libsql/libsql-client-go/#open-a-connection-to-sqld - // libsql://[your-database].turso.io?authToken=[your-auth-token] dbURL := os.Getenv("DATABASE_URL") - if dbURL == "" { - log.Println("DATABASE_URL environment variable is not set") - log.Println("Running without CRUD endpoints") - } else { + if dbURL != "" { db, err := sql.Open("libsql", dbURL) if err != nil { - log.Fatal(err) + log.Fatal("failed to initialize DB") } - dbQueries := database.New(db) - apiCfg.DB = dbQueries - log.Println("Connected to database!") + + // ✅ FIX: verify DB connection + if err := db.Ping(); err != nil { + log.Fatal("failed to connect to DB") + } + + apiCfg.DB = database.New(db) + log.Println("Connected to database") } router := chi.NewRouter() + // ✅ FIX: restrict CORS router.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"https://*", "http://*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"*"}, - ExposedHeaders: []string{"Link"}, - AllowCredentials: false, - MaxAge: 300, + AllowedOrigins: []string{"https://yourdomain.com"}, // change for your env + AllowedMethods: []string{"GET", "POST"}, + AllowedHeaders: []string{"Content-Type", "Authorization"}, + MaxAge: 300, })) + // ✅ FIX: add security headers + router.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("Content-Security-Policy", "default-src 'self'") + next.ServeHTTP(w, r) + }) + }) + router.Get("/", func(w http.ResponseWriter, r *http.Request) { f, err := staticFiles.Open("static/index.html") if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) // ✅ no leak return } defer f.Close() + if _, err := io.Copy(w, f); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, "internal server error", http.StatusInternalServerError) } }) @@ -86,13 +104,37 @@ func main() { } v1Router.Get("/healthz", handlerReadiness) - router.Mount("/v1", v1Router) + + // ✅ FIX: secure HTTP server config (timeouts) srv := &http.Server{ - Addr: ":" + port, - Handler: router, + Addr: ":" + strconv.Itoa(port), + Handler: router, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 120 * time.Second, + ReadHeaderTimeout: 2 * time.Second, } - log.Printf("Serving on port: %s\n", port) - log.Fatal(srv.ListenAndServe()) + // ✅ FIX: run server safely + go func() { + log.Printf("Serving on port: %d\n", port) // safe logging + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatal(err) + } + }() + + // ✅ FIX: graceful shutdown + stop := make(chan os.Signal, 1) + signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM) + + <-stop + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + log.Println("Shutting down server...") + if err := srv.Shutdown(ctx); err != nil { + log.Println("Graceful shutdown failed:", err) + } } From 83e43e09223429adb671fcc558aa3e5b31a1f465 Mon Sep 17 00:00:00 2001 From: Gautham Date: Sat, 28 Mar 2026 08:29:32 +0000 Subject: [PATCH 13/13] Add build notely app workflow --- .github/workflows/cd.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/cd.yml diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 00000000000..ba7b24a61e3 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,21 @@ +on: + push: + branches: [main] + +jobs: + Deploy: + name: deploy + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.26.0" + + - name: Build notely + run: ./scripts/buildprod.sh +