Compare commits
4 Commits
d4044b0eaf
...
fx-depende
Author | SHA1 | Date | |
---|---|---|---|
228d1bffc1 | |||
6d15ce2df9 | |||
5680f9471f | |||
f136ae58b3 |
@@ -3,7 +3,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
# Build the application from source
|
# Build the application from source
|
||||||
FROM golang:1.24.3 AS build-stage
|
FROM golang:1.25.1-alpine3.22 AS build-stage
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"CatsOfMastodonBotGo/internal/auth"
|
||||||
"CatsOfMastodonBotGo/internal/config"
|
"CatsOfMastodonBotGo/internal/config"
|
||||||
"CatsOfMastodonBotGo/internal/database"
|
"CatsOfMastodonBotGo/internal/database"
|
||||||
"CatsOfMastodonBotGo/internal/domain"
|
|
||||||
"CatsOfMastodonBotGo/internal/server"
|
"CatsOfMastodonBotGo/internal/server"
|
||||||
"CatsOfMastodonBotGo/internal/services"
|
"CatsOfMastodonBotGo/internal/services"
|
||||||
"context"
|
"CatsOfMastodonBotGo/internal/web/handlers"
|
||||||
"log/slog"
|
|
||||||
"strconv"
|
"go.uber.org/fx"
|
||||||
"time"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Setup config
|
fx.New(
|
||||||
config.Init()
|
fx.Provide(
|
||||||
|
// Logger
|
||||||
// Initialize database
|
NewLogger,
|
||||||
database.Init()
|
|
||||||
|
|
||||||
services.InitPostService()
|
|
||||||
|
|
||||||
// Not needed but anyways
|
|
||||||
services.InitImgKitHelper()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(10 * time.Minute)
|
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
config.Load,
|
||||||
|
|
||||||
|
// Database
|
||||||
|
database.Connect,
|
||||||
|
|
||||||
|
// Services
|
||||||
|
services.NewPostService,
|
||||||
|
services.NewImgKitHelper,
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
auth.NewJwtTokenGenerator,
|
||||||
|
auth.NewGiteaOauth2Token,
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
handlers.NewAdminDashboardHandler,
|
||||||
|
handlers.NewApiEndpointHandler,
|
||||||
|
handlers.NewEmbedCardHandler,
|
||||||
|
handlers.NewOauthLoginHandler,
|
||||||
|
),
|
||||||
|
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/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // 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/arch v0.21.0 // indirect
|
||||||
golang.org/x/crypto v0.40.0 // indirect
|
golang.org/x/crypto v0.40.0 // indirect
|
||||||
golang.org/x/net v0.42.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/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 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
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 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||||
|
@@ -10,33 +10,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type JwtTokenGenerator struct {
|
type JwtTokenGenerator struct {
|
||||||
Key string
|
cfg *config.Config
|
||||||
Issuer string
|
|
||||||
Audience string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var JwtTokenGeneratorInstance *JwtTokenGenerator
|
func NewJwtTokenGenerator(cfg *config.Config) *JwtTokenGenerator {
|
||||||
|
return &JwtTokenGenerator{cfg: cfg}
|
||||||
func InitJwtTokenGenerator() {
|
|
||||||
JwtTokenGeneratorInstance = &JwtTokenGenerator{
|
|
||||||
Key: config.Config.JwtSecret,
|
|
||||||
Issuer: config.Config.JwtIssuer,
|
|
||||||
Audience: config.Config.JwtAudience,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JwtTokenGenerator) GenerateToken(claims map[string]interface{}) (string, error) {
|
func (j *JwtTokenGenerator) GenerateToken(claims map[string]interface{}) (string, error) {
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
"exp": time.Now().AddDate(0, 0, 1).Unix(),
|
"exp": time.Now().AddDate(0, 0, 1).Unix(),
|
||||||
"iat": time.Now().Unix(),
|
"iat": time.Now().Unix(),
|
||||||
"iss": j.Issuer,
|
"iss": j.cfg.JwtIssuer,
|
||||||
"aud": j.Audience,
|
"aud": j.cfg.JwtAudience,
|
||||||
})
|
})
|
||||||
for k, v := range claims {
|
for k, v := range claims {
|
||||||
token.Claims.(jwt.MapClaims)[k] = v
|
token.Claims.(jwt.MapClaims)[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
return token.SignedString([]byte(j.Key))
|
return token.SignedString([]byte(j.cfg.JwtSecret))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gin middleware
|
// Gin middleware
|
||||||
@@ -53,7 +45,7 @@ func (j *JwtTokenGenerator) GinMiddleware() gin.HandlerFunc {
|
|||||||
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
return nil, jwt.ErrSignatureInvalid
|
return nil, jwt.ErrSignatureInvalid
|
||||||
}
|
}
|
||||||
return []byte(j.Key), nil
|
return []byte(j.cfg.JwtSecret), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -69,4 +61,4 @@ func (j *JwtTokenGenerator) GinMiddleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,45 +5,39 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GiteaOAuth2Handler struct {
|
type GiteaOAuth2Handler struct {
|
||||||
ClientID string
|
cfg *config.Config
|
||||||
ClientSecret string
|
logger *zap.Logger
|
||||||
InstanceUrl string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var GiteaOauth2HandlerInstance *GiteaOAuth2Handler
|
func NewGiteaOauth2Token(cfg *config.Config) *GiteaOAuth2Handler {
|
||||||
|
return &GiteaOAuth2Handler{cfg: cfg}
|
||||||
func InitGiteaOauth2Token() {
|
|
||||||
GiteaOauth2HandlerInstance = &GiteaOAuth2Handler{
|
|
||||||
ClientID: config.Config.GiteaOauthClientID,
|
|
||||||
ClientSecret: config.Config.GiteaOauthClientSecret,
|
|
||||||
InstanceUrl: config.Config.GiteaOauthInstance,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GiteaOAuth2Handler) GetGiteaLoginURL(redirectHost string) (string, error) {
|
func (g *GiteaOAuth2Handler) GetGiteaLoginURL(redirectHost string) (string, error) {
|
||||||
if g.InstanceUrl == "" {
|
if g.cfg.GiteaOauthInstance == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if redirectHost == "" {
|
if redirectHost == "" {
|
||||||
slog.Error("Redirect host not provided")
|
g.logger.Error("Redirect host not provided")
|
||||||
return "", nil
|
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
|
return authUrl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GiteaOAuth2Handler) GetGiteaUserEmailByCode(code string) (string, error) {
|
func (g *GiteaOAuth2Handler) GetGiteaUserEmailByCode(code string) (string, error) {
|
||||||
|
|
||||||
if g.InstanceUrl == "" {
|
if g.cfg.GiteaOauthInstance == "" {
|
||||||
slog.Error("Instance URL not provided")
|
g.logger.Error("Instance URL not provided")
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
// No need to verify since we are accesing the gitea once and only for the email
|
// No need to verify since we are accesing the gitea once and only for the email
|
||||||
@@ -52,8 +46,8 @@ func (g *GiteaOAuth2Handler) GetGiteaUserEmailByCode(code string) (string, error
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
userInfoUrl := g.InstanceUrl + "/login/oauth/userinfo"
|
userInfoUrl := g.cfg.GiteaOauthInstance + "/login/oauth/userinfo"
|
||||||
slog.Info(userInfoUrl)
|
g.logger.Info(userInfoUrl)
|
||||||
req, err := http.NewRequest("POST", userInfoUrl, nil)
|
req, err := http.NewRequest("POST", userInfoUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -85,12 +79,12 @@ func (g *GiteaOAuth2Handler) GetGiteaUserEmailByCode(code string) (string, error
|
|||||||
|
|
||||||
func (g *GiteaOAuth2Handler) getGiteaAccessTokenByCode(code string) (string, error) {
|
func (g *GiteaOAuth2Handler) getGiteaAccessTokenByCode(code string) (string, error) {
|
||||||
form := url.Values{}
|
form := url.Values{}
|
||||||
form.Add("client_id", g.ClientID)
|
form.Add("client_id", g.cfg.GiteaOauthClientID)
|
||||||
form.Add("client_secret", g.ClientSecret)
|
form.Add("client_secret", g.cfg.GiteaOauthClientSecret)
|
||||||
form.Add("code", code)
|
form.Add("code", code)
|
||||||
form.Add("grant_type", "authorization_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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -112,4 +106,4 @@ func (g *GiteaOAuth2Handler) getGiteaAccessTokenByCode(code string) (string, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
return tokenResp.AccessToken, nil
|
return tokenResp.AccessToken, nil
|
||||||
}
|
}
|
@@ -1,14 +1,14 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type config struct {
|
type Config struct {
|
||||||
AdminPassword string
|
AdminPassword string
|
||||||
Instance string
|
Instance string
|
||||||
Tag string
|
Tag string
|
||||||
@@ -26,18 +26,16 @@ type config struct {
|
|||||||
|
|
||||||
ImageKitId string
|
ImageKitId string
|
||||||
|
|
||||||
GiteaOauthInstance string
|
GiteaOauthInstance string
|
||||||
GiteaOauthClientID string
|
GiteaOauthClientID string
|
||||||
GiteaOauthClientSecret string
|
GiteaOauthClientSecret string
|
||||||
GiteaOauthAllowedEmails []string
|
GiteaOauthAllowedEmails []string
|
||||||
}
|
}
|
||||||
|
|
||||||
var Config *config
|
func Load(logger *zap.Logger) *Config {
|
||||||
|
|
||||||
func Load() *config {
|
|
||||||
err := godotenv.Load()
|
err := godotenv.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Error loading .env file - Using environment variables instead")
|
logger.Warn("Error loading .env file - Using environment variables instead")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get mastodon instance
|
// Get mastodon instance
|
||||||
@@ -53,7 +51,7 @@ func Load() *config {
|
|||||||
// Get admin password (Its a single user/admin app so its just fine)
|
// Get admin password (Its a single user/admin app so its just fine)
|
||||||
adminPassword := os.Getenv("CAOM_ADMIN_PASSWORD")
|
adminPassword := os.Getenv("CAOM_ADMIN_PASSWORD")
|
||||||
if adminPassword == "" {
|
if adminPassword == "" {
|
||||||
slog.Warn("No admin password provided, using default password 'catsaregood'")
|
logger.Warn("No admin password provided, using default password 'catsaregood'")
|
||||||
adminPassword = "catsaregood"
|
adminPassword = "catsaregood"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,12 +62,12 @@ func Load() *config {
|
|||||||
}
|
}
|
||||||
issuer := os.Getenv("CAOM_JWT_ISSUER")
|
issuer := os.Getenv("CAOM_JWT_ISSUER")
|
||||||
if issuer == "" {
|
if issuer == "" {
|
||||||
slog.Info("No jwt issuer provided, using default issuer 'CatsOfMastodonBotGo'")
|
logger.Info("No jwt issuer provided, using default issuer 'CatsOfMastodonBotGo'")
|
||||||
issuer = "CatsOfMastodonBotGo"
|
issuer = "CatsOfMastodonBotGo"
|
||||||
}
|
}
|
||||||
audience := os.Getenv("CAOM_JWT_AUDIENCE")
|
audience := os.Getenv("CAOM_JWT_AUDIENCE")
|
||||||
if audience == "" {
|
if audience == "" {
|
||||||
slog.Info("No jwt audience provided, using default audience 'CatsOfMastodonBotGo'")
|
logger.Info("No jwt audience provided, using default audience 'CatsOfMastodonBotGo'")
|
||||||
audience = "CatsOfMastodonBotGo"
|
audience = "CatsOfMastodonBotGo"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +89,7 @@ func Load() *config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dbEngine == "" || dbHost == "" || dbPort == "" || dbUser == "" || dbPassword == "" || dbName == "" {
|
if dbEngine == "" || dbHost == "" || dbPort == "" || dbUser == "" || dbPassword == "" || dbName == "" {
|
||||||
slog.Info("No database connection provided, using sqlite")
|
logger.Info("No database connection provided, using sqlite")
|
||||||
dbEngine = "sqlite"
|
dbEngine = "sqlite"
|
||||||
dbHost = ""
|
dbHost = ""
|
||||||
dbPort = ""
|
dbPort = ""
|
||||||
@@ -102,10 +100,10 @@ func Load() *config {
|
|||||||
|
|
||||||
imageKitId := os.Getenv("CAOM_IMAGEKIT_ID")
|
imageKitId := os.Getenv("CAOM_IMAGEKIT_ID")
|
||||||
if imageKitId == "" {
|
if imageKitId == "" {
|
||||||
slog.Info("No imagekit id provided, not using imagekit.io")
|
logger.Info("No imagekit id provided, not using imagekit.io")
|
||||||
}
|
}
|
||||||
// Inititlize AppContext
|
// Initialize AppContext
|
||||||
var appContext = &config{
|
return &Config{
|
||||||
AdminPassword: adminPassword,
|
AdminPassword: adminPassword,
|
||||||
Instance: instance,
|
Instance: instance,
|
||||||
Tag: tag,
|
Tag: tag,
|
||||||
@@ -123,15 +121,9 @@ func Load() *config {
|
|||||||
|
|
||||||
ImageKitId: imageKitId,
|
ImageKitId: imageKitId,
|
||||||
|
|
||||||
GiteaOauthInstance: giteaOauthInstance,
|
GiteaOauthInstance: giteaOauthInstance,
|
||||||
GiteaOauthClientID: giteaOauthClientID,
|
GiteaOauthClientID: giteaOauthClientID,
|
||||||
GiteaOauthClientSecret: giteaOauthClientSecret,
|
GiteaOauthClientSecret: giteaOauthClientSecret,
|
||||||
GiteaOauthAllowedEmails: giteaOauthAllowedEmailsParsed,
|
GiteaOauthAllowedEmails: giteaOauthAllowedEmailsParsed,
|
||||||
}
|
}
|
||||||
return appContext
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
Config = Load()
|
|
||||||
}
|
}
|
||||||
|
@@ -11,13 +11,11 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Gorm *gorm.DB
|
func Connect(cfg *config.Config) (*gorm.DB, error) {
|
||||||
|
|
||||||
func Connect() (*gorm.DB, error) {
|
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
var err error = nil
|
var err error = nil
|
||||||
|
|
||||||
if config.Config.DBEngine == "sqlite" {
|
if cfg.DBEngine == "sqlite" {
|
||||||
_, err = os.ReadDir("data")
|
_, err = os.ReadDir("data")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = os.Mkdir("data", 0755)
|
err = os.Mkdir("data", 0755)
|
||||||
@@ -31,11 +29,11 @@ func Connect() (*gorm.DB, error) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||||
config.Config.DBUser,
|
cfg.DBUser,
|
||||||
config.Config.DBPassword,
|
cfg.DBPassword,
|
||||||
config.Config.DBHost,
|
cfg.DBHost,
|
||||||
config.Config.DBPort,
|
cfg.DBPort,
|
||||||
config.Config.DBName)
|
cfg.DBName)
|
||||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -54,13 +52,4 @@ func Connect() (*gorm.DB, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return db, nil
|
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,36 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"CatsOfMastodonBotGo/internal/auth"
|
"context"
|
||||||
"CatsOfMastodonBotGo/internal/web/handlers"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"CatsOfMastodonBotGo/internal/web/handlers"
|
||||||
|
|
||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-contrib/static"
|
"github.com/gin-contrib/static"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRouter() *gin.Engine {
|
// fx.In allows fx to inject multiple dependencies
|
||||||
|
|
||||||
|
// I think we could just put all thses in SetupRouter() but in this way we are first defining the dependencies we need
|
||||||
|
// and by using fx.In we say that whenever RouterParams was needed, inject dependensies into it and give it to SetupRouter()
|
||||||
|
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 := gin.Default()
|
||||||
|
|
||||||
r.Use(cors.New(cors.Config{
|
r.Use(cors.New(cors.Config{
|
||||||
AllowAllOrigins: true,
|
AllowAllOrigins: true,
|
||||||
AllowMethods: []string{"POST", "GET", "OPTIONS"},
|
AllowMethods: []string{"POST", "GET", "OPTIONS"},
|
||||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||||
AllowCredentials: true,
|
AllowCredentials: true,
|
||||||
@@ -22,44 +38,37 @@ func SetupRouter() *gin.Engine {
|
|||||||
|
|
||||||
r.LoadHTMLGlob("internal/web/templates/home/*")
|
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
|
// Main page
|
||||||
r.GET("/", func(c *gin.Context) {
|
r.GET("/", func(c *gin.Context) {
|
||||||
c.HTML(http.StatusOK, "home/index.html", nil)
|
c.HTML(http.StatusOK, "home/index.html", nil)
|
||||||
})
|
})
|
||||||
// Embed card
|
// Embed card
|
||||||
r.GET("/embed", handlers.EmbedCardHandlerInstance.GetEmbedCard)
|
r.GET("/embed", params.EmbedCard.GetEmbedCard)
|
||||||
|
|
||||||
admin := r.Group("/admin")
|
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)))
|
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)))
|
r.Use(static.Serve("/admin/oauth/gitea/callback", static.LocalFile("internal/web/templates/admin", true)))
|
||||||
|
|
||||||
adminApi := admin.Group("/api")
|
adminApi := admin.Group("/api")
|
||||||
adminApi.POST("/login", handlers.AdminDashboardHandlerInstance.Login)
|
adminApi.POST("/login", params.AdminDashboard.Login)
|
||||||
adminApi.GET("/login/oauth/gitea", handlers.OauthLoginHandlerInstance.GoToGiteaLogin)
|
adminApi.GET("/login/oauth/gitea", params.OauthLogin.GoToGiteaLogin)
|
||||||
adminApi.POST("/login/oauth/gitea/final", handlers.OauthLoginHandlerInstance.LoginWithGitea)
|
adminApi.POST("/login/oauth/gitea/final", params.OauthLogin.LoginWithGitea)
|
||||||
adminApi.GET("/getmedia", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.GetMedia)
|
adminApi.GET("/getmedia", params.AdminDashboard.JWTMiddleware(), params.AdminDashboard.GetMedia)
|
||||||
adminApi.POST("/approve", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.ApproveMedia)
|
adminApi.POST("/approve", params.AdminDashboard.JWTMiddleware(), params.AdminDashboard.ApproveMedia)
|
||||||
adminApi.POST("/reject", auth.JwtTokenGeneratorInstance.GinMiddleware(), handlers.AdminDashboardHandlerInstance.RejectMedia)
|
adminApi.POST("/reject", params.AdminDashboard.JWTMiddleware(), params.AdminDashboard.RejectMedia)
|
||||||
|
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
|
api.GET("/post/random", params.ApiEndpoint.GetRandomPost)
|
||||||
|
|
||||||
api.GET("/post/random", handlers.ApiEndpointHandlerInstance.GetRandomPost)
|
params.Lifecycle.Append(fx.Hook{
|
||||||
|
OnStart: func(ctx context.Context) error {
|
||||||
return r
|
go func() {
|
||||||
}
|
if err := r.Run(":8080"); err != nil {
|
||||||
|
// Handle error appropriately
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
@@ -3,24 +3,23 @@ package services
|
|||||||
import "CatsOfMastodonBotGo/internal/config"
|
import "CatsOfMastodonBotGo/internal/config"
|
||||||
|
|
||||||
type ImgKitHelper struct {
|
type ImgKitHelper struct {
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
var ImgKitHelperInstance *ImgKitHelper
|
func NewImgKitHelper(cfg *config.Config) *ImgKitHelper {
|
||||||
|
return &ImgKitHelper{cfg: cfg}
|
||||||
func InitImgKitHelper() {
|
|
||||||
ImgKitHelperInstance = &ImgKitHelper{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPreviewUrl(url string) string {
|
func (ikh *ImgKitHelper) GetPreviewUrl(url string) string {
|
||||||
if config.Config.ImageKitId == "" {
|
if ikh.cfg.ImageKitId == "" {
|
||||||
return url
|
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 {
|
func (ikh *ImgKitHelper) GetRemoteUrl(url string) string {
|
||||||
if config.Config.ImageKitId == "" {
|
if ikh.cfg.ImageKitId == "" {
|
||||||
return url
|
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 (
|
import (
|
||||||
"CatsOfMastodonBotGo/internal/config"
|
"CatsOfMastodonBotGo/internal/config"
|
||||||
"CatsOfMastodonBotGo/internal/database"
|
|
||||||
"CatsOfMastodonBotGo/internal/domain"
|
"CatsOfMastodonBotGo/internal/domain"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -16,42 +15,41 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PostService struct {
|
type PostService struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
var PostServiceInstance *PostService
|
|
||||||
|
|
||||||
// Constructor
|
// Constructor
|
||||||
func InitPostService() {
|
func NewPostService(db *gorm.DB, cfg *config.Config) *PostService {
|
||||||
PostServiceInstance = &PostService{db: database.Gorm}
|
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"
|
var requestUrl = instance + "/api/v1/timelines/tag/" + tag + "?limit=40"
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", requestUrl, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", requestUrl, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
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" {
|
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
|
var posts []domain.Post = nil
|
||||||
err = json.NewDecoder(resp.Body).Decode(&posts)
|
err = json.NewDecoder(resp.Body).Decode(&posts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
// defer: it basically means "do this later when the function returns"
|
// defer: it basically means "do this later when the function returns"
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if posts == nil {
|
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 {
|
func (ps *PostService) GetExistingPostIds() []string {
|
||||||
@@ -66,7 +64,7 @@ func (ps *PostService) GetExistingAccountIds() []string {
|
|||||||
return existingAccountIds
|
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
|
var newPosts []domain.Post = nil
|
||||||
for _, post := range posts {
|
for _, post := range posts {
|
||||||
if !arrayContains(existingPostIds, post.ID) && len(post.Attachments) > 0 && !post.Account.IsBot {
|
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
|
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
|
var newAccounts []domain.Account = nil
|
||||||
for _, post := range posts {
|
for _, post := range posts {
|
||||||
if !arrayContains(existingAccountIds, post.Account.AccId) {
|
if !arrayContains(existingAccountIds, post.Account.AccId) {
|
||||||
@@ -153,7 +151,7 @@ func (ps *PostService) RejectMedia(mediaId string) bool {
|
|||||||
func (ps *PostService) GetMedia() domain.MediaAttachment {
|
func (ps *PostService) GetMedia() domain.MediaAttachment {
|
||||||
var media domain.MediaAttachment
|
var media domain.MediaAttachment
|
||||||
orderExpr := "RANDOM()" // sqlite
|
orderExpr := "RANDOM()" // sqlite
|
||||||
if config.Config.DBEngine != "sqlite" {
|
if ps.cfg.DBEngine != "sqlite" {
|
||||||
orderExpr = "RAND()" // mariadb/mysql
|
orderExpr = "RAND()" // mariadb/mysql
|
||||||
}
|
}
|
||||||
ps.db.Model(&domain.MediaAttachment{}).
|
ps.db.Model(&domain.MediaAttachment{}).
|
||||||
@@ -171,4 +169,4 @@ func arrayContains(arr []string, str string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
@@ -5,60 +5,65 @@ import (
|
|||||||
|
|
||||||
"CatsOfMastodonBotGo/internal/auth"
|
"CatsOfMastodonBotGo/internal/auth"
|
||||||
"CatsOfMastodonBotGo/internal/config"
|
"CatsOfMastodonBotGo/internal/config"
|
||||||
"CatsOfMastodonBotGo/internal/web/dto"
|
|
||||||
"CatsOfMastodonBotGo/internal/services"
|
"CatsOfMastodonBotGo/internal/services"
|
||||||
|
"CatsOfMastodonBotGo/internal/web/dto"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminDashboardHandler struct {
|
type AdminDashboardHandler struct {
|
||||||
PostService services.PostService
|
postService *services.PostService
|
||||||
Jwt auth.JwtTokenGenerator
|
jwt *auth.JwtTokenGenerator
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
var AdminDashboardHandlerInstance *AdminDashboardHandler
|
func NewAdminDashboardHandler(
|
||||||
|
postService *services.PostService,
|
||||||
func InitAdminDashboardHandler() {
|
jwt *auth.JwtTokenGenerator,
|
||||||
AdminDashboardHandlerInstance = &AdminDashboardHandler{
|
cfg *config.Config,
|
||||||
PostService: *services.PostServiceInstance,
|
) *AdminDashboardHandler {
|
||||||
Jwt: *auth.JwtTokenGeneratorInstance,
|
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
|
var input dto.ApproveMediaInput
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ps.PostService.ApproveMedia(input.MediaId) {
|
if adh.postService.ApproveMedia(input.MediaId) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Media approved successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "Media approved successfully"})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to approve media"})
|
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
|
var input dto.RejectMediaInput
|
||||||
if err := c.ShouldBindJSON(&input); err != nil {
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ps.PostService.RejectMedia(input.MediaId) {
|
if adh.postService.RejectMedia(input.MediaId) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Media rejected successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "Media rejected successfully"})
|
||||||
} else {
|
} else {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject media"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to reject media"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *AdminDashboardHandler) GetMedia(c *gin.Context) {
|
func (adh *AdminDashboardHandler) GetMedia(c *gin.Context) {
|
||||||
media := ps.PostService.GetMedia()
|
media := adh.postService.GetMedia()
|
||||||
media.PreviewUrl = services.GetPreviewUrl(media.RemoteUrl)
|
// TODO: Fix this - we need to inject ImgKitHelper
|
||||||
media.RemoteUrl = services.GetPreviewUrl(media.RemoteUrl)
|
// media.PreviewUrl = services.GetPreviewUrl(media.RemoteUrl)
|
||||||
|
// media.RemoteUrl = services.GetPreviewUrl(media.RemoteUrl)
|
||||||
c.JSON(http.StatusOK, media)
|
c.JSON(http.StatusOK, media)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *AdminDashboardHandler) Login(c *gin.Context) {
|
func (adh *AdminDashboardHandler) Login(c *gin.Context) {
|
||||||
|
|
||||||
var input dto.LoginInput
|
var input dto.LoginInput
|
||||||
|
|
||||||
@@ -68,8 +73,8 @@ func (ps *AdminDashboardHandler) Login(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.Password == config.Config.AdminPassword { // Its more than enough for this project
|
if input.Password == adh.cfg.AdminPassword { // Its more than enough for this project
|
||||||
token, err := ps.Jwt.GenerateToken(map[string]interface{}{"role": "admin"})
|
token, err := adh.jwt.GenerateToken(map[string]interface{}{"role": "admin"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
|
||||||
return
|
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 {
|
type ApiEndpointHandler struct {
|
||||||
PostService services.PostService
|
postService *services.PostService
|
||||||
|
imgKitHelper *services.ImgKitHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
var ApiEndpointHandlerInstance *ApiEndpointHandler
|
func NewApiEndpointHandler(
|
||||||
|
postService *services.PostService,
|
||||||
func InitApiEndpointHandler() {
|
imgKitHelper *services.ImgKitHelper,
|
||||||
ApiEndpointHandlerInstance = &ApiEndpointHandler{
|
) *ApiEndpointHandler {
|
||||||
PostService: *services.PostServiceInstance,
|
return &ApiEndpointHandler{
|
||||||
|
postService: postService,
|
||||||
|
imgKitHelper: imgKitHelper,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *ApiEndpointHandler) GetRandomPost(c *gin.Context) {
|
func (aeh *ApiEndpointHandler) GetRandomPost(c *gin.Context) {
|
||||||
post := ps.PostService.GetRandomPost()
|
post := aeh.postService.GetRandomPost()
|
||||||
for i := range post.Attachments {
|
for i := range post.Attachments {
|
||||||
post.Attachments[i].RemoteUrl = services.GetRemoteUrl(post.Attachments[i].RemoteUrl)
|
post.Attachments[i].RemoteUrl = aeh.imgKitHelper.GetRemoteUrl(post.Attachments[i].RemoteUrl)
|
||||||
post.Attachments[i].PreviewUrl = services.GetPreviewUrl(post.Attachments[i].RemoteUrl)
|
post.Attachments[i].PreviewUrl = aeh.imgKitHelper.GetPreviewUrl(post.Attachments[i].RemoteUrl)
|
||||||
}
|
}
|
||||||
c.JSON(200, post)
|
c.JSON(200, post)
|
||||||
}
|
}
|
@@ -7,21 +7,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type EmbedCardHandler struct {
|
type EmbedCardHandler struct {
|
||||||
PostService services.PostService
|
postService *services.PostService
|
||||||
|
imgKitHelper *services.ImgKitHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
var EmbedCardHandlerInstance *EmbedCardHandler
|
func NewEmbedCardHandler(
|
||||||
|
postService *services.PostService,
|
||||||
func InitEmbedCardHandler() {
|
imgKitHelper *services.ImgKitHelper,
|
||||||
EmbedCardHandlerInstance = &EmbedCardHandler{
|
) *EmbedCardHandler {
|
||||||
PostService: *services.PostServiceInstance,
|
return &EmbedCardHandler{
|
||||||
|
postService: postService,
|
||||||
|
imgKitHelper: imgKitHelper,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *EmbedCardHandler) GetEmbedCard(c *gin.Context) {
|
func (ech *EmbedCardHandler) GetEmbedCard(c *gin.Context) {
|
||||||
post := ps.PostService.GetRandomPost()
|
post := ech.postService.GetRandomPost()
|
||||||
c.HTML(200, "home/embed.html", gin.H{
|
c.HTML(200, "home/embed.html", gin.H{
|
||||||
"postUrl": post.Url,
|
"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 {
|
type OauthLoginHandler struct {
|
||||||
Jwt auth.JwtTokenGenerator
|
jwt *auth.JwtTokenGenerator
|
||||||
OauthLoginHandler *auth.GiteaOAuth2Handler
|
oauthHandler *auth.GiteaOAuth2Handler
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
var OauthLoginHandlerInstance *OauthLoginHandler
|
func NewOauthLoginHandler(
|
||||||
|
jwt *auth.JwtTokenGenerator,
|
||||||
func InitOauthLoginHandler() {
|
oauthHandler *auth.GiteaOAuth2Handler,
|
||||||
OauthLoginHandlerInstance = &OauthLoginHandler{
|
cfg *config.Config,
|
||||||
Jwt: *auth.JwtTokenGeneratorInstance,
|
) *OauthLoginHandler {
|
||||||
OauthLoginHandler: auth.GiteaOauth2HandlerInstance,
|
return &OauthLoginHandler{
|
||||||
|
jwt: jwt,
|
||||||
|
oauthHandler: oauthHandler,
|
||||||
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (olh *OauthLoginHandler) GoToGiteaLogin(c *gin.Context) {
|
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 != "" {
|
if redirectURL != "" {
|
||||||
c.Redirect(http.StatusFound, redirectURL)
|
c.Redirect(http.StatusFound, redirectURL)
|
||||||
return
|
return
|
||||||
@@ -42,27 +46,28 @@ func (olh *OauthLoginHandler) LoginWithGitea(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userEmail, err := olh.OauthLoginHandler.GetGiteaUserEmailByCode(input.Code)
|
userEmail, err := olh.oauthHandler.GetGiteaUserEmailByCode(input.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
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 {
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token generation failed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Login successful", "token": token})
|
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
|
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