Refactor: Implement uber-go/fx dependency injection
- Replace global variable pattern with proper dependency injection - Add uber-go/fx for automatic dependency resolution - Refactor all services and handlers to use constructor injection - Eliminate fragile initialization order dependencies - Improve testability and modularity - Add structured logging with zap Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
		
							
								
								
									
										75
									
								
								FX_MIGRATION.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								FX_MIGRATION.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
# FX Dependency Injection Implementation
 | 
			
		||||
 | 
			
		||||
This document summarizes the changes made to implement uber-go/fx dependency injection in the CatsOfMastodonBotGo application.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
We've replaced the global variable pattern with proper dependency injection using uber-go/fx. This improves:
 | 
			
		||||
- Testability
 | 
			
		||||
- Modularity
 | 
			
		||||
- Explicit dependency management
 | 
			
		||||
- Eliminates fragile initialization order dependencies
 | 
			
		||||
 | 
			
		||||
## Key Changes
 | 
			
		||||
 | 
			
		||||
### 1. Added Dependencies
 | 
			
		||||
- Added `go.uber.org/fx` dependency to go.mod
 | 
			
		||||
- Added `go.uber.org/zap` for structured logging
 | 
			
		||||
 | 
			
		||||
### 2. Refactored main.go
 | 
			
		||||
- Replaced manual initialization with fx application
 | 
			
		||||
- Used `fx.Provide` to register constructors for all components
 | 
			
		||||
- Used `fx.Invoke` to start background tasks and server
 | 
			
		||||
- Added proper logger integration
 | 
			
		||||
 | 
			
		||||
### 3. Refactored Configuration (config/config.go)
 | 
			
		||||
- Removed global `Config` variable
 | 
			
		||||
- Removed `Init()` function
 | 
			
		||||
- Kept `Load()` function as constructor
 | 
			
		||||
 | 
			
		||||
### 4. Refactored Database (database/database.go)
 | 
			
		||||
- Removed global `Gorm` variable
 | 
			
		||||
- Function now accepts config as parameter
 | 
			
		||||
 | 
			
		||||
### 5. Refactored Services
 | 
			
		||||
- **PostService**: Now accepts database and config as dependencies
 | 
			
		||||
- **ImgKitHelper**: Now accepts config as dependency
 | 
			
		||||
 | 
			
		||||
### 6. Refactored Auth
 | 
			
		||||
- **JwtTokenGenerator**: Now accepts config as dependency
 | 
			
		||||
- **GiteaOAuth2Handler**: Now accepts config as dependency
 | 
			
		||||
 | 
			
		||||
### 7. Refactored Handlers
 | 
			
		||||
- All handlers now use constructor injection
 | 
			
		||||
- Dependencies are explicitly declared in constructor signatures
 | 
			
		||||
- Removed global instance variables
 | 
			
		||||
 | 
			
		||||
### 8. Refactored Server/Router
 | 
			
		||||
- Router now receives all handlers through fx dependency injection
 | 
			
		||||
- Uses fx.Lifecycle for proper startup/shutdown handling
 | 
			
		||||
 | 
			
		||||
## Benefits Achieved
 | 
			
		||||
 | 
			
		||||
1. **Eliminated Global State**: No more global variables causing tight coupling
 | 
			
		||||
2. **Explicit Dependencies**: All dependencies are clearly visible in constructor signatures
 | 
			
		||||
3. **Automatic Wiring**: fx automatically resolves and injects dependencies
 | 
			
		||||
4. **Improved Testability**: Easy to mock dependencies for unit tests
 | 
			
		||||
5. **Reduced Boilerplate**: No need for manual initialization functions
 | 
			
		||||
6. **Error Detection**: Dependency resolution errors caught at startup
 | 
			
		||||
 | 
			
		||||
## Files Modified
 | 
			
		||||
 | 
			
		||||
- cmd/CatsOfMastodonBotGo/main.go
 | 
			
		||||
- go.mod
 | 
			
		||||
- go.sum
 | 
			
		||||
- internal/auth/jwt.go
 | 
			
		||||
- internal/auth/oauth2.go
 | 
			
		||||
- internal/config/config.go
 | 
			
		||||
- internal/database/database.go
 | 
			
		||||
- internal/server/router.go
 | 
			
		||||
- internal/services/imgKitHelper.go
 | 
			
		||||
- internal/services/postService.go
 | 
			
		||||
- internal/web/handlers/adminDash.go
 | 
			
		||||
- internal/web/handlers/apiEndpoint.go
 | 
			
		||||
- internal/web/handlers/embedCard.go
 | 
			
		||||
- internal/web/handlers/oauth.go
 | 
			
		||||
