diff --git a/cmd/CatsOfMastodonBotGo/main.go b/cmd/CatsOfMastodonBotGo/main.go index ee37dd9..73f0704 100644 --- a/cmd/CatsOfMastodonBotGo/main.go +++ b/cmd/CatsOfMastodonBotGo/main.go @@ -1,6 +1,7 @@ package main import ( + "CatsOfMastodonBotGo/internal/auth" "CatsOfMastodonBotGo/internal/config" "CatsOfMastodonBotGo/internal/database" "CatsOfMastodonBotGo/internal/domain" @@ -24,6 +25,8 @@ func main() { // Not needed but anyways services.InitImgKitHelper() + auth.InitGiteaOauth2Token() + ticker := time.NewTicker(10 * time.Minute) runFetchPosts := func() { diff --git a/internal/auth/oauth2.go b/internal/auth/oauth2.go new file mode 100644 index 0000000..a995ce3 --- /dev/null +++ b/internal/auth/oauth2.go @@ -0,0 +1,98 @@ +package auth + +import ( + "CatsOfMastodonBotGo/internal/config" + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" +) + +type GiteaOAuth2Handler struct { + ClientID string + ClientSecret string + InstanceUrl string +} + +var GiteaOauth2HandlerInstance *GiteaOAuth2Handler + +func InitGiteaOauth2Token() { + GiteaOauth2HandlerInstance = &GiteaOAuth2Handler{ + ClientID: config.Config.GiteaOauthClientID, + ClientSecret: config.Config.GiteaOauthClientSecret, + InstanceUrl: config.Config.GiteaOauthInstance, + } +} + +func (g *GiteaOAuth2Handler) GetGiteaLoginURL (redirectHost string) (string) { + authUrl := g.InstanceUrl + "/login/oauth/authorize?client_id=" + g.ClientID + "&redirect_uri=" + redirectHost + "/oath/gitea&scope=openid&response_type=code&response_mode=form_post" + return authUrl +} + +func (g *GiteaOAuth2Handler) GetGiteaUserEmailByCode(code string) (string, error) { + accessToken, err := getGiteaAccessTokenByCode(code) + if err != nil { + return "", err + } + + req, err := http.NewRequest("POST", g.InstanceUrl+"/login/oauth/userinfo", nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer " + accessToken) + req.Header.Set("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var userInfo struct { + Email string `json:"email"` + } + if err := json.Unmarshal(body, &userInfo); err != nil { + return "", err + } + + return userInfo.Email, nil +} + + + +func getGiteaAccessTokenByCode(code string) (string, error) { + form := url.Values{} + form.Add("client_id", GiteaOauth2HandlerInstance.ClientID) + form.Add("client_secret", GiteaOauth2HandlerInstance.ClientSecret) + form.Add("code", code) + form.Add("grant_type", "authorization_code") + + req, err := http.NewRequest("POST", GiteaOauth2HandlerInstance.InstanceUrl+"/login/oauth/access_token", bytes.NewBufferString(form.Encode())) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var tokenResp struct { + AccessToken string `json:"access_token"` + } + if err := json.Unmarshal(body, &tokenResp); err != nil { + return "", err + } + + return tokenResp.AccessToken, nil +} diff --git a/internal/config/config.go b/internal/config/config.go index 6471115..f5d522e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,6 +3,7 @@ package config import ( "log/slog" "os" + "strings" "github.com/joho/godotenv" ) @@ -24,6 +25,11 @@ type config struct { DBName string ImageKitId string + + GiteaOauthInstance string + GiteaOauthClientID string + GiteaOauthClientSecret string + GiteaOauthAllowedEmails []string } var Config *config @@ -74,6 +80,16 @@ func Load() *config { dbPassword := os.Getenv("CAOM_DB_PASSWORD") dbName := os.Getenv("CAOM_DB_NAME") + giteaOauthInstance := os.Getenv("CAOM_GITEA_OAUTH_INSTANCE") + giteaOauthClientID := os.Getenv("CAOM_GITEA_OAUTH_CLIENT_ID") + giteaOauthClientSecret := os.Getenv("CAOM_GITEA_OAUTH_CLIENT_SECRET") + + giteaOauthAllowedEmails := os.Getenv("CAOM_GITEA_OAUTH_ALLOWED_EMAILS") + var giteaOauthAllowedEmailsParsed []string + if giteaOauthAllowedEmails != "" { + giteaOauthAllowedEmailsParsed = strings.Split(giteaOauthAllowedEmails, ",") + } + if dbEngine == "" || dbHost == "" || dbPort == "" || dbUser == "" || dbPassword == "" || dbName == "" { slog.Info("No database connection provided, using sqlite") dbEngine = "sqlite" @@ -106,6 +122,11 @@ func Load() *config { DBName: dbName, ImageKitId: imageKitId, + + GiteaOauthInstance: giteaOauthInstance, + GiteaOauthClientID: giteaOauthClientID, + GiteaOauthClientSecret: giteaOauthClientSecret, + GiteaOauthAllowedEmails: giteaOauthAllowedEmailsParsed, } return appContext diff --git a/internal/server/router.go b/internal/server/router.go index 5e5f60d..96ec37f 100644 --- a/internal/server/router.go +++ b/internal/server/router.go @@ -47,6 +47,7 @@ func SetupRouter() *gin.Engine { adminApi := admin.Group("/api") adminApi.POST("/login", handlers.AdminDashboardHandlerInstance.Login) + adminApi.GET("/login/oauth/gitea", handlers.OauthLoginHandlerInstance.GoToGiteaLogin) adminApi.GET("/getmedia", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.GetMedia) adminApi.POST("/approve", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.ApproveMedia) adminApi.POST("/reject", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.RejectMedia) diff --git a/internal/web/dto/adminDash.go b/internal/web/dto/adminDash.go index 04f7d8b..52720fe 100644 --- a/internal/web/dto/adminDash.go +++ b/internal/web/dto/adminDash.go @@ -11,3 +11,7 @@ type LoginInput struct { type RejectMediaInput struct { MediaId string `json:"mediaId" binding:"required"` } + +type GiteaLoginInput struct { + Code string `json:"code" binding:"required"` +} \ No newline at end of file diff --git a/internal/web/handlers/oauth.go b/internal/web/handlers/oauth.go new file mode 100644 index 0000000..f7a4218 --- /dev/null +++ b/internal/web/handlers/oauth.go @@ -0,0 +1,59 @@ +package handlers + +import ( + "CatsOfMastodonBotGo/internal/auth" + "CatsOfMastodonBotGo/internal/config" + "CatsOfMastodonBotGo/internal/web/dto" + "net/http" + + "github.com/gin-gonic/gin" +) + +type OauthLoginHandler struct { + Jwt auth.JwtTokenGenerator + OauthLoginHandler auth.GiteaOAuth2Handler +} + +var OauthLoginHandlerInstance *OauthLoginHandler + +func InitOauthLoginHandler() { + OauthLoginHandlerInstance = &OauthLoginHandler{ + Jwt: *auth.JwtTokenGeneratorInstance, + } +} + +func (_ *OauthLoginHandler) GoToGiteaLogin(c *gin.Context) { + c.Redirect(http.StatusFound, auth.GiteaOauth2HandlerInstance.GetGiteaLoginURL(c.Request.Host)) +} + +func (olh *OauthLoginHandler) LoginWithGitea(c *gin.Context) { + var input dto.GiteaLoginInput + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + userEmail, err := olh.OauthLoginHandler.GetGiteaUserEmailByCode(input.Code) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + for _, email := range config.Config.GiteaOauthAllowedEmails { + if email == userEmail { + token, err := olh.Jwt.GenerateToken(map[string]interface{}{"role": "admin"}) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Login successful", "token": token}) + } else { + c.JSON(401, gin.H{ + "error": "oath login faied or yyour email does not have access", + }) + return + } + } + +}