This commit is contained in:
lfirmin
2026-05-10 18:07:51 +02:00
parent 50af2452b1
commit a5f888b3c1
59 changed files with 6682 additions and 108 deletions
+157
View File
@@ -0,0 +1,157 @@
package main
import (
"context"
"database/sql"
"fmt"
"os"
"github.com/leolionad58/ticketbot/internal/db"
panelauth "github.com/leolionad58/ticketbot/internal/panel/auth"
)
// handleAdminCLI processes "ticketbot admin <subcommand>" and exits.
// Returns true if an admin subcommand was handled.
func handleAdminCLI(sqldb *sql.DB) bool {
if len(os.Args) < 2 || os.Args[1] != "admin" {
return false
}
repo := db.NewPanelAdminRepo(sqldb)
ctx := context.Background()
if len(os.Args) < 3 {
printAdminUsage()
os.Exit(1)
}
switch os.Args[2] {
case "add":
cliAddAdmin(ctx, repo)
case "remove":
cliRemoveAdmin(ctx, repo)
case "list":
cliListAdmins(ctx, repo)
case "reset-totp":
cliResetTOTP(ctx, repo)
case "reset-password":
cliResetPassword(ctx, repo)
default:
fmt.Fprintf(os.Stderr, "Unknown admin subcommand: %s\n", os.Args[2])
printAdminUsage()
os.Exit(1)
}
return true
}
func cliAddAdmin(ctx context.Context, repo *db.PanelAdminRepo) {
args := os.Args[3:]
discordID := flagValue(args, "--discord-id")
password := flagValue(args, "--password")
superadmin := containsFlag(args, "--superadmin")
if discordID == "" {
fmt.Fprintln(os.Stderr, "Usage: admin add --discord-id <id> [--password <pass>] [--superadmin]")
os.Exit(1)
}
var hash string
if password != "" {
var err error
hash, err = panelauth.HashPassword(password)
must(err, "hash password")
}
a, err := repo.Create(ctx, discordID, "", hash, superadmin)
must(err, "create admin")
fmt.Printf("Admin added: discord_id=%s id=%d superadmin=%v\n", a.DiscordID, a.ID, a.IsSuperadmin)
}
func cliRemoveAdmin(ctx context.Context, repo *db.PanelAdminRepo) {
discordID := flagValue(os.Args[3:], "--discord-id")
if discordID == "" {
fmt.Fprintln(os.Stderr, "Usage: admin remove --discord-id <id>")
os.Exit(1)
}
must(repo.Delete(ctx, discordID), "remove admin")
fmt.Printf("Admin removed: %s\n", discordID)
}
func cliListAdmins(ctx context.Context, repo *db.PanelAdminRepo) {
admins, err := repo.List(ctx)
must(err, "list admins")
if len(admins) == 0 {
fmt.Println("No admins configured.")
return
}
fmt.Printf("%-20s %-30s %-12s %-12s\n", "Discord ID", "Username", "Superadmin", "TOTP")
for _, a := range admins {
fmt.Printf("%-20s %-30s %-12v %-12v\n", a.DiscordID, a.DiscordUsername, a.IsSuperadmin, a.TOTPEnabled)
}
}
func cliResetTOTP(ctx context.Context, repo *db.PanelAdminRepo) {
discordID := flagValue(os.Args[3:], "--discord-id")
if discordID == "" {
fmt.Fprintln(os.Stderr, "Usage: admin reset-totp --discord-id <id>")
os.Exit(1)
}
a, err := repo.GetByDiscordID(ctx, discordID)
must(err, "get admin")
if a == nil {
fmt.Fprintf(os.Stderr, "Admin not found: %s\n", discordID)
os.Exit(1)
}
must(repo.ResetTOTP(ctx, a.ID), "reset totp")
fmt.Printf("TOTP reset for %s — admin must re-setup TOTP on next login.\n", discordID)
}
func cliResetPassword(ctx context.Context, repo *db.PanelAdminRepo) {
args := os.Args[3:]
discordID := flagValue(args, "--discord-id")
password := flagValue(args, "--password")
if discordID == "" || password == "" {
fmt.Fprintln(os.Stderr, "Usage: admin reset-password --discord-id <id> --password <pass>")
os.Exit(1)
}
a, err := repo.GetByDiscordID(ctx, discordID)
must(err, "get admin")
if a == nil {
fmt.Fprintf(os.Stderr, "Admin not found: %s\n", discordID)
os.Exit(1)
}
hash, err := panelauth.HashPassword(password)
must(err, "hash password")
must(repo.UpdatePassword(ctx, a.ID, hash), "update password")
fmt.Printf("Password updated for %s\n", discordID)
}
func flagValue(args []string, flag string) string {
for i := 0; i < len(args)-1; i++ {
if args[i] == flag {
return args[i+1]
}
}
return ""
}
func containsFlag(args []string, flag string) bool {
for _, a := range args {
if a == flag {
return true
}
}
return false
}
func printAdminUsage() {
fmt.Fprintln(os.Stderr, `Usage: ticketbot admin <subcommand> [flags]
Subcommands:
add --discord-id <id> --password <pass> [--superadmin]
remove --discord-id <id>
list
reset-totp --discord-id <id>
reset-password --discord-id <id> --password <pass>`)
}
func newAdminRepo(sqldb *sql.DB) *db.PanelAdminRepo {
return db.NewPanelAdminRepo(sqldb)
}
+72 -7
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"log/slog"
"os"
"os/signal"
@@ -15,6 +16,7 @@ import (
"github.com/leolionad58/ticketbot/internal/discord/components"
"github.com/leolionad58/ticketbot/internal/discord/events"
"github.com/leolionad58/ticketbot/internal/logger"
"github.com/leolionad58/ticketbot/internal/panel"
"github.com/leolionad58/ticketbot/internal/tickets"
"github.com/leolionad58/ticketbot/internal/transcript"
)
@@ -34,8 +36,23 @@ func main() {
must(err, "open db")
defer sqldb.Close()
// CLI admin subcommand — handle and exit before starting the bot
if handleAdminCLI(sqldb) {
return
}
// Migrate YAML panels to DB on first boot
panelRepo := db.NewPanelConfigRepo(sqldb)
if err := panel.MigrateYAMLPanels(context.Background(), panelRepo, cfg, os.Getenv("GUILD_ID")); err != nil {
slog.Warn("YAML panel migration failed", "err", err)
}
ticketRepo := db.NewTicketRepo(sqldb)
claimRepo := db.NewClaimMessageRepo(sqldb)
convocRepo := db.NewConvocationConfigRepo(sqldb)
adminRepo := db.NewPanelAdminRepo(sqldb)
sessionRepo := db.NewPanelSessionRepo(sqldb)
auditRepo := db.NewAuditLogRepo(sqldb)
token := os.Getenv("DISCORD_TOKEN")
if token == "" {
@@ -45,12 +62,27 @@ func main() {
appID := os.Getenv("DISCORD_APP_ID")
guildID := os.Getenv("GUILD_ID")
bot, err := discordbot.New(token, cfgProvider, ticketRepo, claimRepo, guildID)
// Build guild allowlist: config yaml takes priority, env GUILD_ID is always included
allowedGuilds := append([]string{}, cfg.Bot.AllowedGuilds...)
if guildID != "" {
found := false
for _, id := range allowedGuilds {
if id == guildID {
found = true
break
}
}
if !found {
allowedGuilds = append(allowedGuilds, guildID)
}
}
bot, err := discordbot.New(token, cfgProvider, ticketRepo, claimRepo, guildID, allowedGuilds)
must(err, "create bot")
// Services
auth := tickets.NewAuthService(cfgProvider)
ticketSvc := tickets.NewService(ticketRepo, auth, bot.Session, cfgProvider)
ticketSvc := tickets.NewService(ticketRepo, panelRepo, auth, bot.Session, cfgProvider)
claimMgr := claim.NewManager(cfgProvider, ticketRepo, claimRepo)
logSvc := logger.NewDiscordLogger(bot.Session, cfgProvider)
transcriptGen := transcript.NewGenerator("transcripts", bot.Session)
@@ -76,6 +108,7 @@ func main() {
panelComp := &components.PanelComponent{
TicketSvc: ticketSvc,
TicketRepo: ticketRepo,
PanelRepo: panelRepo,
ClaimMgr: claimMgr,
LogSvc: logSvc,
Config: cfgProvider,
@@ -88,14 +121,22 @@ func main() {
Transcript: transcriptGen,
Auth: auth,
}
convocComp := &components.ConvocationComponent{
ConvocRepo: convocRepo,
TicketRepo: ticketRepo,
Auth: auth,
LogSvc: logSvc,
}
// Router
router := &discordbot.Router{
PanelCmd: panelCmd,
TicketCmd: ticketCmd,
ConvocCmd: convocCmd,
PanelComp: panelComp,
TicketComp: ticketComp,
PanelCmd: panelCmd,
TicketCmd: ticketCmd,
ConvocCmd: convocCmd,
PanelComp: panelComp,
TicketComp: ticketComp,
ConvocComp: convocComp,
IsGuildAllowed: bot.IsGuildAllowed,
}
bot.Session.AddHandler(router.Handle)
@@ -123,6 +164,30 @@ func main() {
slog.Warn("config watcher failed to start", "err", err)
}
// Start panel HTTP server if configured
if cfg.Panel.BaseURL != "" || cfg.Panel.Port > 0 {
botSvc := discordbot.NewBotService(bot.Session, panelRepo, ticketRepo, convocRepo, guildID, allowedGuilds)
panelSrv := panel.NewServer(
cfg.Panel,
ticketRepo,
adminRepo,
sessionRepo,
auditRepo,
panelRepo,
convocRepo,
guildID,
botSvc,
)
ctx, cancelPanel := context.WithCancel(context.Background())
go func() {
slog.Info("panel server starting", "port", cfg.Panel.Port)
if err := panelSrv.Start(ctx); err != nil {
slog.Error("panel server stopped", "err", err)
}
}()
defer cancelPanel()
}
slog.Info("ticketbot ready, Ctrl+C to stop")
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)