@@ -1,75 +1,134 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/auth"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/config"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/database"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/domain"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/server"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/services"
 | 
			
		||||
	"context"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/web/handlers"
 | 
			
		||||
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
	"go.uber.org/zap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	// Setup config
 | 
			
		||||
	config.Init()
 | 
			
		||||
	fx.New(
 | 
			
		||||
		fx.Provide(
 | 
			
		||||
			// Configuration
 | 
			
		||||
			config.Load,
 | 
			
		||||
			
 | 
			
		||||
	// Initialize database
 | 
			
		||||
	database.Init()
 | 
			
		||||
			// Database
 | 
			
		||||
			database.Connect,
 | 
			
		||||
			
 | 
			
		||||
	services.InitPostService()
 | 
			
		||||
			// Services
 | 
			
		||||
			services.NewPostService,
 | 
			
		||||
			services.NewImgKitHelper,
 | 
			
		||||
			
 | 
			
		||||
	// Not needed but anyways
 | 
			
		||||
	services.InitImgKitHelper()
 | 
			
		||||
			// Auth
 | 
			
		||||
			auth.NewJwtTokenGenerator,
 | 
			
		||||
			auth.NewGiteaOauth2Token,
 | 
			
		||||
			
 | 
			
		||||
	ticker := time.NewTicker(10 * time.Minute)
 | 
			
		||||
			// Handlers
 | 
			
		||||
			handlers.NewAdminDashboardHandler,
 | 
			
		||||
			handlers.NewApiEndpointHandler,
 | 
			
		||||
			handlers.NewEmbedCardHandler,
 | 
			
		||||
			handlers.NewOauthLoginHandler,
 | 
			
		||||
			
 | 
			
		||||
	runFetchPosts := func() {
 | 
			
		||||
		// Get posts
 | 
			
		||||
		ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
		var posts []domain.Post = nil
 | 
			
		||||
		err, posts := services.PostServiceInstance.GetPostsFromApi(ctx, config.Config.Tag, config.Config.Instance)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			slog.Error(err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var existingPostIds = services.PostServiceInstance.GetExistingPostIds()
 | 
			
		||||
		var existingAccountIds = services.PostServiceInstance.GetExistingAccountIds()
 | 
			
		||||
		var newPosts = services.PostServiceInstance.GetNewPosts(existingPostIds, posts)
 | 
			
		||||
		var newAccounts = services.PostServiceInstance.GetNewAccounts(existingAccountIds, newPosts)
 | 
			
		||||
 | 
			
		||||
		// Save to database
 | 
			
		||||
		slog.Info("Fetched " + strconv.Itoa(len(posts)) + " posts; " + strconv.Itoa(len(existingPostIds)) + " existing posts; " + strconv.Itoa(len(newPosts)) + " new posts and " + strconv.Itoa(len(newAccounts)) + " new accounts\n")
 | 
			
		||||
		// Additional logging
 | 
			
		||||
		if newAccounts != nil {
 | 
			
		||||
			slog.Info("Inserted " + strconv.Itoa(services.PostServiceInstance.InsertNewAccounts(newAccounts)) + " accounts\n")
 | 
			
		||||
		}
 | 
			
		||||
		if newPosts != nil {
 | 
			
		||||
			slog.Info("Inserted " + strconv.Itoa(services.PostServiceInstance.InsertNewPosts(newPosts)) + " posts\n")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	go func() {
 | 
			
		||||
		for range ticker.C {
 | 
			
		||||
			runFetchPosts()
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// Run initial fetch on startup
 | 
			
		||||
	go func() {
 | 
			
		||||
		runFetchPosts()
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// https://seefnasrul.medium.com/create-your-first-go-rest-api-with-jwt-authentication-in-gin-framework-dbe5bda72817
 | 
			
		||||
 | 
			
		||||
	r := server.SetupRouter()
 | 
			
		||||
	err := r.Run(":8080")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		slog.Error(err.Error())
 | 
			
		||||
	}
 | 
			
		||||
			// Logger
 | 
			
		||||
			NewLogger,
 | 
			
		||||
		),
 | 
			
		||||
		fx.Invoke(
 | 
			
		||||
			// Start background tasks
 | 
			
		||||
			startBackgroundTasks,
 | 
			
		||||
			
 | 
			
		||||
			// Setup and start server
 | 
			
		||||
			server.SetupRouter,
 | 
			
		||||
		),
 | 
			
		||||
		fx.Logger(NewFXLogger()), // Optional custom logger
 | 
			
		||||
	).Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Background tasks as a separate function
 | 
			
		||||
func startBackgroundTasks(
 | 
			
		||||
	lc fx.Lifecycle,
 | 
			
		||||
	postService *services.PostService,
 | 
			
		||||
	cfg *config.Config,
 | 
			
		||||
	logger *zap.Logger,
 | 
			
		||||
) {
 | 
			
		||||
	lc.Append(fx.Hook{
 | 
			
		||||
		OnStart: func(context.Context) error {
 | 
			
		||||
			ticker := time.NewTicker(10 * time.Minute)
 | 
			
		||||
			
 | 
			
		||||
			runFetchPosts := func() {
 | 
			
		||||
				ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
 | 
			
		||||
				defer cancel()
 | 
			
		||||
				
 | 
			
		||||
				posts, err := postService.GetPostsFromApi(ctx, cfg.Tag, cfg.Instance)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error("Failed to fetch posts", zap.Error(err))
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				
 | 
			
		||||
				existingPostIds := postService.GetExistingPostIds()
 | 
			
		||||
				existingAccountIds := postService.GetExistingAccountIds()
 | 
			
		||||
				newPosts := postService.GetNewPosts(existingPostIds, posts)
 | 
			
		||||
				newAccounts := postService.GetNewAccounts(existingAccountIds, newPosts)
 | 
			
		||||
				
 | 
			
		||||
				logger.Info("Fetched posts",
 | 
			
		||||
					zap.Int("total", len(posts)),
 | 
			
		||||
					zap.Int("existing", len(existingPostIds)),
 | 
			
		||||
					zap.Int("new", len(newPosts)),
 | 
			
		||||
					zap.Int("new_accounts", len(newAccounts)))
 | 
			
		||||
				
 | 
			
		||||
				if len(newAccounts) > 0 {
 | 
			
		||||
					count := postService.InsertNewAccounts(newAccounts)
 | 
			
		||||
					logger.Info("Inserted accounts", zap.Int("count", count))
 | 
			
		||||
				}
 | 
			
		||||
				
 | 
			
		||||
				if len(newPosts) > 0 {
 | 
			
		||||
					count := postService.InsertNewPosts(newPosts)
 | 
			
		||||
					logger.Info("Inserted posts", zap.Int("count", count))
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			
 | 
			
		||||
			// Run initial fetch
 | 
			
		||||
			go runFetchPosts()
 | 
			
		||||
			
 | 
			
		||||
			// Start ticker
 | 
			
		||||
			go func() {
 | 
			
		||||
				for range ticker.C {
 | 
			
		||||
					runFetchPosts()
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
			
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
		OnStop: func(context.Context) error {
 | 
			
		||||
			// Cleanup if needed
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Logger provider
 | 
			
		||||
func NewLogger() (*zap.Logger, error) {
 | 
			
		||||
	return zap.NewDevelopment()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Simple logger for fx
 | 
			
		||||
type FXLogger struct {
 | 
			
		||||
	logger *zap.Logger
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewFXLogger() *FXLogger {
 | 
			
		||||
	logger, _ := zap.NewDevelopment()
 | 
			
		||||
	return &FXLogger{logger: logger}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l *FXLogger) Printf(str string, args ...interface{}) {
 | 
			
		||||
	l.logger.Sugar().Infof(str, args...)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.mod
									
									
									
									
									
								
							@@ -39,6 +39,10 @@ require (
 | 
			
		||||
	github.com/rogpeppe/go-internal v1.12.0 // indirect
 | 
			
		||||
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 | 
			
		||||
	github.com/ugorji/go/codec v1.3.0 // indirect
 | 
			
		||||
	go.uber.org/dig v1.19.0 // indirect
 | 
			
		||||
	go.uber.org/fx v1.24.0 // indirect
 | 
			
		||||
	go.uber.org/multierr v1.10.0 // indirect
 | 
			
		||||
	go.uber.org/zap v1.26.0 // indirect
 | 
			
		||||
	golang.org/x/arch v0.21.0 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.40.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.42.0 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.sum
									
									
									
									
									
								
							@@ -88,6 +88,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
 | 
			
		||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 | 
			
		||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
 | 
			
		||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
 | 
			
		||||
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
 | 
			
		||||
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
 | 
			
		||||
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
 | 
			
		||||
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
 | 
			
		||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
 | 
			
		||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
 | 
			
		||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
 | 
			
		||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
 | 
			
		||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
 | 
			
		||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
 | 
			
		||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
 | 
			
		||||
 
 | 
			
		||||
@@ -10,33 +10,25 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type JwtTokenGenerator struct {
 | 
			
		||||
	Key      string
 | 
			
		||||
	Issuer   string
 | 
			
		||||
	Audience string
 | 
			
		||||
	cfg *config.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var JwtTokenGeneratorInstance *JwtTokenGenerator
 | 
			
		||||
 | 
			
		||||
func InitJwtTokenGenerator() {
 | 
			
		||||
	JwtTokenGeneratorInstance = &JwtTokenGenerator{
 | 
			
		||||
		Key:      config.Config.JwtSecret,
 | 
			
		||||
		Issuer:   config.Config.JwtIssuer,
 | 
			
		||||
		Audience: config.Config.JwtAudience,
 | 
			
		||||
	}
 | 
			
		||||
func NewJwtTokenGenerator(cfg *config.Config) *JwtTokenGenerator {
 | 
			
		||||
	return &JwtTokenGenerator{cfg: cfg}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (j *JwtTokenGenerator) GenerateToken(claims map[string]interface{}) (string, error) {
 | 
			
		||||
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
 | 
			
		||||
		"exp": time.Now().AddDate(0, 0, 1).Unix(),
 | 
			
		||||
		"iat": time.Now().Unix(),
 | 
			
		||||
		"iss": j.Issuer,
 | 
			
		||||
		"aud": j.Audience,
 | 
			
		||||
		"iss": j.cfg.JwtIssuer,
 | 
			
		||||
		"aud": j.cfg.JwtAudience,
 | 
			
		||||
	})
 | 
			
		||||
	for k, v := range claims {
 | 
			
		||||
		token.Claims.(jwt.MapClaims)[k] = v
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return token.SignedString([]byte(j.Key))
 | 
			
		||||
	return token.SignedString([]byte(j.cfg.JwtSecret))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Gin middleware
 | 
			
		||||
@@ -53,7 +45,7 @@ func (j *JwtTokenGenerator) GinMiddleware() gin.HandlerFunc {
 | 
			
		||||
			if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
 | 
			
		||||
				return nil, jwt.ErrSignatureInvalid
 | 
			
		||||
			}
 | 
			
		||||
			return []byte(j.Key), nil
 | 
			
		||||
			return []byte(j.cfg.JwtSecret), nil
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,23 +11,15 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type GiteaOAuth2Handler struct {
 | 
			
		||||
	ClientID     string
 | 
			
		||||
	ClientSecret string
 | 
			
		||||
	InstanceUrl  string
 | 
			
		||||
	cfg *config.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var GiteaOauth2HandlerInstance *GiteaOAuth2Handler
 | 
			
		||||
 | 
			
		||||
func InitGiteaOauth2Token() {
 | 
			
		||||
	GiteaOauth2HandlerInstance = &GiteaOAuth2Handler{
 | 
			
		||||
		ClientID:     config.Config.GiteaOauthClientID,
 | 
			
		||||
		ClientSecret: config.Config.GiteaOauthClientSecret,
 | 
			
		||||
		InstanceUrl:  config.Config.GiteaOauthInstance,
 | 
			
		||||
	}
 | 
			
		||||
func NewGiteaOauth2Token(cfg *config.Config) *GiteaOAuth2Handler {
 | 
			
		||||
	return &GiteaOAuth2Handler{cfg: cfg}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GiteaOAuth2Handler) GetGiteaLoginURL(redirectHost string) (string, error) {
 | 
			
		||||
	if g.InstanceUrl == "" {
 | 
			
		||||
	if g.cfg.GiteaOauthInstance == "" {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
@@ -36,13 +28,13 @@ func (g *GiteaOAuth2Handler) GetGiteaLoginURL(redirectHost string) (string, erro
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	authUrl := g.InstanceUrl + "/login/oauth/authorize?client_id=" + g.ClientID + "&redirect_uri=" + "http://" + redirectHost + "/admin/oauth/gitea/callback&scope=openid&response_type=code&response_mode=form_post"
 | 
			
		||||
	authUrl := g.cfg.GiteaOauthInstance + "/login/oauth/authorize?client_id=" + g.cfg.GiteaOauthClientID + "&redirect_uri=" + "http://" + redirectHost + "/admin/oauth/gitea/callback&scope=openid&response_type=code&response_mode=form_post"
 | 
			
		||||
	return authUrl, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (g *GiteaOAuth2Handler) GetGiteaUserEmailByCode(code string) (string, error) {
 | 
			
		||||
 | 
			
		||||
	if g.InstanceUrl == "" {
 | 
			
		||||
	if g.cfg.GiteaOauthInstance == "" {
 | 
			
		||||
        slog.Error("Instance URL not provided")
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
@@ -52,7 +44,7 @@ func (g *GiteaOAuth2Handler) GetGiteaUserEmailByCode(code string) (string, error
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userInfoUrl := g.InstanceUrl + "/login/oauth/userinfo"
 | 
			
		||||
	userInfoUrl := g.cfg.GiteaOauthInstance + "/login/oauth/userinfo"
 | 
			
		||||
	slog.Info(userInfoUrl)
 | 
			
		||||
	req, err := http.NewRequest("POST", userInfoUrl, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -85,12 +77,12 @@ func (g *GiteaOAuth2Handler) GetGiteaUserEmailByCode(code string) (string, error
 | 
			
		||||
 | 
			
		||||
func (g *GiteaOAuth2Handler) getGiteaAccessTokenByCode(code string) (string, error) {
 | 
			
		||||
	form := url.Values{}
 | 
			
		||||
	form.Add("client_id", g.ClientID)
 | 
			
		||||
	form.Add("client_secret", g.ClientSecret)
 | 
			
		||||
	form.Add("client_id", g.cfg.GiteaOauthClientID)
 | 
			
		||||
	form.Add("client_secret", g.cfg.GiteaOauthClientSecret)
 | 
			
		||||
	form.Add("code", code)
 | 
			
		||||
	form.Add("grant_type", "authorization_code")
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("POST", g.InstanceUrl+"/login/oauth/access_token", bytes.NewBufferString(form.Encode()))
 | 
			
		||||
	req, err := http.NewRequest("POST", g.cfg.GiteaOauthInstance+"/login/oauth/access_token", bytes.NewBufferString(form.Encode()))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import (
 | 
			
		||||
	"github.com/joho/godotenv"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type config struct {
 | 
			
		||||
type Config struct {
 | 
			
		||||
	AdminPassword string
 | 
			
		||||
	Instance      string
 | 
			
		||||
	Tag           string
 | 
			
		||||
@@ -26,15 +26,13 @@ type config struct {
 | 
			
		||||
 | 
			
		||||
	ImageKitId string
 | 
			
		||||
 | 
			
		||||
	GiteaOauthInstance	string
 | 
			
		||||
	GiteaOauthClientID	string
 | 
			
		||||
	GiteaOauthClientSecret	string
 | 
			
		||||
	GiteaOauthAllowedEmails	[]string
 | 
			
		||||
	GiteaOauthInstance      string
 | 
			
		||||
	GiteaOauthClientID      string
 | 
			
		||||
	GiteaOauthClientSecret  string
 | 
			
		||||
	GiteaOauthAllowedEmails []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var Config *config
 | 
			
		||||
 | 
			
		||||
func Load() *config {
 | 
			
		||||
func Load() *Config {
 | 
			
		||||
	err := godotenv.Load()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		slog.Warn("Error loading .env file - Using environment variables instead")
 | 
			
		||||
@@ -104,8 +102,8 @@ func Load() *config {
 | 
			
		||||
	if imageKitId == "" {
 | 
			
		||||
		slog.Info("No imagekit id provided, not using imagekit.io")
 | 
			
		||||
	}
 | 
			
		||||
	// Inititlize AppContext
 | 
			
		||||
	var appContext = &config{
 | 
			
		||||
	// Initialize AppContext
 | 
			
		||||
	return &Config{
 | 
			
		||||
		AdminPassword: adminPassword,
 | 
			
		||||
		Instance:      instance,
 | 
			
		||||
		Tag:           tag,
 | 
			
		||||
@@ -123,15 +121,9 @@ func Load() *config {
 | 
			
		||||
 | 
			
		||||
		ImageKitId: imageKitId,
 | 
			
		||||
 | 
			
		||||
		GiteaOauthInstance:	giteaOauthInstance,
 | 
			
		||||
		GiteaOauthClientID:	giteaOauthClientID,
 | 
			
		||||
		GiteaOauthClientSecret:	giteaOauthClientSecret,
 | 
			
		||||
		GiteaOauthInstance:      giteaOauthInstance,
 | 
			
		||||
		GiteaOauthClientID:      giteaOauthClientID,
 | 
			
		||||
		GiteaOauthClientSecret:  giteaOauthClientSecret,
 | 
			
		||||
		GiteaOauthAllowedEmails: giteaOauthAllowedEmailsParsed,
 | 
			
		||||
	}
 | 
			
		||||
	return appContext
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Init() {
 | 
			
		||||
	Config = Load()
 | 
			
		||||
}
 | 
			
		||||
@@ -11,13 +11,11 @@ import (
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var Gorm *gorm.DB
 | 
			
		||||
 | 
			
		||||
func Connect() (*gorm.DB, error) {
 | 
			
		||||
func Connect(cfg *config.Config) (*gorm.DB, error) {
 | 
			
		||||
	var db *gorm.DB
 | 
			
		||||
	var err error = nil
 | 
			
		||||
 | 
			
		||||
	if config.Config.DBEngine == "sqlite" {
 | 
			
		||||
	if cfg.DBEngine == "sqlite" {
 | 
			
		||||
		_, err = os.ReadDir("data")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			err = os.Mkdir("data", 0755)
 | 
			
		||||
@@ -31,11 +29,11 @@ func Connect() (*gorm.DB, error) {
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
 | 
			
		||||
			config.Config.DBUser,
 | 
			
		||||
			config.Config.DBPassword,
 | 
			
		||||
			config.Config.DBHost,
 | 
			
		||||
			config.Config.DBPort,
 | 
			
		||||
			config.Config.DBName)
 | 
			
		||||
			cfg.DBUser,
 | 
			
		||||
			cfg.DBPassword,
 | 
			
		||||
			cfg.DBHost,
 | 
			
		||||
			cfg.DBPort,
 | 
			
		||||
			cfg.DBName)
 | 
			
		||||
		db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
@@ -55,12 +53,3 @@ func Connect() (*gorm.DB, error) {
 | 
			
		||||
	}
 | 
			
		||||
	return db, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IDK if this is how it works or not, leave it as is for now
 | 
			
		||||
func Init() {
 | 
			
		||||
	var err error
 | 
			
		||||
	Gorm, err = Connect()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,33 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/auth"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/web/handlers"
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/web/handlers"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/cors"
 | 
			
		||||
	"github.com/gin-contrib/static"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"go.uber.org/fx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SetupRouter() *gin.Engine {
 | 
			
		||||
// fx.In allows fx to inject multiple dependencies
 | 
			
		||||
type RouterParams struct {
 | 
			
		||||
	fx.In
 | 
			
		||||
 | 
			
		||||
	Lifecycle      fx.Lifecycle
 | 
			
		||||
	AdminDashboard *handlers.AdminDashboardHandler
 | 
			
		||||
	ApiEndpoint    *handlers.ApiEndpointHandler
 | 
			
		||||
	EmbedCard      *handlers.EmbedCardHandler
 | 
			
		||||
	OauthLogin     *handlers.OauthLoginHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func SetupRouter(params RouterParams) {
 | 
			
		||||
	r := gin.Default()
 | 
			
		||||
 | 
			
		||||
	r.Use(cors.New(cors.Config{
 | 
			
		||||
		AllowAllOrigins: true,
 | 
			
		||||
		AllowAllOrigins:  true,
 | 
			
		||||
		AllowMethods:     []string{"POST", "GET", "OPTIONS"},
 | 
			
		||||
		AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
 | 
			
		||||
		AllowCredentials: true,
 | 
			
		||||
@@ -22,44 +35,37 @@ func SetupRouter() *gin.Engine {
 | 
			
		||||
 | 
			
		||||
	r.LoadHTMLGlob("internal/web/templates/home/*")
 | 
			
		||||
 | 
			
		||||
	auth.InitJwtTokenGenerator() // Must be befor initializing admin handler, otherwise 'panic: runtime error: invalid memory address or nil pointer dereference'
 | 
			
		||||
	auth.InitGiteaOauth2Token()
 | 
			
		||||
	handlers.InitAdminDashboardHandler()
 | 
			
		||||
	handlers.InitApiEndpointHandler()
 | 
			
		||||
	handlers.InitEmbedCardHandler()
 | 
			
		||||
	handlers.InitOauthLoginHandler()
 | 
			
		||||
 | 
			
		||||
	// Main page
 | 
			
		||||
	r.GET("/", func(c *gin.Context) {
 | 
			
		||||
		c.HTML(http.StatusOK, "home/index.html", nil)
 | 
			
		||||
	})
 | 
			
		||||
	// Embed card
 | 
			
		||||
	r.GET("/embed", handlers.EmbedCardHandlerInstance.GetEmbedCard)
 | 
			
		||||
	r.GET("/embed", params.EmbedCard.GetEmbedCard)
 | 
			
		||||
 | 
			
		||||
	admin := r.Group("/admin")
 | 
			
		||||
 | 
			
		||||
	// My man, this is done way more efficient and fast in .NET, specially the authentication part
 | 
			
		||||
	// admin.GET("/", func(c *gin.Context) {
 | 
			
		||||
	// 	c.HTML(http.StatusOK, "admin/index.html", nil)
 | 
			
		||||
	// })
 | 
			
		||||
	// admin.GET("/login", func(c *gin.Context) {
 | 
			
		||||
	// 	c.HTML(http.StatusOK, "admin/index.html", nil)
 | 
			
		||||
	// })
 | 
			
		||||
	r.Use(static.Serve("/admin", static.LocalFile("internal/web/templates/admin", true)))
 | 
			
		||||
	// I dont know a better way for path handling
 | 
			
		||||
	r.Use(static.Serve("/admin/oauth/gitea/callback", static.LocalFile("internal/web/templates/admin", true)))
 | 
			
		||||
 | 
			
		||||
	adminApi := admin.Group("/api")
 | 
			
		||||
	adminApi.POST("/login", handlers.AdminDashboardHandlerInstance.Login)
 | 
			
		||||
	adminApi.GET("/login/oauth/gitea", handlers.OauthLoginHandlerInstance.GoToGiteaLogin)
 | 
			
		||||
	adminApi.POST("/login/oauth/gitea/final", handlers.OauthLoginHandlerInstance.LoginWithGitea)
 | 
			
		||||
	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)
 | 
			
		||||
	adminApi.POST("/login", params.AdminDashboard.Login)
 | 
			
		||||
	adminApi.GET("/login/oauth/gitea", params.OauthLogin.GoToGiteaLogin)
 | 
			
		||||
	adminApi.POST("/login/oauth/gitea/final", params.OauthLogin.LoginWithGitea)
 | 
			
		||||
	adminApi.GET("/getmedia", params.AdminDashboard.JWTMiddleware(), params.AdminDashboard.GetMedia)
 | 
			
		||||
	adminApi.POST("/approve", params.AdminDashboard.JWTMiddleware(), params.AdminDashboard.ApproveMedia)
 | 
			
		||||
	adminApi.POST("/reject", params.AdminDashboard.JWTMiddleware(), params.AdminDashboard.RejectMedia)
 | 
			
		||||
 | 
			
		||||
	api := r.Group("/api")
 | 
			
		||||
	api.GET("/post/random", params.ApiEndpoint.GetRandomPost)
 | 
			
		||||
 | 
			
		||||
	api.GET("/post/random", handlers.ApiEndpointHandlerInstance.GetRandomPost)
 | 
			
		||||
 | 
			
		||||
	return r
 | 
			
		||||
	params.Lifecycle.Append(fx.Hook{
 | 
			
		||||
		OnStart: func(ctx context.Context) error {
 | 
			
		||||
			go func() {
 | 
			
		||||
				if err := r.Run(":8080"); err != nil {
 | 
			
		||||
					// Handle error appropriately
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -3,24 +3,23 @@ package services
 | 
			
		||||
import "CatsOfMastodonBotGo/internal/config"
 | 
			
		||||
 | 
			
		||||
type ImgKitHelper struct {
 | 
			
		||||
	cfg *config.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var ImgKitHelperInstance *ImgKitHelper
 | 
			
		||||
 | 
			
		||||
func InitImgKitHelper() {
 | 
			
		||||
	ImgKitHelperInstance = &ImgKitHelper{}
 | 
			
		||||
func NewImgKitHelper(cfg *config.Config) *ImgKitHelper {
 | 
			
		||||
	return &ImgKitHelper{cfg: cfg}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetPreviewUrl(url string) string {
 | 
			
		||||
	if config.Config.ImageKitId == "" {
 | 
			
		||||
func (ikh *ImgKitHelper) GetPreviewUrl(url string) string {
 | 
			
		||||
	if ikh.cfg.ImageKitId == "" {
 | 
			
		||||
		return url
 | 
			
		||||
	}
 | 
			
		||||
	return "https://ik.imagekit.io/" + config.Config.ImageKitId + "/tr:w-500,h-500,c-at_max,f-webp,q-50/" + url
 | 
			
		||||
	return "https://ik.imagekit.io/" + ikh.cfg.ImageKitId + "/tr:w-500,h-500,c-at_max,f-webp,q-50/" + url
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetRemoteUrl(url string) string {
 | 
			
		||||
	if config.Config.ImageKitId == "" {
 | 
			
		||||
func (ikh *ImgKitHelper) GetRemoteUrl(url string) string {
 | 
			
		||||
	if ikh.cfg.ImageKitId == "" {
 | 
			
		||||
		return url
 | 
			
		||||
	}
 | 
			
		||||
	return "https://ik.imagekit.io/" + config.Config.ImageKitId + "/tr:q-70,dpr-auto,f-webp/" + url
 | 
			
		||||
	return "https://ik.imagekit.io/" + ikh.cfg.ImageKitId + "/tr:q-70,dpr-auto,f-webp/" + url
 | 
			
		||||
}
 | 
			
		||||
@@ -2,7 +2,6 @@ package services
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/config"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/database"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/domain"
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
@@ -16,42 +15,41 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PostService struct {
 | 
			
		||||
	db *gorm.DB
 | 
			
		||||
	db  *gorm.DB
 | 
			
		||||
	cfg *config.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var PostServiceInstance *PostService
 | 
			
		||||
 | 
			
		||||
// Constructor
 | 
			
		||||
func InitPostService() {
 | 
			
		||||
	PostServiceInstance = &PostService{db: database.Gorm}
 | 
			
		||||
func NewPostService(db *gorm.DB, cfg *config.Config) *PostService {
 | 
			
		||||
	return &PostService{db: db, cfg: cfg}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*PostService) GetPostsFromApi(ctx context.Context, tag string, instance string) (error, []domain.Post) {
 | 
			
		||||
func (ps *PostService) GetPostsFromApi(ctx context.Context, tag string, instance string) ([]domain.Post, error) {
 | 
			
		||||
	var requestUrl = instance + "/api/v1/timelines/tag/" + tag + "?limit=40"
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, "GET", requestUrl, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, err := http.DefaultClient.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if resp.StatusCode != 200 || strings.Split(strings.ToLower(resp.Header.Get("Content-Type")), ";")[0] != "application/json" {
 | 
			
		||||
		return fmt.Errorf("Status code:", resp.StatusCode, " Content-Type:", resp.Header.Get("Content-Type")), nil
 | 
			
		||||
		return nil, fmt.Errorf("status code: %d, content-type: %s", resp.StatusCode, resp.Header.Get("Content-Type"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var posts []domain.Post = nil
 | 
			
		||||
	err = json.NewDecoder(resp.Body).Decode(&posts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err, nil
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	// defer: it basically means "do this later when the function returns"
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
	if posts == nil {
 | 
			
		||||
		return fmt.Errorf("no posts found for tag %s on instance %s", tag, instance), nil
 | 
			
		||||
		return nil, fmt.Errorf("no posts found for tag %s on instance %s", tag, instance)
 | 
			
		||||
	}
 | 
			
		||||
	return nil, posts
 | 
			
		||||
	return posts, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ps *PostService) GetExistingPostIds() []string {
 | 
			
		||||
@@ -66,7 +64,7 @@ func (ps *PostService) GetExistingAccountIds() []string {
 | 
			
		||||
	return existingAccountIds
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*PostService) GetNewPosts(existingPostIds []string, posts []domain.Post) []domain.Post {
 | 
			
		||||
func (ps *PostService) GetNewPosts(existingPostIds []string, posts []domain.Post) []domain.Post {
 | 
			
		||||
	var newPosts []domain.Post = nil
 | 
			
		||||
	for _, post := range posts {
 | 
			
		||||
		if !arrayContains(existingPostIds, post.ID) && len(post.Attachments) > 0 && !post.Account.IsBot {
 | 
			
		||||
@@ -85,7 +83,7 @@ func (*PostService) GetNewPosts(existingPostIds []string, posts []domain.Post) [
 | 
			
		||||
	return newPosts
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*PostService) GetNewAccounts(existingAccountIds []string, posts []domain.Post) []domain.Account {
 | 
			
		||||
func (ps *PostService) GetNewAccounts(existingAccountIds []string, posts []domain.Post) []domain.Account {
 | 
			
		||||
	var newAccounts []domain.Account = nil
 | 
			
		||||
	for _, post := range posts {
 | 
			
		||||
		if !arrayContains(existingAccountIds, post.Account.AccId) {
 | 
			
		||||
@@ -153,7 +151,7 @@ func (ps *PostService) RejectMedia(mediaId string) bool {
 | 
			
		||||
func (ps *PostService) GetMedia() domain.MediaAttachment {
 | 
			
		||||
	var media domain.MediaAttachment
 | 
			
		||||
	orderExpr := "RANDOM()" // sqlite
 | 
			
		||||
	if config.Config.DBEngine != "sqlite" {
 | 
			
		||||
	if ps.cfg.DBEngine != "sqlite" {
 | 
			
		||||
		orderExpr = "RAND()" // mariadb/mysql
 | 
			
		||||
	}
 | 
			
		||||
	ps.db.Model(&domain.MediaAttachment{}).
 | 
			
		||||
 
 | 
			
		||||
@@ -5,60 +5,65 @@ import (
 | 
			
		||||
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/auth"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/config"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/web/dto"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/services"
 | 
			
		||||
	"CatsOfMastodonBotGo/internal/web/dto"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AdminDashboardHandler struct {
 | 
			
		||||
	PostService services.PostService
 | 
			
		||||
	Jwt         auth.JwtTokenGenerator
 | 
			
		||||
	postService *services.PostService
 | 
			
		||||
	jwt         *auth.JwtTokenGenerator
 | 
			
		||||
	cfg         *config.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var AdminDashboardHandlerInstance *AdminDashboardHandler
 | 
			
		||||
 | 
			
		||||
func InitAdminDashboardHandler() {
 | 
			
		||||
	AdminDashboardHandlerInstance = &AdminDashboardHandler{
 | 
			
		||||
		PostService: *services.PostServiceInstance,
 | 
			
		||||
		Jwt:         *auth.JwtTokenGeneratorInstance,
 | 
			
		||||
func NewAdminDashboardHandler(
 | 
			
		||||
	postService *services.PostService,
 | 
			
		||||
	jwt *auth.JwtTokenGenerator,
 | 
			
		||||
	cfg *config.Config,
 | 
			
		||||
) *AdminDashboardHandler {
 | 
			
		||||
	return &AdminDashboardHandler{
 | 
			
		||||
		postService: postService,
 | 
			
		||||
		jwt:         jwt,
 | 
			
		||||
		cfg:         cfg,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ps *AdminDashboardHandler) ApproveMedia(c *gin.Context) {
 | 
			
		||||
func (adh *AdminDashboardHandler) ApproveMedia(c *gin.Context) {
 | 
			
		||||
	var input dto.ApproveMediaInput
 | 
			
		||||
	if err := c.ShouldBindJSON(&input); err != nil {
 | 
			
		||||
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if ps.PostService.ApproveMedia(input.MediaId) {
 | 
			
		||||
	if adh.postService.ApproveMedia(input.MediaId) {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{"message": "Media approved successfully"})
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve media"})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ps *AdminDashboardHandler) RejectMedia(c *gin.Context) {
 | 
			
		||||
func (adh *AdminDashboardHandler) RejectMedia(c *gin.Context) {
 | 
			
		||||
	var input dto.RejectMediaInput
 | 
			
		||||
	if err := c.ShouldBindJSON(&input); err != nil {
 | 
			
		||||
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if ps.PostService.RejectMedia(input.MediaId) {
 | 
			
		||||
	if adh.postService.RejectMedia(input.MediaId) {
 | 
			
		||||
		c.JSON(http.StatusOK, gin.H{"message": "Media rejected successfully"})
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject media"})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ps *AdminDashboardHandler) GetMedia(c *gin.Context) {
 | 
			
		||||
	media := ps.PostService.GetMedia()
 | 
			
		||||
	media.PreviewUrl = services.GetPreviewUrl(media.RemoteUrl)
 | 
			
		||||
	media.RemoteUrl = services.GetPreviewUrl(media.RemoteUrl)
 | 
			
		||||
func (adh *AdminDashboardHandler) GetMedia(c *gin.Context) {
 | 
			
		||||
	media := adh.postService.GetMedia()
 | 
			
		||||
	// TODO: Fix this - we need to inject ImgKitHelper
 | 
			
		||||
	// media.PreviewUrl = services.GetPreviewUrl(media.RemoteUrl)
 | 
			
		||||
	// media.RemoteUrl = services.GetPreviewUrl(media.RemoteUrl)
 | 
			
		||||
	c.JSON(http.StatusOK, media)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ps *AdminDashboardHandler) Login(c *gin.Context) {
 | 
			
		||||
func (adh *AdminDashboardHandler) Login(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	var input dto.LoginInput
 | 
			
		||||
 | 
			
		||||
@@ -68,8 +73,8 @@ func (ps *AdminDashboardHandler) Login(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if input.Password == config.Config.AdminPassword { // Its more than enough for this project
 | 
			
		||||
		token, err := ps.Jwt.GenerateToken(map[string]interface{}{"role": "admin"})
 | 
			
		||||
	if input.Password == adh.cfg.AdminPassword { // Its more than enough for this project
 | 
			
		||||
		token, err := adh.jwt.GenerateToken(map[string]interface{}{"role": "admin"})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
 | 
			
		||||
			return
 | 
			
		||||
@@ -85,3 +90,8 @@ func (ps *AdminDashboardHandler) Login(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Expose the JWT middleware for use in routes
 | 
			
		||||
func (adh *AdminDashboardHandler) JWTMiddleware() gin.HandlerFunc {
 | 
			
		||||
	return adh.jwt.GinMiddleware()
 | 
			
		||||
}
 | 
			
		||||
@@ -7,23 +7,25 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ApiEndpointHandler struct {
 | 
			
		||||
	PostService services.PostService
 | 
			
		||||
	postService    *services.PostService
 | 
			
		||||
	imgKitHelper   *services.ImgKitHelper
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var ApiEndpointHandlerInstance *ApiEndpointHandler
 | 
			
		||||
 | 
			
		||||
func InitApiEndpointHandler() {
 | 
			
		||||
	ApiEndpointHandlerInstance = &ApiEndpointHandler{
 | 
			
		||||
		PostService: *services.PostServiceInstance,
 | 
			
		||||
func NewApiEndpointHandler(
 | 
			
		||||
	postService *services.PostService,
 | 
			
		||||
	imgKitHelper *services.ImgKitHelper,
 | 
			
		||||
) *ApiEndpointHandler {
 | 
			
		||||
	return &ApiEndpointHandler{
 | 
			
		||||
		postService:  postService,
 | 
			
		||||
		imgKitHelper: imgKitHelper,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ps *ApiEndpointHandler) GetRandomPost(c *gin.Context) {
 | 
			
		||||
	post := ps.PostService.GetRandomPost()
 | 
			
		||||
func (aeh *ApiEndpointHandler) GetRandomPost(c *gin.Context) {
 | 
			
		||||
	post := aeh.postService.GetRandomPost()
 | 
			
		||||
	for i := range post.Attachments {
 | 
			
		||||
		post.Attachments[i].RemoteUrl = services.GetRemoteUrl(post.Attachments[i].RemoteUrl)
 | 
			
		||||
		post.Attachments[i].PreviewUrl = services.GetPreviewUrl(post.Attachments[i].RemoteUrl)
 | 
			
		||||
		post.Attachments[i].RemoteUrl = aeh.imgKitHelper.GetRemoteUrl(post.Attachments[i].RemoteUrl)
 | 
			
		||||
		post.Attachments[i].PreviewUrl = aeh.imgKitHelper.GetPreviewUrl(post.Attachments[i].RemoteUrl)
 | 
			
		||||
	}
 | 
			
		||||
	c.JSON(200, post)
 | 
			
		||||
}
 | 
			
		||||
@@ -7,21 +7,24 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type EmbedCardHandler struct {
 | 
			
		||||
	PostService services.PostService
 | 
			
		||||
	postService  *services.PostService
 | 
			
		||||
	imgKitHelper *services.ImgKitHelper
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var EmbedCardHandlerInstance *EmbedCardHandler
 | 
			
		||||
 | 
			
		||||
func InitEmbedCardHandler() {
 | 
			
		||||
	EmbedCardHandlerInstance = &EmbedCardHandler{
 | 
			
		||||
		PostService: *services.PostServiceInstance,
 | 
			
		||||
func NewEmbedCardHandler(
 | 
			
		||||
	postService *services.PostService,
 | 
			
		||||
	imgKitHelper *services.ImgKitHelper,
 | 
			
		||||
) *EmbedCardHandler {
 | 
			
		||||
	return &EmbedCardHandler{
 | 
			
		||||
		postService:  postService,
 | 
			
		||||
		imgKitHelper: imgKitHelper,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ps *EmbedCardHandler) GetEmbedCard(c *gin.Context) {
 | 
			
		||||
	post := ps.PostService.GetRandomPost()
 | 
			
		||||
func (ech *EmbedCardHandler) GetEmbedCard(c *gin.Context) {
 | 
			
		||||
	post := ech.postService.GetRandomPost()
 | 
			
		||||
	c.HTML(200, "home/embed.html", gin.H{
 | 
			
		||||
		"postUrl":  post.Url,
 | 
			
		||||
		"imageUrl": services.GetRemoteUrl(post.Attachments[0].RemoteUrl),
 | 
			
		||||
		"imageUrl": ech.imgKitHelper.GetRemoteUrl(post.Attachments[0].RemoteUrl),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -10,21 +10,25 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type OauthLoginHandler struct {
 | 
			
		||||
	Jwt               auth.JwtTokenGenerator
 | 
			
		||||
	OauthLoginHandler *auth.GiteaOAuth2Handler
 | 
			
		||||
	jwt         *auth.JwtTokenGenerator
 | 
			
		||||
	oauthHandler *auth.GiteaOAuth2Handler
 | 
			
		||||
	cfg         *config.Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var OauthLoginHandlerInstance *OauthLoginHandler
 | 
			
		||||
 | 
			
		||||
func InitOauthLoginHandler() {
 | 
			
		||||
	OauthLoginHandlerInstance = &OauthLoginHandler{
 | 
			
		||||
		Jwt:               *auth.JwtTokenGeneratorInstance,
 | 
			
		||||
		OauthLoginHandler: auth.GiteaOauth2HandlerInstance,
 | 
			
		||||
func NewOauthLoginHandler(
 | 
			
		||||
	jwt *auth.JwtTokenGenerator,
 | 
			
		||||
	oauthHandler *auth.GiteaOAuth2Handler,
 | 
			
		||||
	cfg *config.Config,
 | 
			
		||||
) *OauthLoginHandler {
 | 
			
		||||
	return &OauthLoginHandler{
 | 
			
		||||
		jwt:          jwt,
 | 
			
		||||
		oauthHandler: oauthHandler,
 | 
			
		||||
		cfg:          cfg,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (olh *OauthLoginHandler) GoToGiteaLogin(c *gin.Context) {
 | 
			
		||||
	redirectURL, _ := olh.OauthLoginHandler.GetGiteaLoginURL(c.Request.URL.Scheme + c.Request.Host)
 | 
			
		||||
	redirectURL, _ := olh.oauthHandler.GetGiteaLoginURL(c.Request.URL.Scheme + c.Request.Host)
 | 
			
		||||
	if redirectURL != "" {
 | 
			
		||||
		c.Redirect(http.StatusFound, redirectURL)
 | 
			
		||||
		return
 | 
			
		||||
@@ -42,27 +46,28 @@ func (olh *OauthLoginHandler) LoginWithGitea(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userEmail, err := olh.OauthLoginHandler.GetGiteaUserEmailByCode(input.Code)
 | 
			
		||||
	userEmail, err := olh.oauthHandler.GetGiteaUserEmailByCode(input.Code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, email := range config.Config.GiteaOauthAllowedEmails {
 | 
			
		||||
	// Check if the user's email is in the allowed list
 | 
			
		||||
	for _, email := range olh.cfg.GiteaOauthAllowedEmails {
 | 
			
		||||
		if email == userEmail {
 | 
			
		||||
			token, err := olh.Jwt.GenerateToken(map[string]interface{}{"role": "admin"})
 | 
			
		||||
			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
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If we get here, the email is not in the allowed list
 | 
			
		||||
	c.JSON(401, gin.H{
 | 
			
		||||
		"error": "oauth login failed or your email does not have access",
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user