From b9ca3b4b5d63e44f8d451f02832d22923562cc9b Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Tue, 3 Jun 2025 01:13:21 +0530
Subject: [PATCH 01/32] feat: add github oAuth
---
.env.example | 4 +
.gitignore | 1 +
cmd/main.go | 33 ++++++++
go.mod | 22 ++++++
go.sum | 60 +++++++++++++++
internal/controllers/auth.go | 136 +++++++++++++++++++++++++++++++++
internal/middlewares/logger.go | 35 +++++++++
internal/routes/auth.go | 12 +++
internal/routes/router.go | 23 ++++++
9 files changed, 326 insertions(+)
create mode 100644 .env.example
create mode 100644 .gitignore
create mode 100644 cmd/main.go
create mode 100644 go.mod
create mode 100644 go.sum
create mode 100644 internal/controllers/auth.go
create mode 100644 internal/middlewares/logger.go
create mode 100644 internal/routes/auth.go
create mode 100644 internal/routes/router.go
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..0c9b1ca
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,4 @@
+GITHUB_CLIENT_ID=***
+GITHUB_CLIENT_SECRET=***
+LOG_LEVEL=debug # debug, info, warn, error, fatal
+PROD_DOMAIN=http://example.com # The domain where the app is hosted
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4c49bd7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.env
diff --git a/cmd/main.go b/cmd/main.go
new file mode 100644
index 0000000..50858fa
--- /dev/null
+++ b/cmd/main.go
@@ -0,0 +1,33 @@
+package main
+
+import (
+ log "github.com/sirupsen/logrus"
+ "net/http"
+ "os"
+
+ "github.com/joho/godotenv"
+ "github.com/var-code-5/devboard-backend/internal/routes"
+)
+
+func init() {
+ log.SetOutput(os.Stdout)
+ // log.SetFormatter(&log.JSONFormatter{}) // Uncomment this line to use JSON format for logs
+
+ logLevel, err := log.ParseLevel(os.Getenv("LOG_LEVEL"))
+ if err != nil {
+ logLevel = log.InfoLevel
+ }
+
+ log.SetLevel(logLevel)
+}
+
+func main() {
+ err := godotenv.Load()
+ if err != nil {
+ log.Fatal("Error loading .env file")
+ }
+
+ router := routes.Router()
+ log.Info("Starting server on :3000")
+ log.Fatal(http.ListenAndServe(":3000",router))
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..087c02f
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,22 @@
+module github.com/var-code-5/devboard-backend
+
+go 1.24.3
+
+require github.com/gorilla/mux v1.8.1
+
+require github.com/joho/godotenv v1.5.1
+
+require (
+ github.com/golang/snappy v1.0.0 // indirect
+ github.com/klauspost/compress v1.16.7 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+ github.com/xdg-go/scram v1.1.2 // indirect
+ github.com/xdg-go/stringprep v1.0.4 // indirect
+ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+ go.mongodb.org/mongo-driver/v2 v2.2.1 // indirect
+ golang.org/x/crypto v0.33.0 // indirect
+ golang.org/x/sync v0.11.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.22.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..d202299
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,60 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
+github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
+github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.mongodb.org/mongo-driver/v2 v2.2.1 h1:w5xra3yyu/sGrziMzK1D0cRRaH/b7lWCSsoN6+WV6AM=
+go.mongodb.org/mongo-driver/v2 v2.2.1/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
+golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go
new file mode 100644
index 0000000..9975b78
--- /dev/null
+++ b/internal/controllers/auth.go
@@ -0,0 +1,136 @@
+package controllers
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ log "github.com/sirupsen/logrus"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+)
+
+func generateRandomString(length int) (string, error) {
+ randomBytes := make([]byte, length)
+ _, err := rand.Read(randomBytes)
+ if err != nil {
+ return "", err
+ }
+ return base64.URLEncoding.EncodeToString(randomBytes), nil
+}
+
+func Login(w http.ResponseWriter, r *http.Request) {
+
+ // setting a random string for state param for CSRF protection
+ randomString, err := generateRandomString(8)
+ if err != nil {
+ log.Error("Error generating random string:", err)
+ return
+ }
+
+ cookie := &http.Cookie{
+ Name: "state",
+ Value: randomString,
+ HttpOnly: true,
+ Path: "/api/auth/",
+ MaxAge: 60 * 5, // 5 minutes
+ SameSite: http.SameSiteStrictMode,
+ }
+ http.SetCookie(w, cookie)
+
+ prodDomain := os.Getenv("PROD_DOMAIN")
+ if prodDomain == "" {
+ prodDomain = "http://127.0.0.1:3000"
+ }
+
+ oauth_url := url.URL{
+ Scheme: "https",
+ Host: "github.com",
+ Path: "/login/oauth/authorize",
+ RawQuery: url.Values{
+ "client_id": {os.Getenv("GITHUB_CLIENT_ID")},
+ "response_type": {"code"},
+ "scope": {"repo"},
+ "redirect_uri": {fmt.Sprintf("%s/api/auth/github/oauth2/callback", prodDomain)},
+ "state": {randomString},
+ }.Encode(),
+ }
+
+ http.Redirect(w, r, oauth_url.String(), 302)
+}
+
+type CallbackResponse struct {
+ AccessToken string `json:"access_token"`
+ Scope string `json:"scope"`
+ TokenType string `json:"token_type"`
+}
+
+func jsonErrorResponse(w http.ResponseWriter, status int, message string) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ json.NewEncoder(w).Encode(map[string]string{
+ "error": message,
+ })
+}
+
+func Callback(w http.ResponseWriter, r *http.Request) {
+ //verifying the state parameter to prevent CSRF attacks
+ queries := r.URL.Query()
+ state := queries.Get("state")
+ cookie, err := r.Cookie("state")
+ if err != nil {
+ jsonErrorResponse(w, http.StatusBadRequest, "State cookie not found - CSRF protection failed")
+ log.Error("Error retrieving state cookie:", err)
+ return
+ }
+ if state != cookie.Value {
+ jsonErrorResponse(w, http.StatusBadRequest, "State parameter does not match cookie value - CSRF protection failed")
+ log.Warn("State parameter does not match cookie value")
+ return
+ }
+
+ // Exchanging the code for an access token
+ code := queries.Get("code")
+ github_clientID := os.Getenv("GITHUB_CLIENT_ID")
+ github_clientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
+ if code == "" || github_clientID == "" || github_clientSecret == "" {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Missing code or GitHub client credentials")
+ log.Error("Missing code or GitHub client credentials")
+ return
+ }
+ data := url.Values{
+ "client_id": {github_clientID},
+ "client_secret": {github_clientSecret},
+ "code": {code},
+ }
+ req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", strings.NewReader(data.Encode()))
+ if err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request")
+ log.Error("Error creating request to exchange code for access token:", err)
+ return
+ }
+ req.Header.Add("Accept", "application/json")
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to exchange code for access token")
+ log.Error("Error exchanging code for access token:", err)
+ return
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ jsonErrorResponse(w, resp.StatusCode, "Failed to exchange code for access token")
+ log.Error("Error response from GitHub:", resp.Status)
+ return
+ }
+ var response CallbackResponse
+ if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode access token response")
+ log.Error("Error decoding access token response:", err)
+ return
+ }
+}
diff --git a/internal/middlewares/logger.go b/internal/middlewares/logger.go
new file mode 100644
index 0000000..d553d70
--- /dev/null
+++ b/internal/middlewares/logger.go
@@ -0,0 +1,35 @@
+package middlewares
+
+import (
+ "net/http"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+)
+
+type CustomResponseWriter struct {
+ http.ResponseWriter
+ StatusCode int
+}
+
+func (crw *CustomResponseWriter) WriteHeader(statusCode int) {
+ crw.StatusCode = statusCode
+ crw.ResponseWriter.WriteHeader(statusCode)
+}
+
+func Logger(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ crw := &CustomResponseWriter{ResponseWriter: w, StatusCode: http.StatusOK}
+
+ next.ServeHTTP(crw, r)
+
+ log.WithFields(log.Fields{
+ "method": r.Method,
+ "path": r.URL.Path,
+ "status": crw.StatusCode,
+ "latency_ns": time.Since(start).Nanoseconds(),
+ }).Info("request details")
+
+ })
+}
diff --git a/internal/routes/auth.go b/internal/routes/auth.go
new file mode 100644
index 0000000..784df50
--- /dev/null
+++ b/internal/routes/auth.go
@@ -0,0 +1,12 @@
+package routes
+
+import (
+ "github.com/gorilla/mux"
+ "github.com/var-code-5/devboard-backend/internal/controllers"
+)
+
+func RegisterAuth(r *mux.Router) {
+ s := r.PathPrefix("/auth").Subrouter()
+ s.HandleFunc("/login", controllers.Login).Methods("GET")
+ s.HandleFunc("/github/oauth2/callback", controllers.Callback).Methods("GET")
+}
diff --git a/internal/routes/router.go b/internal/routes/router.go
new file mode 100644
index 0000000..c12f602
--- /dev/null
+++ b/internal/routes/router.go
@@ -0,0 +1,23 @@
+package routes
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/var-code-5/devboard-backend/internal/middlewares"
+)
+
+func Router() *mux.Router {
+ r := mux.NewRouter()
+ r.Use(middlewares.Logger)
+
+ apiRouter := r.PathPrefix("/api").Subrouter()
+ apiRouter.HandleFunc("/ping", ping).Methods("GET")
+ RegisterAuth(apiRouter)
+
+ return r
+}
+
+func ping(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("pong"))
+}
From 115d4fa4b8d37c9905d804ca589eadebf03c3292 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Fri, 13 Jun 2025 18:07:32 +0530
Subject: [PATCH 02/32] feat:add jwt auth
---
.env.example | 12 ++--
cmd/main.go | 15 ++--
go.mod | 11 ++-
go.sum | 12 ++++
internal/controllers/auth.go | 131 +++++++++++++++++++++++++++++++---
internal/controllers/utils.go | 122 +++++++++++++++++++++++++++++++
internal/middlewares/auth.go | 28 ++++++++
internal/models/auth.go | 85 ++++++++++++++++++++++
internal/models/utils.go | 58 +++++++++++++++
internal/routes/auth.go | 5 ++
10 files changed, 455 insertions(+), 24 deletions(-)
create mode 100644 internal/controllers/utils.go
create mode 100644 internal/middlewares/auth.go
create mode 100644 internal/models/auth.go
create mode 100644 internal/models/utils.go
diff --git a/.env.example b/.env.example
index 0c9b1ca..555c898 100644
--- a/.env.example
+++ b/.env.example
@@ -1,4 +1,8 @@
-GITHUB_CLIENT_ID=***
-GITHUB_CLIENT_SECRET=***
-LOG_LEVEL=debug # debug, info, warn, error, fatal
-PROD_DOMAIN=http://example.com # The domain where the app is hosted
\ No newline at end of file
+GITHUB_CLIENT_ID=your_github_client_id
+GITHUB_CLIENT_SECRET=your_github_client_secret
+PROD_DOMAIN=http://localhost:3000
+LOG_LEVEL=debug
+MONGODB_URI=mongodb+srv://username:password@host/database
+MONGODB_DB_NAME=devBoard
+JWT_REFRESH_SECRET=your_refresh_secret_key
+JWT_ACCESS_SECRET=your_access_secret_key
\ No newline at end of file
diff --git a/cmd/main.go b/cmd/main.go
index 50858fa..dd02e80 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -1,15 +1,21 @@
package main
import (
- log "github.com/sirupsen/logrus"
"net/http"
"os"
+ log "github.com/sirupsen/logrus"
+
"github.com/joho/godotenv"
"github.com/var-code-5/devboard-backend/internal/routes"
)
func init() {
+ err := godotenv.Load()
+ if err != nil {
+ log.Fatal("Error loading .env file")
+ }
+
log.SetOutput(os.Stdout)
// log.SetFormatter(&log.JSONFormatter{}) // Uncomment this line to use JSON format for logs
@@ -22,12 +28,7 @@ func init() {
}
func main() {
- err := godotenv.Load()
- if err != nil {
- log.Fatal("Error loading .env file")
- }
-
router := routes.Router()
log.Info("Starting server on :3000")
- log.Fatal(http.ListenAndServe(":3000",router))
+ log.Fatal(http.ListenAndServe(":3000", router))
}
diff --git a/go.mod b/go.mod
index 087c02f..24d9c99 100644
--- a/go.mod
+++ b/go.mod
@@ -4,17 +4,22 @@ go 1.24.3
require github.com/gorilla/mux v1.8.1
-require github.com/joho/godotenv v1.5.1
+require (
+ github.com/joho/godotenv v1.5.1
+ github.com/sirupsen/logrus v1.9.3
+ go.mongodb.org/mongo-driver v1.17.3
+ go.mongodb.org/mongo-driver/v2 v2.2.1
+)
require (
+ github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
- github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/montanaflynn/stats v0.7.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
- go.mongodb.org/mongo-driver/v2 v2.2.1 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.33.0 // indirect
diff --git a/go.sum b/go.sum
index d202299..fcde996 100644
--- a/go.sum
+++ b/go.sum
@@ -1,17 +1,26 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
+github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
+github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -22,6 +31,8 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
+go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver/v2 v2.2.1 h1:w5xra3yyu/sGrziMzK1D0cRRaH/b7lWCSsoN6+WV6AM=
go.mongodb.org/mongo-driver/v2 v2.2.1/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -57,4 +68,5 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go
index 9975b78..c9bdf51 100644
--- a/internal/controllers/auth.go
+++ b/internal/controllers/auth.go
@@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
+ "github.com/var-code-5/devboard-backend/internal/models"
"net/http"
"net/url"
"os"
@@ -36,7 +37,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
HttpOnly: true,
Path: "/api/auth/",
MaxAge: 60 * 5, // 5 minutes
- SameSite: http.SameSiteStrictMode,
+ SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, cookie)
@@ -62,9 +63,11 @@ func Login(w http.ResponseWriter, r *http.Request) {
}
type CallbackResponse struct {
- AccessToken string `json:"access_token"`
- Scope string `json:"scope"`
- TokenType string `json:"token_type"`
+ AccessToken string `json:"access_token"`
+ Scope string `json:"scope"`
+ TokenType string `json:"token_type"`
+ Error string `json:"error,omitempty"`
+ ErrorDescription string `json:"error_description,omitempty"`
}
func jsonErrorResponse(w http.ResponseWriter, status int, message string) {
@@ -105,13 +108,14 @@ func Callback(w http.ResponseWriter, r *http.Request) {
"client_secret": {github_clientSecret},
"code": {code},
}
+
req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", strings.NewReader(data.Encode()))
if err != nil {
jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request")
log.Error("Error creating request to exchange code for access token:", err)
return
}
- req.Header.Add("Accept", "application/json")
+ req.Header.Add("Accept", "application/vnd.github+json")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{}
@@ -122,15 +126,122 @@ func Callback(w http.ResponseWriter, r *http.Request) {
return
}
defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- jsonErrorResponse(w, resp.StatusCode, "Failed to exchange code for access token")
- log.Error("Error response from GitHub:", resp.Status)
- return
- }
var response CallbackResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
jsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode access token response")
log.Error("Error decoding access token response:", err)
return
}
+
+ if resp.StatusCode != http.StatusOK {
+ jsonErrorResponse(w, resp.StatusCode, "Failed to exchange code for access token")
+ log.Error("Error response from GitHub:", response.Error, response.ErrorDescription)
+ return
+ }
+
+ //todo: remove the log in the production environment
+ log.Debug("Access token received successfully:", response.AccessToken)
+
+ req, err = http.NewRequest("GET", "https://api.github.com/user", nil)
+ if err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request to get user info")
+ log.Error("Error creating request to get user info:", err)
+ return
+ }
+ req.Header.Add("Authorization", "Bearer "+response.AccessToken)
+ client = &http.Client{}
+ resp, err = client.Do(req)
+ if err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to get user info")
+ log.Error("Error getting user info from GitHub:", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ var user models.User
+ if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode user info response")
+ log.Error("Error decoding user info response:", err)
+ return
+ }
+
+ user.AccessToken = response.AccessToken
+ if err := models.AddUser(user); err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to add user to database")
+ log.Error("Error adding user to database:", err)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+
+ jwtAccessToken, err := CreateAccessToken(user.Login)
+ if err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create JWT access token")
+ log.Error("Error creating JWT access token:", err)
+ return
+ }
+ jwtRefreshToken, err := CreateRefreshToken(user.Login)
+ if err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create JWT refresh token")
+ log.Error("Error creating JWT refresh token:", err)
+ return
+ }
+
+ json.NewEncoder(w).Encode(map[string]string{
+ "message": "User authenticated successfully",
+ "access_token": jwtAccessToken,
+ "refresh_token": jwtRefreshToken,
+ "login": user.Login,
+ "avatar_url": user.AvatarURL,
+ })
+
+ log.Info("User authenticated successfully:", user.Login)
+}
+
+func RefreshAccessToken(w http.ResponseWriter, r *http.Request) {
+ refreshToken := r.Header.Get("Authorization")
+ if refreshToken == "" {
+ jsonErrorResponse(w, http.StatusUnauthorized, "Refresh token is required")
+ log.Warn("Refresh token is missing in the request header")
+ return
+ }
+
+ token, err := VerifyRefreshToken(strings.Split(refreshToken, " ")[1])
+ if err != nil {
+ jsonErrorResponse(w, http.StatusUnauthorized, "Invalid refresh token")
+ log.Error("Error verifying refresh token:", err)
+ return
+ }
+
+ newAccessToken, err := CreateAccessToken(token.Username)
+ if err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create new access token")
+ log.Error("Error creating new access token:", err)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{
+ "message": "Access token refreshed successfully",
+ "access_token": newAccessToken,
+ })
+ log.Info("Access token refreshed successfully for user:", token.Username)
}
+
+func Logout(w http.ResponseWriter, r *http.Request) {
+ username := r.Context().Value("user").( string )
+ log.Debug("Logging out user:", username)
+ err := models.Logout(username)
+ if err != nil {
+ jsonErrorResponse(w, http.StatusInternalServerError, "Failed to logout user")
+ log.Error("Error logging out user:", err)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(map[string]string{
+ "message": "User logged out successfully",
+ })
+ log.Info("User logged out successfully:", username)
+}
\ No newline at end of file
diff --git a/internal/controllers/utils.go b/internal/controllers/utils.go
new file mode 100644
index 0000000..bac16d3
--- /dev/null
+++ b/internal/controllers/utils.go
@@ -0,0 +1,122 @@
+package controllers
+
+import (
+ "os"
+ "time"
+
+ "github.com/golang-jwt/jwt/v5"
+ log "github.com/sirupsen/logrus"
+)
+
+type Token struct {
+ Username string `json:"username"`
+ Exp int64 `json:"exp"`
+ Iat int64 `json:"iat"`
+ Iss string `json:"iss"`
+}
+
+func CreateAccessToken(username string) (string, error) {
+ var secretKey = os.Getenv("JWT_ACCESS_SECRET")
+ if secretKey == "" {
+ log.Error("JWT_ACCESS_SECRET environment variable is not set")
+ return "", nil
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256,
+ jwt.MapClaims{
+ "username": username,
+ "exp": time.Now().Add(time.Minute * 5).Unix(), // Token valid for 5 minutes
+ "iat": time.Now().Unix(), // Issued at time
+ "iss": "devboard-backend", // Issuer
+ })
+
+ tokenString, err := token.SignedString([]byte(secretKey))
+ if err != nil {
+ log.Error("Error signing access token:", err)
+ return "", err
+ }
+
+ return tokenString, nil
+}
+
+func CreateRefreshToken(username string) (string, error) {
+ var secretKey = os.Getenv("JWT_REFRESH_SECRET")
+ if secretKey == "" {
+ log.Error("JWT_REFRESH_SECRET environment variable is not set")
+ return "", nil
+ }
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256,
+ jwt.MapClaims{
+ "username": username,
+ "exp": time.Now().Add(time.Hour * 24 * 30).Unix(), // Token valid for 30 days
+ "iat": time.Now().Unix(), // Issued at time
+ "iss": "devboard-backend", // Issuer
+ })
+
+ tokenString, err := token.SignedString([]byte(secretKey))
+ if err != nil {
+ log.Error("Error signing refresh token:", err)
+ return "", err
+ }
+
+ return tokenString, nil
+}
+
+func VerifyAccessToken(tokenString string) (Token, error) {
+ var secretKey = os.Getenv("JWT_ACCESS_SECRET")
+ if secretKey == "" {
+ log.Error("JWT_ACCESS_SECRET environment variable is not set")
+ return Token{}, nil
+ }
+
+ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+ return []byte(secretKey), nil
+ })
+ if err != nil {
+ log.Error("Error parsing access token:", err)
+ return Token{}, err
+ }
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+
+ if !ok || !token.Valid {
+ log.Error("Invalid access token")
+ return Token{}, err
+ }
+
+ return Token{
+ Username: claims["username"].(string),
+ Exp: int64(claims["exp"].(float64)),
+ Iat: int64(claims["iat"].(float64)),
+ Iss: claims["iss"].(string),
+ }, nil
+}
+
+func VerifyRefreshToken(tokenString string) (Token, error) {
+ var secretKey = os.Getenv("JWT_REFRESH_SECRET")
+ if secretKey == "" {
+ log.Error("JWT_REFRESH_SECRET environment variable is not set")
+ return Token{}, nil
+ }
+
+ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
+ return []byte(secretKey), nil
+ })
+ if err != nil {
+ log.Error("Error parsing refresh token:", err)
+ return Token{}, err
+ }
+
+ claims, ok := token.Claims.(jwt.MapClaims)
+ if !ok || !token.Valid {
+ log.Error("Invalid refresh token")
+ return Token{}, err
+ }
+
+ return Token{
+ Username: claims["username"].(string),
+ Exp: int64(claims["exp"].(float64)),
+ Iat: int64(claims["iat"].(float64)),
+ Iss: claims["iss"].(string),
+ }, nil
+}
+
diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go
new file mode 100644
index 0000000..29ba7e8
--- /dev/null
+++ b/internal/middlewares/auth.go
@@ -0,0 +1,28 @@
+package middlewares
+
+import (
+ "context"
+ "net/http"
+ "strings"
+
+ "github.com/var-code-5/devboard-backend/internal/controllers"
+)
+
+func VerifyAccessToken(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ tokenString := r.Header.Get("Authorization")
+ if tokenString == "" {
+ http.Error(w, "Authorization header is missing", http.StatusUnauthorized)
+ return
+ }
+ token, err := controllers.VerifyAccessToken(strings.Split(tokenString, " ")[1])
+ if err != nil {
+ http.Error(w, "Invalid access token", http.StatusUnauthorized)
+ return
+ }
+
+ ctx := context.WithValue(r.Context(), "user", token.Username)
+ next.ServeHTTP(w, r.WithContext(ctx))
+
+ })
+}
diff --git a/internal/models/auth.go b/internal/models/auth.go
new file mode 100644
index 0000000..dab7dac
--- /dev/null
+++ b/internal/models/auth.go
@@ -0,0 +1,85 @@
+package models
+
+import (
+ "context"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/bson/primitive"
+ "go.mongodb.org/mongo-driver/mongo"
+)
+
+type User struct {
+ ID primitive.ObjectID `bson:"_id,omitempty" json:"-"` // Remove from JSON unmarshaling
+ GithubID int `json:"id" bson:"github_id"` // Map GitHub's "id" directly to GithubID
+ Login string `json:"login"`
+ NodeID string `json:"node_id"`
+ AvatarURL string `json:"avatar_url"`
+ GravatarID string `json:"gravatar_id"`
+ URL string `json:"url"`
+ HTMLURL string `json:"html_url"`
+ FollowersURL string `json:"followers_url"`
+ FollowingURL string `json:"following_url"`
+ GistsURL string `json:"gists_url"`
+ StarredURL string `json:"starred_url"`
+ SubscriptionsURL string `json:"subscriptions_url"`
+ OrganizationsURL string `json:"organizations_url"`
+ ReposURL string `json:"repos_url"`
+ EventsURL string `json:"events_url"`
+ ReceivedEventsURL string `json:"received_events_url"`
+ Type string `json:"type"`
+ UserViewType string `json:"user_view_type"`
+ SiteAdmin bool `json:"site_admin"`
+ Name string `json:"name"`
+ Company *string `json:"company"`
+ Blog string `json:"blog"`
+ Location *string `json:"location"`
+ Email *string `json:"email"`
+ Hireable *bool `json:"hireable"`
+ Bio *string `json:"bio"`
+ TwitterUsername *string `json:"twitter_username"`
+ NotificationEmail *string `json:"notification_email"`
+ PublicRepos int `json:"public_repos"`
+ PublicGists int `json:"public_gists"`
+ Followers int `json:"followers"`
+ Following int `json:"following"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+ AccessToken string `json:"access_token,omitempty" bson:"access_token,omitempty"`
+}
+
+func AddUser(user User) error {
+ collection, err := GetCollection("users")
+ if err != nil {
+ return err
+ }
+
+ // Check if user already exists in the database by GitHub ID
+ filter := bson.M{"github_id": user.GithubID}
+ existingUser := collection.FindOne(context.Background(), filter)
+ if existingUser.Err() == nil {
+ // User already exists upadte the access token
+ update := bson.M{"$set": bson.M{"access_token": user.AccessToken}}
+ _, err := collection.UpdateOne(context.Background(), filter, update)
+ if err != nil {
+ return err
+ }
+ return nil
+ } else if existingUser.Err() != mongo.ErrNoDocuments {
+ return existingUser.Err()
+ }
+
+ _, err = collection.InsertOne(context.Background(), user)
+ return err
+}
+
+func Logout(username string) error {
+ collection, err := GetCollection("users")
+ if err != nil {
+ return err
+ }
+
+ filter := bson.M{"login": username}
+ update := bson.M{"$set": bson.M{"access_token": ""}}
+ _, err = collection.UpdateOne(context.Background(), filter, update)
+ return err
+}
\ No newline at end of file
diff --git a/internal/models/utils.go b/internal/models/utils.go
new file mode 100644
index 0000000..3822bbc
--- /dev/null
+++ b/internal/models/utils.go
@@ -0,0 +1,58 @@
+package models
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/joho/godotenv"
+ log "github.com/sirupsen/logrus"
+ "go.mongodb.org/mongo-driver/mongo"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+var (
+ client *mongo.Client
+ dbName string
+ database *mongo.Database
+)
+
+func init() {
+ err := godotenv.Load()
+ if err != nil {
+ log.Fatal("Error loading .env file")
+ }
+
+ connectionURL := os.Getenv("MONGODB_URI")
+ dbName := os.Getenv("MONGODB_DB_NAME")
+
+ if connectionURL == "" || dbName == "" {
+ log.Fatal("MONGODB_URI or MONGODB_DB_NAME environment variable is not set")
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ clientOptions := options.Client().ApplyURI(connectionURL)
+ client, err := mongo.Connect(ctx, clientOptions)
+ if err != nil {
+ log.Fatalf("Failed to connect to MongoDB: %v", err)
+ return
+ }
+
+ if err := client.Ping(ctx, nil); err != nil {
+ log.Fatalf("MongoDB ping failed: %v", err)
+ }
+
+ database = client.Database(dbName)
+ log.Info("Successfully connected to MongoDB")
+}
+
+func GetCollection(collectionName string) (*mongo.Collection, error) {
+ if database == nil {
+ return nil, fmt.Errorf("database connection is not initialized")
+ }
+ return database.Collection(collectionName), nil
+}
diff --git a/internal/routes/auth.go b/internal/routes/auth.go
index 784df50..84cd7cf 100644
--- a/internal/routes/auth.go
+++ b/internal/routes/auth.go
@@ -1,12 +1,17 @@
package routes
import (
+ "net/http"
+
"github.com/gorilla/mux"
"github.com/var-code-5/devboard-backend/internal/controllers"
+ "github.com/var-code-5/devboard-backend/internal/middlewares"
)
func RegisterAuth(r *mux.Router) {
s := r.PathPrefix("/auth").Subrouter()
s.HandleFunc("/login", controllers.Login).Methods("GET")
s.HandleFunc("/github/oauth2/callback", controllers.Callback).Methods("GET")
+ s.Handle("/logout",middlewares.VerifyAccessToken(http.HandlerFunc(controllers.Logout))).Methods("GET")
+ s.HandleFunc("/access-token", controllers.RefreshAccessToken).Methods("GET")
}
From 1320a96c5607fd9f6dd303fb9587b8590c7ace87 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Fri, 13 Jun 2025 23:18:43 +0530
Subject: [PATCH 03/32] feat: add docker file
---
docker-compose.yml | 11 +++++++++++
dockerfile | 34 ++++++++++++++++++++++++++++++++++
internal/controllers/auth.go | 29 ++++++++++++++++++++++-------
3 files changed, 67 insertions(+), 7 deletions(-)
create mode 100644 docker-compose.yml
create mode 100644 dockerfile
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c9c070b
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,11 @@
+services:
+ app:
+ build:
+ context: .
+ dockerfile: dockerfile
+ ports:
+ - "3000:3000"
+ env_file:
+ - .env
+ volumes:
+ - .env:/app/.env
\ No newline at end of file
diff --git a/dockerfile b/dockerfile
new file mode 100644
index 0000000..4393c82
--- /dev/null
+++ b/dockerfile
@@ -0,0 +1,34 @@
+FROM golang:1.24-alpine AS builder
+
+WORKDIR /app
+
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o app ./cmd
+
+# Use a minimal Alpine image for the final container
+FROM alpine:3.19
+
+# Add ca-certificates for HTTPS and timezone data
+RUN apk --no-cache add ca-certificates tzdata
+
+# Create a non-root user and group
+RUN addgroup -S appgroup && adduser -S appuser -G appgroup
+
+# Set working directory
+WORKDIR /app
+
+# Copy the binary from the builder stage
+COPY --from=builder /app/app .
+
+# Use the non-root user
+USER appuser
+
+# Expose the port your application runs on (adjust as needed)
+EXPOSE 3000
+
+# Command to run the application
+CMD ["./app"]
\ No newline at end of file
diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go
index c9bdf51..b0bef46 100644
--- a/internal/controllers/auth.go
+++ b/internal/controllers/auth.go
@@ -187,14 +187,29 @@ func Callback(w http.ResponseWriter, r *http.Request) {
return
}
- json.NewEncoder(w).Encode(map[string]string{
- "message": "User authenticated successfully",
- "access_token": jwtAccessToken,
- "refresh_token": jwtRefreshToken,
- "login": user.Login,
- "avatar_url": user.AvatarURL,
- })
+ // json.NewEncoder(w).Encode(map[string]string{
+ // "message": "User authenticated successfully",
+ // "access_token": jwtAccessToken,
+ // "refresh_token": jwtRefreshToken,
+ // "login": user.Login,
+ // "avatar_url": user.AvatarURL,
+ // })
+
+ frontendURL := os.Getenv("FRONTEND_URL")
+ if frontendURL == "" {
+ log.Warn("FRONTEND_URL environment variable is not set")
+ jsonErrorResponse(w, http.StatusInternalServerError, "Frontend URL is not configured")
+ return
+ }
+ redirectURL := fmt.Sprintf("%s?access_token=%s&refresh_token=%s&login=%s&avatar_url=%s",
+ frontendURL,
+ url.QueryEscape(jwtAccessToken),
+ url.QueryEscape(jwtRefreshToken),
+ url.QueryEscape(user.Login),
+ url.QueryEscape(user.AvatarURL),
+ )
+ http.Redirect(w, r, redirectURL, http.StatusSeeOther)
log.Info("User authenticated successfully:", user.Login)
}
From 1df87d93d460f33cc349ede56a5573c58d257ae6 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sat, 14 Jun 2025 13:11:10 +0530
Subject: [PATCH 04/32] feat: add github featch readme
---
docker-compose.yml | 7 ++-
internal/controllers/auth.go | 46 ++++++--------
internal/controllers/github.go | 111 +++++++++++++++++++++++++++++++++
internal/controllers/utils.go | 11 +++-
internal/middlewares/auth.go | 29 +++++++--
internal/models/auth.go | 16 +++++
internal/models/utils.go | 2 +-
internal/routes/github.go | 14 +++++
internal/routes/router.go | 1 +
9 files changed, 202 insertions(+), 35 deletions(-)
create mode 100644 internal/controllers/github.go
create mode 100644 internal/routes/github.go
diff --git a/docker-compose.yml b/docker-compose.yml
index c9c070b..d7ec7fa 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,8 +4,11 @@ services:
context: .
dockerfile: dockerfile
ports:
- - "3000:3000"
+ - "80:3000"
+ # expose port 3000 on the container to port 80 on the host
+ #todo: add reverse proxy if needed
env_file:
- .env
volumes:
- - .env:/app/.env
\ No newline at end of file
+ - .env:/app/.env
+
\ No newline at end of file
diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go
index b0bef46..06cd08a 100644
--- a/internal/controllers/auth.go
+++ b/internal/controllers/auth.go
@@ -70,26 +70,18 @@ type CallbackResponse struct {
ErrorDescription string `json:"error_description,omitempty"`
}
-func jsonErrorResponse(w http.ResponseWriter, status int, message string) {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(status)
- json.NewEncoder(w).Encode(map[string]string{
- "error": message,
- })
-}
-
func Callback(w http.ResponseWriter, r *http.Request) {
//verifying the state parameter to prevent CSRF attacks
queries := r.URL.Query()
state := queries.Get("state")
cookie, err := r.Cookie("state")
if err != nil {
- jsonErrorResponse(w, http.StatusBadRequest, "State cookie not found - CSRF protection failed")
+ JsonErrorResponse(w, http.StatusBadRequest, "State cookie not found - CSRF protection failed")
log.Error("Error retrieving state cookie:", err)
return
}
if state != cookie.Value {
- jsonErrorResponse(w, http.StatusBadRequest, "State parameter does not match cookie value - CSRF protection failed")
+ JsonErrorResponse(w, http.StatusBadRequest, "State parameter does not match cookie value - CSRF protection failed")
log.Warn("State parameter does not match cookie value")
return
}
@@ -99,7 +91,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
github_clientID := os.Getenv("GITHUB_CLIENT_ID")
github_clientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
if code == "" || github_clientID == "" || github_clientSecret == "" {
- jsonErrorResponse(w, http.StatusInternalServerError, "Missing code or GitHub client credentials")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Missing code or GitHub client credentials")
log.Error("Missing code or GitHub client credentials")
return
}
@@ -111,7 +103,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
req, err := http.NewRequest("POST", "https://github.com/login/oauth/access_token", strings.NewReader(data.Encode()))
if err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request")
log.Error("Error creating request to exchange code for access token:", err)
return
}
@@ -121,20 +113,20 @@ func Callback(w http.ResponseWriter, r *http.Request) {
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to exchange code for access token")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to exchange code for access token")
log.Error("Error exchanging code for access token:", err)
return
}
defer resp.Body.Close()
var response CallbackResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode access token response")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode access token response")
log.Error("Error decoding access token response:", err)
return
}
if resp.StatusCode != http.StatusOK {
- jsonErrorResponse(w, resp.StatusCode, "Failed to exchange code for access token")
+ JsonErrorResponse(w, resp.StatusCode, "Failed to exchange code for access token")
log.Error("Error response from GitHub:", response.Error, response.ErrorDescription)
return
}
@@ -144,7 +136,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
req, err = http.NewRequest("GET", "https://api.github.com/user", nil)
if err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request to get user info")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request to get user info")
log.Error("Error creating request to get user info:", err)
return
}
@@ -152,7 +144,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
client = &http.Client{}
resp, err = client.Do(req)
if err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to get user info")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to get user info")
log.Error("Error getting user info from GitHub:", err)
return
}
@@ -160,14 +152,14 @@ func Callback(w http.ResponseWriter, r *http.Request) {
var user models.User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode user info response")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode user info response")
log.Error("Error decoding user info response:", err)
return
}
user.AccessToken = response.AccessToken
if err := models.AddUser(user); err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to add user to database")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to add user to database")
log.Error("Error adding user to database:", err)
return
}
@@ -176,13 +168,13 @@ func Callback(w http.ResponseWriter, r *http.Request) {
jwtAccessToken, err := CreateAccessToken(user.Login)
if err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create JWT access token")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create JWT access token")
log.Error("Error creating JWT access token:", err)
return
}
jwtRefreshToken, err := CreateRefreshToken(user.Login)
if err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create JWT refresh token")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create JWT refresh token")
log.Error("Error creating JWT refresh token:", err)
return
}
@@ -198,7 +190,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
frontendURL := os.Getenv("FRONTEND_URL")
if frontendURL == "" {
log.Warn("FRONTEND_URL environment variable is not set")
- jsonErrorResponse(w, http.StatusInternalServerError, "Frontend URL is not configured")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Frontend URL is not configured")
return
}
@@ -216,21 +208,21 @@ func Callback(w http.ResponseWriter, r *http.Request) {
func RefreshAccessToken(w http.ResponseWriter, r *http.Request) {
refreshToken := r.Header.Get("Authorization")
if refreshToken == "" {
- jsonErrorResponse(w, http.StatusUnauthorized, "Refresh token is required")
+ JsonErrorResponse(w, http.StatusUnauthorized, "Refresh token is required")
log.Warn("Refresh token is missing in the request header")
return
}
token, err := VerifyRefreshToken(strings.Split(refreshToken, " ")[1])
if err != nil {
- jsonErrorResponse(w, http.StatusUnauthorized, "Invalid refresh token")
+ JsonErrorResponse(w, http.StatusUnauthorized, "Invalid refresh token")
log.Error("Error verifying refresh token:", err)
return
}
newAccessToken, err := CreateAccessToken(token.Username)
if err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to create new access token")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create new access token")
log.Error("Error creating new access token:", err)
return
}
@@ -245,11 +237,11 @@ func RefreshAccessToken(w http.ResponseWriter, r *http.Request) {
}
func Logout(w http.ResponseWriter, r *http.Request) {
- username := r.Context().Value("user").( string )
+ username := r.Context().Value("userName").( string )
log.Debug("Logging out user:", username)
err := models.Logout(username)
if err != nil {
- jsonErrorResponse(w, http.StatusInternalServerError, "Failed to logout user")
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to logout user")
log.Error("Error logging out user:", err)
return
}
diff --git a/internal/controllers/github.go b/internal/controllers/github.go
new file mode 100644
index 0000000..df09987
--- /dev/null
+++ b/internal/controllers/github.go
@@ -0,0 +1,111 @@
+package controllers
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+type GitHubFile struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ SHA string `json:"sha"`
+ Size int `json:"size"`
+ URL string `json:"url"`
+ HTMLURL string `json:"html_url"`
+ GitURL string `json:"git_url"`
+ DownloadURL string `json:"download_url"`
+ Type string `json:"type"`
+ Content string `json:"content"`
+ Encoding string `json:"encoding"`
+}
+
+func checkProfileRepo(userName string, token string) bool {
+ url := fmt.Sprintf("https://api.github.com/repos/%s/%s", userName, userName)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return false
+ }
+
+ req.Header.Add("Authorization", "Bearer "+token)
+ req.Header.Add("Accept", "application/vnd.github.v3+json")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return false
+ }
+ defer resp.Body.Close()
+
+ return resp.StatusCode == http.StatusOK
+}
+
+func fetchReadMeFile(userName string, token string) (*GitHubFile, error) {
+ if !checkProfileRepo(userName, token) {
+ return nil, fmt.Errorf("repository %s/%s does not exist or is not accessible", userName, userName)
+ }
+
+ readmeNames := []string{"README.md", "readme.md", "Readme.md", "README.MD"}
+
+ for _, readmeName := range readmeNames {
+ url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", userName, userName, readmeName)
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ continue
+ }
+
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
+ req.Header.Set("Accept", "application/vnd.github.v3+json")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ continue
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusOK {
+ var file GitHubFile
+ if err := json.NewDecoder(resp.Body).Decode(&file); err != nil {
+ continue
+ }
+ return &file, nil
+ }
+ }
+
+ return nil, fmt.Errorf("README file not found in repository %s/%s", userName, userName)
+}
+
+func GetReadMe(w http.ResponseWriter, r *http.Request) {
+ userName := r.Context().Value("userName").(string)
+ token := r.Context().Value("accessToken").(string)
+
+ if userName == "" || token == "" {
+ http.Error(w, "Missing user or token parameter", http.StatusBadRequest)
+ return
+ }
+
+ readmeFile, err := fetchReadMeFile(userName, token)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusNotFound)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(readmeFile); err != nil {
+ http.Error(w, "Failed to encode response", http.StatusInternalServerError)
+ }
+}
+
+type UpdateFileRequest struct {
+ Content string `json:"content" binding:"required"`
+ SHA string `json:"sha" binding:"required"`
+ Owner string `json:"owner" binding:"required"`
+ Repo string `json:"repo" binding:"required"`
+ Message string `json:"message,omitempty"`
+}
+
+func UpdateFile(w http.ResponseWriter, r *http.Request) {
+
+}
\ No newline at end of file
diff --git a/internal/controllers/utils.go b/internal/controllers/utils.go
index bac16d3..9dcca33 100644
--- a/internal/controllers/utils.go
+++ b/internal/controllers/utils.go
@@ -1,6 +1,8 @@
package controllers
import (
+ "encoding/json"
+ "net/http"
"os"
"time"
@@ -15,6 +17,14 @@ type Token struct {
Iss string `json:"iss"`
}
+func JsonErrorResponse(w http.ResponseWriter, status int, message string) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ json.NewEncoder(w).Encode(map[string]string{
+ "error": message,
+ })
+}
+
func CreateAccessToken(username string) (string, error) {
var secretKey = os.Getenv("JWT_ACCESS_SECRET")
if secretKey == "" {
@@ -119,4 +129,3 @@ func VerifyRefreshToken(tokenString string) (Token, error) {
Iss: claims["iss"].(string),
}, nil
}
-
diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go
index 29ba7e8..8e92729 100644
--- a/internal/middlewares/auth.go
+++ b/internal/middlewares/auth.go
@@ -6,23 +6,44 @@ import (
"strings"
"github.com/var-code-5/devboard-backend/internal/controllers"
+ "github.com/var-code-5/devboard-backend/internal/models"
)
+func GetAccessToken(userName string) (string, error) {
+
+ accessToken, err := models.GetAccessToken(userName)
+ if err != nil {
+ return "", err
+ }
+ return accessToken, nil
+}
+
func VerifyAccessToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
- http.Error(w, "Authorization header is missing", http.StatusUnauthorized)
+ controllers.JsonErrorResponse(w, http.StatusUnauthorized, "Authorization header is missing")
return
}
token, err := controllers.VerifyAccessToken(strings.Split(tokenString, " ")[1])
if err != nil {
- http.Error(w, "Invalid access token", http.StatusUnauthorized)
+ controllers.JsonErrorResponse(w, http.StatusUnauthorized, "Invalid access token")
return
}
- ctx := context.WithValue(r.Context(), "user", token.Username)
- next.ServeHTTP(w, r.WithContext(ctx))
+ githubAccessToken, err := GetAccessToken(token.Username)
+ if err != nil {
+ controllers.JsonErrorResponse(w, http.StatusUnauthorized, "Failed to retrieve GitHub access token")
+ return
+ }
+ if githubAccessToken == "" {
+ controllers.JsonErrorResponse(w, http.StatusUnauthorized, "GitHub access token is missing")
+ return
+ }
+ ctx := context.WithValue(r.Context(), "userName", token.Username)
+ ctx = context.WithValue(ctx, "accessToken", githubAccessToken)
+ next.ServeHTTP(w, r.WithContext(ctx))
})
}
+
diff --git a/internal/models/auth.go b/internal/models/auth.go
index dab7dac..75cfa3e 100644
--- a/internal/models/auth.go
+++ b/internal/models/auth.go
@@ -82,4 +82,20 @@ func Logout(username string) error {
update := bson.M{"$set": bson.M{"access_token": ""}}
_, err = collection.UpdateOne(context.Background(), filter, update)
return err
+}
+
+func GetAccessToken(username string) (string, error) {
+ collection, err := GetCollection("users")
+ if err != nil {
+ return "", err
+ }
+
+ filter := bson.M{"login": username}
+ var user User
+ err = collection.FindOne(context.Background(), filter).Decode(&user)
+ if err != nil {
+ return "", err
+ }
+
+ return user.AccessToken, nil
}
\ No newline at end of file
diff --git a/internal/models/utils.go b/internal/models/utils.go
index 3822bbc..0df9ca6 100644
--- a/internal/models/utils.go
+++ b/internal/models/utils.go
@@ -55,4 +55,4 @@ func GetCollection(collectionName string) (*mongo.Collection, error) {
return nil, fmt.Errorf("database connection is not initialized")
}
return database.Collection(collectionName), nil
-}
+}
\ No newline at end of file
diff --git a/internal/routes/github.go b/internal/routes/github.go
new file mode 100644
index 0000000..ebc3336
--- /dev/null
+++ b/internal/routes/github.go
@@ -0,0 +1,14 @@
+package routes
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/var-code-5/devboard-backend/internal/controllers"
+ "github.com/var-code-5/devboard-backend/internal/middlewares"
+)
+
+func RegisterGitHub(r *mux.Router) {
+ s := r.PathPrefix("/github").Subrouter()
+ s.Handle("/readme", middlewares.VerifyAccessToken(http.HandlerFunc(controllers.GetReadMe))).Methods("GET")
+}
diff --git a/internal/routes/router.go b/internal/routes/router.go
index c12f602..bf092a0 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -14,6 +14,7 @@ func Router() *mux.Router {
apiRouter := r.PathPrefix("/api").Subrouter()
apiRouter.HandleFunc("/ping", ping).Methods("GET")
RegisterAuth(apiRouter)
+ RegisterGitHub(apiRouter)
return r
}
From 418937f9aac6d023be67eddf0e7b9fc3acf1fdc5 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sat, 14 Jun 2025 13:38:46 +0530
Subject: [PATCH 05/32] fix: frontend redirection
---
internal/controllers/auth.go | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go
index 06cd08a..abcbcc6 100644
--- a/internal/controllers/auth.go
+++ b/internal/controllers/auth.go
@@ -163,8 +163,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
log.Error("Error adding user to database:", err)
return
}
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusOK)
+
jwtAccessToken, err := CreateAccessToken(user.Login)
if err != nil {
@@ -179,6 +178,8 @@ func Callback(w http.ResponseWriter, r *http.Request) {
return
}
+ // w.Header().Set("Content-Type", "application/json")
+ // w.WriteHeader(http.StatusOK)
// json.NewEncoder(w).Encode(map[string]string{
// "message": "User authenticated successfully",
// "access_token": jwtAccessToken,
@@ -201,7 +202,8 @@ func Callback(w http.ResponseWriter, r *http.Request) {
url.QueryEscape(user.Login),
url.QueryEscape(user.AvatarURL),
)
- http.Redirect(w, r, redirectURL, http.StatusSeeOther)
+
+ http.Redirect(w, r, redirectURL, 302)
log.Info("User authenticated successfully:", user.Login)
}
From 055402d78aa9a0ac3e8d4e0b162894a6a5f4f6c1 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sat, 14 Jun 2025 14:06:41 +0530
Subject: [PATCH 06/32] feat: add cors
---
go.mod | 2 ++
go.sum | 4 ++++
internal/routes/router.go | 19 +++++++++++++++----
3 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/go.mod b/go.mod
index 24d9c99..0a9dbc8 100644
--- a/go.mod
+++ b/go.mod
@@ -12,8 +12,10 @@ require (
)
require (
+ github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
+ github.com/gorilla/handlers v1.5.2 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index fcde996..b3b0752 100644
--- a/go.sum
+++ b/go.sum
@@ -1,12 +1,16 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
+github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
+github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
diff --git a/internal/routes/router.go b/internal/routes/router.go
index bf092a0..7ebbd77 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -2,23 +2,34 @@ package routes
import (
"net/http"
-
"github.com/gorilla/mux"
+ "github.com/gorilla/handlers"
"github.com/var-code-5/devboard-backend/internal/middlewares"
)
func Router() *mux.Router {
r := mux.NewRouter()
+
+ // Add CORS middleware
+ corsHandler := handlers.CORS(
+ handlers.AllowedOrigins([]string{"http://localhost:3000"}),
+ handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
+ handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "X-Requested-With"}),
+ handlers.AllowCredentials(),
+ )
+
+ r.Use(corsHandler)
r.Use(middlewares.Logger)
-
+
apiRouter := r.PathPrefix("/api").Subrouter()
apiRouter.HandleFunc("/ping", ping).Methods("GET")
+
RegisterAuth(apiRouter)
RegisterGitHub(apiRouter)
-
+
return r
}
func ping(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("pong"))
-}
+}
\ No newline at end of file
From 7122e2cdef968a8a2720b808d068a1e2d71f0aec Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sat, 14 Jun 2025 15:08:42 +0530
Subject: [PATCH 07/32] feat: add options for the preflight req
---
.gitignore | 1 +
internal/routes/router.go | 27 +++++++++++++++++++++++----
2 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4c49bd7..335a441 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
.env
+cors-test.html
\ No newline at end of file
diff --git a/internal/routes/router.go b/internal/routes/router.go
index 7ebbd77..9b0c0ca 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -10,17 +10,36 @@ import (
func Router() *mux.Router {
r := mux.NewRouter()
- // Add CORS middleware
+ // Add CORS middleware with more permissive settings for development
corsHandler := handlers.CORS(
- handlers.AllowedOrigins([]string{"http://localhost:3000"}),
- handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
- handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "X-Requested-With"}),
+ handlers.AllowedOrigins([]string{"http://localhost:3000", "http://127.0.0.1:3000", "http://127.0.0.1:5500"}),
+ handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}),
+ handlers.AllowedHeaders([]string{
+ "Content-Type",
+ "Authorization",
+ "X-Requested-With",
+ "Accept",
+ "Origin",
+ "Cache-Control",
+ "X-File-Name",
+ }),
handlers.AllowCredentials(),
+ handlers.ExposedHeaders([]string{"*"}), // Add this for better compatibility
)
+ // Apply CORS middleware to the entire router
r.Use(corsHandler)
r.Use(middlewares.Logger)
+ // Handle preflight requests explicitly
+ r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Accept, Origin, Cache-Control, X-File-Name")
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ w.WriteHeader(http.StatusOK)
+ })
+
apiRouter := r.PathPrefix("/api").Subrouter()
apiRouter.HandleFunc("/ping", ping).Methods("GET")
From f943c2d72376a2826efe965fcfb8ad5089e4ed91 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sat, 14 Jun 2025 18:01:16 +0530
Subject: [PATCH 08/32] feat: add cd on oracle
---
.github/workflows/deploy.yml | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
create mode 100644 .github/workflows/deploy.yml
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..a145ec8
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,32 @@
+name: "Deploy to Oracle Cloud"
+
+on: workflow_dispatch
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up SSH
+ run: |
+ mkdir -p ~/.ssh
+ echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
+ chmod 600 ~/.ssh/id_rsa
+ ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
+
+ - name: Deploy to Oracle Cloud
+ run: |
+ echo "Deploying to Oracle Cloud..."
+ ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
+ echo "Stopping existing services..."
+ docker-compose down
+
+ echo "Navigating to devboard-backend and pulling latest changes..."
+ cd devboard-backend
+ git pull
+
+ echo "Starting services with fresh build..."
+ docker-compose up -d --build
+ EOF
+
+ - name: Notify Deployment Success
+ run: echo "Deployment to Oracle Cloud completed successfully."
From 987b6352842e281bd7d556436cb1547877940e2f Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sat, 14 Jun 2025 18:36:40 +0530
Subject: [PATCH 09/32] fix: cd workflow
---
.github/workflows/deploy.yml | 29 +++++++++++++++++++++++++----
1 file changed, 25 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index a145ec8..403246f 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -17,16 +17,37 @@ jobs:
run: |
echo "Deploying to Oracle Cloud..."
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
- echo "Stopping existing services..."
- docker-compose down
echo "Navigating to devboard-backend and pulling latest changes..."
cd devboard-backend
git pull
+ echo "Stopping existing services..."
+ docker compose down
+
echo "Starting services with fresh build..."
- docker-compose up -d --build
+ docker compose up -d --build
EOF
+ - name: Cleanup SSH
+ if: always()
+ run: |
+ rm -f ~/.ssh/id_rsa
+
- name: Notify Deployment Success
- run: echo "Deployment to Oracle Cloud completed successfully."
+ run: |
+ cat << 'EOF'
+ Deployment to Oracle Cloud completed successfully.
+
+ _____ _ _
+ | __ \ | | | |
+ | | | | ___ _ __ | | ___ _ _ ___ __| |
+ | | | |/ _ \ '_ \| |/ _ \| | | |/ _ \/ _` |
+ | |__| | __/ |_) | | (_) | |_| | __/ (_| |
+ |_____/ \___| .__/|_|\___/ \__, |\___|\__,_|
+ | | __/ |
+ |_| |___/
+ EOF
+
+
+
From a914759af1970021955e84ece85ada927b613d4e Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sat, 14 Jun 2025 18:42:51 +0530
Subject: [PATCH 10/32] fix: yml formating
---
.github/workflows/deploy.yml | 46 +++++++++++++++---------------------
1 file changed, 19 insertions(+), 27 deletions(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 403246f..5f87a52 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,5 +1,4 @@
name: "Deploy to Oracle Cloud"
-
on: workflow_dispatch
jobs:
@@ -12,42 +11,35 @@ jobs:
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
-
+
- name: Deploy to Oracle Cloud
run: |
echo "Deploying to Oracle Cloud..."
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
-
- echo "Navigating to devboard-backend and pulling latest changes..."
- cd devboard-backend
- git pull
-
- echo "Stopping existing services..."
- docker compose down
-
- echo "Starting services with fresh build..."
- docker compose up -d --build
+ echo "Navigating to devboard-backend and pulling latest changes..."
+ cd devboard-backend
+ git pull
+ echo "Stopping existing services..."
+ docker compose down
+ echo "Starting services with fresh build..."
+ docker compose up -d --build
EOF
-
+
- name: Cleanup SSH
if: always()
run: |
rm -f ~/.ssh/id_rsa
-
+
- name: Notify Deployment Success
run: |
cat << 'EOF'
Deployment to Oracle Cloud completed successfully.
-
- _____ _ _
- | __ \ | | | |
- | | | | ___ _ __ | | ___ _ _ ___ __| |
- | | | |/ _ \ '_ \| |/ _ \| | | |/ _ \/ _` |
- | |__| | __/ |_) | | (_) | |_| | __/ (_| |
- |_____/ \___| .__/|_|\___/ \__, |\___|\__,_|
- | | __/ |
- |_| |___/
- EOF
-
-
-
+ _____ _ _
+ | __ \ | | | |
+ | | | | ___ _ __| | ___ _ _ ___ __| |
+ | | | |/ _ \ '_ \ |/ _ \| | | |/ _ \/ _` |
+ | |__| | __/ |_) | | (_) | |_| | __/ (_| |
+ |_____/ \___| .__/|_|\___/ \__, |\___|\__,_|
+ | | __/ |
+ |_| |___/
+ EOF
\ No newline at end of file
From 7da87ce5af5cb43711e5663c5a9f104c95673f2d Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 09:30:12 +0530
Subject: [PATCH 11/32] feat: add redis cache
---
docker-compose.yml | 15 +++++-
go.mod | 3 ++
go.sum | 6 +++
internal/cache/auth.go | 90 ++++++++++++++++++++++++++++++++++++
internal/cache/utils.go | 33 +++++++++++++
internal/controllers/auth.go | 66 +++++++++++++++++---------
internal/middlewares/auth.go | 20 +++-----
internal/models/auth.go | 23 +++++++--
8 files changed, 215 insertions(+), 41 deletions(-)
create mode 100644 internal/cache/auth.go
create mode 100644 internal/cache/utils.go
diff --git a/docker-compose.yml b/docker-compose.yml
index d7ec7fa..eb6ed6f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,4 +11,17 @@ services:
- .env
volumes:
- .env:/app/.env
-
\ No newline at end of file
+ depends_on:
+ - redis
+
+ redis:
+ image: redis:alpine
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis-data:/data
+ command: redis-server --appendonly yes
+ restart: always
+
+volumes:
+ redis-data:
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 0a9dbc8..7801251 100644
--- a/go.mod
+++ b/go.mod
@@ -12,12 +12,15 @@ require (
)
require (
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
+ github.com/redis/go-redis/v9 v9.10.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
diff --git a/go.sum b/go.sum
index b3b0752..edf8ec8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,10 @@
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
@@ -21,6 +25,8 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
+github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/internal/cache/auth.go b/internal/cache/auth.go
new file mode 100644
index 0000000..b4fdea5
--- /dev/null
+++ b/internal/cache/auth.go
@@ -0,0 +1,90 @@
+package cache
+
+import (
+ "context"
+ "encoding/json"
+ "time"
+
+ "github.com/redis/go-redis/v9"
+ "github.com/var-code-5/devboard-backend/internal/models"
+)
+
+var ctx = context.Background()
+
+type TokenData struct {
+ GithubAccessToken string `json:"github_access_token"`
+ RefreshToken string `json:"refresh_token"`
+}
+
+func SetTokens(username string, accessToken string, refreshToken string) error {
+ client := GetClient()
+ tokenData := TokenData{
+ GithubAccessToken: accessToken,
+ RefreshToken: refreshToken,
+ }
+
+ jsonData, err := json.Marshal(tokenData)
+ if err != nil {
+ return err
+ }
+
+ err = client.Set(ctx, username, jsonData, time.Hour*1).Err()
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func GetTokens(username string) (*TokenData, error) {
+ client := GetClient()
+
+ // Get JSON data from Redis
+ jsonData, err := client.Get(ctx, username).Result()
+ if err != nil {
+ if err == redis.Nil {
+ // Fallback to database
+ accessToken, err := models.GetAccessToken(username)
+ if err != nil {
+ return nil, err
+ }
+ refreshToken, err := models.GetRefreshToken(username)
+ if err != nil {
+ return nil, err
+ }
+
+ if accessToken == "" && refreshToken == "" {
+ return nil, nil // No tokens found
+ }
+
+ tokenData := &TokenData{
+ GithubAccessToken: accessToken,
+ RefreshToken: refreshToken,
+ }
+
+ err = SetTokens(username, accessToken, refreshToken)
+ if err != nil {
+ return nil, err
+ }
+ return tokenData, nil
+ }
+ return nil, err
+ }
+
+ var tokenData TokenData
+ err = json.Unmarshal([]byte(jsonData), &tokenData)
+ if err != nil {
+ return nil, err
+ }
+
+ return &tokenData, nil
+}
+
+func Logout(username string) error {
+ client := GetClient()
+ // Remove the token from cache
+ err := client.Del(ctx, username).Err()
+ if err != nil {
+ return err
+ }
+ return models.Logout(username)
+}
\ No newline at end of file
diff --git a/internal/cache/utils.go b/internal/cache/utils.go
new file mode 100644
index 0000000..f75ebb3
--- /dev/null
+++ b/internal/cache/utils.go
@@ -0,0 +1,33 @@
+package cache
+
+import (
+ "os"
+
+ "github.com/joho/godotenv"
+ "github.com/redis/go-redis/v9"
+)
+
+var client *redis.Client
+
+func init() {
+ err := godotenv.Load()
+ if err != nil {
+ panic("Error loading .env file")
+ }
+
+ redisUrl := os.Getenv("REDIS_URL")
+ opt, err := redis.ParseURL(redisUrl)
+ if err != nil {
+ panic(err)
+ }
+
+ client = redis.NewClient(opt)
+}
+
+
+func GetClient() *redis.Client {
+ if client == nil {
+ panic("Redis client is not initialized")
+ }
+ return client
+}
diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go
index abcbcc6..ccc0a36 100644
--- a/internal/controllers/auth.go
+++ b/internal/controllers/auth.go
@@ -5,12 +5,14 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
- log "github.com/sirupsen/logrus"
- "github.com/var-code-5/devboard-backend/internal/models"
"net/http"
"net/url"
"os"
"strings"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/var-code-5/devboard-backend/internal/cache"
+ "github.com/var-code-5/devboard-backend/internal/models"
)
func generateRandomString(length int) (string, error) {
@@ -157,12 +159,6 @@ func Callback(w http.ResponseWriter, r *http.Request) {
return
}
- user.AccessToken = response.AccessToken
- if err := models.AddUser(user); err != nil {
- JsonErrorResponse(w, http.StatusInternalServerError, "Failed to add user to database")
- log.Error("Error adding user to database:", err)
- return
- }
jwtAccessToken, err := CreateAccessToken(user.Login)
@@ -178,15 +174,20 @@ func Callback(w http.ResponseWriter, r *http.Request) {
return
}
- // w.Header().Set("Content-Type", "application/json")
- // w.WriteHeader(http.StatusOK)
- // json.NewEncoder(w).Encode(map[string]string{
- // "message": "User authenticated successfully",
- // "access_token": jwtAccessToken,
- // "refresh_token": jwtRefreshToken,
- // "login": user.Login,
- // "avatar_url": user.AvatarURL,
- // })
+ // set db and cache
+ user.AccessToken = response.AccessToken
+ user.RefreshToken = jwtRefreshToken
+ if err := models.AddUser(user); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to add user to database")
+ log.Error("Error adding user to database:", err)
+ return
+ }
+
+ if err := cache.SetTokens(user.Login, user.AccessToken, jwtRefreshToken); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to store tokens in cache")
+ log.Error("Error storing tokens in cache:", err)
+ return
+ }
frontendURL := os.Getenv("FRONTEND_URL")
if frontendURL == "" {
@@ -202,7 +203,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
url.QueryEscape(user.Login),
url.QueryEscape(user.AvatarURL),
)
-
+
http.Redirect(w, r, redirectURL, 302)
log.Info("User authenticated successfully:", user.Login)
}
@@ -222,6 +223,25 @@ func RefreshAccessToken(w http.ResponseWriter, r *http.Request) {
return
}
+ if token.Username == "" {
+ JsonErrorResponse(w, http.StatusUnauthorized, "Invalid refresh token - username not found")
+ log.Warn("Invalid refresh token - username not found")
+ return
+ }
+
+ tokens, err := cache.GetTokens(token.Username)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to fetch tokens")
+ log.Error("Error getting tokens from cache:", err)
+ return
+ }
+
+ if tokens.RefreshToken != strings.Split(refreshToken, " ")[1] {
+ JsonErrorResponse(w, http.StatusUnauthorized, "Invalid refresh token")
+ log.Warn("Invalid refresh token")
+ return
+ }
+
newAccessToken, err := CreateAccessToken(token.Username)
if err != nil {
JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create new access token")
@@ -232,16 +252,16 @@ func RefreshAccessToken(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{
- "message": "Access token refreshed successfully",
- "access_token": newAccessToken,
+ "message": "Access token refreshed successfully",
+ "access_token": newAccessToken,
})
log.Info("Access token refreshed successfully for user:", token.Username)
}
func Logout(w http.ResponseWriter, r *http.Request) {
- username := r.Context().Value("userName").( string )
+ username := r.Context().Value("userName").(string)
log.Debug("Logging out user:", username)
- err := models.Logout(username)
+ err := cache.Logout(username)
if err != nil {
JsonErrorResponse(w, http.StatusInternalServerError, "Failed to logout user")
log.Error("Error logging out user:", err)
@@ -253,4 +273,4 @@ func Logout(w http.ResponseWriter, r *http.Request) {
"message": "User logged out successfully",
})
log.Info("User logged out successfully:", username)
-}
\ No newline at end of file
+}
diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go
index 8e92729..c08cb6d 100644
--- a/internal/middlewares/auth.go
+++ b/internal/middlewares/auth.go
@@ -5,19 +5,10 @@ import (
"net/http"
"strings"
+ "github.com/var-code-5/devboard-backend/internal/cache"
"github.com/var-code-5/devboard-backend/internal/controllers"
- "github.com/var-code-5/devboard-backend/internal/models"
)
-func GetAccessToken(userName string) (string, error) {
-
- accessToken, err := models.GetAccessToken(userName)
- if err != nil {
- return "", err
- }
- return accessToken, nil
-}
-
func VerifyAccessToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
@@ -31,18 +22,19 @@ func VerifyAccessToken(next http.Handler) http.Handler {
return
}
- githubAccessToken, err := GetAccessToken(token.Username)
+ tokens, err := cache.GetTokens(token.Username)
+
if err != nil {
controllers.JsonErrorResponse(w, http.StatusUnauthorized, "Failed to retrieve GitHub access token")
return
}
- if githubAccessToken == "" {
- controllers.JsonErrorResponse(w, http.StatusUnauthorized, "GitHub access token is missing")
+ if tokens.GithubAccessToken == "" {
+ controllers.JsonErrorResponse(w, http.StatusUnauthorized, "GitHub access token is missing please login")
return
}
ctx := context.WithValue(r.Context(), "userName", token.Username)
- ctx = context.WithValue(ctx, "accessToken", githubAccessToken)
+ ctx = context.WithValue(ctx, "accessToken", tokens.GithubAccessToken)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
diff --git a/internal/models/auth.go b/internal/models/auth.go
index 75cfa3e..92f2584 100644
--- a/internal/models/auth.go
+++ b/internal/models/auth.go
@@ -45,6 +45,7 @@ type User struct {
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
AccessToken string `json:"access_token,omitempty" bson:"access_token,omitempty"`
+ RefreshToken string `json:"refresh_token,omitempty" bson:"refresh_token,omitempty"`
}
func AddUser(user User) error {
@@ -57,8 +58,8 @@ func AddUser(user User) error {
filter := bson.M{"github_id": user.GithubID}
existingUser := collection.FindOne(context.Background(), filter)
if existingUser.Err() == nil {
- // User already exists upadte the access token
- update := bson.M{"$set": bson.M{"access_token": user.AccessToken}}
+ // User already exists update the access token
+ update := bson.M{"$set": bson.M{"access_token": user.AccessToken, "refresh_token": user.RefreshToken}}
_, err := collection.UpdateOne(context.Background(), filter, update)
if err != nil {
return err
@@ -79,7 +80,7 @@ func Logout(username string) error {
}
filter := bson.M{"login": username}
- update := bson.M{"$set": bson.M{"access_token": ""}}
+ update := bson.M{"$set": bson.M{"access_token": "", "refresh_token": ""}}
_, err = collection.UpdateOne(context.Background(), filter, update)
return err
}
@@ -98,4 +99,20 @@ func GetAccessToken(username string) (string, error) {
}
return user.AccessToken, nil
+}
+
+func GetRefreshToken(username string) (string, error) {
+ collection, err := GetCollection("users")
+ if err != nil {
+ return "", err
+ }
+
+ filter := bson.M{"login": username}
+ var user User
+ err = collection.FindOne(context.Background(), filter).Decode(&user)
+ if err != nil {
+ return "", err
+ }
+
+ return user.RefreshToken, nil
}
\ No newline at end of file
From 59cd247397c2c4558a2bd403b1298912a8f4ed2a Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 11:01:50 +0530
Subject: [PATCH 12/32] refactor: to use docker hub image
---
docker-compose.yml | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index eb6ed6f..0b9c03b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,8 +1,9 @@
services:
app:
- build:
- context: .
- dockerfile: dockerfile
+ # build:
+ # context: .
+ # dockerfile: dockerfile
+ image: varcode05/devboard-backend:latest
ports:
- "80:3000"
# expose port 3000 on the container to port 80 on the host
From dc39a7cb0b59daa8e33f04fda6b5dda1f581097f Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 11:21:04 +0530
Subject: [PATCH 13/32] feat: add image autobuild workflow
---
.github/workflows/deploy.yml | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 5f87a52..b4aa99e 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -5,6 +5,29 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: true
+ tags: |
+ varcode05/devboard-backend:latest
+ varcode05/devboard-backend:${{ github.sha }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
- name: Set up SSH
run: |
mkdir -p ~/.ssh
From d1dbb62691b4c01d5cf7b617c869af1fe5aaf8f5 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 13:10:16 +0530
Subject: [PATCH 14/32] feat: add upadte and create readme routes
---
internal/controllers/github.go | 229 ++++++++++++++++++++++++++++++++-
internal/routes/github.go | 7 +-
2 files changed, 228 insertions(+), 8 deletions(-)
diff --git a/internal/controllers/github.go b/internal/controllers/github.go
index df09987..1849b55 100644
--- a/internal/controllers/github.go
+++ b/internal/controllers/github.go
@@ -1,9 +1,12 @@
package controllers
import (
+ "bytes"
"encoding/json"
"fmt"
"net/http"
+
+ log "github.com/sirupsen/logrus"
)
type GitHubFile struct {
@@ -99,13 +102,229 @@ func GetReadMe(w http.ResponseWriter, r *http.Request) {
}
type UpdateFileRequest struct {
- Content string `json:"content" binding:"required"`
- SHA string `json:"sha" binding:"required"`
- Owner string `json:"owner" binding:"required"`
- Repo string `json:"repo" binding:"required"`
- Message string `json:"message,omitempty"`
+ Content string `json:"content" binding:"required"`
+ SHA string `json:"sha" binding:"required"`
+ Message string `json:"message,omitempty"`
+}
+
+type GitHubUpdateResponse struct {
+ Content struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ SHA string `json:"sha"`
+ Size int `json:"size"`
+ URL string `json:"url"`
+ HTMLURL string `json:"html_url"`
+ GitURL string `json:"git_url"`
+ DownloadURL string `json:"download_url"`
+ Type string `json:"type"`
+ } `json:"content"`
+ Commit struct {
+ SHA string `json:"sha"`
+ URL string `json:"url"`
+ HTMLURL string `json:"html_url"`
+ Author struct {
+ Date string `json:"date"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ } `json:"author"`
+ Committer struct {
+ Date string `json:"date"`
+ Name string `json:"name"`
+ Email string `json:"email"`
+ } `json:"committer"`
+ Message string `json:"message"`
+ } `json:"commit"`
}
func UpdateFile(w http.ResponseWriter, r *http.Request) {
+ var updateReq UpdateFileRequest
+ if err := json.NewDecoder(r.Body).Decode(&updateReq); err != nil {
+ JsonErrorResponse(w, http.StatusBadRequest, "Invalid request body")
+ return
+ }
+
+ if updateReq.Content == "" || updateReq.SHA == "" {
+ JsonErrorResponse(w, http.StatusBadRequest, "Missing required fields")
+ return
+ }
+
+ userName := r.Context().Value("userName").(string)
+ url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/README.md", userName, userName)
+ if updateReq.Message == "" {
+ updateReq.Message = "feat: Update README.md"
+ }
+
+ reqBody := map[string]interface{}{
+ "message": updateReq.Message,
+ "content": updateReq.Content,
+ "sha": updateReq.SHA,
+ }
+
+ reqBodyJSON, _ := json.Marshal(reqBody)
+
+ req, err := http.NewRequest("PUT", url, bytes.NewBuffer(reqBodyJSON))
+ if err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request")
+ log.Errorf("Failed to create request: %v", err)
+ return
+ }
+
+ token := r.Context().Value("accessToken").(string)
+ req.Header.Add("Authorization", "Bearer "+token)
+ req.Header.Add("Accept", "application/vnd.github.v3+json")
+ req.Header.Add("Content-Type", "application/json")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to update file")
+ log.Errorf("Failed to update file: %v", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ log.Error("Error updating README file:", err)
+
+ // Check if it's a specific GitHub API error
+ if resp.StatusCode == http.StatusConflict {
+ JsonErrorResponse(w, http.StatusConflict, "File has been modified. Please refresh and try again.")
+ return
+ } else if resp.StatusCode == http.StatusNotFound {
+ JsonErrorResponse(w, http.StatusNotFound, "Repository or file not found")
+ return
+ } else if resp.StatusCode == http.StatusForbidden {
+ JsonErrorResponse(w, http.StatusForbidden, "Permission denied. Check repository access.")
+ return
+ }
+
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to update README file")
+ return
+ }
+
+ var updateResponse GitHubUpdateResponse
+ if err := json.NewDecoder(resp.Body).Decode(&updateResponse); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode response")
+ log.Errorf("Failed to decode response: %v", err)
+ return
+ }
+
+ log.Info("README file updated successfully for user:", userName)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ if err := json.NewEncoder(w).Encode(updateResponse); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to encode response")
+ log.Errorf("Failed to encode response: %v", err)
+ return
+ }
+}
+
+func createRepo(userName string, token string) error {
+ url := fmt.Sprintf("https://api.github.com/user/repos")
+ reqBody := map[string]interface{}{
+ "name": userName,
+ "private": false,
+ }
+
+ reqBodyJSON, _ := json.Marshal(reqBody)
+
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBodyJSON))
+ if err != nil {
+ return fmt.Errorf("failed to create request: %v", err)
+ }
+
+ req.Header.Add("Authorization", "Bearer "+token)
+ req.Header.Add("Accept", "application/vnd.github.v3+json")
+ req.Header.Add("Content-Type", "application/json")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to create repository: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusCreated {
+ return fmt.Errorf("failed to create repository: %v", err)
+ }
+ return nil
+}
+
+type CreateFileRequest struct {
+ Content string `json:"content" binding:"required"`
+ Message string `json:"message,omitempty"`
+}
+
+func CreateFile(w http.ResponseWriter, r *http.Request) {
+ userName := r.Context().Value("userName").(string)
+ token := r.Context().Value("accessToken").(string)
+
+ if userName == "" || token == "" {
+ JsonErrorResponse(w, http.StatusBadRequest, "Missing user or token parameter")
+ return
+ }
+
+ if !checkProfileRepo(userName, token) {
+ if err := createRepo(userName, token); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+ }
+
+ url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/README.md", userName, userName)
+ var createReq CreateFileRequest
+ if err := json.NewDecoder(r.Body).Decode(&createReq); err != nil {
+ JsonErrorResponse(w, http.StatusBadRequest, "Invalid request body")
+ return
+ }
+
+ if createReq.Message == "" {
+ createReq.Message = "feat: Create README.md"
+ }
+
+ reqBody := map[string]interface{}{
+ "message": createReq.Message,
+ "content": createReq.Content,
+ }
+ reqBodyJSON, _ := json.Marshal(reqBody)
+
+ req,err := http.NewRequest("PUT", url, bytes.NewBuffer(reqBodyJSON))
+ if err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create request")
+ log.Errorf("Failed to create request: %v", err)
+ return
+ }
+ req.Header.Add("Authorization", "Bearer "+token)
+ req.Header.Add("Accept", "application/vnd.github.v3+json")
+ req.Header.Add("Content-Type", "application/json")
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create file")
+ log.Errorf("Failed to create file: %v", err)
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusCreated {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to create file")
+ log.Errorf("Failed to create file: %v", err)
+ return
+ }
+ var createResponse GitHubUpdateResponse
+ if err := json.NewDecoder(resp.Body).Decode(&createResponse); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to decode response")
+ log.Errorf("Failed to decode response: %v", err)
+ return
+ }
+ log.Info("README file created successfully for user:", userName)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ if err := json.NewEncoder(w).Encode(createResponse); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to encode response")
+ log.Errorf("Failed to encode response: %v", err)
+ return
+ }
}
\ No newline at end of file
diff --git a/internal/routes/github.go b/internal/routes/github.go
index ebc3336..e241e15 100644
--- a/internal/routes/github.go
+++ b/internal/routes/github.go
@@ -1,8 +1,6 @@
package routes
import (
- "net/http"
-
"github.com/gorilla/mux"
"github.com/var-code-5/devboard-backend/internal/controllers"
"github.com/var-code-5/devboard-backend/internal/middlewares"
@@ -10,5 +8,8 @@ import (
func RegisterGitHub(r *mux.Router) {
s := r.PathPrefix("/github").Subrouter()
- s.Handle("/readme", middlewares.VerifyAccessToken(http.HandlerFunc(controllers.GetReadMe))).Methods("GET")
+ s.Use(middlewares.VerifyAccessToken)
+ s.HandleFunc("/readme", controllers.GetReadMe).Methods("GET")
+ s.HandleFunc("/readme", controllers.UpdateFile).Methods("PATCH")
+ s.HandleFunc("/readme", controllers.CreateFile).Methods("POST")
}
From 90dac400b9ab4d2d07f09c57ad9c3a41c5d160c8 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 13:25:53 +0530
Subject: [PATCH 15/32] fix: error handling for readme post
---
internal/controllers/github.go | 4 ++++
internal/routes/github.go | 6 ++++--
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/internal/controllers/github.go b/internal/controllers/github.go
index 1849b55..caa0ce0 100644
--- a/internal/controllers/github.go
+++ b/internal/controllers/github.go
@@ -271,6 +271,10 @@ func CreateFile(w http.ResponseWriter, r *http.Request) {
JsonErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
+ }else{
+ log.Info("Repository already exists for user:", userName)
+ JsonErrorResponse(w, http.StatusConflict, "Repository already exists use PATCH method to update README.md")
+ return
}
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/README.md", userName, userName)
diff --git a/internal/routes/github.go b/internal/routes/github.go
index e241e15..fabc8bf 100644
--- a/internal/routes/github.go
+++ b/internal/routes/github.go
@@ -9,7 +9,9 @@ import (
func RegisterGitHub(r *mux.Router) {
s := r.PathPrefix("/github").Subrouter()
s.Use(middlewares.VerifyAccessToken)
+
+ // Add OPTIONS method to handle CORS preflight requests
s.HandleFunc("/readme", controllers.GetReadMe).Methods("GET")
- s.HandleFunc("/readme", controllers.UpdateFile).Methods("PATCH")
- s.HandleFunc("/readme", controllers.CreateFile).Methods("POST")
+ s.HandleFunc("/readme", controllers.UpdateFile).Methods("PATCH", "OPTIONS")
+ s.HandleFunc("/readme", controllers.CreateFile).Methods("POST", "OPTIONS")
}
From 18623d112cc2a3c8bb3bb6902a4a4a3ad4b82649 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 14:29:57 +0530
Subject: [PATCH 16/32] fix: option token checking in middleare
---
internal/cache/utils.go | 17 +++++++++++++----
internal/controllers/github.go | 6 ++++--
internal/middlewares/auth.go | 8 ++++++++
3 files changed, 25 insertions(+), 6 deletions(-)
diff --git a/internal/cache/utils.go b/internal/cache/utils.go
index f75ebb3..ccde9ce 100644
--- a/internal/cache/utils.go
+++ b/internal/cache/utils.go
@@ -1,8 +1,10 @@
package cache
import (
+ "context"
"os"
+ log "github.com/sirupsen/logrus"
"github.com/joho/godotenv"
"github.com/redis/go-redis/v9"
)
@@ -12,22 +14,29 @@ var client *redis.Client
func init() {
err := godotenv.Load()
if err != nil {
- panic("Error loading .env file")
+ log.Error("Error loading .env file")
}
redisUrl := os.Getenv("REDIS_URL")
opt, err := redis.ParseURL(redisUrl)
if err != nil {
- panic(err)
+ log.Error(err)
}
client = redis.NewClient(opt)
-}
+ // Ping the Redis server to check connection
+ ctx := context.Background()
+ _, err = client.Ping(ctx).Result()
+ if err != nil {
+ log.Error("Failed to connect to Redis: " + err.Error())
+ }
+}
func GetClient() *redis.Client {
if client == nil {
- panic("Redis client is not initialized")
+ log.Error("Redis client is not initialized")
}
return client
}
+
diff --git a/internal/controllers/github.go b/internal/controllers/github.go
index caa0ce0..b5a8d1c 100644
--- a/internal/controllers/github.go
+++ b/internal/controllers/github.go
@@ -91,13 +91,15 @@ func GetReadMe(w http.ResponseWriter, r *http.Request) {
readmeFile, err := fetchReadMeFile(userName, token)
if err != nil {
- http.Error(w, err.Error(), http.StatusNotFound)
+ JsonErrorResponse(w, http.StatusNotFound, "README file not found in repository")
+ log.Errorf("Failed to fetch README file for user %s: %v", userName, err)
return
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(readmeFile); err != nil {
- http.Error(w, "Failed to encode response", http.StatusInternalServerError)
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to encode response")
+ log.Errorf("Failed to encode response: %v", err)
}
}
diff --git a/internal/middlewares/auth.go b/internal/middlewares/auth.go
index c08cb6d..f0d0e59 100644
--- a/internal/middlewares/auth.go
+++ b/internal/middlewares/auth.go
@@ -11,6 +11,14 @@ import (
func VerifyAccessToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
+ // Handle CORS preflight requests
+ // This allows the browser to make requests without being blocked by CORS policy
+ if r.Method == http.MethodOptions {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
controllers.JsonErrorResponse(w, http.StatusUnauthorized, "Authorization header is missing")
From 6552dca01705384eafd3ec1816a6b17782179108 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 15:55:11 +0530
Subject: [PATCH 17/32] fix: exiting repo but no readme
---
internal/controllers/github.go | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/internal/controllers/github.go b/internal/controllers/github.go
index b5a8d1c..63fe9bb 100644
--- a/internal/controllers/github.go
+++ b/internal/controllers/github.go
@@ -275,8 +275,12 @@ func CreateFile(w http.ResponseWriter, r *http.Request) {
}
}else{
log.Info("Repository already exists for user:", userName)
- JsonErrorResponse(w, http.StatusConflict, "Repository already exists use PATCH method to update README.md")
- return
+ _, err := fetchReadMeFile(userName, token)
+ if err == nil {
+ JsonErrorResponse(w, http.StatusConflict, "README file already exists in the repository use PATCH method to update it")
+ log.Warnf("README file already exists for user %s", userName)
+ return
+ }
}
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/README.md", userName, userName)
From 3684c4f26bea660e1f11bddfa87440d5121acce9 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 16:21:46 +0530
Subject: [PATCH 18/32] fix: better logging
---
internal/routes/router.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/internal/routes/router.go b/internal/routes/router.go
index 9b0c0ca..dcd8e85 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -27,9 +27,9 @@ func Router() *mux.Router {
handlers.ExposedHeaders([]string{"*"}), // Add this for better compatibility
)
+ r.Use(middlewares.Logger)
// Apply CORS middleware to the entire router
r.Use(corsHandler)
- r.Use(middlewares.Logger)
// Handle preflight requests explicitly
r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
From 0f8f0178d026a83055afe7624abbb918dce9c3ab Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 16:30:57 +0530
Subject: [PATCH 19/32] test: docker image
---
cmd/main.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/cmd/main.go b/cmd/main.go
index dd02e80..66ec07b 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -29,6 +29,7 @@ func init() {
func main() {
router := routes.Router()
+ log.Info("Router initialized successfully")
log.Info("Starting server on :3000")
log.Fatal(http.ListenAndServe(":3000", router))
}
From 886440a896aa8c8ff19e42b2173bc8f2f66003ff Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 15 Jun 2025 16:42:25 +0530
Subject: [PATCH 20/32] fix: docker image issue
---
.github/workflows/deploy.yml | 22 +++++++++++++---------
docker-compose.yml | 12 ++++++------
2 files changed, 19 insertions(+), 15 deletions(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index b4aa99e..0fc5d3e 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -1,22 +1,21 @@
name: "Deploy to Oracle Cloud"
on: workflow_dispatch
-
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
-
+
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
+
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
-
+
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
@@ -26,8 +25,8 @@ jobs:
varcode05/devboard-backend:latest
varcode05/devboard-backend:${{ github.sha }}
cache-from: type=gha
- cache-to: type=gha,mode=max
-
+ cache-to: type=gha,mode=max
+
- name: Set up SSH
run: |
mkdir -p ~/.ssh
@@ -38,14 +37,19 @@ jobs:
- name: Deploy to Oracle Cloud
run: |
echo "Deploying to Oracle Cloud..."
- ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
+ ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << EOF
echo "Navigating to devboard-backend and pulling latest changes..."
cd devboard-backend
git pull
+
echo "Stopping existing services..."
docker compose down
- echo "Starting services with fresh build..."
- docker compose up -d --build
+
+ echo "Setting IMAGE_TAG environment variable..."
+ export IMAGE_TAG=${{ github.sha }}
+
+ echo "Starting services with specific SHA tag..."
+ IMAGE_TAG=${{ github.sha }} docker compose up -d
EOF
- name: Cleanup SSH
diff --git a/docker-compose.yml b/docker-compose.yml
index 0b9c03b..d91484d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,13 +1,13 @@
services:
app:
# build:
- # context: .
- # dockerfile: dockerfile
- image: varcode05/devboard-backend:latest
+ # context: .
+ # dockerfile: dockerfile
+ image: varcode05/devboard-backend:${IMAGE_TAG:-latest}
ports:
- - "80:3000"
- # expose port 3000 on the container to port 80 on the host
- #todo: add reverse proxy if needed
+ - "80:3000"
+ # expose port 3000 on the container to port 80 on the host
+ #todo: add reverse proxy if needed
env_file:
- .env
volumes:
From 78c3a52145a0047ff76a785cd5cea7d213aaaa89 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Wed, 2 Jul 2025 17:12:45 +0530
Subject: [PATCH 21/32] feat: add traefik ssl
---
docker-compose.yml | 50 +++++++++++++++++++++++++++++++++++++---------
1 file changed, 41 insertions(+), 9 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index d91484d..145b220 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,20 +1,51 @@
+version: "3.8"
+
services:
+ traefik:
+ image: traefik:v2.10
+ command:
+ - --api.dashboard=true
+ - --api.insecure=false
+ - --entrypoints.web.address=:80
+ - --entrypoints.websecure.address=:443
+ - --providers.docker=true
+ - --certificatesresolvers.myresolver.acme.httpchallenge=true
+ - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
+ - --certificatesresolvers.myresolver.acme.email=you@example.com
+ - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - ./letsencrypt:/letsencrypt
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.traefik.rule=Host(`devboard.varshith.tech`) && PathPrefix(`/traefik-dashboard`)"
+ - "traefik.http.routers.traefik.entrypoints=websecure"
+ - "traefik.http.routers.traefik.tls.certresolver=myresolver"
+ - "traefik.http.routers.traefik.service=api@internal"
+ - "traefik.http.routers.traefik.middlewares=auth"
+ - "traefik.http.middlewares.auth.basicauth.users=varcode:$apr1$ufU1xjii$psczrBqtSdyyHJVKh2khL1"
+
+
app:
- # build:
- # context: .
- # dockerfile: dockerfile
image: varcode05/devboard-backend:${IMAGE_TAG:-latest}
- ports:
- - "80:3000"
- # expose port 3000 on the container to port 80 on the host
- #todo: add reverse proxy if needed
env_file:
- .env
volumes:
- .env:/app/.env
depends_on:
- redis
-
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.app.rule=Host(`devboard.varshith.tech`)"
+ - "traefik.http.routers.app.entrypoints=websecure"
+ - "traefik.http.routers.app.tls.certresolver=myresolver"
+ - "traefik.http.services.app.loadbalancer.server.port=3000"
+ restart: always
+
+
redis:
image: redis:alpine
ports:
@@ -25,4 +56,5 @@ services:
restart: always
volumes:
- redis-data:
\ No newline at end of file
+ redis-data:
+ letsencrypt:
From fa66557ec09af4bf7624ee31b2afe5ea0ea8252d Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Wed, 2 Jul 2025 17:17:57 +0530
Subject: [PATCH 22/32] fix: add email
---
docker-compose.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 145b220..0c8011b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -11,7 +11,7 @@ services:
- --providers.docker=true
- --certificatesresolvers.myresolver.acme.httpchallenge=true
- --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
- - --certificatesresolvers.myresolver.acme.email=you@example.com
+ - --certificatesresolvers.myresolver.acme.email=varshithisgod@gmail.com
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
From 952c158f1e890f2b0a50c3eb123231fa7a04bf3f Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Wed, 2 Jul 2025 17:34:57 +0530
Subject: [PATCH 23/32] fix: docker-compose indenting
---
docker-compose.yml | 55 ++++++++++++++++++++++------------------------
1 file changed, 26 insertions(+), 29 deletions(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 0c8011b..74ffef7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,33 +1,31 @@
version: "3.8"
-
services:
traefik:
- image: traefik:v2.10
- command:
- - --api.dashboard=true
- - --api.insecure=false
- - --entrypoints.web.address=:80
- - --entrypoints.websecure.address=:443
- - --providers.docker=true
- - --certificatesresolvers.myresolver.acme.httpchallenge=true
- - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
- - --certificatesresolvers.myresolver.acme.email=varshithisgod@gmail.com
- - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
- ports:
- - "80:80"
- - "443:443"
- volumes:
- - /var/run/docker.sock:/var/run/docker.sock:ro
- - ./letsencrypt:/letsencrypt
- labels:
- - "traefik.enable=true"
- - "traefik.http.routers.traefik.rule=Host(`devboard.varshith.tech`) && PathPrefix(`/traefik-dashboard`)"
- - "traefik.http.routers.traefik.entrypoints=websecure"
- - "traefik.http.routers.traefik.tls.certresolver=myresolver"
- - "traefik.http.routers.traefik.service=api@internal"
- - "traefik.http.routers.traefik.middlewares=auth"
- - "traefik.http.middlewares.auth.basicauth.users=varcode:$apr1$ufU1xjii$psczrBqtSdyyHJVKh2khL1"
-
+ image: traefik:v2.10
+ command:
+ - --api.dashboard=true
+ - --api.insecure=false
+ - --entrypoints.web.address=:80
+ - --entrypoints.websecure.address=:443
+ - --providers.docker=true
+ - --certificatesresolvers.myresolver.acme.httpchallenge=true
+ - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
+ - --certificatesresolvers.myresolver.acme.email=varshithisgod@gmail.com
+ - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
+ ports:
+ - "80:80"
+ - "443:443"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock:ro
+ - ./letsencrypt:/letsencrypt
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.traefik.rule=Host(`devboard.varshith.tech`) && PathPrefix(`/traefik-dashboard`)"
+ - "traefik.http.routers.traefik.entrypoints=websecure"
+ - "traefik.http.routers.traefik.tls.certresolver=myresolver"
+ - "traefik.http.routers.traefik.service=api@internal"
+ - "traefik.http.routers.traefik.middlewares=auth"
+ - "traefik.http.middlewares.auth.basicauth.users=varcode:$$apr1$$3msTBU5l$$sH0xSDeZajSQ0QeV1.u1Y/"
app:
image: varcode05/devboard-backend:${IMAGE_TAG:-latest}
@@ -45,7 +43,6 @@ services:
- "traefik.http.services.app.loadbalancer.server.port=3000"
restart: always
-
redis:
image: redis:alpine
ports:
@@ -57,4 +54,4 @@ services:
volumes:
redis-data:
- letsencrypt:
+ letsencrypt:
\ No newline at end of file
From 2be96db0f098448d9e56b322fafb92ab4343f89a Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Wed, 2 Jul 2025 22:07:17 +0530
Subject: [PATCH 24/32] fix: cors issue using env
---
internal/routes/router.go | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/internal/routes/router.go b/internal/routes/router.go
index dcd8e85..d078164 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -2,8 +2,10 @@ package routes
import (
"net/http"
- "github.com/gorilla/mux"
+ "os"
+
"github.com/gorilla/handlers"
+ "github.com/gorilla/mux"
"github.com/var-code-5/devboard-backend/internal/middlewares"
)
@@ -11,8 +13,9 @@ func Router() *mux.Router {
r := mux.NewRouter()
// Add CORS middleware with more permissive settings for development
+ frontendUrl := os.Getenv("FRONTEND_URL")
corsHandler := handlers.CORS(
- handlers.AllowedOrigins([]string{"http://localhost:3000", "http://127.0.0.1:3000", "http://127.0.0.1:5500"}),
+ handlers.AllowedOrigins([]string{"http://localhost:3000", "http://127.0.0.1:3000", "http://127.0.0.1:5500", frontendUrl}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}),
handlers.AllowedHeaders([]string{
"Content-Type",
From f606d76d1b1f3243ac22de47d543b77ad44c02f3 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Thu, 10 Jul 2025 19:29:31 +0530
Subject: [PATCH 25/32] feat: add widgets
---
.env.example | 15 +-
.gitignore | 3 +-
go.mod | 7 +-
go.sum | 6 +-
internal/controllers/widgets.go | 408 ++++++++++++++++++++++++++++++++
internal/models/widgets.go | 122 ++++++++++
internal/routes/router.go | 5 +-
internal/routes/widget.go | 24 ++
8 files changed, 575 insertions(+), 15 deletions(-)
create mode 100644 internal/controllers/widgets.go
create mode 100644 internal/models/widgets.go
create mode 100644 internal/routes/widget.go
diff --git a/.env.example b/.env.example
index 555c898..062a554 100644
--- a/.env.example
+++ b/.env.example
@@ -1,8 +1,11 @@
-GITHUB_CLIENT_ID=your_github_client_id
-GITHUB_CLIENT_SECRET=your_github_client_secret
-PROD_DOMAIN=http://localhost:3000
+GITHUB_CLIENT_ID=your_client_id_here
+GITHUB_CLIENT_SECRET=your_client_secret_here
+PROD_DOMAIN=http://localhost
LOG_LEVEL=debug
-MONGODB_URI=mongodb+srv://username:password@host/database
+MONGODB_URI=mongodb+srv://your_mongodb_uri_here
MONGODB_DB_NAME=devBoard
-JWT_REFRESH_SECRET=your_refresh_secret_key
-JWT_ACCESS_SECRET=your_access_secret_key
\ No newline at end of file
+JWT_REFRESH_SECRET=your_refresh_secret_here
+JWT_ACCESS_SECRET=your_access_secret_here
+FRONTEND_URL=http://localhost:3000
+REDIS_URL=redis://localhost:6379
+GH_PAT=your_github_pat_here
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 335a441..dbb833f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.env
-cors-test.html
\ No newline at end of file
+cors-test.html
+localtest.go
diff --git a/go.mod b/go.mod
index 7801251..0b0afae 100644
--- a/go.mod
+++ b/go.mod
@@ -5,22 +5,21 @@ go 1.24.3
require github.com/gorilla/mux v1.8.1
require (
+ github.com/golang-jwt/jwt/v5 v5.2.2
+ github.com/gorilla/handlers v1.5.2
github.com/joho/godotenv v1.5.1
+ github.com/redis/go-redis/v9 v9.10.0
github.com/sirupsen/logrus v1.9.3
go.mongodb.org/mongo-driver v1.17.3
- go.mongodb.org/mongo-driver/v2 v2.2.1
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
- github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
- github.com/gorilla/handlers v1.5.2 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
- github.com/redis/go-redis/v9 v9.10.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
diff --git a/go.sum b/go.sum
index edf8ec8..cd8e7ef 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,7 @@
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -43,8 +47,6 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
-go.mongodb.org/mongo-driver/v2 v2.2.1 h1:w5xra3yyu/sGrziMzK1D0cRRaH/b7lWCSsoN6+WV6AM=
-go.mongodb.org/mongo-driver/v2 v2.2.1/go.mod h1:qQkDMhCGWl3FN509DfdPd4GRBLU/41zqF/k8eTRceps=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
diff --git a/internal/controllers/widgets.go b/internal/controllers/widgets.go
new file mode 100644
index 0000000..3d8c94c
--- /dev/null
+++ b/internal/controllers/widgets.go
@@ -0,0 +1,408 @@
+package controllers
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gorilla/mux"
+ log "github.com/sirupsen/logrus"
+ "github.com/var-code-5/devboard-backend/internal/models"
+)
+
+func AddWidget(w http.ResponseWriter, r *http.Request) {
+ var newWidget models.NewWidget
+ if err := json.NewDecoder(r.Body).Decode(&newWidget); err != nil {
+ JsonErrorResponse(w, http.StatusBadRequest, "Invalid request body")
+ return
+ }
+ userName := r.Context().Value("userName").(string)
+ newWidget.CreatedBy = userName
+
+ keys := extractPlaceholders(newWidget.Content)
+ err := validatePlaceholders(keys)
+
+ if err != nil {
+ JsonErrorResponse(w, http.StatusBadRequest, err.Error())
+ return
+ }
+
+ if err := models.AddWidget(newWidget); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ w.WriteHeader(http.StatusCreated)
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]string{"message": "Widget added successfully"}
+ json.NewEncoder(w).Encode(response)
+}
+
+func GetWidget(w http.ResponseWriter, r *http.Request) {
+ userName := r.Context().Value("userName").(string)
+ widgetName := r.URL.Query().Get("name")
+
+ if widgetName == "" {
+ JsonErrorResponse(w, http.StatusBadRequest, "Widget name is required")
+ return
+ }
+
+ widget, err := models.GetWidget(userName, widgetName)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusNotFound, err.Error())
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(widget)
+}
+
+//todo: add cache
+func GetUserWidgets(w http.ResponseWriter, r *http.Request) {
+ userName := r.Context().Value("userName").(string)
+
+ widgets, err := models.GetAllWidgets(userName)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ if len(widgets) == 0 {
+ JsonErrorResponse(w, http.StatusNotFound, "No widgets found for the user")
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(widgets)
+}
+
+func EditWidget(w http.ResponseWriter, r *http.Request) {
+ var updatedWidget models.NewWidget
+ if err := json.NewDecoder(r.Body).Decode(&updatedWidget); err != nil {
+ JsonErrorResponse(w, http.StatusBadRequest, "Invalid request body")
+ return
+ }
+ userName := r.Context().Value("userName").(string)
+ updatedWidget.CreatedBy = userName
+
+ if err := models.EditWidget(userName, updatedWidget.Name, updatedWidget); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]string{"message": "Widget updated successfully"}
+ json.NewEncoder(w).Encode(response)
+}
+
+func DeleteWidget(w http.ResponseWriter, r *http.Request) {
+ userName := r.Context().Value("userName").(string)
+ widgetName := r.URL.Query().Get("name")
+
+ if widgetName == "" {
+ JsonErrorResponse(w, http.StatusBadRequest, "Widget name is required")
+ return
+ }
+
+ if err := models.DeleteWidget(userName, widgetName); err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, err.Error())
+ return
+ }
+
+ w.WriteHeader(http.StatusOK)
+ w.Header().Set("Content-Type", "application/json")
+ response := map[string]string{"message": "Widget deleted successfully"}
+ json.NewEncoder(w).Encode(response)
+}
+
+var rePlaceholder = regexp.MustCompile(`{{\s*(\w+)\s*}}`)
+
+var validPlaceholders = map[string]bool{
+ "totalCommitContributions": true,
+ "totalIssueContributions": true,
+ "totalPullRequestContributions": true,
+ "totalRepositoriesWithContributedCommits": true,
+ "totalStars": true,
+ "totalRepositories": true,
+ "username": true,
+}
+
+func validatePlaceholders(keys []string) error {
+ for _, key := range keys {
+ if !validPlaceholders[key] {
+ return fmt.Errorf("invalid placeholder: %s", key)
+ }
+ }
+ return nil
+}
+
+func extractPlaceholders(s string) []string {
+ matches := rePlaceholder.FindAllStringSubmatch(s, -1)
+ var keys []string
+ for _, m := range matches {
+ keys = append(keys, m[1]) // m[1] is the inner word
+ }
+ return keys
+}
+
+func makeGraphQLQuery(query map[string]interface{}) (map[string]interface{}, error) {
+ githubAccessToken := os.Getenv("GH_PAT")
+ if githubAccessToken == "" {
+ return nil, fmt.Errorf("GitHub access token not set")
+ }
+
+ payload, err := json.Marshal(query)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequest("POST", "https://api.github.com/graphql", strings.NewReader(string(payload)))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Bearer "+githubAccessToken)
+ req.Header.Set("Content-Type", "application/json")
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("GitHub API request failed: %s", resp.Status)
+ }
+
+ var result map[string]interface{}
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, err
+ }
+
+ return result, nil
+}
+
+func buildQueryString(fields map[string]interface{}) string {
+ var parts []string
+
+ for field, value := range fields {
+ switch field {
+ case "contributionsCollection":
+ if fieldMap, ok := value.(map[string]interface{}); ok {
+ from := fieldMap["from"].(string)
+ to := fieldMap["to"].(string)
+ parts = append(parts, fmt.Sprintf(`contributionsCollection(from: "%s", to: "%s") {
+ totalCommitContributions
+ totalIssueContributions
+ totalPullRequestContributions
+ totalRepositoriesWithContributedCommits
+ }`, from, to))
+ }
+ case "repositories":
+ parts = append(parts, `repositories(first: 100, ownerAffiliations: OWNER) {
+ nodes {
+ stargazerCount
+ }
+ }`)
+ case "login":
+ parts = append(parts, "login")
+ }
+ }
+
+ return strings.Join(parts, "\n")
+}
+
+func buildGraphQLQuery(username string, keys []string) map[string]interface{} {
+ currentYear := time.Now().Year()
+ fromDate := fmt.Sprintf("%d-01-01T00:00:00Z", currentYear)
+ toDate := fmt.Sprintf("%d-12-31T23:59:59Z", currentYear)
+
+ needsContributions := false
+ needsRepositories := false
+
+ for _, key := range keys {
+ switch key {
+ case "totalCommitContributions", "totalIssueContributions",
+ "totalPullRequestContributions", "totalRepositoriesWithContributedCommits":
+ needsContributions = true
+ case "totalStars", "totalRepositories":
+ needsRepositories = true
+ }
+ }
+
+ queryFields := make(map[string]interface{})
+
+ if needsContributions {
+ queryFields["contributionsCollection"] = map[string]interface{}{
+ "from": fromDate,
+ "to": toDate,
+ "totalCommitContributions": true,
+ "totalIssueContributions": true,
+ "totalPullRequestContributions": true,
+ "totalRepositoriesWithContributedCommits": true,
+ }
+ }
+
+ if needsRepositories {
+ queryFields["repositories"] = map[string]interface{}{
+ "first": 100,
+ "ownerAffiliations": "OWNER",
+ "nodes": map[string]interface{}{
+ "stargazerCount": true,
+ },
+ }
+ }
+
+ queryFields["login"] = true
+
+ query := map[string]interface{}{
+ "query": fmt.Sprintf(`
+ query {
+ user(login: "%s") {
+ %s
+ }
+ }
+ `, username, buildQueryString(queryFields)),
+ }
+
+ return query
+}
+
+func resolveVariable(username string, keys []string) (map[string]string, error) {
+ if err := validatePlaceholders(keys); err != nil {
+ return nil, err
+ }
+
+ query := buildGraphQLQuery(username, keys)
+ result, err := makeGraphQLQuery(query)
+ if err != nil {
+ return nil, err
+ }
+
+ resolvedVars := make(map[string]string)
+
+ if errors, ok := result["errors"]; ok {
+ return nil, fmt.Errorf("GraphQL errors: %v", errors)
+ }
+
+ data, ok := result["data"].(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("invalid response format")
+ }
+
+ user, ok := data["user"].(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("user not found")
+ }
+
+ // extract contributions data
+ if contributions, ok := user["contributionsCollection"].(map[string]interface{}); ok {
+ if val, exists := contributions["totalCommitContributions"]; exists {
+ resolvedVars["totalCommitContributions"] = fmt.Sprintf("%.0f", val)
+ }
+ if val, exists := contributions["totalIssueContributions"]; exists {
+ resolvedVars["totalIssueContributions"] = fmt.Sprintf("%.0f", val)
+ }
+ if val, exists := contributions["totalPullRequestContributions"]; exists {
+ resolvedVars["totalPullRequestContributions"] = fmt.Sprintf("%.0f", val)
+ }
+ if val, exists := contributions["totalRepositoriesWithContributedCommits"]; exists {
+ resolvedVars["totalRepositoriesWithContributedCommits"] = fmt.Sprintf("%.0f", val)
+ }
+ }
+
+ // extract repository data
+ if repositories, ok := user["repositories"].(map[string]interface{}); ok {
+ if nodes, ok := repositories["nodes"].([]interface{}); ok {
+ totalStars := 0
+ totalRepos := len(nodes)
+
+ for _, node := range nodes {
+ if repo, ok := node.(map[string]interface{}); ok {
+ if stars, ok := repo["stargazerCount"]; ok {
+ if starCount, ok := stars.(float64); ok {
+ totalStars += int(starCount)
+ }
+ }
+ }
+ }
+
+ resolvedVars["totalStars"] = strconv.Itoa(totalStars)
+ resolvedVars["totalRepositories"] = strconv.Itoa(totalRepos)
+ }
+ }
+
+ if login, ok := user["login"]; ok {
+ resolvedVars["username"] = login.(string)
+ }
+
+ return resolvedVars, nil
+}
+
+func replacePlaceholders(username, svg string) (string, error) {
+ keys := extractPlaceholders(svg)
+ if len(keys) == 0 {
+ return svg, nil
+ }
+
+ uniqueKeys := make([]string, 0, len(keys))
+ seen := make(map[string]bool)
+ for _, key := range keys {
+ if !seen[key] {
+ uniqueKeys = append(uniqueKeys, key)
+ seen[key] = true
+ }
+ }
+
+ resolvedVars, err := resolveVariable(username, uniqueKeys)
+ if err != nil {
+ return "", err
+ }
+
+ result := svg
+ for _, key := range uniqueKeys {
+ if val, exists := resolvedVars[key]; exists {
+ result = strings.ReplaceAll(result, "{{"+key+"}}", val)
+ } else {
+ result = strings.ReplaceAll(result, "{{"+key+"}}", "N/A")
+ }
+ }
+
+ return result, nil
+}
+
+//todo: add cache for multiple retrievals
+func GetWidgetImage(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ username := vars["username"]
+ widgetOwner := vars["widgetOwner"]
+ widgetName := vars["widgetName"]
+ if username == "" || widgetName == "" || widgetOwner == "" {
+ JsonErrorResponse(w, http.StatusBadRequest, "Username, widget owner, and widget name are required")
+ log.Errorf("Username, widget owner, or widget name is empty ")
+ return
+ }
+ image, err := models.GetWidget(widgetOwner, widgetName)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusNotFound, err.Error())
+ log.Errorf("Error fetching widget: %v", err)
+ return
+ }
+
+ finalSVG, err := replacePlaceholders(username, image.Content)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusInternalServerError, err.Error())
+ log.Errorf("Error replacing placeholders: %v", err)
+ return
+ }
+
+ w.Header().Set("Content-Type", "image/svg+xml")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(finalSVG))
+}
diff --git a/internal/models/widgets.go b/internal/models/widgets.go
new file mode 100644
index 0000000..0200ac2
--- /dev/null
+++ b/internal/models/widgets.go
@@ -0,0 +1,122 @@
+package models
+
+import (
+ "context"
+ "fmt"
+
+ "go.mongodb.org/mongo-driver/bson"
+)
+
+type NewWidget struct {
+ Name string `json:"name"`
+ Content string `json:"content"`
+ Size struct {
+ Width int `json:"width"`
+ Height int `json:"height"`
+ } `json:"size"`
+ IsPrivate bool `json:"is_private" bson:"is_private"`
+ Tags []string `json:"tags"`
+ CreatedBy string `json:"created_by" omitempty:"true" bson:"created_by"`
+}
+
+func AddWidget(widget NewWidget) error {
+ collection, err := GetCollection("widgets")
+ if err != nil {
+ return err
+ }
+
+ // check if the widget name already exists for the user
+ filter := bson.M{"name": widget.Name, "created_by": widget.CreatedBy}
+ count, err := collection.CountDocuments(context.Background(), filter)
+ if err != nil {
+ return fmt.Errorf("failed to check widget existence: %v", err)
+ }
+ if count > 0 {
+ return fmt.Errorf("widget with name '%s' already exists", widget.Name)
+ }
+
+ _, err = collection.InsertOne(context.Background(), widget)
+ if err != nil {
+ return fmt.Errorf("failed to add widget: %v", err)
+ }
+
+ return nil
+}
+
+func GetWidget(username, widgetName string) (NewWidget, error) {
+ collection, err := GetCollection("widgets")
+ if err != nil {
+ return NewWidget{}, err
+ }
+
+ filter := bson.M{"name": widgetName, "created_by": username}
+ var widget NewWidget
+ err = collection.FindOne(context.Background(), filter).Decode(&widget)
+ if err != nil {
+ if err.Error() == "mongo: no documents in result" {
+ return NewWidget{}, fmt.Errorf("widget '%s' not found for user '%s'", widgetName, username)
+ }
+ return NewWidget{}, fmt.Errorf("failed to get widget: %v", err)
+ }
+
+ return widget, nil
+}
+
+func GetAllWidgets(username string) ([]NewWidget, error) {
+ collection, err := GetCollection("widgets")
+ if err != nil {
+ return nil, err
+ }
+
+ filter := bson.M{"created_by": username}
+ cursor, err := collection.Find(context.Background(), filter)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get widgets: %v", err)
+ }
+ defer cursor.Close(context.Background())
+
+ var widgets []NewWidget
+ if err := cursor.All(context.Background(), &widgets); err != nil {
+ return nil, fmt.Errorf("failed to decode widgets: %v", err)
+ }
+
+ return widgets, nil
+}
+
+func EditWidget(username, widgetName string, updatedWidget NewWidget) error {
+ collection, err := GetCollection("widgets")
+ if err != nil {
+ return err
+ }
+
+ filter := bson.M{"name": widgetName, "created_by": username}
+ update := bson.M{"$set": updatedWidget}
+
+ result, err := collection.UpdateOne(context.Background(), filter, update)
+ if err != nil {
+ return fmt.Errorf("failed to update widget: %v", err)
+ }
+ if result.MatchedCount == 0 {
+ return fmt.Errorf("widget '%s' not found for user '%s'", widgetName, username)
+ }
+
+ return nil
+}
+
+func DeleteWidget(username, widgetName string) error {
+ collection, err := GetCollection("widgets")
+ if err != nil {
+ return err
+ }
+
+ filter := bson.M{"name": widgetName, "created_by": username}
+ result, err := collection.DeleteOne(context.Background(), filter)
+ if err != nil {
+ return fmt.Errorf("failed to delete widget: %v", err)
+ }
+ if result.DeletedCount == 0 {
+ return fmt.Errorf("widget '%s' not found for user '%s'", widgetName, username)
+ }
+
+ return nil
+}
\ No newline at end of file
diff --git a/internal/routes/router.go b/internal/routes/router.go
index d078164..dcd7b54 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -48,10 +48,11 @@ func Router() *mux.Router {
RegisterAuth(apiRouter)
RegisterGitHub(apiRouter)
-
+ RegisterWidget(apiRouter)
return r
}
func ping(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("pong"))
+ w.Header().Set("Content-Type", "application/json")
+ w.Write([]byte(`{"message": "pong"}`))
}
\ No newline at end of file
diff --git a/internal/routes/widget.go b/internal/routes/widget.go
new file mode 100644
index 0000000..abf0a14
--- /dev/null
+++ b/internal/routes/widget.go
@@ -0,0 +1,24 @@
+package routes
+
+import (
+ "github.com/gorilla/mux"
+ "github.com/var-code-5/devboard-backend/internal/controllers"
+ "github.com/var-code-5/devboard-backend/internal/middlewares"
+)
+
+func RegisterWidget(r *mux.Router) {
+ s := r.PathPrefix("/widget").Subrouter()
+ s.Use(middlewares.VerifyAccessToken)
+
+ s.HandleFunc("/", controllers.AddWidget).Methods("POST", "OPTIONS")
+ s.HandleFunc("/", controllers.GetWidget).Methods("GET", "OPTIONS") // it should have a query parameter for widget name
+ s.HandleFunc("/all", controllers.GetUserWidgets).Methods("GET", "OPTIONS")
+ s.HandleFunc("/", controllers.DeleteWidget).Methods("DELETE", "OPTIONS")// it should have a query parameter for widget name
+ s.HandleFunc("/", controllers.EditWidget).Methods("PATCH", "OPTIONS")
+
+
+ // for serving widget images
+ w := r.PathPrefix("/widget-img").Subrouter()
+
+ w.HandleFunc("/{username}/{widgetOwner}/{widgetName}", controllers.GetWidgetImage).Methods("GET", "OPTIONS")
+}
From caac12eeb11aadb9a8a594b64a348fc96b5ebe70 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sat, 12 Jul 2025 14:18:04 +0530
Subject: [PATCH 26/32] refactor: change the cors lib
---
go.mod | 3 +--
go.sum | 6 ++---
internal/routes/router.go | 54 ++++++++++++++-------------------------
3 files changed, 22 insertions(+), 41 deletions(-)
diff --git a/go.mod b/go.mod
index 0b0afae..fa6e609 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,9 @@ require github.com/gorilla/mux v1.8.1
require (
github.com/golang-jwt/jwt/v5 v5.2.2
- github.com/gorilla/handlers v1.5.2
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.10.0
+ github.com/rs/cors v1.11.1
github.com/sirupsen/logrus v1.9.3
go.mongodb.org/mongo-driver v1.17.3
)
@@ -16,7 +16,6 @@ require (
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
- github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
diff --git a/go.sum b/go.sum
index cd8e7ef..33e4cb7 100644
--- a/go.sum
+++ b/go.sum
@@ -9,16 +9,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
-github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
-github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
@@ -31,6 +27,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
+github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
+github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/internal/routes/router.go b/internal/routes/router.go
index dcd7b54..b19fc8a 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -4,55 +4,39 @@ import (
"net/http"
"os"
- "github.com/gorilla/handlers"
+ // "github.com/gorilla/handlers"
"github.com/gorilla/mux"
+ "github.com/rs/cors"
"github.com/var-code-5/devboard-backend/internal/middlewares"
)
-func Router() *mux.Router {
+func Router() http.Handler {
r := mux.NewRouter()
-
- // Add CORS middleware with more permissive settings for development
+
frontendUrl := os.Getenv("FRONTEND_URL")
- corsHandler := handlers.CORS(
- handlers.AllowedOrigins([]string{"http://localhost:3000", "http://127.0.0.1:3000", "http://127.0.0.1:5500", frontendUrl}),
- handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}),
- handlers.AllowedHeaders([]string{
- "Content-Type",
- "Authorization",
- "X-Requested-With",
- "Accept",
- "Origin",
- "Cache-Control",
- "X-File-Name",
- }),
- handlers.AllowCredentials(),
- handlers.ExposedHeaders([]string{"*"}), // Add this for better compatibility
- )
-
- r.Use(middlewares.Logger)
- // Apply CORS middleware to the entire router
- r.Use(corsHandler)
-
- // Handle preflight requests explicitly
- r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000")
- w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With, Accept, Origin, Cache-Control, X-File-Name")
- w.Header().Set("Access-Control-Allow-Credentials", "true")
- w.WriteHeader(http.StatusOK)
+
+ c := cors.New(cors.Options{
+ AllowedOrigins: []string{"http://localhost:3000","http://127.0.0.1:3000","http://localhost:5500","http://127.0.0.1:5500", frontendUrl},
+ AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
+ AllowedHeaders: []string{"Authorization", "Content-Type", "X-Requested-With", "Accept", "Origin"},
+ AllowCredentials: true,
+ //Debug: true, // Enable for debugging CORS issues
})
-
+
+ r.Use(middlewares.Logger)
+
apiRouter := r.PathPrefix("/api").Subrouter()
apiRouter.HandleFunc("/ping", ping).Methods("GET")
-
+
RegisterAuth(apiRouter)
RegisterGitHub(apiRouter)
RegisterWidget(apiRouter)
- return r
+
+ handler := c.Handler(r)
+ return handler
}
func ping(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message": "pong"}`))
-}
\ No newline at end of file
+}
From ca48c7de7334ac2cceac949160e23958f974078e Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 13 Jul 2025 18:48:51 +0530
Subject: [PATCH 27/32] feat: add marketplace endpoint
---
internal/cache/market_place.go | 65 ++++++++++++++++++++++++++++
internal/controllers/market_place.go | 47 ++++++++++++++++++++
internal/controllers/widgets.go | 14 ++++--
internal/models/market_place.go | 41 ++++++++++++++++++
internal/routes/market_place.go | 15 +++++++
internal/routes/router.go | 1 +
6 files changed, 180 insertions(+), 3 deletions(-)
create mode 100644 internal/cache/market_place.go
create mode 100644 internal/controllers/market_place.go
create mode 100644 internal/models/market_place.go
create mode 100644 internal/routes/market_place.go
diff --git a/internal/cache/market_place.go b/internal/cache/market_place.go
new file mode 100644
index 0000000..c6b5f06
--- /dev/null
+++ b/internal/cache/market_place.go
@@ -0,0 +1,65 @@
+package cache
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/redis/go-redis/v9"
+ "github.com/var-code-5/devboard-backend/internal/models"
+)
+
+func GetMarketPlace(widgetType string, length int, offset int) ([]interface{}, error) {
+ client := GetClient()
+ data, err := client.Get(ctx, fmt.Sprintf("marketplace:%s:%d:%d", widgetType, length, offset)).Result()
+
+ if err != nil {
+ if err == redis.Nil {
+ widgets, err := models.GetMarketPlace(widgetType, length, offset)
+ if err != nil {
+ log.Error("Failed to fetch widgets from database:", err)
+ return nil, fmt.Errorf("failed to fetch widgets from database: %v", err)
+ }
+ data, err := json.Marshal(widgets)
+ if err != nil {
+ log.Error("Failed to marshal widgets:", err)
+ return nil, fmt.Errorf("failed to marshal widgets: %v", err)
+ }
+ err = client.Set(ctx, fmt.Sprintf("marketplace:%s:%d:%d", widgetType, length, offset), data, time.Minute*5).Err() // store in cache for 5 min
+ if err != nil {
+ log.Error("Failed to set data in cache:", err)
+ return nil, fmt.Errorf("failed to set data in cache: %v", err)
+ }
+ var result []interface{}
+ if err := json.Unmarshal([]byte(data), &result); err != nil {
+ return nil, err
+ }
+ return result, nil
+ } else {
+ log.Error("Error fetching data from cache:", err)
+ return nil, fmt.Errorf("error fetching data from cache: %v", err)
+ }
+ }
+ var result []interface{}
+ if err := json.Unmarshal([]byte(data), &result); err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
+func ClearMarketPlaceCache(widgetType string) error {
+ client := GetClient()
+ keys, err := client.Keys(ctx, fmt.Sprintf("marketplace:%s:*", widgetType)).Result()
+ if err != nil {
+ log.Error("Failed to fetch keys for marketplace cache:", err)
+ return fmt.Errorf("failed to fetch keys for marketplace cache: %v", err)
+ }
+ for _, key := range keys {
+ if err := client.Del(ctx, key).Err(); err != nil {
+ log.Error("Failed to delete key from cache:", err)
+ return fmt.Errorf("failed to delete key from cache: %v", err)
+ }
+ }
+ return nil
+}
\ No newline at end of file
diff --git a/internal/controllers/market_place.go b/internal/controllers/market_place.go
new file mode 100644
index 0000000..aceb431
--- /dev/null
+++ b/internal/controllers/market_place.go
@@ -0,0 +1,47 @@
+package controllers
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/var-code-5/devboard-backend/internal/cache"
+)
+
+func GetMarketPlace(w http.ResponseWriter, r *http.Request) {
+ widgetType := r.URL.Query().Get("widgetType")
+ lengthStr := r.URL.Query().Get("length")
+ offsetStr := r.URL.Query().Get("offset")
+ if _, valid := validPlaceholders[widgetType]; !valid {
+ JsonErrorResponse(w, http.StatusBadRequest, "Invalid widget type")
+ return
+ }
+ if lengthStr == "" || offsetStr == "" {
+ lengthStr = "10" // Default length
+ offsetStr = "0" // Default offset
+ }
+
+ length, err := strconv.Atoi(lengthStr)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusBadRequest, "Invalid length")
+ return
+ }
+ offset, err := strconv.Atoi(offsetStr)
+ if err != nil {
+ JsonErrorResponse(w, http.StatusBadRequest, "Invalid offset")
+ return
+ }
+
+ widgets, err := cache.GetMarketPlace(widgetType, length, offset)
+ if err != nil {
+ log.Error("Failed to fetch market place:", err)
+ JsonErrorResponse(w, http.StatusInternalServerError, "Failed to fetch widgets")
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(widgets)
+ log.Info("Market place fetched successfully for widget type:", widgetType)
+}
diff --git a/internal/controllers/widgets.go b/internal/controllers/widgets.go
index 3d8c94c..abf3f1d 100644
--- a/internal/controllers/widgets.go
+++ b/internal/controllers/widgets.go
@@ -12,6 +12,7 @@ import (
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
+ "github.com/var-code-5/devboard-backend/internal/cache"
"github.com/var-code-5/devboard-backend/internal/models"
)
@@ -32,11 +33,18 @@ func AddWidget(w http.ResponseWriter, r *http.Request) {
return
}
+ // for using the marketplace
+ newWidget.Tags = append(newWidget.Tags, keys...)
+
if err := models.AddWidget(newWidget); err != nil {
JsonErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
+ for _, tag := range newWidget.Tags {
+ _ = cache.ClearMarketPlaceCache(tag)
+ }
+
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
response := map[string]string{"message": "Widget added successfully"}
@@ -62,7 +70,7 @@ func GetWidget(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(widget)
}
-//todo: add cache
+// todo: add cache
func GetUserWidgets(w http.ResponseWriter, r *http.Request) {
userName := r.Context().Value("userName").(string)
@@ -76,7 +84,7 @@ func GetUserWidgets(w http.ResponseWriter, r *http.Request) {
JsonErrorResponse(w, http.StatusNotFound, "No widgets found for the user")
return
}
-
+
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(widgets)
}
@@ -377,7 +385,7 @@ func replacePlaceholders(username, svg string) (string, error) {
return result, nil
}
-//todo: add cache for multiple retrievals
+// todo: add cache for multiple retrievals
func GetWidgetImage(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
username := vars["username"]
diff --git a/internal/models/market_place.go b/internal/models/market_place.go
new file mode 100644
index 0000000..2dc9aae
--- /dev/null
+++ b/internal/models/market_place.go
@@ -0,0 +1,41 @@
+package models
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "go.mongodb.org/mongo-driver/bson"
+ "go.mongodb.org/mongo-driver/mongo/options"
+)
+
+func GetMarketPlace(widgetType string, length int, offset int) ([]bson.M, error) {
+ collection, err := GetCollection("widgets")
+ if err != nil {
+ return nil, err
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ filter := bson.M{
+ "tags": widgetType, // tags is an array — mongoDB matches if any element equals widgetType
+ }
+
+ findOptions := options.Find()
+ findOptions.SetLimit(int64(length))
+ findOptions.SetSkip(int64(offset))
+
+ cursor, err := collection.Find(ctx, filter, findOptions)
+ if err != nil {
+ return nil, fmt.Errorf("failed to find widgets: %v", err)
+ }
+ defer cursor.Close(ctx)
+
+ var widgets []bson.M
+ if err := cursor.All(ctx, &widgets); err != nil {
+ return nil, fmt.Errorf("failed to decode widgets: %v", err)
+ }
+
+ return widgets, nil
+}
diff --git a/internal/routes/market_place.go b/internal/routes/market_place.go
new file mode 100644
index 0000000..3105259
--- /dev/null
+++ b/internal/routes/market_place.go
@@ -0,0 +1,15 @@
+package routes
+
+import (
+ "github.com/gorilla/mux"
+ "github.com/var-code-5/devboard-backend/internal/controllers"
+ "github.com/var-code-5/devboard-backend/internal/middlewares"
+)
+
+func RegisterMarketPlace(r *mux.Router) {
+ s := r.PathPrefix("/marketplace").Subrouter()
+
+ // Add OPTIONS method to handle CORS preflight requests
+ s.HandleFunc("/", controllers.GetMarketPlace).Methods("GET", "OPTIONS")
+ s.Use(middlewares.VerifyAccessToken)
+}
diff --git a/internal/routes/router.go b/internal/routes/router.go
index b19fc8a..8a41314 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -31,6 +31,7 @@ func Router() http.Handler {
RegisterAuth(apiRouter)
RegisterGitHub(apiRouter)
RegisterWidget(apiRouter)
+ RegisterMarketPlace(apiRouter)
handler := c.Handler(r)
return handler
From 84bc36f53491a5a26e91a7f75f457212baef9e8e Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Mon, 4 Aug 2025 15:46:59 +0530
Subject: [PATCH 28/32] feat: add documentation for the app
---
README.md | 79 ++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 61 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 8f25179..5739308 100644
--- a/README.md
+++ b/README.md
@@ -2,43 +2,86 @@
- < Insert Project Title Here >
- < Insert Project Description Here >
+ DevBoard Backend
+ A Go-based backend API service for the DevBoard application, providing authentication, GitHub integration, and marketplace functionality
---
[](https://dsc.community.dev/vellore-institute-of-technology/)
[](https://discord.gg/498KVdSKWR)
-[](INSERT_LINK_FOR_DOCS_HERE)
- [](INSERT_UI_LINK_HERE)
+[](https://devboard.varshith.tech/api/docs)
+ [](https://devboard.varshith.tech)
## Features
-- [ ] < feature >
-- [ ] < feature >
-- [ ] < feature >
-- [ ] < feature >
+- [x] GitHub OAuth Authentication
+- [x] JWT-based Authorization
+- [x] Redis Caching
+- [x] MongoDB Integration
+- [x] GitHub API Integration
+- [x] Marketplace API
+- [x] Widgets Management
## Dependencies
- - < dependency >
- - < dependency >
+ - Go 1.24+
+ - MongoDB
+ - Redis
+ - Docker & Docker Compose (for deployment)
## Running
+### Prerequisites
+1. Create a `.env` file in the root directory with the following environment variables:
-< directions to install >
```bash
-< insert code >
+# Server Configuration
+LOG_LEVEL=info
+PORT=3000
+
+# MongoDB Configuration
+MONGODB_URI=mongodb://localhost:27017
+MONGODB_DB_NAME=devboard
+
+# Redis Configuration
+REDIS_URL=redis://localhost:6379
+
+# GitHub OAuth Configuration
+GITHUB_CLIENT_ID=your_github_client_id
+GITHUB_CLIENT_SECRET=your_github_client_secret
+GH_PAT=your_github_personal_access_token
+
+# JWT Configuration
+JWT_ACCESS_SECRET=your_jwt_access_secret
+JWT_REFRESH_SECRET=your_jwt_refresh_secret
+
+# Frontend Configuration
+FRONTEND_URL=http://localhost:5173
+PROD_DOMAIN=devboard.example.com
+```
+
+### Development Setup
+
+```bash
+# Clone the repository
+git clone https://github.com/var-code-5/devboard-backend.git
+cd devboard-backend
+
+# Download dependencies
+go mod download
+
+# Run the server
+go run cmd/main.go
```
-< directions to execute >
+### Docker Deployment
```bash
-< insert code >
+# Build and run using Docker Compose
+docker-compose up -d
```
## Contributors
@@ -46,15 +89,15 @@
|
- John Doe
+ Varshith
-
+
-
+
-
+
From 2133e763f756cd622d13e49a61ef88b90f4ba734 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Tue, 5 Aug 2025 23:56:51 +0530
Subject: [PATCH 29/32] feat: add encryption to the github tokens
---
.env.example | 3 +-
README.md | 5 +++
internal/models/auth.go | 27 +++++++++++++--
internal/models/utils.go | 72 ++++++++++++++++++++++++++++++++++++++--
4 files changed, 102 insertions(+), 5 deletions(-)
diff --git a/.env.example b/.env.example
index 062a554..755e4ec 100644
--- a/.env.example
+++ b/.env.example
@@ -8,4 +8,5 @@ JWT_REFRESH_SECRET=your_refresh_secret_here
JWT_ACCESS_SECRET=your_access_secret_here
FRONTEND_URL=http://localhost:3000
REDIS_URL=redis://localhost:6379
-GH_PAT=your_github_pat_here
\ No newline at end of file
+GH_PAT=your_github_pat_here
+ENCRYPTION_KEY=your_32_byte_random_secret_key
\ No newline at end of file
diff --git a/README.md b/README.md
index 5739308..a28753d 100644
--- a/README.md
+++ b/README.md
@@ -61,8 +61,13 @@ JWT_REFRESH_SECRET=your_jwt_refresh_secret
# Frontend Configuration
FRONTEND_URL=http://localhost:5173
PROD_DOMAIN=devboard.example.com
+
+# Encryption for the tokens
+ENCRYPTION_KEY=your_32_byte_random_secret_key
+
```
+
### Development Setup
```bash
diff --git a/internal/models/auth.go b/internal/models/auth.go
index 92f2584..5f5a439 100644
--- a/internal/models/auth.go
+++ b/internal/models/auth.go
@@ -54,6 +54,22 @@ func AddUser(user User) error {
return err
}
+ if user.AccessToken != "" {
+ encryptedToken, err := EncryptToken(user.AccessToken)
+ if err != nil {
+ return err
+ }
+ user.AccessToken = encryptedToken
+ }
+
+ if user.RefreshToken != "" {
+ encryptedRefresh, err := EncryptToken(user.RefreshToken)
+ if err != nil {
+ return err
+ }
+ user.RefreshToken = encryptedRefresh
+ }
+
// Check if user already exists in the database by GitHub ID
filter := bson.M{"github_id": user.GithubID}
existingUser := collection.FindOne(context.Background(), filter)
@@ -98,7 +114,10 @@ func GetAccessToken(username string) (string, error) {
return "", err
}
- return user.AccessToken, nil
+ if user.AccessToken != "" {
+ return DecryptToken(user.AccessToken)
+ }
+ return "", nil
}
func GetRefreshToken(username string) (string, error) {
@@ -114,5 +133,9 @@ func GetRefreshToken(username string) (string, error) {
return "", err
}
- return user.RefreshToken, nil
+ if user.RefreshToken != ""{
+ return DecryptToken(user.RefreshToken)
+ }
+
+ return "", nil
}
\ No newline at end of file
diff --git a/internal/models/utils.go b/internal/models/utils.go
index 0df9ca6..aa8f99d 100644
--- a/internal/models/utils.go
+++ b/internal/models/utils.go
@@ -2,7 +2,12 @@ package models
import (
"context"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/base64"
"fmt"
+ "io"
"os"
"time"
@@ -18,15 +23,24 @@ var (
database *mongo.Database
)
+var secretKey []byte
+
func init() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
-
+
connectionURL := os.Getenv("MONGODB_URI")
dbName := os.Getenv("MONGODB_DB_NAME")
+ // Get encryption key from environment
+ key := os.Getenv("ENCRYPTION_KEY")
+ if key == "" {
+ panic("ENCRYPTION_KEY environment variable must be set")
+ }
+ secretKey = []byte(key)
+
if connectionURL == "" || dbName == "" {
log.Fatal("MONGODB_URI or MONGODB_DB_NAME environment variable is not set")
return
@@ -55,4 +69,58 @@ func GetCollection(collectionName string) (*mongo.Collection, error) {
return nil, fmt.Errorf("database connection is not initialized")
}
return database.Collection(collectionName), nil
-}
\ No newline at end of file
+}
+
+// EncryptToken encrypts a token using AES-GCM
+func EncryptToken(tokenString string) (string, error) {
+ plaintext := []byte(tokenString)
+
+ block, err := aes.NewCipher(secretKey)
+ if err != nil {
+ return "", err
+ }
+
+ aesGCM, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+
+ nonce := make([]byte, aesGCM.NonceSize())
+ if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
+ return "", err
+ }
+
+ ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil)
+ return base64.StdEncoding.EncodeToString(ciphertext), nil
+}
+
+// DecryptToken decrypts an encrypted token
+func DecryptToken(encryptedToken string) (string, error) {
+ ciphertext, err := base64.StdEncoding.DecodeString(encryptedToken)
+ if err != nil {
+ return "", err
+ }
+
+ block, err := aes.NewCipher(secretKey)
+ if err != nil {
+ return "", err
+ }
+
+ aesGCM, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+
+ nonceSize := aesGCM.NonceSize()
+ if len(ciphertext) < nonceSize {
+ return "", err
+ }
+
+ nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
+ plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return "", err
+ }
+
+ return string(plaintext), nil
+}
From 3f8b7238efb98963049396db0dfed650f976cbf7 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 10 Aug 2025 17:17:48 +0530
Subject: [PATCH 30/32] chore: update cors
---
internal/routes/router.go | 30 +++++++++++++++++++++++++++---
1 file changed, 27 insertions(+), 3 deletions(-)
diff --git a/internal/routes/router.go b/internal/routes/router.go
index 8a41314..2e2fe57 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -1,10 +1,10 @@
package routes
import (
+ "log"
"net/http"
"os"
- // "github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/rs/cors"
"github.com/var-code-5/devboard-backend/internal/middlewares"
@@ -14,17 +14,40 @@ func Router() http.Handler {
r := mux.NewRouter()
frontendUrl := os.Getenv("FRONTEND_URL")
+ if frontendUrl == "" {
+ log.Println("Warning: FRONTEND_URL environment variable not set")
+ } else {
+ log.Printf("Using frontend URL: %s", frontendUrl)
+ }
+
+ allowedOrigins := []string{
+ "http://localhost:3000",
+ "http://127.0.0.1:3000",
+ "http://localhost:5500",
+ "http://127.0.0.1:5500",
+ }
+
+ // Only add frontendUrl if it's not empty
+ if frontendUrl != "" {
+ allowedOrigins = append(allowedOrigins, frontendUrl)
+ }
c := cors.New(cors.Options{
- AllowedOrigins: []string{"http://localhost:3000","http://127.0.0.1:3000","http://localhost:5500","http://127.0.0.1:5500", frontendUrl},
+ AllowedOrigins: allowedOrigins,
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
AllowedHeaders: []string{"Authorization", "Content-Type", "X-Requested-With", "Accept", "Origin"},
+ ExposedHeaders: []string{"Content-Length", "Content-Type"},
AllowCredentials: true,
- //Debug: true, // Enable for debugging CORS issues
+ Debug: true, // Temporarily enable for debugging CORS issues
})
r.Use(middlewares.Logger)
+ // Handle OPTIONS requests globally
+ r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
apiRouter := r.PathPrefix("/api").Subrouter()
apiRouter.HandleFunc("/ping", ping).Methods("GET")
@@ -33,6 +56,7 @@ func Router() http.Handler {
RegisterWidget(apiRouter)
RegisterMarketPlace(apiRouter)
+ // Apply CORS to the entire router
handler := c.Handler(r)
return handler
}
From 3e1d8491ad5b7a96a13905b2c3808a4f8afc44d4 Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 10 Aug 2025 17:29:45 +0530
Subject: [PATCH 31/32] fix: handle cors using rs
---
internal/routes/router.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/internal/routes/router.go b/internal/routes/router.go
index 2e2fe57..5b683c0 100644
--- a/internal/routes/router.go
+++ b/internal/routes/router.go
@@ -44,9 +44,9 @@ func Router() http.Handler {
r.Use(middlewares.Logger)
// Handle OPTIONS requests globally
- r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- })
+ // r.Methods("OPTIONS").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // w.WriteHeader(http.StatusOK)
+ // })
apiRouter := r.PathPrefix("/api").Subrouter()
apiRouter.HandleFunc("/ping", ping).Methods("GET")
From 693bf01625d0a1d487bd0c215f982f065999eceb Mon Sep 17 00:00:00 2001
From: var-code-5
Date: Sun, 10 Aug 2025 17:36:51 +0530
Subject: [PATCH 32/32] fix: trailing slash in cors
---
internal/controllers/auth.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go
index ccc0a36..9fc9306 100644
--- a/internal/controllers/auth.go
+++ b/internal/controllers/auth.go
@@ -196,7 +196,7 @@ func Callback(w http.ResponseWriter, r *http.Request) {
return
}
- redirectURL := fmt.Sprintf("%s?access_token=%s&refresh_token=%s&login=%s&avatar_url=%s",
+ redirectURL := fmt.Sprintf("%s/?access_token=%s&refresh_token=%s&login=%s&avatar_url=%s",
frontendURL,
url.QueryEscape(jwtAccessToken),
url.QueryEscape(jwtRefreshToken),
|