-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Initial code for web dashboard #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,9 @@ import ( | |
|
|
||
| "github.com/crazyuploader/rdctl-bot/internal/bot" | ||
| "github.com/crazyuploader/rdctl-bot/internal/config" | ||
| "github.com/crazyuploader/rdctl-bot/internal/db" | ||
| "github.com/crazyuploader/rdctl-bot/internal/realdebrid" | ||
| "github.com/crazyuploader/rdctl-bot/internal/web" | ||
| "github.com/spf13/cobra" | ||
| "github.com/spf13/viper" | ||
| ) | ||
|
|
@@ -152,6 +155,12 @@ func runBot(cmd *cobra.Command, args []string) { | |
| return | ||
| } | ||
|
|
||
| // Initialize database | ||
| database, err := db.Init(cfg.Database.GetDSN()) | ||
| if err != nil { | ||
| log.Fatalf("Failed to initialize database: %v", err) | ||
| } | ||
|
|
||
| // Log configuration details | ||
| log.Printf("Allowed chat IDs: %v", cfg.Telegram.AllowedChatIDs) | ||
| log.Printf("Super admin IDs: %v", cfg.Telegram.SuperAdminIDs) | ||
|
|
@@ -169,6 +178,20 @@ func runBot(cmd *cobra.Command, args []string) { | |
| log.Fatalf("Failed to create bot: %v", err) | ||
| } | ||
|
|
||
| go func() { | ||
| // Create dependencies for web handlers | ||
| deps := web.Dependencies{ | ||
| RDClient: realdebrid.NewClient(cfg.RealDebrid.BaseURL, cfg.RealDebrid.APIToken, cfg.RealDebrid.Proxy, time.Duration(cfg.RealDebrid.Timeout)*time.Second), | ||
| UserRepo: db.NewUserRepository(database), | ||
| ActivityRepo: db.NewActivityRepository(database), | ||
| TorrentRepo: db.NewTorrentRepository(database), | ||
| DownloadRepo: db.NewDownloadRepository(database), | ||
| CommandRepo: db.NewCommandRepository(database), | ||
| Config: cfg, | ||
| } | ||
| web.Start(deps) | ||
| }() | ||
|
|
||
|
Comment on lines
+181
to
+194
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Web server lacks graceful shutdown and error propagation; integrate with context
Suggested approach:
Apply this diff here (paired with changes in internal/web/server.go): - go func() {
- // Create dependencies for web handlers
- deps := web.Dependencies{
+ // Create dependencies for web handlers
+ deps := web.Dependencies{
RDClient: realdebrid.NewClient(cfg.RealDebrid.BaseURL, cfg.RealDebrid.APIToken, cfg.RealDebrid.Proxy, time.Duration(cfg.RealDebrid.Timeout)*time.Second),
UserRepo: db.NewUserRepository(database),
ActivityRepo: db.NewActivityRepository(database),
TorrentRepo: db.NewTorrentRepository(database),
DownloadRepo: db.NewDownloadRepository(database),
CommandRepo: db.NewCommandRepository(database),
Config: cfg,
- }
- web.Start(deps)
- }()
+ }
+ webErrCh := make(chan error, 1)
+ // Create shutdown context before launching web
+ // (Move the signal.NotifyContext block above this call.)
+ go func() { webErrCh <- web.Start(ctx, deps) }()And extend the wait loop: select {
case <-ctx.Done():
...
-case err := <-errCh:
+case err := <-errCh:
...
+case err := <-webErrCh:
+ log.Printf("Web server encountered an error: %v", err)
+ log.Println("Initiating shutdown due to web error...")
}In func Start(ctx context.Context, deps Dependencies) error {
app := fiber.New()
// ...
go func() {
<-ctx.Done()
_ = app.Shutdown()
}()
return app.Listen(deps.Config.Web.ListenAddr)
}🤖 Prompt for AI Agents |
||
| // Setup graceful shutdown using context with signal notification | ||
| ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) | ||
| defer stop() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ go 1.25.3 | |
|
|
||
| require ( | ||
| github.com/go-telegram/bot v1.17.0 | ||
| github.com/gofiber/fiber/v2 v2.52.9 | ||
| github.com/spf13/cobra v1.10.1 | ||
| github.com/spf13/viper v1.21.0 | ||
| golang.org/x/text v0.30.0 | ||
|
|
@@ -13,22 +14,32 @@ require ( | |
| ) | ||
|
|
||
| require ( | ||
| github.com/andybalholm/brotli v1.1.0 // indirect | ||
| github.com/fsnotify/fsnotify v1.9.0 // indirect | ||
| github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | ||
| github.com/google/uuid v1.6.0 // indirect | ||
| github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||
| github.com/jackc/pgpassfile v1.0.0 // indirect | ||
| github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect | ||
| github.com/jackc/pgx/v5 v5.6.0 // indirect | ||
| github.com/jackc/puddle/v2 v2.2.2 // indirect | ||
| github.com/jinzhu/inflection v1.0.0 // indirect | ||
| github.com/jinzhu/now v1.1.5 // indirect | ||
| github.com/klauspost/compress v1.17.9 // indirect | ||
| github.com/mattn/go-colorable v0.1.13 // indirect | ||
| github.com/mattn/go-isatty v0.0.20 // indirect | ||
| github.com/mattn/go-runewidth v0.0.16 // indirect | ||
| github.com/pelletier/go-toml/v2 v2.2.4 // indirect | ||
| github.com/rivo/uniseg v0.2.0 // indirect | ||
| github.com/sagikazarmark/locafero v0.11.0 // indirect | ||
| github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect | ||
| github.com/spf13/afero v1.15.0 // indirect | ||
| github.com/spf13/cast v1.10.0 // indirect | ||
| github.com/spf13/pflag v1.0.10 // indirect | ||
| github.com/subosito/gotenv v1.6.0 // indirect | ||
| github.com/valyala/bytebufferpool v1.0.0 // indirect | ||
| github.com/valyala/fasthttp v1.51.0 // indirect | ||
| github.com/valyala/tcplisten v1.0.0 // indirect | ||
| go.yaml.in/yaml/v3 v3.0.4 // indirect | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Broken indirect module path: gopkg.in/yaml.v3, not go.yaml.in/yaml/v3 This will break -go.yaml.in/yaml/v3 v3.0.4 // indirect
+gopkg.in/yaml.v3 v3.0.4 // indirectAfter the change, run: go mod tidy🤖 Prompt for AI Agents |
||
| golang.org/x/crypto v0.35.0 // indirect | ||
| golang.org/x/sync v0.17.0 // indirect | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,139 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package web | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "log" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strconv" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/crazyuploader/rdctl-bot/internal/realdebrid" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/gofiber/fiber/v2" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetStatus retrieves the Real-Debrid account status | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) GetStatus(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| user, err := d.RDClient.GetUser() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.JSON(fiber.Map{"success": true, "data": user}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetTorrents retrieves the list of active torrents | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) GetTorrents(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| limit, _ := strconv.Atoi(c.Query("limit", "50")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| offset, _ := strconv.Atoi(c.Query("offset", "0")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| torrents, err := d.RDClient.GetTorrents(limit, offset) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Format status and size for frontend convenience | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for i := range torrents { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| torrents[i].Status = realdebrid.FormatStatus(torrents[i].Status) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.JSON(fiber.Map{"success": true, "data": torrents}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add validation for pagination parameters. The handler silently ignores parsing errors and doesn't validate limit/offset ranges, which could lead to resource exhaustion or unexpected behavior. Add proper validation: func (d *Dependencies) GetTorrents(c *fiber.Ctx) error {
- limit, _ := strconv.Atoi(c.Query("limit", "50"))
- offset, _ := strconv.Atoi(c.Query("offset", "0"))
+ limit, err := strconv.Atoi(c.Query("limit", "50"))
+ if err != nil || limit < 1 || limit > 100 {
+ limit = 50
+ }
+ offset, err := strconv.Atoi(c.Query("offset", "0"))
+ if err != nil || offset < 0 {
+ offset = 0
+ }
torrents, err := d.RDClient.GetTorrents(limit, offset)This prevents:
📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetTorrentInfo retrieves detailed information about a single torrent | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) GetTorrentInfo(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id := c.Params("id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| torrent, err := d.RDClient.GetTorrentInfo(id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| torrent.Status = realdebrid.FormatStatus(torrent.Status) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.JSON(fiber.Map{"success": true, "data": torrent}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // AddTorrent adds a new torrent from a magnet link | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) AddTorrent(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var body struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Magnet string `json:"magnet"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := c.BodyParser(&body); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"success": false, "error": "Invalid request body"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if body.Magnet == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"success": false, "error": "Magnet link is required"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resp, err := d.RDClient.AddMagnet(body.Magnet) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Automatically select all files | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := d.RDClient.SelectAllFiles(resp.ID); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.Printf("Failed to select files for torrent %s: %v", resp.ID, err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Non-fatal, just log it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusCreated).JSON(fiber.Map{"success": true, "data": resp}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // DeleteTorrent deletes a torrent | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) DeleteTorrent(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id := c.Params("id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := d.RDClient.DeleteTorrent(id); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusOK).JSON(fiber.Map{"success": true, "message": "Torrent deleted successfully"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetDownloads retrieves the download history | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) GetDownloads(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| limit, _ := strconv.Atoi(c.Query("limit", "50")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| offset, _ := strconv.Atoi(c.Query("offset", "0")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| downloads, err := d.RDClient.GetDownloads(limit, offset) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.JSON(fiber.Map{"success": true, "data": downloads}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+86
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same pagination validation issue as GetTorrents. This handler has the same lack of input validation for limit and offset parameters. Apply the same validation fix as recommended for 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // UnrestrictLink unrestricts a hoster link | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) UnrestrictLink(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var body struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Link string `json:"link"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := c.BodyParser(&body); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"success": false, "error": "Invalid request body"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if body.Link == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"success": false, "error": "Link is required"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unrestricted, err := d.RDClient.UnrestrictLink(body.Link) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.JSON(fiber.Map{"success": true, "data": unrestricted}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // DeleteDownload deletes a download from history | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) DeleteDownload(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id := c.Params("id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := d.RDClient.DeleteDownload(id); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusOK).JSON(fiber.Map{"success": true, "message": "Download link removed successfully"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetUserStats retrieves statistics for a user | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (d *Dependencies) GetUserStats(c *fiber.Ctx) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userID, err := strconv.ParseUint(c.Params("id"), 10, 64) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"success": false, "error": "Invalid user ID"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stats, err := d.CommandRepo.GetUserStats(uint(userID)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"success": false, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.JSON(fiber.Map{"success": true, "data": stats}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package web | ||
|
|
||
| import ( | ||
| "github.com/gofiber/fiber/v2" | ||
| ) | ||
|
|
||
| // APIKeyAuth is a middleware for simple API key authentication | ||
| func APIKeyAuth(apiKey string) fiber.Handler { | ||
| return func(c *fiber.Ctx) error { | ||
| providedKey := c.Get("X-API-Key") | ||
| if providedKey == "" { | ||
| providedKey = c.Query("api_key") | ||
| } | ||
|
Comment on lines
+10
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not accept API key via query param; use constant‑time compare Query params can leak via logs, proxies, and browser history. Also, plain string equality is timing‑sensitive. Use header only and constant‑time comparison. package web
import (
+ "crypto/subtle"
"github.com/gofiber/fiber/v2"
)
// APIKeyAuth is a middleware for simple API key authentication
func APIKeyAuth(apiKey string) fiber.Handler {
return func(c *fiber.Ctx) error {
- providedKey := c.Get("X-API-Key")
- if providedKey == "" {
- providedKey = c.Query("api_key")
- }
+ providedKey := c.Get("X-API-Key")
- if providedKey != apiKey {
+ if subtle.ConstantTimeCompare([]byte(providedKey), []byte(apiKey)) != 1 {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"success": false,
"message": "Unauthorized: Invalid or missing API Key",
})
}
return c.Next()
}
}Optionally, support Also applies to: 15-21 🤖 Prompt for AI Agents |
||
|
|
||
| if providedKey != apiKey { | ||
| return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ | ||
| "success": false, | ||
| "message": "Unauthorized: Invalid or missing API Key", | ||
| }) | ||
| } | ||
| return c.Next() | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Avoid os.Exit in NewClient; return an error instead
internal/realdebrid/client.go: NewClientcallsos.Exit(1)on invalid proxy URL (per snippet), which can terminate the whole process from here. Change it to return(*Client, error)and handle the error at the call site.Would you like a patch for
internal/realdebrid/client.goand the call sites?🏁 Script executed:
Length of output: 5714
🏁 Script executed:
Length of output: 1090
🏁 Script executed:
Length of output: 46
🏁 Script executed:
Length of output: 525
🏁 Script executed:
Length of output: 635
🏁 Script executed:
Length of output: 212
Return error from NewClient instead of calling os.Exit
The
NewClientfunction ininternal/realdebrid/client.gocallsos.Exit(1)when proxy URL parsing fails, terminating the entire process. This is problematic because:*Client, noterror)cmd/rdctl-bot/main.go:184), causing unexpected process termination from a background taskChange
NewClientsignature tofunc NewClient(...) (*Client, error)and return the error. Update call sites ininternal/bot/bot.go:58andcmd/rdctl-bot/main.go:184to handle the returned error.🤖 Prompt for AI Agents