From 729080928c3ee93639fd8967a18a4800e5d8d45b Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 20 Jun 2025 10:10:41 +0800 Subject: [PATCH 01/24] add fixtures --- storage/fixture/experience.json | 96 +++++++++++++++++++++++++++++++++ storage/fixture/profile.json | 10 ++++ storage/fixture/projects.json | 95 ++++++++++++++++++++++++++++++++ storage/fixture/social.json | 40 ++++++++++++++ storage/fixture/talks.json | 35 ++++++++++++ 5 files changed, 276 insertions(+) create mode 100644 storage/fixture/experience.json create mode 100644 storage/fixture/profile.json create mode 100644 storage/fixture/projects.json create mode 100644 storage/fixture/social.json create mode 100644 storage/fixture/talks.json diff --git a/storage/fixture/experience.json b/storage/fixture/experience.json new file mode 100644 index 00000000..a4cfb8ff --- /dev/null +++ b/storage/fixture/experience.json @@ -0,0 +1,96 @@ +{ + "version": "1.0.0", + "date": [ + { + "uuid": "c17a68bc-8832-4d44-b2ed-f9587cf14cd1", + "company": "Perx Technologies", + "employment_type": "Full-Time", + "location_type": "On-Site", + "position": "Head of Engineering", + "start_date": "June, 2024", + "end_date": "April, 2025", + "summary": "Led and integrated cross-functional engineering teams (DevOps, Infrastructure, Data, Frontend, Backend, Support) across time zones, fostering open communication and accountability. Scaled team growth and operations from Singapore, optimized performance (database queries from 3 s to 800 ms; API calls from 2 s to 100 ms), implemented cloud cost savings, and partnered with C-level leaders to expand engineering initiatives.", + "country": "Singapore", + "city": "Singapore", + "skills": "Executive Leadership, Strategic Planning, Engineering Management, Cross-functional Team Leadership, Technical Architecture." + }, + { + "uuid": "99db1ca0-948e-40b1-984f-e3b157a5d336", + "company": "Aspire", + "employment_type": "Full-Time", + "location_type": "On-Site", + "position": "Senior Software Engineer & Manager", + "start_date": "January, 2022", + "end_date": "April, 2024", + "summary": "Led a 12-person APAC team overseeing the software development lifecycle, mentorship, technical direction, and system architecture design. Engineered critical financial systems—prioritized payment request queues and automated credit schemas—and spearheaded SEA wallets from architecture through integration, unifying payment workflows and ledger synchronization. Improved debit account balance queries for real-time access and boosted API response times. Resolved data inconsistencies, refactored code for reliability, designed flexible scheduled payment solutions, and directed the transition from a monolithic to microservices architecture, significantly enhancing platform scalability and maintainability.", + "country": "Singapore", + "city": "Singapore", + "skills": "Leadership, Strategic Planning, Engineering Management, Cross-functional Team Leadership, Technical Architecture." + }, + { + "uuid": "01e33400-6957-4d16-8edb-0802a49e445e", + "company": "BeMyGuest - Tours & Activities", + "employment_type": "Full-Time", + "location_type": "On-Site", + "position": "Engineering Lead", + "start_date": "September, 2017", + "end_date": "November, 2021", + "summary": "Developed and maintained inventory systems with availability calculations and time-slot capacity management. Led SaaS platform development for auto-recurring subscription payments and invoicing. Owned integration of Adyen, Stripe, and PayPal gateways for new white-label e-commerce accounts, and implemented third-party booking supplier APIs across B2B, B2C, and white-label channels, supporting mission-critical operations in Southeast Asian markets.", + "country": "Singapore", + "city": "Singapore", + "skills": "Leadership, Strategic Planning, Cross-functional Team Leadership, Engineering Management, Technical Architecture." + }, + { + "uuid": "1ba5d878-3c48-4d94-aded-4a4294c26e12", + "company": "Freelance", + "employment_type": "Contractor", + "location_type": "Remote", + "position": "Web Developer", + "start_date": "June, 2014", + "end_date": "September, 2017", + "summary": "Built diverse web applications for SMEs—including e-commerce, POS, medical history, and neighborhood feedback platforms—using PHP, Laravel, VueJS, and MySQL. I also designed and delivered a multi-city drop-shipment warehouse management system, enabling real-time inventory control linked to financial reporting and distribution across multiple locations.", + "country": "United States", + "city": "Oklahoma City", + "skills": "Leadership, Strategic Planning, Strategy Alignment, Cross-functional Team Leadership, Complexity Management" + }, + { + "uuid": "8501d986-144d-4f4d-bd3f-7fb066028142", + "company": "Websarrollo", + "employment_type": "Full-Time", + "location_type": "On-Site", + "position": "Founder & Software Engineer", + "start_date": "February, 2011", + "end_date": "May, 2014", + "summary": "Led a team of designers and PHP developers, managing nationwide client projects and overseeing the full app development lifecycle—including iOS/Android social networking apps. I built CMS, shipping-tracking, e-commerce, web portfolio, college enrollment, and university survey systems, plus a City Hall Administrative System covering accounts payable, HR, payroll, treasury, and tax modules. My work leveraged PHP, jQuery (and jQuery Mobile), Cordova-JS, MySQL, HTML5, AngularJS, and Laravel 5, integrating third-party APIs and Facebook/Twitter logins within a SCRUM framework.", + "country": "Venezuela", + "city": "Valencia", + "skills": "Leadership, Strategic Planning, Strategy Alignment, Team Development, Complexity Management." + }, + { + "uuid": "82076e4e-6099-457f-8ed5-b10585125ce5", + "company": "Encava", + "employment_type": "Full-Time", + "location_type": "On-Site", + "position": "Web Developer", + "start_date": "May, 2009", + "end_date": "February, 2011", + "summary": "Maintained the company’s AS400 administrative system and spearheaded development of department-specific applications—an e-commerce inventory control for retail, web reporting for production-line quality control, an online appointment system for the medical department, and a visitor registration/management tool. I leveraged PHP, jQuery, MySQL, HTML5, and AS400 within a SCRUM framework.", + "country": "Venezuela", + "city": "Valencia", + "skills": "Creative Problem Solving, Analytical Skills, Strategy Alignment, Strategic Planning, Complexity Management." + }, + { + "uuid": "d8c3957c-99dd-401a-9d95-f2b6b1dc021a", + "company": "Forja Centro", + "employment_type": "Full-Time", + "location_type": "On-Site", + "position": "Web Developer", + "start_date": "March, 2008", + "end_date": "April, 2009", + "summary": "Maintained a Visual Basic administrative system and built internal applications to streamline operations—mail management, mechanical design support, sales-report automation, and web-based customer invoicing. I trained staff on these tools and provided technical support for Windows 8 and PC servers using PHP, jQuery, MySQL, HTML, and SQL Server.", + "country": "Venezuela", + "city": "Valencia", + "skills": "Creative Problem Solving, Analytical Skills, Strategy Alignment, Strategic Planning, Complexity Management." + } + ] +} diff --git a/storage/fixture/profile.json b/storage/fixture/profile.json new file mode 100644 index 00000000..b134b187 --- /dev/null +++ b/storage/fixture/profile.json @@ -0,0 +1,10 @@ +{ + "version": "1.0.0", + "data": { + "nickname": "gus", + "handle": "gocanto", + "name": "Gustavo Ocanto", + "email": "otnacog@example.com", + "profession": "Software Engineer" + } +} diff --git a/storage/fixture/projects.json b/storage/fixture/projects.json new file mode 100644 index 00000000..ff5756de --- /dev/null +++ b/storage/fixture/projects.json @@ -0,0 +1,95 @@ +{ + "version": "1.0.0", + "data": [ + { + "uuid": "00a0a12e-6af0-4f5a-b96d-3c95cc7c365c", + "language": "PHP / Vue", + "title": "Think of your energy as an invisible compass.", + "excerpt": "After experiencing the highs and lows of going into business with a family member, I reached a significant turning point in my life.", + "url": "https://github.com/aurachakra", + "created_at": "2023-02-25", + "updated_at": "2023-10-05" + }, + { + "uuid": "00a0a12e-6af0-4f5a-b96d-3c95cc7c365c", + "language": "Vue / TypeScript", + "title": "Gus's personal website.", + "excerpt": "Gus is a full-stack Software Engineer who has been building web technologies for more two decades.", + "url": "https://github.com/gocantodev/client", + "created_at": "2021-11-03", + "updated_at": "2024-09-29" + }, + { + "uuid": "dc67854e-c8bd-4461-baba-8972bee7bfb5", + "language": "GO", + "title": "users-grpc-service", + "excerpt": "users server & client communications service.", + "url": "https://github.com/gocanto/users-grpc-service", + "created_at": "2022-04-17", + "updated_at": "2025-04-22" + }, + { + "uuid": "32fd43ce-d957-4ad2-9d71-b57f71444f2a", + "language": "PHP", + "title": "laravel-simple-pdf", + "excerpt": "Simple laravel PDF generator.", + "url": "https://github.com/gocanto/laravel-simple-pdf", + "created_at": "2019-06-11", + "updated_at": "2020-12-26" + }, + { + "uuid": "b48d8098-962b-4ff9-884e-264ab33256c9", + "language": "Vue / JS", + "title": "vuemit", + "excerpt": "The smallest Vue.js events handler.", + "url": "https://github.com/gocanto/vuemit", + "created_at": "2017-02-01", + "updated_at": "2021-08-11" + }, + { + "uuid": "19acd1d7-80ca-4828-88da-d3641f8d05e1", + "language": "Vue / JS", + "title": "google-autocomplete", + "excerpt": "Google Autocomplete Vue Component.", + "url": "https://github.com/gocanto/google-autocomplete", + "created_at": "2016-07-02", + "updated_at": "2021-08-11" + }, + { + "uuid": "98b5d71a-1c78-4639-a9ed-343a8ba8c328", + "language": "GO", + "title": "converter-go", + "excerpt": "Currency converter that's data-agnostic.", + "url": "https://github.com/gocanto/go-converter", + "created_at": "2021-09-02", + "updated_at": "2021-10-11" + }, + { + "uuid": "3ce8b01f-406a-474c-80f3-8426617b42fe", + "language": "PHP", + "title": "http-client", + "excerpt": "Http client that handles retries, logging & dynamic headers.", + "url": "https://github.com/gocanto/http-client", + "created_at": "2019-07-01", + "updated_at": "2022-12-22" + }, + { + "uuid": "e517a966-f7d0-46a1-9ee4-494b38a116e5", + "language": "PHP", + "title": "converter", + "excerpt": "Immutable PHP currency converter that's data-agnostic.", + "url": "https://github.com/gocanto/converter", + "created_at": "2019-06-07", + "updated_at": "2019-06-11" + }, + { + "uuid": "928ac7e8-d0ba-4075-9c22-67050ab03755", + "language": "PHP", + "title": "Laravel Framework", + "excerpt": "Contributions to the Laravel Framework.", + "url": "https://github.com/laravel/framework/pulls?q=is%3Apr+is%3Aclosed+author%3Agocanto", + "created_at": "2017-07-06", + "updated_at": "2022-09-15" + } + ] +} diff --git a/storage/fixture/social.json b/storage/fixture/social.json new file mode 100644 index 00000000..2493de44 --- /dev/null +++ b/storage/fixture/social.json @@ -0,0 +1,40 @@ +{ + "version": "1.0.0", + "data": [ + { + "uuid": "a8a6d3a0-4a8d-4a1f-8a48-3c3b5b6f3a6e", + "handle": "@gocanto", + "url": "https://x.com/gocanto", + "description": "Follow me in X.", + "name": "x" + }, + { + "uuid": "f4b1b3e1-7b3b-4c1e-9e7b-9c6d3b5a2e1a", + "handle": "gocanto", + "url": "https://www.youtube.com/@gocanto", + "description": "Subscribe to my YouTube channel.", + "name": "youtube" + }, + { + "uuid": "c3e2a1b4-9c8d-4f3e-a2b1-1b3c4d5e6f7a", + "handle": "gocanto", + "url": "https://www.instagram.com/gocanto", + "description": "Follow me in Instagram.", + "name": "instagram" + }, + { + "uuid": "d1e9c8b2-3a4d-4e5f-b1a2-c3d4e5f6a7b8", + "handle": "gocanto", + "url": "https://www.linkedin.com/in/gocanto/", + "description": "Follow me in LinkedIn.", + "name": "linkedin" + }, + { + "uuid": "b2a1c3d4-e5f6-4a7b-8c9d-1a2b3c4d5e6f", + "handle": "gocanto", + "url": "https://github.com/gocanto", + "description": "Follow me in GitHub.", + "name": "github" + } + ] +} diff --git a/storage/fixture/talks.json b/storage/fixture/talks.json new file mode 100644 index 00000000..3b1eb3b1 --- /dev/null +++ b/storage/fixture/talks.json @@ -0,0 +1,35 @@ +{ + "version": "1.0.0", + "data": [ + { + "uuid": "b222d84c-5bbe-4c21-8ba8-a9baa7e5eaa9", + "title": "Deprecating APIs in production environments.", + "subject": "PHP APIs", + "location": "Singapore", + "url": "https://engineers.sg/v/3204", + "photo": "talks/003.jpg", + "created_at": "2019-02-11", + "updated_at": "2019-02-11" + }, + { + "uuid": "249c50ad-2fd8-45af-a429-5e25d05a6bdd", + "title": "Bootstrapping to objects to control 3rd party integrations.", + "subject": "Systems design patters and conventions.", + "location": "Singapore", + "url": "https://engineers.sg/v/3052", + "photo": "talks/002.jpg", + "created_at": "2018-12-04", + "updated_at": "2018-12-04" + }, + { + "uuid": "36c88e42-b04d-4be1-a183-c53439468769", + "title": "Restful controllers in Laravel to stay lean at the HTTP layer.", + "subject": "Actions abstractions in Laravel controllers.", + "location": "Singapore", + "url": "https://engineers.sg/v/2907", + "photo": "talks/001.jpg", + "created_at": "2018-10-04", + "updated_at": "2018-10-04" + } + ] +} From 58770480b548c4f2ab8c9cf1b6e90197af53a178 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 20 Jun 2025 17:13:54 +0800 Subject: [PATCH 02/24] start working on endpoints --- main.go | 127 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 34 deletions(-) diff --git a/main.go b/main.go index 7a343890..7124e63a 100644 --- a/main.go +++ b/main.go @@ -1,50 +1,109 @@ package main import ( - _ "github.com/lib/pq" - "github.com/oullin/boost" - "github.com/oullin/env" - "github.com/oullin/pkg" - "log/slog" - "net/http" + "encoding/json" + _ "github.com/lib/pq" + "log" + "net/http" + "os" ) -var environment *env.Environment -var validator *pkg.Validator +const file = "./storage/fixture/profile.json" -func init() { - secrets, validate := boost.Spark("./.env") +// ErrorResponse defines the structure for a JSON error response object. +type ErrorResponse struct { + Error string `json:"error"` +} - environment = secrets - validator = validate +// apiError represents an application-level error, including an HTTP status code. +type apiError struct { + Message string + Status int } -func main() { - dbConnection := boost.MakeDbConnection(environment) - logs := boost.MakeLogs(environment) - localSentry := boost.MakeSentry(environment) +// Error makes apiError satisfy the standard error interface. +func (e *apiError) Error() string { + return e.Message +} + +// apiHandler is a custom type for handlers that return an apiError. +type apiHandler func(http.ResponseWriter, *http.Request) *apiError + +// makeApiHandler is a wrapper that converts our custom apiHandler into a standard +// http.HandlerFunc. It centrally handles turning an apiError into a JSON response. +func makeApiHandler(fn apiHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := fn(w, r); err != nil { + log.Printf("API Error: %s, Status: %d", err.Message, err.Status) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.Status) + resp := ErrorResponse{Error: err.Message} + if jsonErr := json.NewEncoder(w).Encode(resp); jsonErr != nil { + log.Printf("Could not encode error response: %v", jsonErr) + } + } + } +} - defer (*logs).Close() - defer (*dbConnection).Close() +// authMiddleware now correctly wraps an apiHandler and returns an apiHandler. +// This allows it to return a structured apiError, integrating it cleanly +// into our unified error handling pattern. +func authMiddleware(next apiHandler) apiHandler { + return func(w http.ResponseWriter, r *http.Request) *apiError { + username := r.Header.Get("X-Username") + if username != "gocanto" { + log.Printf("Unauthorized access attempt by user: '%s'", username) + // Return a structured error instead of writing directly to the response. + return &apiError{ + Message: "Unauthorized", + Status: http.StatusUnauthorized, + } + } - mux := http.NewServeMux() + log.Println("Successfully authenticated user: gocanto") + // If auth succeeds, call the next handler in the chain. + return next(w, r) + } +} + +// handleGetUser returns an *apiError on failure and nil on success. +func handleGetUser(w http.ResponseWriter, r *http.Request) *apiError { + jsonBytes, err := os.ReadFile(file) + if err != nil { + log.Printf("Error reading profile file: %v", err) + return &apiError{ + Message: "Internal Server Error: could not read profile data", + Status: http.StatusInternalServerError, + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(jsonBytes) + if err != nil { + log.Printf("Error writing response: %v", err) + } - app := boost.MakeApp(mux, &boost.App{ - Validator: validator, - Logs: logs, - DbConnection: dbConnection, - Env: environment, - Mux: mux, - Sentry: localSentry, - }) + return nil // A nil return indicates success. +} + +func main() { + // Create a new ServeMux, which is the standard practice for new Go services. + mux := http.NewServeMux() - app.RegisterUsers() + // The handler chain is now cleaner and correctly typed: + // 1. `handleGetUser` is the core logic (type apiHandler). + // 2. `authMiddleware` wraps it, also returning an apiHandler. + // 3. `makeApiHandler` wraps the entire chain to handle errors and convert to http.HandlerFunc. + userHandler := makeApiHandler(authMiddleware(handleGetUser)) + mux.HandleFunc("GET /profile", userHandler) - (*dbConnection).Ping() - slog.Info("Starting new server on :" + environment.Network.HttpPort) + addr := ":8080" + log.Printf("Server starting on %s", addr) + log.Println("Ensure you have a '" + file + "' file relative to the executable.") - if err := http.ListenAndServe(environment.Network.GetHostURL(), mux); err != nil { - slog.Error("Error starting server", "error", err) - panic("Error starting server." + err.Error()) - } + // Start the HTTP server with the new mux. + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("Could not start server: %s\n", err) + } } From b2e965dfa921792036fc13cf68fc7127ba7f421a Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 20 Jun 2025 17:14:30 +0800 Subject: [PATCH 03/24] format --- main.go | 140 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/main.go b/main.go index 7124e63a..3ecf6a88 100644 --- a/main.go +++ b/main.go @@ -1,29 +1,29 @@ package main import ( - "encoding/json" - _ "github.com/lib/pq" - "log" - "net/http" - "os" + "encoding/json" + _ "github.com/lib/pq" + "log" + "net/http" + "os" ) const file = "./storage/fixture/profile.json" // ErrorResponse defines the structure for a JSON error response object. type ErrorResponse struct { - Error string `json:"error"` + Error string `json:"error"` } // apiError represents an application-level error, including an HTTP status code. type apiError struct { - Message string - Status int + Message string + Status int } // Error makes apiError satisfy the standard error interface. func (e *apiError) Error() string { - return e.Message + return e.Message } // apiHandler is a custom type for handlers that return an apiError. @@ -32,78 +32,78 @@ type apiHandler func(http.ResponseWriter, *http.Request) *apiError // makeApiHandler is a wrapper that converts our custom apiHandler into a standard // http.HandlerFunc. It centrally handles turning an apiError into a JSON response. func makeApiHandler(fn apiHandler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if err := fn(w, r); err != nil { - log.Printf("API Error: %s, Status: %d", err.Message, err.Status) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(err.Status) - resp := ErrorResponse{Error: err.Message} - if jsonErr := json.NewEncoder(w).Encode(resp); jsonErr != nil { - log.Printf("Could not encode error response: %v", jsonErr) - } - } - } + return func(w http.ResponseWriter, r *http.Request) { + if err := fn(w, r); err != nil { + log.Printf("API Error: %s, Status: %d", err.Message, err.Status) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.Status) + resp := ErrorResponse{Error: err.Message} + if jsonErr := json.NewEncoder(w).Encode(resp); jsonErr != nil { + log.Printf("Could not encode error response: %v", jsonErr) + } + } + } } // authMiddleware now correctly wraps an apiHandler and returns an apiHandler. // This allows it to return a structured apiError, integrating it cleanly // into our unified error handling pattern. func authMiddleware(next apiHandler) apiHandler { - return func(w http.ResponseWriter, r *http.Request) *apiError { - username := r.Header.Get("X-Username") - if username != "gocanto" { - log.Printf("Unauthorized access attempt by user: '%s'", username) - // Return a structured error instead of writing directly to the response. - return &apiError{ - Message: "Unauthorized", - Status: http.StatusUnauthorized, - } - } - - log.Println("Successfully authenticated user: gocanto") - // If auth succeeds, call the next handler in the chain. - return next(w, r) - } + return func(w http.ResponseWriter, r *http.Request) *apiError { + username := r.Header.Get("X-Username") + if username != "gocanto" { + log.Printf("Unauthorized access attempt by user: '%s'", username) + // Return a structured error instead of writing directly to the response. + return &apiError{ + Message: "Unauthorized", + Status: http.StatusUnauthorized, + } + } + + log.Println("Successfully authenticated user: gocanto") + // If auth succeeds, call the next handler in the chain. + return next(w, r) + } } // handleGetUser returns an *apiError on failure and nil on success. func handleGetUser(w http.ResponseWriter, r *http.Request) *apiError { - jsonBytes, err := os.ReadFile(file) - if err != nil { - log.Printf("Error reading profile file: %v", err) - return &apiError{ - Message: "Internal Server Error: could not read profile data", - Status: http.StatusInternalServerError, - } - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, err = w.Write(jsonBytes) - if err != nil { - log.Printf("Error writing response: %v", err) - } - - return nil // A nil return indicates success. + jsonBytes, err := os.ReadFile(file) + if err != nil { + log.Printf("Error reading profile file: %v", err) + return &apiError{ + Message: "Internal Server Error: could not read profile data", + Status: http.StatusInternalServerError, + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(jsonBytes) + if err != nil { + log.Printf("Error writing response: %v", err) + } + + return nil // A nil return indicates success. } func main() { - // Create a new ServeMux, which is the standard practice for new Go services. - mux := http.NewServeMux() - - // The handler chain is now cleaner and correctly typed: - // 1. `handleGetUser` is the core logic (type apiHandler). - // 2. `authMiddleware` wraps it, also returning an apiHandler. - // 3. `makeApiHandler` wraps the entire chain to handle errors and convert to http.HandlerFunc. - userHandler := makeApiHandler(authMiddleware(handleGetUser)) - mux.HandleFunc("GET /profile", userHandler) - - addr := ":8080" - log.Printf("Server starting on %s", addr) - log.Println("Ensure you have a '" + file + "' file relative to the executable.") - - // Start the HTTP server with the new mux. - if err := http.ListenAndServe(addr, mux); err != nil { - log.Fatalf("Could not start server: %s\n", err) - } + // Create a new ServeMux, which is the standard practice for new Go services. + mux := http.NewServeMux() + + // The handler chain is now cleaner and correctly typed: + // 1. `handleGetUser` is the core logic (type apiHandler). + // 2. `authMiddleware` wraps it, also returning an apiHandler. + // 3. `makeApiHandler` wraps the entire chain to handle errors and convert to http.HandlerFunc. + userHandler := makeApiHandler(authMiddleware(handleGetUser)) + mux.HandleFunc("GET /profile", userHandler) + + addr := ":8080" + log.Printf("Server starting on %s", addr) + log.Println("Ensure you have a '" + file + "' file relative to the executable.") + + // Start the HTTP server with the new mux. + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("Could not start server: %s\n", err) + } } From 50d8e54d3b1eb520f81f369e716358ee64205e4a Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 20 Jun 2025 17:26:32 +0800 Subject: [PATCH 04/24] token + chain middleware --- main.go | 171 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 101 insertions(+), 70 deletions(-) diff --git a/main.go b/main.go index 3ecf6a88..3e6af355 100644 --- a/main.go +++ b/main.go @@ -1,109 +1,140 @@ package main import ( - "encoding/json" - _ "github.com/lib/pq" - "log" - "net/http" - "os" + "encoding/json" + _ "github.com/lib/pq" + "log" + "net/http" + "os" ) const file = "./storage/fixture/profile.json" // ErrorResponse defines the structure for a JSON error response object. type ErrorResponse struct { - Error string `json:"error"` + Error string `json:"error"` } // apiError represents an application-level error, including an HTTP status code. type apiError struct { - Message string - Status int + Message string + Status int } // Error makes apiError satisfy the standard error interface. func (e *apiError) Error() string { - return e.Message + return e.Message } // apiHandler is a custom type for handlers that return an apiError. type apiHandler func(http.ResponseWriter, *http.Request) *apiError +// middleware is a function that takes an apiHandler and returns one. +type middleware func(apiHandler) apiHandler + +// Chain applies a list of middlewares to a final apiHandler. +// It builds the chain in reverse, so the first middleware in the list +// is the outermost one, executing first. +func chain(h apiHandler, mws ...middleware) apiHandler { + for i := len(mws) - 1; i >= 0; i-- { + h = mws[i](h) + } + return h +} + // makeApiHandler is a wrapper that converts our custom apiHandler into a standard // http.HandlerFunc. It centrally handles turning an apiError into a JSON response. func makeApiHandler(fn apiHandler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if err := fn(w, r); err != nil { - log.Printf("API Error: %s, Status: %d", err.Message, err.Status) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(err.Status) - resp := ErrorResponse{Error: err.Message} - if jsonErr := json.NewEncoder(w).Encode(resp); jsonErr != nil { - log.Printf("Could not encode error response: %v", jsonErr) - } - } - } + return func(w http.ResponseWriter, r *http.Request) { + if err := fn(w, r); err != nil { + log.Printf("API Error: %s, Status: %d", err.Message, err.Status) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.Status) + resp := ErrorResponse{Error: err.Message} + if jsonErr := json.NewEncoder(w).Encode(resp); jsonErr != nil { + log.Printf("Could not encode error response: %v", jsonErr) + } + } + } } // authMiddleware now correctly wraps an apiHandler and returns an apiHandler. // This allows it to return a structured apiError, integrating it cleanly // into our unified error handling pattern. func authMiddleware(next apiHandler) apiHandler { - return func(w http.ResponseWriter, r *http.Request) *apiError { - username := r.Header.Get("X-Username") - if username != "gocanto" { - log.Printf("Unauthorized access attempt by user: '%s'", username) - // Return a structured error instead of writing directly to the response. - return &apiError{ - Message: "Unauthorized", - Status: http.StatusUnauthorized, - } - } - - log.Println("Successfully authenticated user: gocanto") - // If auth succeeds, call the next handler in the chain. - return next(w, r) - } + return func(w http.ResponseWriter, r *http.Request) *apiError { + username := r.Header.Get("X-Username") + if username != "gocanto" { + log.Printf("Unauthorized access attempt by user: '%s'", username) + // Return a structured error instead of writing directly to the response. + return &apiError{ + Message: "Unauthorized", + Status: http.StatusUnauthorized, + } + } + + log.Println("Successfully authenticated user: gocanto") + // If auth succeeds, call the next handler in the chain. + return next(w, r) + } +} + +// tokenCheckMiddleware is a secondary middleware to check for a specific token. +// It also wraps an apiHandler and returns an apiHandler, allowing it to be chained. +func tokenCheckMiddleware(next apiHandler) apiHandler { + return func(w http.ResponseWriter, r *http.Request) *apiError { + token := r.Header.Get("X-Token") + if token != "3" { + log.Printf("Forbidden: Invalid token received ('%s')", token) + // Using 403 Forbidden to differentiate from 401 Unauthorized. + return &apiError{ + Message: "Forbidden", + Status: http.StatusForbidden, + } + } + + log.Println("Token validation successful") + // If the token is valid, proceed to the next handler. + return next(w, r) + } } // handleGetUser returns an *apiError on failure and nil on success. func handleGetUser(w http.ResponseWriter, r *http.Request) *apiError { - jsonBytes, err := os.ReadFile(file) - if err != nil { - log.Printf("Error reading profile file: %v", err) - return &apiError{ - Message: "Internal Server Error: could not read profile data", - Status: http.StatusInternalServerError, - } - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, err = w.Write(jsonBytes) - if err != nil { - log.Printf("Error writing response: %v", err) - } - - return nil // A nil return indicates success. + jsonBytes, err := os.ReadFile(file) + if err != nil { + log.Printf("Error reading profile file: %v", err) + return &apiError{ + Message: "Internal Server Error: could not read profile data", + Status: http.StatusInternalServerError, + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(jsonBytes) + if err != nil { + log.Printf("Error writing response: %v", err) + } + + return nil // A nil return indicates success. } func main() { - // Create a new ServeMux, which is the standard practice for new Go services. - mux := http.NewServeMux() - - // The handler chain is now cleaner and correctly typed: - // 1. `handleGetUser` is the core logic (type apiHandler). - // 2. `authMiddleware` wraps it, also returning an apiHandler. - // 3. `makeApiHandler` wraps the entire chain to handle errors and convert to http.HandlerFunc. - userHandler := makeApiHandler(authMiddleware(handleGetUser)) - mux.HandleFunc("GET /profile", userHandler) - - addr := ":8080" - log.Printf("Server starting on %s", addr) - log.Println("Ensure you have a '" + file + "' file relative to the executable.") - - // Start the HTTP server with the new mux. - if err := http.ListenAndServe(addr, mux); err != nil { - log.Fatalf("Could not start server: %s\n", err) - } + // Create a new ServeMux, which is the standard practice for new Go services. + mux := http.NewServeMux() + + // Using the chain function makes adding new middlewares much cleaner. + // The execution order is left-to-right: authMiddleware, then tokenCheckMiddleware. + userHandler := makeApiHandler(chain(handleGetUser, authMiddleware, tokenCheckMiddleware)) + mux.HandleFunc("GET /profile", userHandler) + + addr := ":8080" + log.Printf("Server starting on %s", addr) + log.Println("Ensure you have a 'store/profile.json' file relative to the executable.") + + // Start the HTTP server with the new mux. + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("Could not start server: %s\n", err) + } } From b7948c888759bb4590e227a673a7b7bb5695c414 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Fri, 20 Jun 2025 17:26:44 +0800 Subject: [PATCH 05/24] format --- main.go | 174 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/main.go b/main.go index 3e6af355..891357b7 100644 --- a/main.go +++ b/main.go @@ -1,29 +1,29 @@ package main import ( - "encoding/json" - _ "github.com/lib/pq" - "log" - "net/http" - "os" + "encoding/json" + _ "github.com/lib/pq" + "log" + "net/http" + "os" ) const file = "./storage/fixture/profile.json" // ErrorResponse defines the structure for a JSON error response object. type ErrorResponse struct { - Error string `json:"error"` + Error string `json:"error"` } // apiError represents an application-level error, including an HTTP status code. type apiError struct { - Message string - Status int + Message string + Status int } // Error makes apiError satisfy the standard error interface. func (e *apiError) Error() string { - return e.Message + return e.Message } // apiHandler is a custom type for handlers that return an apiError. @@ -36,105 +36,105 @@ type middleware func(apiHandler) apiHandler // It builds the chain in reverse, so the first middleware in the list // is the outermost one, executing first. func chain(h apiHandler, mws ...middleware) apiHandler { - for i := len(mws) - 1; i >= 0; i-- { - h = mws[i](h) - } - return h + for i := len(mws) - 1; i >= 0; i-- { + h = mws[i](h) + } + return h } // makeApiHandler is a wrapper that converts our custom apiHandler into a standard // http.HandlerFunc. It centrally handles turning an apiError into a JSON response. func makeApiHandler(fn apiHandler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if err := fn(w, r); err != nil { - log.Printf("API Error: %s, Status: %d", err.Message, err.Status) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(err.Status) - resp := ErrorResponse{Error: err.Message} - if jsonErr := json.NewEncoder(w).Encode(resp); jsonErr != nil { - log.Printf("Could not encode error response: %v", jsonErr) - } - } - } + return func(w http.ResponseWriter, r *http.Request) { + if err := fn(w, r); err != nil { + log.Printf("API Error: %s, Status: %d", err.Message, err.Status) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.Status) + resp := ErrorResponse{Error: err.Message} + if jsonErr := json.NewEncoder(w).Encode(resp); jsonErr != nil { + log.Printf("Could not encode error response: %v", jsonErr) + } + } + } } // authMiddleware now correctly wraps an apiHandler and returns an apiHandler. // This allows it to return a structured apiError, integrating it cleanly // into our unified error handling pattern. func authMiddleware(next apiHandler) apiHandler { - return func(w http.ResponseWriter, r *http.Request) *apiError { - username := r.Header.Get("X-Username") - if username != "gocanto" { - log.Printf("Unauthorized access attempt by user: '%s'", username) - // Return a structured error instead of writing directly to the response. - return &apiError{ - Message: "Unauthorized", - Status: http.StatusUnauthorized, - } - } - - log.Println("Successfully authenticated user: gocanto") - // If auth succeeds, call the next handler in the chain. - return next(w, r) - } + return func(w http.ResponseWriter, r *http.Request) *apiError { + username := r.Header.Get("X-Username") + if username != "gocanto" { + log.Printf("Unauthorized access attempt by user: '%s'", username) + // Return a structured error instead of writing directly to the response. + return &apiError{ + Message: "Unauthorized", + Status: http.StatusUnauthorized, + } + } + + log.Println("Successfully authenticated user: gocanto") + // If auth succeeds, call the next handler in the chain. + return next(w, r) + } } // tokenCheckMiddleware is a secondary middleware to check for a specific token. // It also wraps an apiHandler and returns an apiHandler, allowing it to be chained. func tokenCheckMiddleware(next apiHandler) apiHandler { - return func(w http.ResponseWriter, r *http.Request) *apiError { - token := r.Header.Get("X-Token") - if token != "3" { - log.Printf("Forbidden: Invalid token received ('%s')", token) - // Using 403 Forbidden to differentiate from 401 Unauthorized. - return &apiError{ - Message: "Forbidden", - Status: http.StatusForbidden, - } - } - - log.Println("Token validation successful") - // If the token is valid, proceed to the next handler. - return next(w, r) - } + return func(w http.ResponseWriter, r *http.Request) *apiError { + token := r.Header.Get("X-Token") + if token != "3" { + log.Printf("Forbidden: Invalid token received ('%s')", token) + // Using 403 Forbidden to differentiate from 401 Unauthorized. + return &apiError{ + Message: "Forbidden", + Status: http.StatusForbidden, + } + } + + log.Println("Token validation successful") + // If the token is valid, proceed to the next handler. + return next(w, r) + } } // handleGetUser returns an *apiError on failure and nil on success. func handleGetUser(w http.ResponseWriter, r *http.Request) *apiError { - jsonBytes, err := os.ReadFile(file) - if err != nil { - log.Printf("Error reading profile file: %v", err) - return &apiError{ - Message: "Internal Server Error: could not read profile data", - Status: http.StatusInternalServerError, - } - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, err = w.Write(jsonBytes) - if err != nil { - log.Printf("Error writing response: %v", err) - } - - return nil // A nil return indicates success. + jsonBytes, err := os.ReadFile(file) + if err != nil { + log.Printf("Error reading profile file: %v", err) + return &apiError{ + Message: "Internal Server Error: could not read profile data", + Status: http.StatusInternalServerError, + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(jsonBytes) + if err != nil { + log.Printf("Error writing response: %v", err) + } + + return nil // A nil return indicates success. } func main() { - // Create a new ServeMux, which is the standard practice for new Go services. - mux := http.NewServeMux() - - // Using the chain function makes adding new middlewares much cleaner. - // The execution order is left-to-right: authMiddleware, then tokenCheckMiddleware. - userHandler := makeApiHandler(chain(handleGetUser, authMiddleware, tokenCheckMiddleware)) - mux.HandleFunc("GET /profile", userHandler) - - addr := ":8080" - log.Printf("Server starting on %s", addr) - log.Println("Ensure you have a 'store/profile.json' file relative to the executable.") - - // Start the HTTP server with the new mux. - if err := http.ListenAndServe(addr, mux); err != nil { - log.Fatalf("Could not start server: %s\n", err) - } + // Create a new ServeMux, which is the standard practice for new Go services. + mux := http.NewServeMux() + + // Using the chain function makes adding new middlewares much cleaner. + // The execution order is left-to-right: authMiddleware, then tokenCheckMiddleware. + userHandler := makeApiHandler(chain(handleGetUser, authMiddleware, tokenCheckMiddleware)) + mux.HandleFunc("GET /profile", userHandler) + + addr := ":8080" + log.Printf("Server starting on %s", addr) + log.Println("Ensure you have a 'store/profile.json' file relative to the executable.") + + // Start the HTTP server with the new mux. + if err := http.ListenAndServe(addr, mux); err != nil { + log.Fatalf("Could not start server: %s\n", err) + } } From 642628747f3e4be12c8990881b12977e4a12a627 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 10:51:51 +0800 Subject: [PATCH 06/24] stract http pkg --- boost/boost.go | 5 +- main.go | 134 +++++++++----------------------- pkg/auth/token.go | 5 +- pkg/http/handler.go | 27 +++++++ pkg/http/middleware/pipeline.go | 21 +++++ pkg/http/middleware/token.go | 39 ++++++++++ pkg/http/middleware/username.go | 31 ++++++++ pkg/http/schema.go | 25 ++++++ 8 files changed, 182 insertions(+), 105 deletions(-) create mode 100644 pkg/http/handler.go create mode 100644 pkg/http/middleware/pipeline.go create mode 100644 pkg/http/middleware/token.go create mode 100644 pkg/http/middleware/username.go create mode 100644 pkg/http/schema.go diff --git a/boost/boost.go b/boost/boost.go index 7a57a29a..4b1c061b 100644 --- a/boost/boost.go +++ b/boost/boost.go @@ -62,9 +62,8 @@ func MakeEnv(values map[string]string, validate *pkg.Validator) *env.Environment port, _ := strconv.Atoi(values["ENV_DB_PORT"]) token := auth.Token{ - Username: strings.TrimSpace(values["ENV_APP_TOKEN_USERNAME"]), - Public: strings.TrimSpace(values["ENV_APP_TOKEN_PUBLIC"]), - Private: strings.TrimSpace(values["ENV_APP_TOKEN_PRIVATE"]), + Public: strings.TrimSpace(values["ENV_APP_TOKEN_PUBLIC"]), + Private: strings.TrimSpace(values["ENV_APP_TOKEN_PRIVATE"]), } app := env.AppEnvironment{ diff --git a/main.go b/main.go index 891357b7..3933bfd4 100644 --- a/main.go +++ b/main.go @@ -1,117 +1,30 @@ package main import ( - "encoding/json" _ "github.com/lib/pq" + "github.com/oullin/boost" + "github.com/oullin/env" + "github.com/oullin/pkg/http" + "github.com/oullin/pkg/http/middleware" "log" - "net/http" + baseHttp "net/http" "os" ) const file = "./storage/fixture/profile.json" -// ErrorResponse defines the structure for a JSON error response object. -type ErrorResponse struct { - Error string `json:"error"` -} - -// apiError represents an application-level error, including an HTTP status code. -type apiError struct { - Message string - Status int -} - -// Error makes apiError satisfy the standard error interface. -func (e *apiError) Error() string { - return e.Message -} - -// apiHandler is a custom type for handlers that return an apiError. -type apiHandler func(http.ResponseWriter, *http.Request) *apiError - -// middleware is a function that takes an apiHandler and returns one. -type middleware func(apiHandler) apiHandler - -// Chain applies a list of middlewares to a final apiHandler. -// It builds the chain in reverse, so the first middleware in the list -// is the outermost one, executing first. -func chain(h apiHandler, mws ...middleware) apiHandler { - for i := len(mws) - 1; i >= 0; i-- { - h = mws[i](h) - } - return h -} - -// makeApiHandler is a wrapper that converts our custom apiHandler into a standard -// http.HandlerFunc. It centrally handles turning an apiError into a JSON response. -func makeApiHandler(fn apiHandler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if err := fn(w, r); err != nil { - log.Printf("API Error: %s, Status: %d", err.Message, err.Status) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(err.Status) - resp := ErrorResponse{Error: err.Message} - if jsonErr := json.NewEncoder(w).Encode(resp); jsonErr != nil { - log.Printf("Could not encode error response: %v", jsonErr) - } - } - } -} - -// authMiddleware now correctly wraps an apiHandler and returns an apiHandler. -// This allows it to return a structured apiError, integrating it cleanly -// into our unified error handling pattern. -func authMiddleware(next apiHandler) apiHandler { - return func(w http.ResponseWriter, r *http.Request) *apiError { - username := r.Header.Get("X-Username") - if username != "gocanto" { - log.Printf("Unauthorized access attempt by user: '%s'", username) - // Return a structured error instead of writing directly to the response. - return &apiError{ - Message: "Unauthorized", - Status: http.StatusUnauthorized, - } - } - - log.Println("Successfully authenticated user: gocanto") - // If auth succeeds, call the next handler in the chain. - return next(w, r) - } -} - -// tokenCheckMiddleware is a secondary middleware to check for a specific token. -// It also wraps an apiHandler and returns an apiHandler, allowing it to be chained. -func tokenCheckMiddleware(next apiHandler) apiHandler { - return func(w http.ResponseWriter, r *http.Request) *apiError { - token := r.Header.Get("X-Token") - if token != "3" { - log.Printf("Forbidden: Invalid token received ('%s')", token) - // Using 403 Forbidden to differentiate from 401 Unauthorized. - return &apiError{ - Message: "Forbidden", - Status: http.StatusForbidden, - } - } - - log.Println("Token validation successful") - // If the token is valid, proceed to the next handler. - return next(w, r) - } -} - -// handleGetUser returns an *apiError on failure and nil on success. -func handleGetUser(w http.ResponseWriter, r *http.Request) *apiError { +func handleGetUser(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { jsonBytes, err := os.ReadFile(file) if err != nil { log.Printf("Error reading profile file: %v", err) - return &apiError{ + return &http.ApiError{ Message: "Internal Server Error: could not read profile data", - Status: http.StatusInternalServerError, + Status: baseHttp.StatusInternalServerError, } } w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) + w.WriteHeader(baseHttp.StatusOK) _, err = w.Write(jsonBytes) if err != nil { log.Printf("Error writing response: %v", err) @@ -120,13 +33,36 @@ func handleGetUser(w http.ResponseWriter, r *http.Request) *apiError { return nil // A nil return indicates success. } +var environment *env.Environment + +//var validator *pkg.Validator + +func init() { + secrets, _ := boost.Spark("./.env") + + environment = secrets + //validator = validate +} + func main() { // Create a new ServeMux, which is the standard practice for new Go services. - mux := http.NewServeMux() + mux := baseHttp.NewServeMux() + pipelines := middleware.Pipeline{ + Env: environment, + } + + tokenMid := middleware.MakeTokenMiddleware(environment.App.Credentials) // Using the chain function makes adding new middlewares much cleaner. // The execution order is left-to-right: authMiddleware, then tokenCheckMiddleware. - userHandler := makeApiHandler(chain(handleGetUser, authMiddleware, tokenCheckMiddleware)) + userHandler := http.MakeApiHandler( + pipelines.Chain( + handleGetUser, + middleware.UsernameCheck, + tokenMid.Handle, + ), + ) + mux.HandleFunc("GET /profile", userHandler) addr := ":8080" @@ -134,7 +70,7 @@ func main() { log.Println("Ensure you have a 'store/profile.json' file relative to the executable.") // Start the HTTP server with the new mux. - if err := http.ListenAndServe(addr, mux); err != nil { + if err := baseHttp.ListenAndServe(addr, mux); err != nil { log.Fatalf("Could not start server: %s\n", err) } } diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 86b74584..df6c798a 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -7,9 +7,8 @@ import ( ) type Token struct { - Username string `validate:"required,lowercase,alpha,min=5"` - Public string `validate:"required,min=10"` - Private string `validate:"required,min=10"` + Public string `validate:"required,min=10"` + Private string `validate:"required,min=10"` } func (t Token) IsInvalid(seed string) bool { diff --git a/pkg/http/handler.go b/pkg/http/handler.go new file mode 100644 index 00000000..bd769f68 --- /dev/null +++ b/pkg/http/handler.go @@ -0,0 +1,27 @@ +package http + +import ( + "encoding/json" + "log/slog" + baseHttp "net/http" +) + +func MakeApiHandler(fn ApiHandler) baseHttp.HandlerFunc { + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) { + if err := fn(w, r); err != nil { + slog.Info("API Error: %s, Status: %d", err.Message, err.Status) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.Status) + + resp := ErrorResponse{ + Error: err.Message, + Status: err.Status, + } + + if result := json.NewEncoder(w).Encode(resp); result != nil { + slog.Error("Could not encode error response", "error", result) + } + } + } +} diff --git a/pkg/http/middleware/pipeline.go b/pkg/http/middleware/pipeline.go new file mode 100644 index 00000000..643a74a2 --- /dev/null +++ b/pkg/http/middleware/pipeline.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "github.com/oullin/env" + "github.com/oullin/pkg/http" +) + +type Pipeline struct { + Env *env.Environment +} + +// Chain applies a list of middleware handlers to a final ApiHandler. +// It builds the chain in reverse, so the first middleware +// in the list is the outermost one, executing first. +func (m Pipeline) Chain(h http.ApiHandler, handlers ...http.Middleware) http.ApiHandler { + for i := len(handlers) - 1; i >= 0; i-- { + h = handlers[i](h) + } + + return h +} diff --git a/pkg/http/middleware/token.go b/pkg/http/middleware/token.go new file mode 100644 index 00000000..f95256eb --- /dev/null +++ b/pkg/http/middleware/token.go @@ -0,0 +1,39 @@ +package middleware + +import ( + "github.com/oullin/pkg/auth" + "github.com/oullin/pkg/http" + "log/slog" + baseHtpp "net/http" +) + +const tokenHeader = "X-API-Key" + +type TokenCheckMiddleware struct { + token auth.Token +} + +func MakeTokenMiddleware(token auth.Token) TokenCheckMiddleware { + return TokenCheckMiddleware{ + token: token, + } +} + +func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { + return func(w baseHtpp.ResponseWriter, r *baseHtpp.Request) *http.ApiError { + + if t.token.IsInvalid(r.Header.Get(tokenHeader)) { + message := "Forbidden: Invalid API seed" + slog.Error(message) + + return &http.ApiError{ + Message: message, + Status: baseHtpp.StatusForbidden, + } + } + + slog.Info("Token validation successful") + + return next(w, r) + } +} diff --git a/pkg/http/middleware/username.go b/pkg/http/middleware/username.go new file mode 100644 index 00000000..923d1cce --- /dev/null +++ b/pkg/http/middleware/username.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "fmt" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "strings" +) + +const usernameHeader = "X-API-Username" + +func UsernameCheck(next http.ApiHandler) http.ApiHandler { + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + username := strings.TrimSpace(r.Header.Get(usernameHeader)) + + if username != "gocanto" { + message := fmt.Sprintf("Forbidden: Invalid API username received ('%s')", username) + slog.Error(message) + + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusUnauthorized, + } + } + + slog.Info("Successfully authenticated user: gocanto") + + return next(w, r) + } +} diff --git a/pkg/http/schema.go b/pkg/http/schema.go new file mode 100644 index 00000000..6f5329a9 --- /dev/null +++ b/pkg/http/schema.go @@ -0,0 +1,25 @@ +package http + +import baseHttp "net/http" + +type ErrorResponse struct { + Error string `json:"error"` + Status int `json:"status"` +} + +type ApiError struct { + Message string `json:"message"` + Status int `json:"status"` +} + +func (e *ApiError) Error() string { + if e == nil { + return "Internal Server Error" + } + + return e.Message +} + +type ApiHandler func(baseHttp.ResponseWriter, *baseHttp.Request) *ApiError + +type Middleware func(ApiHandler) ApiHandler From 74124a8f8197e8563dda944aa8777d6392a60a2d Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 10:52:04 +0800 Subject: [PATCH 07/24] format --- pkg/http/middleware/pipeline.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/http/middleware/pipeline.go b/pkg/http/middleware/pipeline.go index 643a74a2..5ca24c5d 100644 --- a/pkg/http/middleware/pipeline.go +++ b/pkg/http/middleware/pipeline.go @@ -1,21 +1,21 @@ package middleware import ( - "github.com/oullin/env" - "github.com/oullin/pkg/http" + "github.com/oullin/env" + "github.com/oullin/pkg/http" ) type Pipeline struct { - Env *env.Environment + Env *env.Environment } // Chain applies a list of middleware handlers to a final ApiHandler. // It builds the chain in reverse, so the first middleware // in the list is the outermost one, executing first. func (m Pipeline) Chain(h http.ApiHandler, handlers ...http.Middleware) http.ApiHandler { - for i := len(handlers) - 1; i >= 0; i-- { - h = handlers[i](h) - } + for i := len(handlers) - 1; i >= 0; i-- { + h = handlers[i](h) + } - return h + return h } From 35a61483426f7458583333dbc4bf280dfade9fdf Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 10:54:04 +0800 Subject: [PATCH 08/24] remove username --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index 76041aef..732da9d9 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,6 @@ ENV_HTTP_HOST=localhost ENV_HTTP_PORT=8080 # --- App super admin credentials -ENV_APP_TOKEN_USERNAME="" ENV_APP_TOKEN_PUBLIC="" ENV_APP_TOKEN_PRIVATE="" From 44012ed3fc389302ce0949490428c4b42d6ed06a Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 11:47:41 +0800 Subject: [PATCH 09/24] format --- boost/app.go | 23 +------ handler/user/admin.go | 39 ----------- handler/user/create.go | 123 ---------------------------------- handler/user/repository.go | 98 --------------------------- handler/user/schema.go | 56 ---------------- main.go | 40 +++++++---- pkg/middleware/middlewares.go | 49 -------------- pkg/middleware/schema.go | 22 ------ 8 files changed, 28 insertions(+), 422 deletions(-) delete mode 100644 handler/user/admin.go delete mode 100644 handler/user/create.go delete mode 100644 handler/user/repository.go delete mode 100644 handler/user/schema.go delete mode 100644 pkg/middleware/middlewares.go delete mode 100644 pkg/middleware/schema.go diff --git a/boost/app.go b/boost/app.go index 168e1036..5476746b 100644 --- a/boost/app.go +++ b/boost/app.go @@ -3,10 +3,8 @@ package boost import ( "github.com/oullin/database" "github.com/oullin/env" - "github.com/oullin/handler/user" "github.com/oullin/pkg" "github.com/oullin/pkg/llogs" - "github.com/oullin/pkg/middleware" "net/http" ) @@ -14,32 +12,13 @@ type App struct { Validator *pkg.Validator `validate:"required"` Logs *llogs.Driver `validate:"required"` DbConnection *database.Connection `validate:"required"` - AdminUser *user.AdminUser `validate:"required"` Env *env.Environment `validate:"required"` Mux *http.ServeMux `validate:"required"` Sentry *pkg.Sentry `validate:"required"` } -func MakeApp(mux *http.ServeMux, app *App) *App { +func MakeApp(mux *http.ServeMux, app App) App { app.Mux = mux return app } - -func (app App) RegisterUsers() { - stack := middleware.MakeMiddlewareStack(app.Env, func(seed string) bool { - return app.AdminUser.IsAllowed(seed) - }) - - handler := user.RequestHandler{ - Repository: user.MakeRepository(app.DbConnection, app.AdminUser), - Validator: app.Validator, - } - - app.Mux.HandleFunc("POST /users", pkg.CreateHandle( - stack.Push( - handler.Create, - stack.AdminUser, - ), - )) -} diff --git a/handler/user/admin.go b/handler/user/admin.go deleted file mode 100644 index 3e76f8b1..00000000 --- a/handler/user/admin.go +++ /dev/null @@ -1,39 +0,0 @@ -package user - -import ( - "crypto/sha256" - "encoding/hex" - "strings" -) - -const adminUserName = "gocanto" - -type AdminUser struct { - PublicToken string `validate:"required,min=10"` - PrivateToken string `validate:"required,min=10"` -} - -func (ga AdminUser) IsAllowed(seed string) bool { - token := strings.Trim(ga.PublicToken, " ") - salt := strings.Trim(ga.PrivateToken, " ") - externalSalt := strings.Trim(seed, " ") - - if salt != externalSalt { - return false - } - - hash := sha256.New() - hash.Write([]byte(externalSalt)) - bytes := hash.Sum(hash.Sum(nil)) - - encodeToString := strings.Trim( - hex.EncodeToString(bytes), - " ", - ) - - return token == encodeToString -} - -func (ga AdminUser) IsNotAllowed(seed string) bool { - return !ga.IsAllowed(seed) -} diff --git a/handler/user/create.go b/handler/user/create.go deleted file mode 100644 index 3dd06d15..00000000 --- a/handler/user/create.go +++ /dev/null @@ -1,123 +0,0 @@ -package user - -import ( - "encoding/json" - "errors" - "fmt" - "github.com/oullin/env" - "github.com/oullin/pkg" - "github.com/oullin/pkg/media" - "github.com/oullin/pkg/request" - "github.com/oullin/pkg/response" - "io" - "mime/multipart" - "net/http" -) - -func (handler RequestHandler) Create(w http.ResponseWriter, r *http.Request) *response.Response { - var rawRequest RawCreateRequestBag - - multipartRequest, err := request.MakeMultipartRequest(r, &rawRequest) - defer multipartRequest.Close(nil) - - if err != nil { - return response.BadRequest("issues creating the request", err) - } - - err = multipartRequest.ParseRawData(extractData) - if err != nil { - return response.BadRequest("NEW: Error getting multipart reader", err) - } - - var requestBag CreateRequestBag - if err = json.Unmarshal(rawRequest.payload, &requestBag); err != nil { - return response.BadRequest("Invalid request payload: malformed JSON", err) - } - - validate := handler.Validator - if rejects, err := validate.Rejects(requestBag); rejects { - return response.Forbidden("Validation failed", validate.GetErrors(), err) - } - - if result := handler.Repository.FindByUserName(requestBag.Username); result != nil { - return response.Unprocessable(fmt.Sprintf("user '%s' already exists", requestBag.Username), nil) - } - - profilePic, err := media.MakeMedia( - requestBag.Username, - multipartRequest.GetFile(), - multipartRequest.GetHeaderName(), - ) - - if err != nil { - return response.BadRequest("Error handling the given file", err) - } - - if err := profilePic.Upload(media.GetUsersImagesDir()); err != nil { - return response.BadRequest("Error saving the given file", err) - } - - requestBag.PublicToken = r.Header.Get(env.ApiKeyHeader) - requestBag.PictureFileName = profilePic.GetFileName() - requestBag.ProfilePictureURL = profilePic.GetFilePath(requestBag.Username) - - created, err := handler.Repository.Create(requestBag) - - if err != nil { - return response.InternalServerError(err.Error(), err) - } - - payload := map[string]any{ - "message": "User created successfully!", - "user": map[string]string{ - "uuid": created.UUID, - "picture_file_name": requestBag.PictureFileName, - "profile_picture_url": requestBag.ProfilePictureURL, - }, - } - - return pkg.SendJSON(w, http.StatusCreated, payload) -} - -func extractData[T media.MultipartFormInterface](reader *multipart.Reader, data T) error { - for { - part, err := reader.NextPart() - - if err == io.EOF { - break - } - - if err != nil { - return err - } - - switch part.FormName() { - - case "data": - if part.FileName() != "" { - return errors.New("expected 'data' to be a JSON text field") - } - - if dataBytes, err := io.ReadAll(part); err != nil { - return errors.New("Error reading data field" + err.Error()) - } else { - data.SetPayload(dataBytes) - } - - case "profile_picture_url": - - if fileBytes, err := io.ReadAll(part); err != nil { - return errors.New("Error reading file" + err.Error()) - } else { - data.SetFile(fileBytes) - data.SetHeaderName(part.FileName()) - } - } - - if err = part.Close(); err != nil { - return errors.New("Issue closing the multi-part reader" + err.Error()) - } - } - - return nil -} diff --git a/handler/user/repository.go b/handler/user/repository.go deleted file mode 100644 index 27f3ff15..00000000 --- a/handler/user/repository.go +++ /dev/null @@ -1,98 +0,0 @@ -package user - -import ( - "fmt" - "github.com/google/uuid" - "github.com/oullin/database" - "github.com/oullin/pkg" - "github.com/oullin/pkg/gorm" - "strings" - "time" -) - -type Repository struct { - Connection *database.Connection - Admin *AdminUser -} - -func MakeRepository(model *database.Connection, admin *AdminUser) *Repository { - return &Repository{ - Connection: model, - Admin: admin, - } -} - -func (r Repository) Create(attr CreateRequestBag) (*CreatedUser, error) { - password, err := pkg.MakePassword(attr.Password) - - if err != nil { - return nil, err - } - - user := &database.User{ - UUID: uuid.New().String(), - FirstName: attr.FirstName, - LastName: attr.LastName, - Username: attr.Username, - DisplayName: attr.DisplayName, - Email: attr.Email, - PasswordHash: password.GetHash(), - PublicToken: attr.PublicToken, - Bio: attr.Bio, - PictureFileName: attr.PictureFileName, - ProfilePictureURL: attr.ProfilePictureURL, - VerifiedAt: time.Now(), - IsAdmin: strings.Trim(attr.Username, " ") == adminUserName, - } - - result := r.Connection.Sql().Create(&user) - - if result.Error != nil { - return nil, result.Error - } - - return &CreatedUser{ - UUID: user.UUID, - }, nil -} - -func (r Repository) FindByUserName(username string) *database.User { - user := &database.User{} - - result := r.Connection.Sql(). - Where("username = ?", username). - First(&user) - - if gorm.HasDbIssues(result.Error) { - return nil - } - - if strings.Trim(user.UUID, " ") != "" { - return user - } - - return nil -} - -func (r Repository) FindPosts(author database.User) ([]database.Post, error) { - var posts []database.Post - - err := r.Connection.Sql(). - Model(&database.Post{}). - Where("author_id = ?", author.ID). - Where("published_at IS NOT NULL"). - Where("deleted_at IS NULL"). - Order("created_at desc"). - Find(&posts). - Error - - if gorm.IsNotFound(err) { - return nil, fmt.Errorf("posts not found for author [%s]: %s", author.Username, err.Error()) - } - - if gorm.IsFoundButHasErrors(err) { - return nil, fmt.Errorf("issue retrieving author's [%s] posts: %s", author.Username, err.Error()) - } - - return posts, nil -} diff --git a/handler/user/schema.go b/handler/user/schema.go deleted file mode 100644 index dd662de9..00000000 --- a/handler/user/schema.go +++ /dev/null @@ -1,56 +0,0 @@ -package user - -import "github.com/oullin/pkg" - -type RequestHandler struct { - Validator *pkg.Validator - Repository *Repository -} - -type CreatedUser struct { - UUID string `json:"uuid"` -} - -type CreateRequestBag struct { - FirstName string `json:"first_name" validate:"required,min=4,max=250"` - LastName string `json:"last_name" validate:"required,min=4,max=250"` - Username string `json:"username" validate:"required,alphanum,min=4,max=50"` - DisplayName string `json:"display_name" validate:"omitempty,min=3,max=255"` - Email string `json:"email" validate:"required,email,max=250"` - Password string `json:"password" validate:"required,min=8"` - PublicToken string `json:"public_token"` - PasswordConfirmation string `json:"password_confirmation" validate:"required,eqfield=Password"` - Bio string `json:"bio" validate:"omitempty"` - PictureFileName string `json:"picture_file_name" validate:"omitempty"` - ProfilePictureURL string `json:"profile_picture_url" validate:"omitempty,url,max=2048"` -} - -type RawCreateRequestBag struct { - file []byte - payload []byte - headerName string -} - -func (n *RawCreateRequestBag) SetFile(file []byte) { - n.file = file -} - -func (n *RawCreateRequestBag) SetPayload(payload []byte) { - n.payload = payload -} - -func (n *RawCreateRequestBag) SetHeaderName(headerName string) { - n.headerName = headerName -} - -func (n *RawCreateRequestBag) GetFile() []byte { - return n.file -} - -func (n *RawCreateRequestBag) GetPayload() []byte { - return n.payload -} - -func (n *RawCreateRequestBag) GetHeaderName() string { - return n.headerName -} diff --git a/main.go b/main.go index 3933bfd4..d58b2da4 100644 --- a/main.go +++ b/main.go @@ -4,9 +4,11 @@ import ( _ "github.com/lib/pq" "github.com/oullin/boost" "github.com/oullin/env" + "github.com/oullin/pkg" "github.com/oullin/pkg/http" "github.com/oullin/pkg/http/middleware" "log" + "log/slog" baseHttp "net/http" "os" ) @@ -34,27 +36,40 @@ func handleGetUser(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiErro } var environment *env.Environment - -//var validator *pkg.Validator +var validator *pkg.Validator func init() { - secrets, _ := boost.Spark("./.env") + secrets, validate := boost.Spark("./.env") environment = secrets - //validator = validate + validator = validate } func main() { - // Create a new ServeMux, which is the standard practice for new Go services. + dbConnection := boost.MakeDbConnection(environment) + logs := boost.MakeLogs(environment) + localSentry := boost.MakeSentry(environment) + + defer (*logs).Close() + defer (*dbConnection).Close() + mux := baseHttp.NewServeMux() + + _ = boost.MakeApp(mux, boost.App{ + Validator: validator, + Logs: logs, + DbConnection: dbConnection, + Env: environment, + Mux: mux, + Sentry: localSentry, + }) + pipelines := middleware.Pipeline{ Env: environment, } tokenMid := middleware.MakeTokenMiddleware(environment.App.Credentials) - // Using the chain function makes adding new middlewares much cleaner. - // The execution order is left-to-right: authMiddleware, then tokenCheckMiddleware. userHandler := http.MakeApiHandler( pipelines.Chain( handleGetUser, @@ -65,12 +80,11 @@ func main() { mux.HandleFunc("GET /profile", userHandler) - addr := ":8080" - log.Printf("Server starting on %s", addr) - log.Println("Ensure you have a 'store/profile.json' file relative to the executable.") + (*dbConnection).Ping() + slog.Info("Starting new server on :" + environment.Network.HttpPort) - // Start the HTTP server with the new mux. - if err := baseHttp.ListenAndServe(addr, mux); err != nil { - log.Fatalf("Could not start server: %s\n", err) + if err := baseHttp.ListenAndServe(environment.Network.GetHostURL(), mux); err != nil { + slog.Error("Error starting server", "error", err) + panic("Error starting server." + err.Error()) } } diff --git a/pkg/middleware/middlewares.go b/pkg/middleware/middlewares.go deleted file mode 100644 index 3e56860d..00000000 --- a/pkg/middleware/middlewares.go +++ /dev/null @@ -1,49 +0,0 @@ -package middleware - -import ( - "fmt" - "github.com/oullin/env" - "github.com/oullin/pkg" - "github.com/oullin/pkg/response" - "log/slog" - "net/http" -) - -func (s MiddlewaresStack) Logging(next pkg.BaseHandler) pkg.BaseHandler { - return func(w http.ResponseWriter, r *http.Request) *response.Response { - slog.Info(fmt.Sprintf("Incoming request: [method:%s] [path:%s].", r.Method, r.URL.Path)) - - err := next(w, r) - - if err != nil { - slog.Error(fmt.Sprintf("Handler returned error: %s", err)) - } - - return err - } -} - -func (s MiddlewaresStack) AdminUser(next pkg.BaseHandler) pkg.BaseHandler { - return func(w http.ResponseWriter, r *http.Request) *response.Response { - salt := r.Header.Get(env.ApiKeyHeader) - - if s.isAdminUser(salt) { - return next(w, r) - } - - return response.Unauthorized("Unauthorized", nil) - } -} - -func (s MiddlewaresStack) isAdminUser(seed string) bool { - return s.userAdminResolver(seed) -} - -func (s MiddlewaresStack) Push(handler pkg.BaseHandler, middlewares ...Middleware) pkg.BaseHandler { - // Apply middleware in reverse order, so the first middleware in the list is executed first. - for i := len(middlewares) - 1; i >= 0; i-- { - handler = middlewares[i](handler) - } - - return handler -} diff --git a/pkg/middleware/schema.go b/pkg/middleware/schema.go deleted file mode 100644 index 0c9d1dbe..00000000 --- a/pkg/middleware/schema.go +++ /dev/null @@ -1,22 +0,0 @@ -package middleware - -import ( - "github.com/oullin/env" - "github.com/oullin/pkg" -) - -type MiddlewaresStack struct { - env *env.Environment - middleware []Middleware - userAdminResolver func(seed string) bool -} - -type Middleware func(pkg.BaseHandler) pkg.BaseHandler - -func MakeMiddlewareStack(env *env.Environment, userAdminResolver func(seed string) bool) *MiddlewaresStack { - return &MiddlewaresStack{ - env: env, - userAdminResolver: userAdminResolver, - middleware: []Middleware{}, - } -} From 34fdbc391fdb3fa96d89116716a36d2e5716cce4 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 11:50:05 +0800 Subject: [PATCH 10/24] naming --- boost/app.go | 24 ------------------------ boost/{boost.go => factory.go} | 16 ++++++++++++++++ boost/{spark.go => ignite.go} | 2 +- cli/main.go | 2 +- database/seeder/main.go | 2 +- main.go | 2 +- 6 files changed, 20 insertions(+), 28 deletions(-) delete mode 100644 boost/app.go rename boost/{boost.go => factory.go} (89%) rename boost/{spark.go => ignite.go} (82%) diff --git a/boost/app.go b/boost/app.go deleted file mode 100644 index 5476746b..00000000 --- a/boost/app.go +++ /dev/null @@ -1,24 +0,0 @@ -package boost - -import ( - "github.com/oullin/database" - "github.com/oullin/env" - "github.com/oullin/pkg" - "github.com/oullin/pkg/llogs" - "net/http" -) - -type App struct { - Validator *pkg.Validator `validate:"required"` - Logs *llogs.Driver `validate:"required"` - DbConnection *database.Connection `validate:"required"` - Env *env.Environment `validate:"required"` - Mux *http.ServeMux `validate:"required"` - Sentry *pkg.Sentry `validate:"required"` -} - -func MakeApp(mux *http.ServeMux, app App) App { - app.Mux = mux - - return app -} diff --git a/boost/boost.go b/boost/factory.go similarity index 89% rename from boost/boost.go rename to boost/factory.go index 4b1c061b..49af0db2 100644 --- a/boost/boost.go +++ b/boost/factory.go @@ -9,11 +9,27 @@ import ( "github.com/oullin/pkg/auth" "github.com/oullin/pkg/llogs" "log" + "net/http" "strconv" "strings" "time" ) +type App struct { + Env *env.Environment `validate:"required"` + Mux *http.ServeMux `validate:"required"` + Logs *llogs.Driver `validate:"required"` + Sentry *pkg.Sentry `validate:"required"` + Validator *pkg.Validator `validate:"required"` + DbConnection *database.Connection `validate:"required"` +} + +func MakeApp(mux *http.ServeMux, app App) App { + app.Mux = mux + + return app +} + func MakeSentry(env *env.Environment) *pkg.Sentry { cOptions := sentry.ClientOptions{ Dsn: env.Sentry.DSN, diff --git a/boost/spark.go b/boost/ignite.go similarity index 82% rename from boost/spark.go rename to boost/ignite.go index 4f87dce7..03d73e0e 100644 --- a/boost/spark.go +++ b/boost/ignite.go @@ -6,7 +6,7 @@ import ( "github.com/oullin/pkg" ) -func Spark(envPath string) (*env.Environment, *pkg.Validator) { +func Ignite(envPath string) (*env.Environment, *pkg.Validator) { validate := pkg.GetDefaultValidator() envMap, err := godotenv.Read(envPath) diff --git a/cli/main.go b/cli/main.go index 34a5de51..79996bee 100644 --- a/cli/main.go +++ b/cli/main.go @@ -16,7 +16,7 @@ var guard gate.Guard var environment *env.Environment func init() { - secrets, _ := boost.Spark("./../.env") + secrets, _ := boost.Ignite("./../.env") environment = secrets guard = gate.MakeGuard(environment.App.Credentials) diff --git a/database/seeder/main.go b/database/seeder/main.go index 40a5df84..c783ffd6 100644 --- a/database/seeder/main.go +++ b/database/seeder/main.go @@ -13,7 +13,7 @@ import ( var environment *env.Environment func init() { - secrets, _ := boost.Spark("./.env") + secrets, _ := boost.Ignite("./.env") environment = secrets } diff --git a/main.go b/main.go index d58b2da4..a93e0766 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,7 @@ var environment *env.Environment var validator *pkg.Validator func init() { - secrets, validate := boost.Spark("./.env") + secrets, validate := boost.Ignite("./.env") environment = secrets validator = validate From e7e96d65fbd3ae43d7e63c480b85db6985fdefd4 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 13:42:03 +0800 Subject: [PATCH 11/24] start working on router --- boost/app.go | 44 ++++++++++++++++++ boost/factory.go | 20 +------- boost/router.go | 41 +++++++++++++++++ database/seeder/main.go | 6 +-- database/seeder/seeds/factory.go | 2 +- handler/profile.go | 36 +++++++++++++++ main.go | 78 ++++++-------------------------- 7 files changed, 142 insertions(+), 85 deletions(-) create mode 100644 boost/app.go create mode 100644 boost/router.go create mode 100644 handler/profile.go diff --git a/boost/app.go b/boost/app.go new file mode 100644 index 00000000..7490886d --- /dev/null +++ b/boost/app.go @@ -0,0 +1,44 @@ +package boost + +import ( + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg" + "github.com/oullin/pkg/llogs" +) + +type App struct { + env *env.Environment + router *Router + logs *llogs.Driver + sentry *pkg.Sentry + validator *pkg.Validator + db *database.Connection +} + +func MakeApp(env *env.Environment, validator *pkg.Validator) App { + return App{ + env: env, + validator: validator, + db: MakeDbConnection(env), + logs: MakeLogs(env), + } +} + +func (a *App) CloseLogs() { + driver := *a.logs + driver.Close() +} + +func (a *App) CloseDB() { + driver := *a.db + driver.Close() +} + +func (a *App) GetEnv() *env.Environment { + return a.env +} + +func (a *App) GetDB() *database.Connection { + return a.db +} diff --git a/boost/factory.go b/boost/factory.go index 49af0db2..ea7f2f5e 100644 --- a/boost/factory.go +++ b/boost/factory.go @@ -9,27 +9,11 @@ import ( "github.com/oullin/pkg/auth" "github.com/oullin/pkg/llogs" "log" - "net/http" "strconv" "strings" "time" ) -type App struct { - Env *env.Environment `validate:"required"` - Mux *http.ServeMux `validate:"required"` - Logs *llogs.Driver `validate:"required"` - Sentry *pkg.Sentry `validate:"required"` - Validator *pkg.Validator `validate:"required"` - DbConnection *database.Connection `validate:"required"` -} - -func MakeApp(mux *http.ServeMux, app App) App { - app.Mux = mux - - return app -} - func MakeSentry(env *env.Environment) *pkg.Sentry { cOptions := sentry.ClientOptions{ Dsn: env.Sentry.DSN, @@ -66,7 +50,7 @@ func MakeLogs(env *env.Environment) *llogs.Driver { lDriver, err := llogs.MakeFilesLogs(env) if err != nil { - panic("Logs: error opening logs file: " + err.Error()) + panic("logs: error opening logs file: " + err.Error()) } return &lDriver @@ -130,7 +114,7 @@ func MakeEnv(values map[string]string, validate *pkg.Validator) *env.Environment } if _, err := validate.Rejects(logsCreds); err != nil { - panic(errorSufix + "invalid [Logs Creds] model: " + validate.GetErrorsAsJason()) + panic(errorSufix + "invalid [logs Creds] model: " + validate.GetErrorsAsJason()) } if _, err := validate.Rejects(net); err != nil { diff --git a/boost/router.go b/boost/router.go new file mode 100644 index 00000000..826aa626 --- /dev/null +++ b/boost/router.go @@ -0,0 +1,41 @@ +package boost + +import ( + "github.com/oullin/env" + "github.com/oullin/handler" + "github.com/oullin/pkg/http" + "github.com/oullin/pkg/http/middleware" + baseHttp "net/http" +) + +type Router struct { + Env *env.Environment + Mux *baseHttp.ServeMux + Pipeline middleware.Pipeline +} + +func MakeRouter(mux *baseHttp.ServeMux) *Router { + return &Router{ + Mux: mux, + } +} + +func (r *Router) Profile(fixture string) { + tokenMiddleware := middleware.MakeTokenMiddleware( + r.Env.App.Credentials, + ) + + profileHandler := handler.ProfileHandler{ + Fixture: fixture, + } + + getHandler := http.MakeApiHandler( + r.Pipeline.Chain( + profileHandler.Handle, + middleware.UsernameCheck, + tokenMiddleware.Handle, + ), + ) + + r.Mux.HandleFunc("GET /profile", getHandler) +} diff --git a/database/seeder/main.go b/database/seeder/main.go index c783ffd6..a9669531 100644 --- a/database/seeder/main.go +++ b/database/seeder/main.go @@ -30,11 +30,11 @@ func main() { // [1] --- Create the Seeder Runner. seeder := seeds.MakeSeeder(dbConnection, environment) - // [2] --- Truncate the DB. + // [2] --- Truncate the db. if err := seeder.TruncateDB(); err != nil { panic(err) } else { - cli.Successln("DB Truncated successfully ...") + cli.Successln("db Truncated successfully ...") time.Sleep(2 * time.Second) } @@ -114,5 +114,5 @@ func main() { wg.Wait() - cli.Magentaln("DB seeded as expected ....") + cli.Magentaln("db seeded as expected ....") } diff --git a/database/seeder/seeds/factory.go b/database/seeder/seeds/factory.go index c1ebcee4..97acb9cd 100644 --- a/database/seeder/seeds/factory.go +++ b/database/seeder/seeds/factory.go @@ -23,7 +23,7 @@ func MakeSeeder(dbConnection *database.Connection, environment *env.Environment) func (s *Seeder) TruncateDB() error { if s.environment.App.IsProduction() { - return fmt.Errorf("cannot truncate DB at the seeder level") + return fmt.Errorf("cannot truncate db at the seeder level") } truncate := database.MakeTruncate(s.dbConn, s.environment) diff --git a/handler/profile.go b/handler/profile.go new file mode 100644 index 00000000..37e90f8d --- /dev/null +++ b/handler/profile.go @@ -0,0 +1,36 @@ +package handler + +import ( + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "os" +) + +type ProfileHandler struct { + Fixture string +} + +func (h ProfileHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + fixture, err := os.ReadFile(h.Fixture) + + if err != nil { + slog.Error("Error reading profile file: %v", err) + + return &http.ApiError{ + Message: "Internal Server Error: could not read profile data", + Status: baseHttp.StatusInternalServerError, + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(baseHttp.StatusOK) + + _, err = w.Write(fixture) + + if err != nil { + slog.Error("Error writing response: %v", err) + } + + return nil // A nil return indicates success. +} diff --git a/main.go b/main.go index a93e0766..bd071069 100644 --- a/main.go +++ b/main.go @@ -3,87 +3,39 @@ package main import ( _ "github.com/lib/pq" "github.com/oullin/boost" - "github.com/oullin/env" - "github.com/oullin/pkg" - "github.com/oullin/pkg/http" "github.com/oullin/pkg/http/middleware" - "log" "log/slog" baseHttp "net/http" - "os" ) const file = "./storage/fixture/profile.json" -func handleGetUser(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - jsonBytes, err := os.ReadFile(file) - if err != nil { - log.Printf("Error reading profile file: %v", err) - return &http.ApiError{ - Message: "Internal Server Error: could not read profile data", - Status: baseHttp.StatusInternalServerError, - } - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(baseHttp.StatusOK) - _, err = w.Write(jsonBytes) - if err != nil { - log.Printf("Error writing response: %v", err) - } - - return nil // A nil return indicates success. -} - -var environment *env.Environment -var validator *pkg.Validator +var app boost.App func init() { secrets, validate := boost.Ignite("./.env") - environment = secrets - validator = validate + app = boost.MakeApp(secrets, validate) } func main() { - dbConnection := boost.MakeDbConnection(environment) - logs := boost.MakeLogs(environment) - localSentry := boost.MakeSentry(environment) - - defer (*logs).Close() - defer (*dbConnection).Close() - - mux := baseHttp.NewServeMux() - - _ = boost.MakeApp(mux, boost.App{ - Validator: validator, - Logs: logs, - DbConnection: dbConnection, - Env: environment, - Mux: mux, - Sentry: localSentry, - }) - - pipelines := middleware.Pipeline{ - Env: environment, + defer app.CloseDB() + defer app.CloseLogs() + + router := boost.Router{ + Env: app.GetEnv(), + Mux: baseHttp.NewServeMux(), + Pipeline: middleware.Pipeline{ + Env: app.GetEnv(), + }, } - tokenMid := middleware.MakeTokenMiddleware(environment.App.Credentials) - - userHandler := http.MakeApiHandler( - pipelines.Chain( - handleGetUser, - middleware.UsernameCheck, - tokenMid.Handle, - ), - ) - - mux.HandleFunc("GET /profile", userHandler) + router.Profile(file) - (*dbConnection).Ping() - slog.Info("Starting new server on :" + environment.Network.HttpPort) + app.GetDB().Ping() + slog.Info("Starting new server on :" + app.GetEnv().Network.HttpPort) - if err := baseHttp.ListenAndServe(environment.Network.GetHostURL(), mux); err != nil { + if err := baseHttp.ListenAndServe(app.GetEnv().Network.GetHostURL(), router.Mux); err != nil { slog.Error("Error starting server", "error", err) panic("Error starting server." + err.Error()) } From 468645750bb7cb5fa26bd4ae5a605977668bd5ca Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 13:46:36 +0800 Subject: [PATCH 12/24] remove const --- boost/router.go | 6 ++---- handler/profile.go | 10 ++++++++-- main.go | 4 +--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/boost/router.go b/boost/router.go index 826aa626..852fe430 100644 --- a/boost/router.go +++ b/boost/router.go @@ -20,14 +20,12 @@ func MakeRouter(mux *baseHttp.ServeMux) *Router { } } -func (r *Router) Profile(fixture string) { +func (r *Router) Profile() { tokenMiddleware := middleware.MakeTokenMiddleware( r.Env.App.Credentials, ) - profileHandler := handler.ProfileHandler{ - Fixture: fixture, - } + profileHandler := handler.MakeProfileHandler() getHandler := http.MakeApiHandler( r.Pipeline.Chain( diff --git a/handler/profile.go b/handler/profile.go index 37e90f8d..e25b372f 100644 --- a/handler/profile.go +++ b/handler/profile.go @@ -8,11 +8,17 @@ import ( ) type ProfileHandler struct { - Fixture string + content string +} + +func MakeProfileHandler() ProfileHandler { + return ProfileHandler{ + content: "./storage/fixture/profile.json", + } } func (h ProfileHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - fixture, err := os.ReadFile(h.Fixture) + fixture, err := os.ReadFile(h.content) if err != nil { slog.Error("Error reading profile file: %v", err) diff --git a/main.go b/main.go index bd071069..99e8d058 100644 --- a/main.go +++ b/main.go @@ -8,8 +8,6 @@ import ( baseHttp "net/http" ) -const file = "./storage/fixture/profile.json" - var app boost.App func init() { @@ -30,7 +28,7 @@ func main() { }, } - router.Profile(file) + router.Profile() app.GetDB().Ping() slog.Info("Starting new server on :" + app.GetEnv().Network.HttpPort) From e0cc3cee7c67e7e5fcc6bec54524f3b7f5a5e7d6 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 14:05:58 +0800 Subject: [PATCH 13/24] extract app --- boost/app.go | 39 +++++++++++++++++++++------------------ boost/helpers.go | 33 +++++++++++++++++++++++++++++++++ env/app.go | 1 - main.go | 17 +++++------------ 4 files changed, 59 insertions(+), 31 deletions(-) create mode 100644 boost/helpers.go diff --git a/boost/app.go b/boost/app.go index 7490886d..8da58720 100644 --- a/boost/app.go +++ b/boost/app.go @@ -4,41 +4,44 @@ import ( "github.com/oullin/database" "github.com/oullin/env" "github.com/oullin/pkg" + "github.com/oullin/pkg/http/middleware" "github.com/oullin/pkg/llogs" + baseHttp "net/http" ) type App struct { - env *env.Environment router *Router - logs *llogs.Driver sentry *pkg.Sentry + logs *llogs.Driver validator *pkg.Validator + env *env.Environment db *database.Connection } -func MakeApp(env *env.Environment, validator *pkg.Validator) App { - return App{ +func MakeApp(env *env.Environment, validator *pkg.Validator) *App { + app := App{ env: env, validator: validator, - db: MakeDbConnection(env), logs: MakeLogs(env), + sentry: MakeSentry(env), + db: MakeDbConnection(env), } -} -func (a *App) CloseLogs() { - driver := *a.logs - driver.Close() -} + router := Router{ + Env: env, + Mux: baseHttp.NewServeMux(), + Pipeline: middleware.Pipeline{ + Env: env, + }, + } -func (a *App) CloseDB() { - driver := *a.db - driver.Close() -} + app.SetRouter(router) -func (a *App) GetEnv() *env.Environment { - return a.env + return &app } -func (a *App) GetDB() *database.Connection { - return a.db +func (a *App) Boot() { + router := *a.router + + router.Profile() } diff --git a/boost/helpers.go b/boost/helpers.go new file mode 100644 index 00000000..f4047b4a --- /dev/null +++ b/boost/helpers.go @@ -0,0 +1,33 @@ +package boost + +import ( + "github.com/oullin/database" + "github.com/oullin/env" + baseHttp "net/http" +) + +func (a *App) SetRouter(router Router) { + a.router = &router +} + +func (a *App) CloseLogs() { + driver := *a.logs + driver.Close() +} + +func (a *App) CloseDB() { + driver := *a.db + driver.Close() +} + +func (a *App) GetEnv() *env.Environment { + return a.env +} + +func (a *App) GetDB() *database.Connection { + return a.db +} + +func (a *App) GetMux() *baseHttp.ServeMux { + return a.router.Mux +} diff --git a/env/app.go b/env/app.go index b3d874b2..d296a84a 100644 --- a/env/app.go +++ b/env/app.go @@ -5,7 +5,6 @@ import "github.com/oullin/pkg/auth" const local = "local" const staging = "staging" const production = "production" -const ApiKeyHeader = "X-API-Key" type AppEnvironment struct { Name string `validate:"required,min=4"` diff --git a/main.go b/main.go index 99e8d058..15f6adf4 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,11 @@ package main import ( _ "github.com/lib/pq" "github.com/oullin/boost" - "github.com/oullin/pkg/http/middleware" "log/slog" baseHttp "net/http" ) -var app boost.App +var app *boost.App func init() { secrets, validate := boost.Ignite("./.env") @@ -20,20 +19,14 @@ func main() { defer app.CloseDB() defer app.CloseLogs() - router := boost.Router{ - Env: app.GetEnv(), - Mux: baseHttp.NewServeMux(), - Pipeline: middleware.Pipeline{ - Env: app.GetEnv(), - }, - } - - router.Profile() + app.Boot() + // --- Testing app.GetDB().Ping() slog.Info("Starting new server on :" + app.GetEnv().Network.HttpPort) + // --- - if err := baseHttp.ListenAndServe(app.GetEnv().Network.GetHostURL(), router.Mux); err != nil { + if err := baseHttp.ListenAndServe(app.GetEnv().Network.GetHostURL(), app.GetMux()); err != nil { slog.Error("Error starting server", "error", err) panic("Error starting server." + err.Error()) } From ebfea3adaf2ad5ac4bbd5e369c01029e55d88ffb Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 14:38:38 +0800 Subject: [PATCH 14/24] helpers --- handler/profile.go | 14 +++----------- handler/response.go | 17 +++++++++++++++++ pkg/http/message.go | 13 +++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 handler/response.go create mode 100644 pkg/http/message.go diff --git a/handler/profile.go b/handler/profile.go index e25b372f..50ec3cfb 100644 --- a/handler/profile.go +++ b/handler/profile.go @@ -23,19 +23,11 @@ func (h ProfileHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) * if err != nil { slog.Error("Error reading profile file: %v", err) - return &http.ApiError{ - Message: "Internal Server Error: could not read profile data", - Status: baseHttp.StatusInternalServerError, - } + return http.InternalError("could not read profile data") } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(baseHttp.StatusOK) - - _, err = w.Write(fixture) - - if err != nil { - slog.Error("Error writing response: %v", err) + if err := writeJSON(fixture, w); err != nil { + return http.InternalError(err.Error()) } return nil // A nil return indicates success. diff --git a/handler/response.go b/handler/response.go new file mode 100644 index 00000000..55f7860e --- /dev/null +++ b/handler/response.go @@ -0,0 +1,17 @@ +package handler + +import ( + "fmt" + baseHttp "net/http" +) + +func writeJSON(content []byte, w baseHttp.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(baseHttp.StatusOK) + + if _, err := w.Write(content); err != nil { + return fmt.Errorf("error writing response: %v", err) + } + + return nil +} diff --git a/pkg/http/message.go b/pkg/http/message.go new file mode 100644 index 00000000..85280ab8 --- /dev/null +++ b/pkg/http/message.go @@ -0,0 +1,13 @@ +package http + +import ( + "fmt" + baseHttp "net/http" +) + +func InternalError(msg string) *ApiError { + return &ApiError{ + Message: fmt.Sprintf("Internal Server Error: %s", msg), + Status: baseHttp.StatusInternalServerError, + } +} From 3145f5dc48fb02859f32ddcd5ad7dee23f8d1fc8 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:04:51 +0800 Subject: [PATCH 15/24] add rources --- boost/app.go | 70 +++++++++++++++++++++++-------------------- boost/router.go | 64 +++++++++++++++++++++++++++++++-------- handler/experience.go | 34 +++++++++++++++++++++ handler/projects.go | 34 +++++++++++++++++++++ handler/social.go | 34 +++++++++++++++++++++ handler/talks.go | 34 +++++++++++++++++++++ 6 files changed, 225 insertions(+), 45 deletions(-) create mode 100644 handler/experience.go create mode 100644 handler/projects.go create mode 100644 handler/social.go create mode 100644 handler/talks.go diff --git a/boost/app.go b/boost/app.go index 8da58720..39658add 100644 --- a/boost/app.go +++ b/boost/app.go @@ -1,47 +1,51 @@ package boost import ( - "github.com/oullin/database" - "github.com/oullin/env" - "github.com/oullin/pkg" - "github.com/oullin/pkg/http/middleware" - "github.com/oullin/pkg/llogs" - baseHttp "net/http" + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg" + "github.com/oullin/pkg/http/middleware" + "github.com/oullin/pkg/llogs" + baseHttp "net/http" ) type App struct { - router *Router - sentry *pkg.Sentry - logs *llogs.Driver - validator *pkg.Validator - env *env.Environment - db *database.Connection + router *Router + sentry *pkg.Sentry + logs *llogs.Driver + validator *pkg.Validator + env *env.Environment + db *database.Connection } func MakeApp(env *env.Environment, validator *pkg.Validator) *App { - app := App{ - env: env, - validator: validator, - logs: MakeLogs(env), - sentry: MakeSentry(env), - db: MakeDbConnection(env), - } - - router := Router{ - Env: env, - Mux: baseHttp.NewServeMux(), - Pipeline: middleware.Pipeline{ - Env: env, - }, - } - - app.SetRouter(router) - - return &app + app := App{ + env: env, + validator: validator, + logs: MakeLogs(env), + sentry: MakeSentry(env), + db: MakeDbConnection(env), + } + + router := Router{ + Env: env, + Mux: baseHttp.NewServeMux(), + Pipeline: middleware.Pipeline{ + Env: env, + }, + } + + app.SetRouter(router) + + return &app } func (a *App) Boot() { - router := *a.router + router := *a.router - router.Profile() + router.Profile() + router.Experience() + router.Projects() + router.Social() + router.Talks() } diff --git a/boost/router.go b/boost/router.go index 852fe430..b280b9d4 100644 --- a/boost/router.go +++ b/boost/router.go @@ -14,26 +14,66 @@ type Router struct { Pipeline middleware.Pipeline } -func MakeRouter(mux *baseHttp.ServeMux) *Router { - return &Router{ - Mux: mux, - } -} - -func (r *Router) Profile() { +func (r *Router) PipelineFor(apiHandler http.ApiHandler) baseHttp.HandlerFunc { tokenMiddleware := middleware.MakeTokenMiddleware( r.Env.App.Credentials, ) - profileHandler := handler.MakeProfileHandler() - - getHandler := http.MakeApiHandler( + return http.MakeApiHandler( r.Pipeline.Chain( - profileHandler.Handle, + apiHandler, middleware.UsernameCheck, tokenMiddleware.Handle, ), ) +} + +func (r *Router) Profile() { + abstract := handler.MakeProfileHandler() + + resolver := r.PipelineFor( + abstract.Handle, + ) + + r.Mux.HandleFunc("GET /profile", resolver) +} + +func (r *Router) Experience() { + abstract := handler.MakeExperienceHandler() + + resolver := r.PipelineFor( + abstract.Handle, + ) + + r.Mux.HandleFunc("GET /experience", resolver) +} + +func (r *Router) Projects() { + abstract := handler.MakeProjectsHandler() + + resolver := r.PipelineFor( + abstract.Handle, + ) + + r.Mux.HandleFunc("GET /projects", resolver) +} + +func (r *Router) Social() { + abstract := handler.MakeSocialHandler() + + resolver := r.PipelineFor( + abstract.Handle, + ) + + r.Mux.HandleFunc("GET /social", resolver) +} + +func (r *Router) Talks() { + abstract := handler.MakeTalks() + + resolver := r.PipelineFor( + abstract.Handle, + ) - r.Mux.HandleFunc("GET /profile", getHandler) + r.Mux.HandleFunc("GET /talks", resolver) } diff --git a/handler/experience.go b/handler/experience.go new file mode 100644 index 00000000..b0a372b0 --- /dev/null +++ b/handler/experience.go @@ -0,0 +1,34 @@ +package handler + +import ( + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "os" +) + +type ExperienceHandler struct { + content string +} + +func MakeExperienceHandler() ExperienceHandler { + return ExperienceHandler{ + content: "./storage/fixture/experience.json", + } +} + +func (h ExperienceHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + fixture, err := os.ReadFile(h.content) + + if err != nil { + slog.Error("Error reading experience file: %v", err) + + return http.InternalError("could not read experience data") + } + + if err := writeJSON(fixture, w); err != nil { + return http.InternalError(err.Error()) + } + + return nil // A nil return indicates success. +} diff --git a/handler/projects.go b/handler/projects.go new file mode 100644 index 00000000..fcd37b90 --- /dev/null +++ b/handler/projects.go @@ -0,0 +1,34 @@ +package handler + +import ( + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "os" +) + +type ProjectsHandler struct { + content string +} + +func MakeProjectsHandler() ProjectsHandler { + return ProjectsHandler{ + content: "./storage/fixture/projects.json", + } +} + +func (h ProjectsHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + fixture, err := os.ReadFile(h.content) + + if err != nil { + slog.Error("Error reading projects file: %v", err) + + return http.InternalError("could not read projects data") + } + + if err := writeJSON(fixture, w); err != nil { + return http.InternalError(err.Error()) + } + + return nil // A nil return indicates success. +} diff --git a/handler/social.go b/handler/social.go new file mode 100644 index 00000000..b33e6ec3 --- /dev/null +++ b/handler/social.go @@ -0,0 +1,34 @@ +package handler + +import ( + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "os" +) + +type SocialHandler struct { + content string +} + +func MakeSocialHandler() SocialHandler { + return SocialHandler{ + content: "./storage/fixture/social.json", + } +} + +func (h SocialHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + fixture, err := os.ReadFile(h.content) + + if err != nil { + slog.Error("Error reading social file: %v", err) + + return http.InternalError("could not read social data") + } + + if err := writeJSON(fixture, w); err != nil { + return http.InternalError(err.Error()) + } + + return nil // A nil return indicates success. +} diff --git a/handler/talks.go b/handler/talks.go new file mode 100644 index 00000000..0caa2861 --- /dev/null +++ b/handler/talks.go @@ -0,0 +1,34 @@ +package handler + +import ( + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "os" +) + +type Talks struct { + content string +} + +func MakeTalks() Talks { + return Talks{ + content: "./storage/fixture/talks.json", + } +} + +func (h Talks) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + fixture, err := os.ReadFile(h.content) + + if err != nil { + slog.Error("Error reading talks file: %v", err) + + return http.InternalError("could not read talks data") + } + + if err := writeJSON(fixture, w); err != nil { + return http.InternalError(err.Error()) + } + + return nil // A nil return indicates success. +} From baf3d5df8f97d24146e65a6208011c8ba06c240f Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:07:36 +0800 Subject: [PATCH 16/24] format --- boost/app.go | 74 ++++++++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/boost/app.go b/boost/app.go index 39658add..00021077 100644 --- a/boost/app.go +++ b/boost/app.go @@ -1,51 +1,51 @@ package boost import ( - "github.com/oullin/database" - "github.com/oullin/env" - "github.com/oullin/pkg" - "github.com/oullin/pkg/http/middleware" - "github.com/oullin/pkg/llogs" - baseHttp "net/http" + "github.com/oullin/database" + "github.com/oullin/env" + "github.com/oullin/pkg" + "github.com/oullin/pkg/http/middleware" + "github.com/oullin/pkg/llogs" + baseHttp "net/http" ) type App struct { - router *Router - sentry *pkg.Sentry - logs *llogs.Driver - validator *pkg.Validator - env *env.Environment - db *database.Connection + router *Router + sentry *pkg.Sentry + logs *llogs.Driver + validator *pkg.Validator + env *env.Environment + db *database.Connection } func MakeApp(env *env.Environment, validator *pkg.Validator) *App { - app := App{ - env: env, - validator: validator, - logs: MakeLogs(env), - sentry: MakeSentry(env), - db: MakeDbConnection(env), - } - - router := Router{ - Env: env, - Mux: baseHttp.NewServeMux(), - Pipeline: middleware.Pipeline{ - Env: env, - }, - } - - app.SetRouter(router) - - return &app + app := App{ + env: env, + validator: validator, + logs: MakeLogs(env), + sentry: MakeSentry(env), + db: MakeDbConnection(env), + } + + router := Router{ + Env: env, + Mux: baseHttp.NewServeMux(), + Pipeline: middleware.Pipeline{ + Env: env, + }, + } + + app.SetRouter(router) + + return &app } func (a *App) Boot() { - router := *a.router + router := *a.router - router.Profile() - router.Experience() - router.Projects() - router.Social() - router.Talks() + router.Profile() + router.Experience() + router.Projects() + router.Social() + router.Talks() } From f335c1fc9790481f439bd39845ef43fed5b667ba Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:24:39 +0800 Subject: [PATCH 17/24] duplicated uuid --- storage/fixture/projects.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/storage/fixture/projects.json b/storage/fixture/projects.json index ff5756de..2c6bfb6e 100644 --- a/storage/fixture/projects.json +++ b/storage/fixture/projects.json @@ -11,7 +11,7 @@ "updated_at": "2023-10-05" }, { - "uuid": "00a0a12e-6af0-4f5a-b96d-3c95cc7c365c", + "uuid": "0b8e6ef9-8b4f-426f-b30a-b887c5a05030", "language": "Vue / TypeScript", "title": "Gus's personal website.", "excerpt": "Gus is a full-stack Software Engineer who has been building web technologies for more two decades.", From 0a101e90922845a32ec97939cf972ed4ac276e06 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:25:37 +0800 Subject: [PATCH 18/24] Fix semantic inconsistency between status code and error message. --- pkg/http/middleware/username.go | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/http/middleware/username.go b/pkg/http/middleware/username.go index 923d1cce..d3fda2d1 100644 --- a/pkg/http/middleware/username.go +++ b/pkg/http/middleware/username.go @@ -1,31 +1,31 @@ package middleware import ( - "fmt" - "github.com/oullin/pkg/http" - "log/slog" - baseHttp "net/http" - "strings" + "fmt" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "strings" ) const usernameHeader = "X-API-Username" func UsernameCheck(next http.ApiHandler) http.ApiHandler { - return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - username := strings.TrimSpace(r.Header.Get(usernameHeader)) + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + username := strings.TrimSpace(r.Header.Get(usernameHeader)) - if username != "gocanto" { - message := fmt.Sprintf("Forbidden: Invalid API username received ('%s')", username) - slog.Error(message) + if username != "gocanto" { + message := fmt.Sprintf("Unauthorized: Invalid API username received ('%s')", username) + slog.Error(message) - return &http.ApiError{ - Message: message, - Status: baseHttp.StatusUnauthorized, - } - } + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusUnauthorized, + } + } - slog.Info("Successfully authenticated user: gocanto") + slog.Info("Successfully authenticated user: gocanto") - return next(w, r) - } + return next(w, r) + } } From 301d0eba17bfc5baeeeaf9f383e459c638ad1551 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:26:16 +0800 Subject: [PATCH 19/24] Fix logging level for error messages. --- pkg/http/handler.go | 34 +++++++++++++++---------------- pkg/http/middleware/username.go | 36 ++++++++++++++++----------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/pkg/http/handler.go b/pkg/http/handler.go index bd769f68..e4db9c06 100644 --- a/pkg/http/handler.go +++ b/pkg/http/handler.go @@ -1,27 +1,27 @@ package http import ( - "encoding/json" - "log/slog" - baseHttp "net/http" + "encoding/json" + "log/slog" + baseHttp "net/http" ) func MakeApiHandler(fn ApiHandler) baseHttp.HandlerFunc { - return func(w baseHttp.ResponseWriter, r *baseHttp.Request) { - if err := fn(w, r); err != nil { - slog.Info("API Error: %s, Status: %d", err.Message, err.Status) + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) { + if err := fn(w, r); err != nil { + slog.Error("API Error: %s, Status: %d", err.Message, err.Status) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(err.Status) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.Status) - resp := ErrorResponse{ - Error: err.Message, - Status: err.Status, - } + resp := ErrorResponse{ + Error: err.Message, + Status: err.Status, + } - if result := json.NewEncoder(w).Encode(resp); result != nil { - slog.Error("Could not encode error response", "error", result) - } - } - } + if result := json.NewEncoder(w).Encode(resp); result != nil { + slog.Error("Could not encode error response", "error", result) + } + } + } } diff --git a/pkg/http/middleware/username.go b/pkg/http/middleware/username.go index d3fda2d1..73565ad0 100644 --- a/pkg/http/middleware/username.go +++ b/pkg/http/middleware/username.go @@ -1,31 +1,31 @@ package middleware import ( - "fmt" - "github.com/oullin/pkg/http" - "log/slog" - baseHttp "net/http" - "strings" + "fmt" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "strings" ) const usernameHeader = "X-API-Username" func UsernameCheck(next http.ApiHandler) http.ApiHandler { - return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - username := strings.TrimSpace(r.Header.Get(usernameHeader)) + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + username := strings.TrimSpace(r.Header.Get(usernameHeader)) - if username != "gocanto" { - message := fmt.Sprintf("Unauthorized: Invalid API username received ('%s')", username) - slog.Error(message) + if username != "gocanto" { + message := fmt.Sprintf("Unauthorized: Invalid API username received ('%s')", username) + slog.Error(message) - return &http.ApiError{ - Message: message, - Status: baseHttp.StatusUnauthorized, - } - } + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusUnauthorized, + } + } - slog.Info("Successfully authenticated user: gocanto") + slog.Info("Successfully authenticated user: gocanto") - return next(w, r) - } + return next(w, r) + } } From ca745f1b45aab8bb95d6ba6c4d7917bbd1059303 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:28:29 +0800 Subject: [PATCH 20/24] Fix slog.Error formatting issue. --- handler/experience.go | 2 +- handler/profile.go | 2 +- handler/projects.go | 34 +++++++++++++++++----------------- handler/social.go | 2 +- handler/talks.go | 2 +- pkg/http/handler.go | 34 +++++++++++++++++----------------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/handler/experience.go b/handler/experience.go index b0a372b0..028f893d 100644 --- a/handler/experience.go +++ b/handler/experience.go @@ -21,7 +21,7 @@ func (h ExperienceHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request fixture, err := os.ReadFile(h.content) if err != nil { - slog.Error("Error reading experience file: %v", err) + slog.Error("Error reading projects file", "error", err) return http.InternalError("could not read experience data") } diff --git a/handler/profile.go b/handler/profile.go index 50ec3cfb..c7613c39 100644 --- a/handler/profile.go +++ b/handler/profile.go @@ -21,7 +21,7 @@ func (h ProfileHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) * fixture, err := os.ReadFile(h.content) if err != nil { - slog.Error("Error reading profile file: %v", err) + slog.Error("Error reading projects file", "error", err) return http.InternalError("could not read profile data") } diff --git a/handler/projects.go b/handler/projects.go index fcd37b90..9a7eb810 100644 --- a/handler/projects.go +++ b/handler/projects.go @@ -1,34 +1,34 @@ package handler import ( - "github.com/oullin/pkg/http" - "log/slog" - baseHttp "net/http" - "os" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "os" ) type ProjectsHandler struct { - content string + content string } func MakeProjectsHandler() ProjectsHandler { - return ProjectsHandler{ - content: "./storage/fixture/projects.json", - } + return ProjectsHandler{ + content: "./storage/fixture/projects.json", + } } func (h ProjectsHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - fixture, err := os.ReadFile(h.content) + fixture, err := os.ReadFile(h.content) - if err != nil { - slog.Error("Error reading projects file: %v", err) + if err != nil { + slog.Error("Error reading projects file", "error", err) - return http.InternalError("could not read projects data") - } + return http.InternalError("could not read projects data") + } - if err := writeJSON(fixture, w); err != nil { - return http.InternalError(err.Error()) - } + if err := writeJSON(fixture, w); err != nil { + return http.InternalError(err.Error()) + } - return nil // A nil return indicates success. + return nil // A nil return indicates success. } diff --git a/handler/social.go b/handler/social.go index b33e6ec3..89131176 100644 --- a/handler/social.go +++ b/handler/social.go @@ -21,7 +21,7 @@ func (h SocialHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *h fixture, err := os.ReadFile(h.content) if err != nil { - slog.Error("Error reading social file: %v", err) + slog.Error("Error reading projects file", "error", err) return http.InternalError("could not read social data") } diff --git a/handler/talks.go b/handler/talks.go index 0caa2861..b71dd0b6 100644 --- a/handler/talks.go +++ b/handler/talks.go @@ -21,7 +21,7 @@ func (h Talks) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiE fixture, err := os.ReadFile(h.content) if err != nil { - slog.Error("Error reading talks file: %v", err) + slog.Error("Error reading projects file", "error", err) return http.InternalError("could not read talks data") } diff --git a/pkg/http/handler.go b/pkg/http/handler.go index e4db9c06..1777da54 100644 --- a/pkg/http/handler.go +++ b/pkg/http/handler.go @@ -1,27 +1,27 @@ package http import ( - "encoding/json" - "log/slog" - baseHttp "net/http" + "encoding/json" + "log/slog" + baseHttp "net/http" ) func MakeApiHandler(fn ApiHandler) baseHttp.HandlerFunc { - return func(w baseHttp.ResponseWriter, r *baseHttp.Request) { - if err := fn(w, r); err != nil { - slog.Error("API Error: %s, Status: %d", err.Message, err.Status) + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) { + if err := fn(w, r); err != nil { + slog.Error("API Error: %s, Status: %d", err.Message, err.Status) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(err.Status) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(err.Status) - resp := ErrorResponse{ - Error: err.Message, - Status: err.Status, - } + resp := ErrorResponse{ + Error: err.Message, + Status: err.Status, + } - if result := json.NewEncoder(w).Encode(resp); result != nil { - slog.Error("Could not encode error response", "error", result) - } - } - } + if result := json.NewEncoder(w).Encode(resp); result != nil { + slog.Error("Could not encode error response", "error", result) + } + } + } } From ff8f251fe0482fdc1782c52b81c0dd54e901678f Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:29:11 +0800 Subject: [PATCH 21/24] Fix typo in import alias. --- handler/projects.go | 34 +++++++++++++++--------------- pkg/http/middleware/token.go | 40 ++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/handler/projects.go b/handler/projects.go index 9a7eb810..2268d86c 100644 --- a/handler/projects.go +++ b/handler/projects.go @@ -1,34 +1,34 @@ package handler import ( - "github.com/oullin/pkg/http" - "log/slog" - baseHttp "net/http" - "os" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" + "os" ) type ProjectsHandler struct { - content string + content string } func MakeProjectsHandler() ProjectsHandler { - return ProjectsHandler{ - content: "./storage/fixture/projects.json", - } + return ProjectsHandler{ + content: "./storage/fixture/projects.json", + } } func (h ProjectsHandler) Handle(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - fixture, err := os.ReadFile(h.content) + fixture, err := os.ReadFile(h.content) - if err != nil { - slog.Error("Error reading projects file", "error", err) + if err != nil { + slog.Error("Error reading projects file", "error", err) - return http.InternalError("could not read projects data") - } + return http.InternalError("could not read projects data") + } - if err := writeJSON(fixture, w); err != nil { - return http.InternalError(err.Error()) - } + if err := writeJSON(fixture, w); err != nil { + return http.InternalError(err.Error()) + } - return nil // A nil return indicates success. + return nil // A nil return indicates success. } diff --git a/pkg/http/middleware/token.go b/pkg/http/middleware/token.go index f95256eb..6bba1d40 100644 --- a/pkg/http/middleware/token.go +++ b/pkg/http/middleware/token.go @@ -1,39 +1,39 @@ package middleware import ( - "github.com/oullin/pkg/auth" - "github.com/oullin/pkg/http" - "log/slog" - baseHtpp "net/http" + "github.com/oullin/pkg/auth" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" ) const tokenHeader = "X-API-Key" type TokenCheckMiddleware struct { - token auth.Token + token auth.Token } func MakeTokenMiddleware(token auth.Token) TokenCheckMiddleware { - return TokenCheckMiddleware{ - token: token, - } + return TokenCheckMiddleware{ + token: token, + } } func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { - return func(w baseHtpp.ResponseWriter, r *baseHtpp.Request) *http.ApiError { + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - if t.token.IsInvalid(r.Header.Get(tokenHeader)) { - message := "Forbidden: Invalid API seed" - slog.Error(message) + if t.token.IsInvalid(r.Header.Get(tokenHeader)) { + message := "Forbidden: Invalid API seed" + slog.Error(message) - return &http.ApiError{ - Message: message, - Status: baseHtpp.StatusForbidden, - } - } + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusForbidden, + } + } - slog.Info("Token validation successful") + slog.Info("Token validation successful") - return next(w, r) - } + return next(w, r) + } } From e043fda995c33127ec4eb54e5ff55756bf19f161 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:31:15 +0800 Subject: [PATCH 22/24] Add nil checks for safe resource cleanup --- boost/helpers.go | 34 ++++++++++++++++++++---------- pkg/http/middleware/token.go | 40 ++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/boost/helpers.go b/boost/helpers.go index f4047b4a..747652e9 100644 --- a/boost/helpers.go +++ b/boost/helpers.go @@ -1,33 +1,45 @@ package boost import ( - "github.com/oullin/database" - "github.com/oullin/env" - baseHttp "net/http" + "github.com/oullin/database" + "github.com/oullin/env" + baseHttp "net/http" ) func (a *App) SetRouter(router Router) { - a.router = &router + a.router = &router } func (a *App) CloseLogs() { - driver := *a.logs - driver.Close() + if a.logs == nil { + return + } + + driver := *a.logs + driver.Close() } func (a *App) CloseDB() { - driver := *a.db - driver.Close() + if a.db == nil { + return + } + + driver := *a.db + driver.Close() } func (a *App) GetEnv() *env.Environment { - return a.env + return a.env } func (a *App) GetDB() *database.Connection { - return a.db + return a.db } func (a *App) GetMux() *baseHttp.ServeMux { - return a.router.Mux + if a.router == nil { + return nil + } + + return a.router.Mux } diff --git a/pkg/http/middleware/token.go b/pkg/http/middleware/token.go index 6bba1d40..d1807afe 100644 --- a/pkg/http/middleware/token.go +++ b/pkg/http/middleware/token.go @@ -1,39 +1,39 @@ package middleware import ( - "github.com/oullin/pkg/auth" - "github.com/oullin/pkg/http" - "log/slog" - baseHttp "net/http" + "github.com/oullin/pkg/auth" + "github.com/oullin/pkg/http" + "log/slog" + baseHttp "net/http" ) const tokenHeader = "X-API-Key" type TokenCheckMiddleware struct { - token auth.Token + token auth.Token } func MakeTokenMiddleware(token auth.Token) TokenCheckMiddleware { - return TokenCheckMiddleware{ - token: token, - } + return TokenCheckMiddleware{ + token: token, + } } func (t TokenCheckMiddleware) Handle(next http.ApiHandler) http.ApiHandler { - return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { + return func(w baseHttp.ResponseWriter, r *baseHttp.Request) *http.ApiError { - if t.token.IsInvalid(r.Header.Get(tokenHeader)) { - message := "Forbidden: Invalid API seed" - slog.Error(message) + if t.token.IsInvalid(r.Header.Get(tokenHeader)) { + message := "Forbidden: Invalid API key" + slog.Error(message) - return &http.ApiError{ - Message: message, - Status: baseHttp.StatusForbidden, - } - } + return &http.ApiError{ + Message: message, + Status: baseHttp.StatusForbidden, + } + } - slog.Info("Token validation successful") + slog.Info("Token validation successful") - return next(w, r) - } + return next(w, r) + } } From 093c810da395c029f639c0537a5cb075e311392d Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:32:58 +0800 Subject: [PATCH 23/24] format --- boost/helpers.go | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/boost/helpers.go b/boost/helpers.go index 747652e9..1c56e36d 100644 --- a/boost/helpers.go +++ b/boost/helpers.go @@ -1,45 +1,45 @@ package boost import ( - "github.com/oullin/database" - "github.com/oullin/env" - baseHttp "net/http" + "github.com/oullin/database" + "github.com/oullin/env" + baseHttp "net/http" ) func (a *App) SetRouter(router Router) { - a.router = &router + a.router = &router } func (a *App) CloseLogs() { - if a.logs == nil { - return - } + if a.logs == nil { + return + } - driver := *a.logs - driver.Close() + driver := *a.logs + driver.Close() } func (a *App) CloseDB() { - if a.db == nil { - return - } + if a.db == nil { + return + } - driver := *a.db - driver.Close() + driver := *a.db + driver.Close() } func (a *App) GetEnv() *env.Environment { - return a.env + return a.env } func (a *App) GetDB() *database.Connection { - return a.db + return a.db } func (a *App) GetMux() *baseHttp.ServeMux { - if a.router == nil { - return nil - } + if a.router == nil { + return nil + } - return a.router.Mux + return a.router.Mux } From d36cb4b8d30d6c1117993622c58b15dfd2b756c5 Mon Sep 17 00:00:00 2001 From: Gustavo Ocanto Date: Mon, 23 Jun 2025 15:34:18 +0800 Subject: [PATCH 24/24] Add nil check before dereferencing router. --- boost/app.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/boost/app.go b/boost/app.go index 00021077..257bec6e 100644 --- a/boost/app.go +++ b/boost/app.go @@ -41,6 +41,10 @@ func MakeApp(env *env.Environment, validator *pkg.Validator) *App { } func (a *App) Boot() { + if a.router == nil { + panic("Router is not set") + } + router := *a.router router.Profile()