This commit is contained in:
Mukhtar Akere
2025-02-09 23:47:02 +01:00
parent 1614e29f8f
commit c386495d3d
18 changed files with 469 additions and 18 deletions

View File

@@ -7,3 +7,5 @@ docker-compose.yml
*.magnet
**.torrent
torrents.json
**/dist/
*.json

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ dist/
tmp/**
torrents.json
logs/**
auth.json

View File

@@ -31,25 +31,34 @@ RUN --mount=type=cache,target=/go/pkg/mod \
# Stage 2: Create directory structure
FROM alpine:3.19 as dirsetup
RUN mkdir -p /logs && \
chmod 777 /logs && \
touch /logs/decypharr.log && \
chmod 666 /logs/decypharr.log
RUN mkdir -p /data/logs && \
chmod 777 /data/logs && \
touch /data/logs/decypharr.log && \
chmod 666 /data/logs/decypharr.log
# Stage 3: Final image
FROM gcr.io/distroless/static-debian12:nonroot
LABEL version = "${VERSION}-${CHANNEL}"
LABEL org.opencontainers.image.source = "https://github.com/sirrobot01/debrid-blackhole"
LABEL org.opencontainers.image.title = "debrid-blackhole"
LABEL org.opencontainers.image.authors = "sirrobot01"
LABEL org.opencontainers.image.documentation = "https://github.com/sirrobot01/debrid-blackhole/blob/main/README.md"
# Copy binaries
COPY --from=builder --chown=nonroot:nonroot /blackhole /blackhole
COPY --from=builder --chown=nonroot:nonroot /healthcheck /healthcheck
# Copy pre-made directory structure
COPY --from=dirsetup --chown=nonroot:nonroot /logs /logs
COPY --from=dirsetup --chown=nonroot:nonroot /data /data
# Metadata
ENV LOG_PATH=/logs
ENV LOG_PATH=/data/logs
EXPOSE 8181 8282
VOLUME ["/app"]
VOLUME ["/data", "/app"]
USER nonroot:nonroot
HEALTHCHECK CMD ["/healthcheck"]
CMD ["/blackhole", "--config", "/app/config.json"]
CMD ["/blackhole", "--config", "/data"]

View File

@@ -61,7 +61,7 @@ services:
user: "1000:1000"
volumes:
- /mnt/:/mnt
- ~/plex/configs/blackhole/config.json:/app/config.json # Config file, see below
- ~/plex/configs/blackhole/:/data # Path to the config file. config.json
environment:
- PUID=1000
- PGID=1000
@@ -78,7 +78,7 @@ services:
Download the binary from the releases page and run it with the config file.
```bash
./blackhole --config /path/to/config.json
./blackhole --config /path/to/
```
### Usage
@@ -104,7 +104,7 @@ Download the binary from the releases page and run it with the config file.
#### Basic Sample Config
This is the default config file. You can create a `config.json` file in the root directory of the project or mount it in the docker-compose file.
This is the default config file. You can create a `config.json` file in the root directory of the project or mount it to /data in the docker-compose file.
```json
{
"debrids": [
@@ -145,6 +145,7 @@ Full config are [here](doc/config.full.json)
- The `log_level` key is used to set the log level of the application. The default value is `info`. log level can be set to `debug`, `info`, `warn`, `error`
- The `max_cache_size` key is used to set the maximum number of infohashes that can be stored in the availability cache. This is used to prevent round trip to the debrid provider when using the proxy/Qbittorrent. The default value is `1000`
- The `allowed_file_types` key is an array of allowed file types that can be downloaded. By default, all movie, tv show and music file types are allowed
- `use_auth` is used to enable basic authentication for the UI.
##### Debrid Config
- The `debrids` key is an array of debrid providers

View File

@@ -3,11 +3,13 @@ package cmd
import (
"context"
"github.com/sirrobot01/debrid-blackhole/internal/config"
"github.com/sirrobot01/debrid-blackhole/internal/logger"
"github.com/sirrobot01/debrid-blackhole/pkg/arr"
"github.com/sirrobot01/debrid-blackhole/pkg/debrid"
"github.com/sirrobot01/debrid-blackhole/pkg/proxy"
"github.com/sirrobot01/debrid-blackhole/pkg/qbit"
"github.com/sirrobot01/debrid-blackhole/pkg/repair"
"github.com/sirrobot01/debrid-blackhole/pkg/version"
"log"
"sync"
)
@@ -15,6 +17,13 @@ import (
func Start(ctx context.Context) error {
cfg := config.GetConfig()
_log := logger.GetLogger(cfg.LogLevel)
_log.Debug().Msgf("Config Loaded: %s", cfg.JsonFile())
_log.Debug().Msgf("Default Log Level: %s", cfg.LogLevel)
_log.Info().Msgf("Version: %s", version.GetInfo().String())
deb := debrid.NewDebrid()
arrs := arr.NewStorage()
_repair := repair.NewRepair(deb.Get(), arrs)

View File

@@ -75,5 +75,6 @@
"log_level": "info",
"min_file_size": "",
"max_file_size": "",
"allowed_file_types": []
"allowed_file_types": [],
"use_auth": false
}

11
go.mod
View File

@@ -1,6 +1,8 @@
module github.com/sirrobot01/debrid-blackhole
go 1.22
go 1.23
toolchain go1.23.2
require (
github.com/anacrolix/torrent v1.55.0
@@ -12,6 +14,7 @@ require (
github.com/rs/zerolog v1.33.0
github.com/valyala/fasthttp v1.55.0
github.com/valyala/fastjson v1.6.4
golang.org/x/crypto v0.33.0
golang.org/x/time v0.8.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
@@ -23,6 +26,8 @@ require (
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.4.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -32,6 +37,6 @@ require (
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)

12
go.sum
View File

@@ -115,6 +115,10 @@ github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORR
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
@@ -219,6 +223,10 @@ go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -259,10 +267,14 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"sync"
)
@@ -57,6 +58,11 @@ type Repair struct {
SkipDeletion bool `json:"skip_deletion"`
}
type Auth struct {
Username string `json:"username"`
Password string `json:"password"`
}
type Config struct {
LogLevel string `json:"log_level"`
Debrid Debrid `json:"debrid"`
@@ -69,6 +75,16 @@ type Config struct {
AllowedExt []string `json:"allowed_file_types"`
MinFileSize string `json:"min_file_size"` // Minimum file size to download, 10MB, 1GB, etc
MaxFileSize string `json:"max_file_size"` // Maximum file size to download (0 means no limit)
Path string `json:"-"` // Path to save the config file
UseAuth bool `json:"use_auth"`
Auth *Auth `json:"-"`
}
func (c *Config) JsonFile() string {
return filepath.Join(c.Path, "config.json")
}
func (c *Config) AuthFile() string {
return filepath.Join(c.Path, "auth.json")
}
func (c *Config) loadConfig() error {
@@ -76,7 +92,8 @@ func (c *Config) loadConfig() error {
if configPath == "" {
return fmt.Errorf("config path not set")
}
file, err := os.ReadFile(configPath)
c.Path = configPath
file, err := os.ReadFile(c.JsonFile())
if err != nil {
return err
}
@@ -93,6 +110,9 @@ func (c *Config) loadConfig() error {
c.AllowedExt = getDefaultExtensions()
}
// Load the auth file
c.Auth = c.GetAuth()
// Validate the config
//if err := validateConfig(c); err != nil {
// return nil, err
@@ -178,6 +198,12 @@ func validateConfig(config *Config) error {
}
func SetConfigPath(path string) {
// Backward compatibility
// Check if the path is not a dir
if fi, err := os.Stat(path); err == nil && !fi.IsDir() {
// Get the directory of the file
path = filepath.Dir(path)
}
configPath = path
}
@@ -227,3 +253,35 @@ func (c *Config) IsSizeAllowed(size int64) bool {
}
return true
}
func (c *Config) GetAuth() *Auth {
if !c.UseAuth {
return nil
}
if c.Auth == nil {
c.Auth = &Auth{}
if _, err := os.Stat(c.AuthFile()); err == nil {
file, err := os.ReadFile(c.AuthFile())
if err == nil {
_ = json.Unmarshal(file, c.Auth)
}
}
}
return c.Auth
}
func (c *Config) SaveAuth(auth *Auth) error {
c.Auth = auth
data, err := json.Marshal(auth)
if err != nil {
return err
}
return os.WriteFile(c.AuthFile(), data, 0644)
}
func (c *Config) NeedsSetup() bool {
if c.UseAuth {
return c.GetAuth().Username == ""
}
return false
}

View File

@@ -7,6 +7,12 @@ import (
"os"
"path/filepath"
"strings"
"sync"
)
var (
once sync.Once
logger zerolog.Logger
)
func GetLogPath() string {
@@ -78,3 +84,10 @@ func NewLogger(prefix string, level string, output *os.File) zerolog.Logger {
}
return logger
}
func GetLogger(level string) zerolog.Logger {
once.Do(func() {
logger = NewLogger("Decypharr", level, os.Stdout)
})
return logger
}

View File

@@ -112,6 +112,11 @@
{{ template "repair" . }}
{{ else if eq .Page "config" }}
{{ template "config" . }}
{{ else if eq .Page "login" }}
{{ template "login" . }}
{{ else if eq .Page "setup" }}
{{ template "setup" . }}
{{ else }}
{{ end }}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>

View File

@@ -0,0 +1,131 @@
{{ define "login" }}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="mb-0 text-center">Login</h4>
</div>
<div class="card-body">
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Login</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
username: document.getElementById('username').value,
password: document.getElementById('password').value
};
try {
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (response.ok) {
window.location.href = '/';
} else {
createToast('Invalid credentials', 'error');
}
} catch (error) {
console.error('Login error:', error);
createToast('Login failed', 'error');
}
});
</script>
{{ end }}
{{ define "setup" }}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="mb-0 text-center">First Time Setup</h4>
</div>
<div class="card-body">
<form id="setupForm">
<div class="mb-3">
<label for="username" class="form-label">Choose Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Choose Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Set Credentials</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.getElementById('setupForm').addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
if (password !== confirmPassword) {
createToast('Passwords do not match', 'error');
return;
}
const formData = {
username: document.getElementById('username').value,
password: password,
confirmPassword: confirmPassword
};
try {
const response = await fetch('/setup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (response.ok) {
window.location.href = '/';
} else {
const error = await response.text();
createToast(error, 'error');
}
} catch (error) {
console.error('Setup error:', error);
createToast('Setup failed', 'error');
}
});
</script>
{{ end }}

View File

@@ -0,0 +1,32 @@
{{ define "setup" }}
<div class="container mt-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-header">
<h4 class="mb-0 text-center">First Time Setup</h4>
</div>
<div class="card-body">
<form id="setupForm" method="POST" action="/setup">
<div class="mb-3">
<label for="username" class="form-label">Choose Username</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Choose Password</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-3">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Set Credentials</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,50 @@
package server
import (
"github.com/sirrobot01/debrid-blackhole/internal/config"
"golang.org/x/crypto/bcrypt"
"net/http"
)
func (u *UIHandler) authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if setup is needed
cfg := config.GetConfig()
if cfg.NeedsSetup() && r.URL.Path != "/setup" {
http.Redirect(w, r, "/setup", http.StatusSeeOther)
return
}
// Skip auth check for setup page
if r.URL.Path == "/setup" {
next.ServeHTTP(w, r)
return
}
session, _ := store.Get(r, "auth-session")
auth, ok := session.Values["authenticated"].(bool)
if !ok || !auth {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
next.ServeHTTP(w, r)
})
}
func (u *UIHandler) verifyAuth(username, password string) bool {
// If you're storing hashed password, use bcrypt to compare
if username == "" {
return false
}
auth := config.GetConfig().GetAuth()
if auth == nil {
return false
}
if username != auth.Username {
return false
}
err := bcrypt.CompareHashAndPassword([]byte(auth.Password), []byte(password))
return err == nil
}

View File

@@ -4,9 +4,11 @@ import (
"embed"
"encoding/json"
"fmt"
"github.com/gorilla/sessions"
"github.com/sirrobot01/debrid-blackhole/internal/config"
"github.com/sirrobot01/debrid-blackhole/internal/request"
"github.com/sirrobot01/debrid-blackhole/internal/utils"
"golang.org/x/crypto/bcrypt"
"html/template"
"net/http"
"strings"
@@ -56,7 +58,10 @@ type UIHandler struct {
debug bool
}
var templates *template.Template
var (
store = sessions.NewCookieStore([]byte("your-secret-key")) // Change this to a secure key
templates *template.Template
)
func init() {
templates = template.Must(template.ParseFS(
@@ -66,7 +71,112 @@ func init() {
"templates/download.html",
"templates/repair.html",
"templates/config.html",
"templates/login.html",
"templates/setup.html",
))
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 7,
HttpOnly: false,
}
}
func (u *UIHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
data := map[string]interface{}{
"Page": "login",
"Title": "Login",
}
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
var credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&credentials); err != nil {
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
if u.verifyAuth(credentials.Username, credentials.Password) {
session, _ := store.Get(r, "auth-session")
session.Values["authenticated"] = true
session.Values["username"] = credentials.Username
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
}
func (u *UIHandler) LogoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "auth-session")
session.Values["authenticated"] = false
session.Options.MaxAge = -1
session.Save(r, w)
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
func (u *UIHandler) SetupHandler(w http.ResponseWriter, r *http.Request) {
cfg := config.GetConfig()
authCfg := cfg.GetAuth()
if !cfg.NeedsSetup() {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
if r.Method == "GET" {
data := map[string]interface{}{
"Page": "setup",
"Title": "Setup",
}
if err := templates.ExecuteTemplate(w, "layout", data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
// Handle POST (setup attempt)
username := r.FormValue("username")
password := r.FormValue("password")
confirmPassword := r.FormValue("confirmPassword")
if password != confirmPassword {
http.Error(w, "Passwords do not match", http.StatusBadRequest)
return
}
// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
http.Error(w, "Error processing password", http.StatusInternalServerError)
return
}
// Set the credentials
authCfg.Username = username
authCfg.Password = string(hashedPassword)
if err := cfg.SaveAuth(authCfg); err != nil {
http.Error(w, "Error saving credentials", http.StatusInternalServerError)
return
}
// Create a session
session, _ := store.Get(r, "auth-session")
session.Values["authenticated"] = true
session.Values["username"] = username
session.Save(r, w)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func (u *UIHandler) IndexHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -6,7 +6,13 @@ import (
)
func (u *UIHandler) Routes(r chi.Router) http.Handler {
r.Get("/login", u.LoginHandler)
r.Post("/login", u.LoginHandler)
r.Get("/setup", u.SetupHandler)
r.Post("/setup", u.SetupHandler)
r.Group(func(r chi.Router) {
r.Use(u.authMiddleware)
r.Get("/", u.IndexHandler)
r.Get("/download", u.DownloadHandler)
r.Get("/repair", u.RepairHandler)

View File

@@ -37,7 +37,7 @@ func NewQBit(deb *debrid.DebridService, logger zerolog.Logger, arrs *arr.Storage
DownloadFolder: cfg.DownloadFolder,
Categories: cfg.Categories,
Debrid: deb,
Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/app/torrents.json")),
Storage: NewTorrentStorage(cmp.Or(os.Getenv("TORRENT_FILE"), "/data/torrents.json")),
Repair: _repair,
logger: logger,
Arrs: arrs,

View File

@@ -1,10 +1,16 @@
package version
import "fmt"
type Info struct {
Version string `json:"version"`
Channel string `json:"channel"`
}
func (i Info) String() string {
return fmt.Sprintf("%s-%s", i.Version, i.Channel)
}
var (
Version = ""
Channel = ""