563 lines
14 KiB
Go
563 lines
14 KiB
Go
package admin
|
|
|
|
import (
|
|
"html/template"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.nakama.town/fmartingr/butterrobot/internal/config"
|
|
"git.nakama.town/fmartingr/butterrobot/internal/db"
|
|
"git.nakama.town/fmartingr/butterrobot/internal/model"
|
|
"git.nakama.town/fmartingr/butterrobot/internal/plugin"
|
|
"github.com/gorilla/sessions"
|
|
)
|
|
|
|
const (
|
|
// Session store key
|
|
sessionKey = "butterrobot-session"
|
|
|
|
// Template directory
|
|
templateDir = "./internal/admin/templates"
|
|
)
|
|
|
|
// FlashMessage represents a flash message
|
|
type FlashMessage struct {
|
|
Category string
|
|
Message string
|
|
}
|
|
|
|
// TemplateData holds data for rendering templates
|
|
type TemplateData struct {
|
|
User *model.User
|
|
LoggedIn bool
|
|
Title string
|
|
Path string
|
|
Flash []FlashMessage
|
|
Plugins map[string]model.Plugin
|
|
Channels []*model.Channel
|
|
Channel *model.Channel
|
|
ChannelPlugin *model.ChannelPlugin
|
|
}
|
|
|
|
// Admin represents the admin interface
|
|
type Admin struct {
|
|
config *config.Config
|
|
db *db.Database
|
|
store *sessions.CookieStore
|
|
templates map[string]*template.Template
|
|
baseTemplate *template.Template
|
|
}
|
|
|
|
// New creates a new Admin instance
|
|
func New(cfg *config.Config, database *db.Database) *Admin {
|
|
// Create session store
|
|
store := sessions.NewCookieStore([]byte(cfg.SecretKey))
|
|
|
|
// Load templates
|
|
templates := make(map[string]*template.Template)
|
|
|
|
// Create a template function map with helper functions
|
|
funcMap := template.FuncMap{
|
|
"contains": strings.Contains,
|
|
}
|
|
|
|
// Create a custom template with functions
|
|
baseTemplate := template.New("_base.html").Funcs(funcMap)
|
|
baseTemplate = template.Must(baseTemplate.ParseFiles(filepath.Join(templateDir, "_base.html")))
|
|
|
|
// Parse and register all templates
|
|
templateFiles := []string{
|
|
"index.html",
|
|
"login.html",
|
|
"channel_list.html",
|
|
"channel_detail.html",
|
|
"plugin_list.html",
|
|
"channel_plugins_list.html",
|
|
}
|
|
|
|
for _, tf := range templateFiles {
|
|
// Create a clone of the base template
|
|
t, err := template.Must(baseTemplate.Clone()).ParseFiles(filepath.Join(templateDir, tf))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
templates[tf] = t
|
|
}
|
|
|
|
return &Admin{
|
|
config: cfg,
|
|
db: database,
|
|
store: store,
|
|
templates: templates,
|
|
baseTemplate: baseTemplate,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers admin routes on the given router
|
|
func (a *Admin) RegisterRoutes(mux *http.ServeMux) {
|
|
// Register admin routes
|
|
mux.HandleFunc("/admin/", a.handleIndex)
|
|
mux.HandleFunc("/admin/login", a.handleLogin)
|
|
mux.HandleFunc("/admin/logout", a.handleLogout)
|
|
mux.HandleFunc("/admin/plugins", a.handlePluginList)
|
|
mux.HandleFunc("/admin/channels", a.handleChannelList)
|
|
mux.HandleFunc("/admin/channels/", a.handleChannelDetail)
|
|
mux.HandleFunc("/admin/channelplugins", a.handleChannelPluginList)
|
|
mux.HandleFunc("/admin/channelplugins/", a.handleChannelPluginDetailOrDelete)
|
|
}
|
|
|
|
// getCurrentUser gets the current user from the session
|
|
func (a *Admin) getCurrentUser(r *http.Request) *model.User {
|
|
session, _ := a.store.Get(r, sessionKey)
|
|
|
|
// Check if user is logged in
|
|
userID, ok := session.Values["user_id"].(int64)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Get user from database
|
|
user, err := a.db.GetUserByID(userID)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
return user
|
|
}
|
|
|
|
// isLoggedIn checks if the user is logged in
|
|
func (a *Admin) isLoggedIn(r *http.Request) bool {
|
|
session, _ := a.store.Get(r, sessionKey)
|
|
return session.Values["logged_in"] == true
|
|
}
|
|
|
|
// addFlash adds a flash message to the session
|
|
func (a *Admin) addFlash(w http.ResponseWriter, r *http.Request, message string, category string) {
|
|
session, _ := a.store.Get(r, sessionKey)
|
|
|
|
// Add flash message
|
|
flashes := session.Flashes()
|
|
if flashes == nil {
|
|
flashes = make([]interface{}, 0)
|
|
}
|
|
|
|
flash := FlashMessage{
|
|
Category: category,
|
|
Message: message,
|
|
}
|
|
|
|
session.AddFlash(flash)
|
|
session.Save(r, w)
|
|
}
|
|
|
|
// getFlashes gets all flash messages from the session
|
|
func (a *Admin) getFlashes(w http.ResponseWriter, r *http.Request) []FlashMessage {
|
|
session, _ := a.store.Get(r, sessionKey)
|
|
|
|
// Get flash messages
|
|
flashes := session.Flashes()
|
|
messages := make([]FlashMessage, 0, len(flashes))
|
|
|
|
for _, f := range flashes {
|
|
if flash, ok := f.(FlashMessage); ok {
|
|
messages = append(messages, flash)
|
|
}
|
|
}
|
|
|
|
// Save session to clear flashes
|
|
session.Save(r, w)
|
|
|
|
return messages
|
|
}
|
|
|
|
// requireLogin middleware checks if the user is logged in
|
|
func (a *Admin) requireLogin(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if !a.isLoggedIn(r) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
// render renders a template with the given data
|
|
func (a *Admin) render(w http.ResponseWriter, r *http.Request, templateName string, data TemplateData) {
|
|
// Add current user data
|
|
data.User = a.getCurrentUser(r)
|
|
data.LoggedIn = a.isLoggedIn(r)
|
|
data.Path = r.URL.Path
|
|
data.Flash = a.getFlashes(w, r)
|
|
|
|
// Get template
|
|
tmpl, ok := a.templates[templateName]
|
|
if !ok {
|
|
http.Error(w, "Template not found", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Render template
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := tmpl.Execute(w, data); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
// handleIndex handles the admin index route
|
|
func (a *Admin) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/admin/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
// Redirect to login if not logged in
|
|
if !a.isLoggedIn(r) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Redirect to channel list
|
|
http.Redirect(w, r, "/admin/channels", http.StatusSeeOther)
|
|
}
|
|
|
|
// handleLogin handles the login route
|
|
func (a *Admin) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
// If already logged in, redirect to index
|
|
if a.isLoggedIn(r) {
|
|
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Handle login form submission
|
|
if r.Method == http.MethodPost {
|
|
// Parse form
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check credentials
|
|
username := r.FormValue("username")
|
|
password := r.FormValue("password")
|
|
|
|
user, err := a.db.CheckCredentials(username, password)
|
|
if err != nil || user == nil {
|
|
a.addFlash(w, r, "Incorrect credentials", "danger")
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Set session
|
|
session, _ := a.store.Get(r, sessionKey)
|
|
session.Values["logged_in"] = true
|
|
session.Values["user_id"] = user.ID
|
|
|
|
// Set session expiration
|
|
session.Options.MaxAge = 3600 * 24 * 7 // 1 week
|
|
session.Save(r, w)
|
|
|
|
a.addFlash(w, r, "You were logged in", "success")
|
|
|
|
// Redirect to index
|
|
next := r.URL.Query().Get("next")
|
|
if next == "" {
|
|
next = "/admin/"
|
|
}
|
|
http.Redirect(w, r, next, http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Render login template
|
|
a.render(w, r, "login.html", TemplateData{
|
|
Title: "Login",
|
|
})
|
|
}
|
|
|
|
// handleLogout handles the logout route
|
|
func (a *Admin) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
// Clear session
|
|
session, _ := a.store.Get(r, sessionKey)
|
|
session.Values = make(map[interface{}]interface{})
|
|
session.Options.MaxAge = -1 // Delete session
|
|
session.Save(r, w)
|
|
|
|
a.addFlash(w, r, "You were logged out", "success")
|
|
|
|
// Redirect to login
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
}
|
|
|
|
// handlePluginList handles the plugin list route
|
|
func (a *Admin) handlePluginList(w http.ResponseWriter, r *http.Request) {
|
|
// Check if user is logged in
|
|
if !a.isLoggedIn(r) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Get available plugins
|
|
plugins := plugin.GetAvailablePlugins()
|
|
|
|
// Render template
|
|
a.render(w, r, "plugin_list.html", TemplateData{
|
|
Title: "Plugins",
|
|
Plugins: plugins,
|
|
})
|
|
}
|
|
|
|
// handleChannelList handles the channel list route
|
|
func (a *Admin) handleChannelList(w http.ResponseWriter, r *http.Request) {
|
|
// Check if user is logged in
|
|
if !a.isLoggedIn(r) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Get all channels
|
|
channels, err := a.db.GetAllChannels()
|
|
if err != nil {
|
|
http.Error(w, "Failed to get channels", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Render template
|
|
a.render(w, r, "channel_list.html", TemplateData{
|
|
Title: "Channels",
|
|
Channels: channels,
|
|
})
|
|
}
|
|
|
|
// handleChannelDetail handles the channel detail route
|
|
func (a *Admin) handleChannelDetail(w http.ResponseWriter, r *http.Request) {
|
|
// Check if user is logged in
|
|
if !a.isLoggedIn(r) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Extract channel ID from path
|
|
path := r.URL.Path
|
|
if path == "/admin/channels/" {
|
|
http.Redirect(w, r, "/admin/channels", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
channelID := strings.TrimPrefix(path, "/admin/channels/")
|
|
if strings.Contains(channelID, "/") {
|
|
// Handle delete request
|
|
if strings.HasSuffix(path, "/delete") && r.Method == http.MethodPost {
|
|
channelID = strings.TrimSuffix(channelID, "/delete")
|
|
|
|
// Delete channel
|
|
id, err := strconv.ParseInt(channelID, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "Invalid channel ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := a.db.DeleteChannel(id); err != nil {
|
|
http.Error(w, "Failed to delete channel", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
a.addFlash(w, r, "Channel removed", "success")
|
|
http.Redirect(w, r, "/admin/channels", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
// Convert channel ID to int64
|
|
id, err := strconv.ParseInt(channelID, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "Invalid channel ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Handle form submission
|
|
if r.Method == http.MethodPost {
|
|
// Parse form
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check if the form was submitted
|
|
if r.FormValue("form_submitted") == "true" {
|
|
// Update channel
|
|
enabled := r.FormValue("enabled") == "true"
|
|
if err := a.db.UpdateChannel(id, enabled); err != nil {
|
|
http.Error(w, "Failed to update channel", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
a.addFlash(w, r, "Channel updated", "success")
|
|
http.Redirect(w, r, "/admin/channels/"+channelID, http.StatusSeeOther)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Get channel
|
|
channel, err := a.db.GetChannelByID(id)
|
|
if err != nil {
|
|
http.Error(w, "Channel not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Get available plugins
|
|
plugins := plugin.GetAvailablePlugins()
|
|
|
|
// Render template
|
|
a.render(w, r, "channel_detail.html", TemplateData{
|
|
Title: "Channel: " + channel.PlatformChannelID,
|
|
Channel: channel,
|
|
Plugins: plugins,
|
|
})
|
|
}
|
|
|
|
// handleChannelPluginList handles the channel plugin list route
|
|
func (a *Admin) handleChannelPluginList(w http.ResponseWriter, r *http.Request) {
|
|
// Check if user is logged in
|
|
if !a.isLoggedIn(r) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Handle form submission
|
|
if r.Method == http.MethodPost {
|
|
// Parse form
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Extract form data
|
|
channelID, err := strconv.ParseInt(r.FormValue("channel_id"), 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "Invalid channel ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
pluginID := r.FormValue("plugin_id")
|
|
enabled := r.FormValue("enabled") == "y"
|
|
|
|
// Create channel plugin
|
|
config := make(map[string]interface{})
|
|
_, err = a.db.CreateChannelPlugin(channelID, pluginID, enabled, config)
|
|
if err == db.ErrDuplicated {
|
|
a.addFlash(w, r, "Plugin "+pluginID+" is already present on the channel", "danger")
|
|
} else if err != nil {
|
|
http.Error(w, "Failed to create channel plugin", http.StatusInternalServerError)
|
|
return
|
|
} else {
|
|
a.addFlash(w, r, "Plugin "+pluginID+" added to the channel", "success")
|
|
}
|
|
|
|
// Redirect back
|
|
referer := r.Header.Get("Referer")
|
|
if referer == "" {
|
|
referer = "/admin/channelplugins"
|
|
}
|
|
http.Redirect(w, r, referer, http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Get all channels
|
|
channels, err := a.db.GetAllChannels()
|
|
if err != nil {
|
|
http.Error(w, "Failed to get channels", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Render template
|
|
a.render(w, r, "channel_plugins_list.html", TemplateData{
|
|
Title: "Channel Plugins",
|
|
Channels: channels,
|
|
Plugins: plugin.GetAvailablePlugins(),
|
|
})
|
|
}
|
|
|
|
// handleChannelPluginDetailOrDelete handles the channel plugin detail or delete route
|
|
func (a *Admin) handleChannelPluginDetailOrDelete(w http.ResponseWriter, r *http.Request) {
|
|
// Check if user is logged in
|
|
if !a.isLoggedIn(r) {
|
|
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Extract channel plugin ID from path
|
|
path := r.URL.Path
|
|
if path == "/admin/channelplugins/" {
|
|
http.Redirect(w, r, "/admin/channelplugins", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
channelPluginID := strings.TrimPrefix(path, "/admin/channelplugins/")
|
|
|
|
// Handle delete request
|
|
if strings.HasSuffix(channelPluginID, "/delete") && r.Method == http.MethodPost {
|
|
channelPluginID = strings.TrimSuffix(channelPluginID, "/delete")
|
|
|
|
// Delete channel plugin
|
|
id, err := strconv.ParseInt(channelPluginID, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "Invalid channel plugin ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := a.db.DeleteChannelPlugin(id); err != nil {
|
|
http.Error(w, "Failed to delete channel plugin", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
a.addFlash(w, r, "Plugin removed", "success")
|
|
|
|
// Redirect back
|
|
referer := r.Header.Get("Referer")
|
|
if referer == "" {
|
|
referer = "/admin/channelplugins"
|
|
}
|
|
http.Redirect(w, r, referer, http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Handle update request
|
|
if r.Method == http.MethodPost {
|
|
// Parse form
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Convert channel plugin ID to int64
|
|
id, err := strconv.ParseInt(channelPluginID, 10, 64)
|
|
if err != nil {
|
|
http.Error(w, "Invalid channel plugin ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Update channel plugin
|
|
enabled := r.FormValue("enabled") == "true"
|
|
if err := a.db.UpdateChannelPlugin(id, enabled); err != nil {
|
|
http.Error(w, "Failed to update channel plugin", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
a.addFlash(w, r, "Plugin updated", "success")
|
|
|
|
// Redirect back
|
|
referer := r.Header.Get("Referer")
|
|
if referer == "" {
|
|
referer = "/admin/channelplugins"
|
|
}
|
|
http.Redirect(w, r, referer, http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Redirect to channel plugins list
|
|
http.Redirect(w, r, "/admin/channelplugins", http.StatusSeeOther)
|
|
}
|