A deployable markdown editor that connects with your self hosted files and lets you edit in a beautiful interface
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add GitHub OAuth integration and HTTP routing

- Implement GitHub OAuth provider setup with goth
- Add session management with encrypted cookies
- Create authentication middleware for protected routes
- Implement auth handlers (login, callback, user info, logout)
- Set up chi router with CORS support
- Add logging middleware for HTTP requests
- Create health check endpoint
- Integrate all components into main server
- Build test successful

+531 -11
+15 -9
backend/cmd/server/main.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "fmt" 6 5 "log" 7 6 "net/http" 8 7 "os" ··· 11 10 "time" 12 11 13 12 "github.com/joho/godotenv" 13 + "github.com/yourusername/markedit/internal/api" 14 + "github.com/yourusername/markedit/internal/auth" 14 15 "github.com/yourusername/markedit/internal/database" 15 16 ) 16 17 ··· 43 44 log.Fatalf("Failed to run migrations: %v", err) 44 45 } 45 46 47 + // Initialize authentication 48 + if err := auth.InitSessions(); err != nil { 49 + log.Fatalf("Failed to initialize sessions: %v", err) 50 + } 51 + 52 + if err := auth.SetupProviders(); err != nil { 53 + log.Fatalf("Failed to setup OAuth providers: %v", err) 54 + } 55 + 56 + // Create router 57 + router := api.NewRouter(db) 58 + 46 59 // Create HTTP server 47 60 srv := &http.Server{ 48 61 Addr: ":" + port, 49 - Handler: http.HandlerFunc(healthCheckHandler), 62 + Handler: router, 50 63 ReadTimeout: 15 * time.Second, 51 64 WriteTimeout: 15 * time.Second, 52 65 IdleTimeout: 60 * time.Second, ··· 77 90 78 91 log.Println("Server exited") 79 92 } 80 - 81 - // Temporary health check handler 82 - func healthCheckHandler(w http.ResponseWriter, r *http.Request) { 83 - w.Header().Set("Content-Type", "application/json") 84 - w.WriteHeader(http.StatusOK) 85 - fmt.Fprintf(w, `{"status":"ok","version":"0.1.0"}`) 86 - }
+11 -2
backend/go.mod
··· 2 2 3 3 go 1.24.1 4 4 5 - require github.com/joho/godotenv v1.5.1 5 + require ( 6 + github.com/go-chi/chi/v5 v5.2.4 7 + github.com/go-chi/cors v1.2.2 8 + github.com/gorilla/sessions v1.4.0 9 + github.com/joho/godotenv v1.5.1 10 + github.com/markbates/goth v1.82.0 11 + modernc.org/sqlite v1.44.3 12 + ) 6 13 7 14 require ( 8 15 github.com/dustin/go-humanize v1.0.1 // indirect 9 16 github.com/google/uuid v1.6.0 // indirect 17 + github.com/gorilla/mux v1.8.1 // indirect 18 + github.com/gorilla/securecookie v1.1.2 // indirect 10 19 github.com/mattn/go-isatty v0.0.20 // indirect 11 20 github.com/ncruces/go-strftime v1.0.0 // indirect 12 21 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 13 22 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect 23 + golang.org/x/oauth2 v0.27.0 // indirect 14 24 golang.org/x/sys v0.37.0 // indirect 15 25 modernc.org/libc v1.67.6 // indirect 16 26 modernc.org/mathutil v1.7.1 // indirect 17 27 modernc.org/memory v1.11.0 // indirect 18 - modernc.org/sqlite v1.44.3 // indirect 19 28 )
+56
backend/go.sum
··· 1 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 1 3 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 2 4 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 + github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= 6 + github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= 7 + github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= 8 + github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 9 + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 + github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 12 + github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 13 + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= 14 + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 3 15 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 16 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 + github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 18 + github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 19 + github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 20 + github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 21 + github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 22 + github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 23 + github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 24 + github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 5 25 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 6 26 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 27 + github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ= 28 + github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk= 7 29 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 8 30 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 9 31 github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= 10 32 github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 33 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 34 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 35 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 12 36 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 37 + github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 38 + github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 13 39 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= 14 40 golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 41 + golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 42 + golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 43 + golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 44 + golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 45 + golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 46 + golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 15 47 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 48 golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 17 49 golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 50 + golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 51 + golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 52 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 53 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 54 + modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= 55 + modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 56 + modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= 57 + modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= 58 + modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= 59 + modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= 60 + modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= 61 + modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 62 + modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= 63 + modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= 64 + modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= 65 + modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= 18 66 modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= 19 67 modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= 20 68 modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 21 69 modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 22 70 modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= 23 71 modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= 72 + modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= 73 + modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 74 + modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 75 + modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 24 76 modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= 25 77 modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= 78 + modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 79 + modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 80 + modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 81 + modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+171
backend/internal/api/handlers/auth.go
··· 1 + package handlers 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "log" 7 + "net/http" 8 + "os" 9 + "time" 10 + 11 + "github.com/markbates/goth/gothic" 12 + "github.com/yourusername/markedit/internal/auth" 13 + "github.com/yourusername/markedit/internal/database" 14 + ) 15 + 16 + // AuthHandler handles authentication endpoints 17 + type AuthHandler struct { 18 + db *database.DB 19 + } 20 + 21 + // NewAuthHandler creates a new auth handler 22 + func NewAuthHandler(db *database.DB) *AuthHandler { 23 + return &AuthHandler{db: db} 24 + } 25 + 26 + // UserResponse represents the current user 27 + type UserResponse struct { 28 + ID int `json:"id"` 29 + Username string `json:"username"` 30 + AvatarURL string `json:"avatar_url"` 31 + Provider string `json:"provider"` 32 + } 33 + 34 + // BeginAuth starts the OAuth flow 35 + func (h *AuthHandler) BeginAuth(w http.ResponseWriter, r *http.Request) { 36 + // Get the auth URL from goth 37 + gothic.BeginAuthHandler(w, r) 38 + } 39 + 40 + // CallbackAuth handles the OAuth callback 41 + func (h *AuthHandler) CallbackAuth(w http.ResponseWriter, r *http.Request) { 42 + // Complete OAuth flow 43 + gothUser, err := gothic.CompleteUserAuth(w, r) 44 + if err != nil { 45 + log.Printf("OAuth callback error: %v", err) 46 + http.Error(w, "Authentication failed", http.StatusInternalServerError) 47 + return 48 + } 49 + 50 + // Create or update user in database 51 + user := &database.User{ 52 + GithubID: 0, // We'll parse this from the user ID 53 + Username: gothUser.NickName, 54 + Email: gothUser.Email, 55 + AvatarURL: gothUser.AvatarURL, 56 + } 57 + 58 + // Parse GitHub ID from RawData 59 + if id, ok := gothUser.RawData["id"].(float64); ok { 60 + user.GithubID = int(id) 61 + } 62 + 63 + if err := h.db.CreateUser(user); err != nil { 64 + log.Printf("Failed to create user: %v", err) 65 + http.Error(w, "Failed to create user", http.StatusInternalServerError) 66 + return 67 + } 68 + 69 + // Save access token 70 + token := &database.AuthToken{ 71 + UserID: user.ID, 72 + Provider: "github", 73 + AccessToken: gothUser.AccessToken, 74 + RefreshToken: gothUser.RefreshToken, 75 + ExpiresAt: gothUser.ExpiresAt, 76 + } 77 + 78 + if err := h.db.SaveAuthToken(token); err != nil { 79 + log.Printf("Failed to save token: %v", err) 80 + } 81 + 82 + // Create session 83 + session, err := auth.GetSession(r) 84 + if err != nil { 85 + log.Printf("Failed to get session: %v", err) 86 + http.Error(w, "Failed to create session", http.StatusInternalServerError) 87 + return 88 + } 89 + 90 + auth.SetUserID(session, user.ID) 91 + if err := auth.SaveSession(r, w, session); err != nil { 92 + log.Printf("Failed to save session: %v", err) 93 + http.Error(w, "Failed to save session", http.StatusInternalServerError) 94 + return 95 + } 96 + 97 + // Redirect to frontend 98 + frontendURL := os.Getenv("FRONTEND_URL") 99 + if frontendURL == "" { 100 + frontendURL = "http://localhost:4321" 101 + } 102 + 103 + http.Redirect(w, r, frontendURL+"/dashboard", http.StatusTemporaryRedirect) 104 + } 105 + 106 + // GetCurrentUser returns the currently authenticated user 107 + func (h *AuthHandler) GetCurrentUser(w http.ResponseWriter, r *http.Request) { 108 + session, err := auth.GetSession(r) 109 + if err != nil { 110 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 111 + return 112 + } 113 + 114 + userID, ok := auth.GetUserID(session) 115 + if !ok { 116 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 117 + return 118 + } 119 + 120 + user, err := h.db.GetUserByID(userID) 121 + if err != nil { 122 + http.Error(w, "User not found", http.StatusNotFound) 123 + return 124 + } 125 + 126 + response := UserResponse{ 127 + ID: user.ID, 128 + Username: user.Username, 129 + AvatarURL: user.AvatarURL, 130 + Provider: "github", 131 + } 132 + 133 + w.Header().Set("Content-Type", "application/json") 134 + json.NewEncoder(w).Encode(response) 135 + } 136 + 137 + // Logout logs out the current user 138 + func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) { 139 + session, err := auth.GetSession(r) 140 + if err != nil { 141 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 142 + return 143 + } 144 + 145 + auth.ClearSession(session) 146 + if err := auth.SaveSession(r, w, session); err != nil { 147 + log.Printf("Failed to save session: %v", err) 148 + } 149 + 150 + w.Header().Set("Content-Type", "application/json") 151 + json.NewEncoder(w).Encode(map[string]bool{"success": true}) 152 + } 153 + 154 + // LoginURL returns the GitHub login URL 155 + func (h *AuthHandler) LoginURL(w http.ResponseWriter, r *http.Request) { 156 + githubCallbackURL := os.Getenv("GITHUB_REDIRECT_URL") 157 + if githubCallbackURL == "" { 158 + githubCallbackURL = "http://localhost:8080/api/auth/github/callback" 159 + } 160 + 161 + clientID := os.Getenv("GITHUB_CLIENT_ID") 162 + authURL := fmt.Sprintf( 163 + "https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&scope=user%%20repo&state=%d", 164 + clientID, 165 + githubCallbackURL, 166 + time.Now().Unix(), 167 + ) 168 + 169 + w.Header().Set("Content-Type", "application/json") 170 + json.NewEncoder(w).Encode(map[string]string{"auth_url": authURL}) 171 + }
+22
backend/internal/api/handlers/health.go
··· 1 + package handlers 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + ) 7 + 8 + // HealthResponse represents the health check response 9 + type HealthResponse struct { 10 + Status string `json:"status"` 11 + Version string `json:"version"` 12 + } 13 + 14 + // HealthCheck handles the health check endpoint 15 + func HealthCheck(w http.ResponseWriter, r *http.Request) { 16 + w.Header().Set("Content-Type", "application/json") 17 + w.WriteHeader(http.StatusOK) 18 + json.NewEncoder(w).Encode(HealthResponse{ 19 + Status: "ok", 20 + Version: "0.1.0", 21 + }) 22 + }
+36
backend/internal/api/middleware/auth.go
··· 1 + package middleware 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/yourusername/markedit/internal/auth" 7 + "github.com/yourusername/markedit/internal/database" 8 + ) 9 + 10 + // RequireAuth ensures the user is authenticated 11 + func RequireAuth(db *database.DB) func(http.Handler) http.Handler { 12 + return func(next http.Handler) http.Handler { 13 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 + session, err := auth.GetSession(r) 15 + if err != nil { 16 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 17 + return 18 + } 19 + 20 + userID, ok := auth.GetUserID(session) 21 + if !ok || userID == 0 { 22 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 23 + return 24 + } 25 + 26 + // Verify user exists in database 27 + _, err = db.GetUserByID(userID) 28 + if err != nil { 29 + http.Error(w, "Unauthorized", http.StatusUnauthorized) 30 + return 31 + } 32 + 33 + next.ServeHTTP(w, r) 34 + }) 35 + } 36 + }
+37
backend/internal/api/middleware/logger.go
··· 1 + package middleware 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "time" 7 + ) 8 + 9 + // Logger logs HTTP requests 10 + func Logger(next http.Handler) http.Handler { 11 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 + start := time.Now() 13 + 14 + // Create a response writer wrapper to capture status code 15 + wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} 16 + 17 + next.ServeHTTP(wrapped, r) 18 + 19 + log.Printf( 20 + "%s %s %d %s", 21 + r.Method, 22 + r.RequestURI, 23 + wrapped.statusCode, 24 + time.Since(start), 25 + ) 26 + }) 27 + } 28 + 29 + type responseWriter struct { 30 + http.ResponseWriter 31 + statusCode int 32 + } 33 + 34 + func (rw *responseWriter) WriteHeader(code int) { 35 + rw.statusCode = code 36 + rw.ResponseWriter.WriteHeader(code) 37 + }
+62
backend/internal/api/router.go
··· 1 + package api 2 + 3 + import ( 4 + "net/http" 5 + "os" 6 + "strings" 7 + 8 + "github.com/go-chi/chi/v5" 9 + chimiddleware "github.com/go-chi/chi/v5/middleware" 10 + "github.com/go-chi/cors" 11 + "github.com/yourusername/markedit/internal/api/handlers" 12 + "github.com/yourusername/markedit/internal/api/middleware" 13 + "github.com/yourusername/markedit/internal/database" 14 + ) 15 + 16 + // NewRouter creates and configures the HTTP router 17 + func NewRouter(db *database.DB) http.Handler { 18 + r := chi.NewRouter() 19 + 20 + // Middleware 21 + r.Use(chimiddleware.RequestID) 22 + r.Use(chimiddleware.RealIP) 23 + r.Use(middleware.Logger) 24 + r.Use(chimiddleware.Recoverer) 25 + 26 + // CORS configuration 27 + allowedOrigins := os.Getenv("ALLOWED_ORIGINS") 28 + if allowedOrigins == "" { 29 + allowedOrigins = "http://localhost:4321" 30 + } 31 + 32 + r.Use(cors.Handler(cors.Options{ 33 + AllowedOrigins: strings.Split(allowedOrigins, ","), 34 + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, 35 + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, 36 + ExposedHeaders: []string{"Link"}, 37 + AllowCredentials: true, 38 + MaxAge: 300, 39 + })) 40 + 41 + // Create handlers 42 + authHandler := handlers.NewAuthHandler(db) 43 + 44 + // Public routes 45 + r.Get("/api/health", handlers.HealthCheck) 46 + r.Get("/api/auth/github/login", authHandler.BeginAuth) 47 + r.Get("/api/auth/github/callback", authHandler.CallbackAuth) 48 + 49 + // Protected routes 50 + r.Group(func(r chi.Router) { 51 + r.Use(middleware.RequireAuth(db)) 52 + 53 + r.Get("/api/auth/user", authHandler.GetCurrentUser) 54 + r.Post("/api/auth/logout", authHandler.Logout) 55 + 56 + // Repository routes (to be implemented) 57 + // r.Get("/api/repos", repoHandler.ListRepos) 58 + // r.Get("/api/repos/{owner}/{repo}/files", repoHandler.ListFiles) 59 + }) 60 + 61 + return r 62 + }
+36
backend/internal/auth/github.go
··· 1 + package auth 2 + 3 + import ( 4 + "fmt" 5 + "os" 6 + 7 + "github.com/markbates/goth" 8 + "github.com/markbates/goth/providers/github" 9 + ) 10 + 11 + // SetupProviders initializes OAuth providers 12 + func SetupProviders() error { 13 + githubClientID := os.Getenv("GITHUB_CLIENT_ID") 14 + githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET") 15 + githubCallbackURL := os.Getenv("GITHUB_REDIRECT_URL") 16 + 17 + if githubClientID == "" || githubClientSecret == "" { 18 + return fmt.Errorf("GitHub OAuth credentials not configured") 19 + } 20 + 21 + if githubCallbackURL == "" { 22 + githubCallbackURL = "http://localhost:8080/api/auth/github/callback" 23 + } 24 + 25 + // Initialize GitHub provider 26 + goth.UseProviders( 27 + github.New( 28 + githubClientID, 29 + githubClientSecret, 30 + githubCallbackURL, 31 + "user", "repo", // OAuth scopes 32 + ), 33 + ) 34 + 35 + return nil 36 + }
+85
backend/internal/auth/session.go
··· 1 + package auth 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "os" 7 + "strconv" 8 + 9 + "github.com/gorilla/sessions" 10 + ) 11 + 12 + const ( 13 + SessionName = "markedit-session" 14 + UserIDKey = "user_id" 15 + ) 16 + 17 + var store *sessions.CookieStore 18 + 19 + // InitSessions initializes the session store 20 + func InitSessions() error { 21 + sessionSecret := os.Getenv("SESSION_SECRET") 22 + if sessionSecret == "" { 23 + return fmt.Errorf("SESSION_SECRET not configured") 24 + } 25 + 26 + if len(sessionSecret) < 32 { 27 + return fmt.Errorf("SESSION_SECRET must be at least 32 characters") 28 + } 29 + 30 + store = sessions.NewCookieStore([]byte(sessionSecret)) 31 + 32 + // Configure session options 33 + sessionSecure := os.Getenv("SESSION_SECURE") == "true" 34 + maxAge := 86400 // 24 hours default 35 + 36 + if maxAgeStr := os.Getenv("SESSION_MAX_AGE"); maxAgeStr != "" { 37 + if val, err := strconv.Atoi(maxAgeStr); err == nil { 38 + maxAge = val 39 + } 40 + } 41 + 42 + store.Options = &sessions.Options{ 43 + Path: "/", 44 + MaxAge: maxAge, 45 + HttpOnly: true, 46 + Secure: sessionSecure, 47 + SameSite: http.SameSiteLaxMode, 48 + } 49 + 50 + return nil 51 + } 52 + 53 + // GetSession retrieves the session for a request 54 + func GetSession(r *http.Request) (*sessions.Session, error) { 55 + return store.Get(r, SessionName) 56 + } 57 + 58 + // SaveSession saves the session 59 + func SaveSession(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { 60 + return session.Save(r, w) 61 + } 62 + 63 + // SetUserID sets the user ID in the session 64 + func SetUserID(session *sessions.Session, userID int) { 65 + session.Values[UserIDKey] = userID 66 + } 67 + 68 + // GetUserID gets the user ID from the session 69 + func GetUserID(session *sessions.Session) (int, bool) { 70 + val, ok := session.Values[UserIDKey] 71 + if !ok { 72 + return 0, false 73 + } 74 + 75 + userID, ok := val.(int) 76 + return userID, ok 77 + } 78 + 79 + // ClearSession clears the session 80 + func ClearSession(session *sessions.Session) { 81 + session.Options.MaxAge = -1 82 + for key := range session.Values { 83 + delete(session.Values, key) 84 + } 85 + }