chore: migrate admin general
This commit is contained in:
parent
10bbd908e6
commit
9a7e83ae5f
6
.golangci.toml
Normal file
6
.golangci.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#:schema https://golangci-lint.run/jsonschema/golangci.jsonschema.json
|
||||||
|
version = "2"
|
||||||
|
|
||||||
|
[[linters.exclusions.rules]]
|
||||||
|
linters = [ "errcheck" ]
|
||||||
|
source = "^\\s*defer\\s+"
|
@ -162,13 +162,17 @@ func (api *API) registerWebAppRoutes(router *gin.Engine) {
|
|||||||
// TODO
|
// TODO
|
||||||
router.GET("/login", api.appGetLogin)
|
router.GET("/login", api.appGetLogin)
|
||||||
router.GET("/register", api.appGetRegister)
|
router.GET("/register", api.appGetRegister)
|
||||||
|
|
||||||
|
// DONE
|
||||||
|
router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin)
|
||||||
|
router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction)
|
||||||
|
|
||||||
|
// TODO - WIP
|
||||||
router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs)
|
router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs)
|
||||||
router.GET("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminImport)
|
router.GET("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminImport)
|
||||||
router.POST("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminImport)
|
router.POST("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminImport)
|
||||||
router.GET("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminUsers)
|
router.GET("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminUsers)
|
||||||
router.POST("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appUpdateAdminUsers)
|
router.POST("/admin/users", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appUpdateAdminUsers)
|
||||||
router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin)
|
|
||||||
router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction)
|
|
||||||
|
|
||||||
// Demo mode enabled configuration
|
// Demo mode enabled configuration
|
||||||
if api.cfg.DemoMode {
|
if api.cfg.DemoMode {
|
||||||
|
@ -27,6 +27,8 @@ import (
|
|||||||
"reichard.io/antholume/database"
|
"reichard.io/antholume/database"
|
||||||
"reichard.io/antholume/metadata"
|
"reichard.io/antholume/metadata"
|
||||||
"reichard.io/antholume/utils"
|
"reichard.io/antholume/utils"
|
||||||
|
"reichard.io/antholume/web/models"
|
||||||
|
"reichard.io/antholume/web/pages"
|
||||||
)
|
)
|
||||||
|
|
||||||
type adminAction string
|
type adminAction string
|
||||||
@ -96,21 +98,31 @@ type importResult struct {
|
|||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) appPerformAdminAction(c *gin.Context) {
|
func (api *API) appGetAdmin(c *gin.Context) {
|
||||||
templateVars, _ := api.getBaseTemplateVars("admin", c)
|
api.renderPage(c, &pages.AdminGeneral{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) appPerformAdminAction(c *gin.Context) {
|
||||||
var rAdminAction requestAdminAction
|
var rAdminAction requestAdminAction
|
||||||
if err := c.ShouldBind(&rAdminAction); err != nil {
|
if err := c.ShouldBind(&rAdminAction); err != nil {
|
||||||
log.Error("Invalid Form Bind: ", err)
|
log.Error("invalid or missing form values")
|
||||||
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var allNotifications []*models.Notification
|
||||||
switch rAdminAction.Action {
|
switch rAdminAction.Action {
|
||||||
|
case adminRestore:
|
||||||
|
api.processRestoreFile(rAdminAction, c)
|
||||||
|
return
|
||||||
|
case adminBackup:
|
||||||
|
api.processBackup(c, rAdminAction.BackupTypes)
|
||||||
|
return
|
||||||
case adminMetadataMatch:
|
case adminMetadataMatch:
|
||||||
// TODO
|
allNotifications = append(allNotifications, &models.Notification{
|
||||||
// 1. Documents xref most recent metadata table?
|
Type: models.NotificationTypeError,
|
||||||
// 2. Select all / deselect?
|
Content: "Metadata match not implemented",
|
||||||
|
})
|
||||||
case adminCacheTables:
|
case adminCacheTables:
|
||||||
go func() {
|
go func() {
|
||||||
err := api.db.CacheTempTables(c)
|
err := api.db.CacheTempTables(c)
|
||||||
@ -118,50 +130,14 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
|||||||
log.Error("Unable to cache temp tables: ", err)
|
log.Error("Unable to cache temp tables: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
case adminRestore:
|
|
||||||
api.processRestoreFile(rAdminAction, c)
|
|
||||||
return
|
|
||||||
case adminBackup:
|
|
||||||
// Vacuum
|
|
||||||
_, err := api.db.DB.ExecContext(c, "VACUUM;")
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to vacuum DB: ", err)
|
|
||||||
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set Headers
|
allNotifications = append(allNotifications, &models.Notification{
|
||||||
c.Header("Content-type", "application/octet-stream")
|
Type: models.NotificationTypeSuccess,
|
||||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"AnthoLumeBackup_%s.zip\"", time.Now().Format("20060102150405")))
|
Content: "Initiated table cache",
|
||||||
|
|
||||||
// Stream Backup ZIP Archive
|
|
||||||
c.Stream(func(w io.Writer) bool {
|
|
||||||
var directories []string
|
|
||||||
for _, item := range rAdminAction.BackupTypes {
|
|
||||||
switch item {
|
|
||||||
case backupCovers:
|
|
||||||
directories = append(directories, "covers")
|
|
||||||
case backupDocuments:
|
|
||||||
directories = append(directories, "documents")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := api.createBackup(c, w, directories)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Backup Error: ", err)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.HTML(http.StatusOK, "page/admin", templateVars)
|
api.renderPage(c, &pages.AdminGeneral{}, allNotifications...)
|
||||||
}
|
|
||||||
|
|
||||||
func (api *API) appGetAdmin(c *gin.Context) {
|
|
||||||
templateVars, _ := api.getBaseTemplateVars("admin", c)
|
|
||||||
c.HTML(http.StatusOK, "page/admin", templateVars)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) appGetAdminLogs(c *gin.Context) {
|
func (api *API) appGetAdminLogs(c *gin.Context) {
|
||||||
@ -534,6 +510,40 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
|
|||||||
c.HTML(http.StatusOK, "page/admin-import-results", templateVars)
|
c.HTML(http.StatusOK, "page/admin-import-results", templateVars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *API) processBackup(c *gin.Context, backupTypes []backupType) {
|
||||||
|
// Vacuum
|
||||||
|
_, err := api.db.DB.ExecContext(c, "VACUUM;")
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to vacuum DB: ", err)
|
||||||
|
appErrorPage(c, http.StatusInternalServerError, "Unable to vacuum database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Headers
|
||||||
|
c.Header("Content-type", "application/octet-stream")
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"AnthoLumeBackup_%s.zip\"", time.Now().Format("20060102150405")))
|
||||||
|
|
||||||
|
// Stream Backup ZIP Archive
|
||||||
|
c.Stream(func(w io.Writer) bool {
|
||||||
|
var directories []string
|
||||||
|
for _, item := range backupTypes {
|
||||||
|
switch item {
|
||||||
|
case backupCovers:
|
||||||
|
directories = append(directories, "covers")
|
||||||
|
case backupDocuments:
|
||||||
|
directories = append(directories, "documents")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := api.createBackup(c, w, directories)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Backup Error: ", err)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Context) {
|
func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Context) {
|
||||||
// Validate Type & Derive Extension on MIME
|
// Validate Type & Derive Extension on MIME
|
||||||
uploadedFile, err := rAdminAction.RestoreFile.Open()
|
uploadedFile, err := rAdminAction.RestoreFile.Open()
|
||||||
@ -790,7 +800,7 @@ func (api *API) createBackup(ctx context.Context, w io.Writer, directories []str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ar.Close()
|
_ = ar.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -12,14 +12,17 @@ func Notifications(notifications []*models.Notification) g.Node {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Class("fixed flex flex-col gap-2 bottom-0 right-0 p-2 sm:p-4 text-white dark:text-black"),
|
h.Class("fixed flex flex-col gap-2 bottom-0 right-0 text-white dark:text-black"),
|
||||||
g.Group(sliceutils.Map(notifications, notificationNode)),
|
g.Group(sliceutils.Map(notifications, notificationNode)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func notificationNode(n *models.Notification) g.Node {
|
func notificationNode(n *models.Notification) g.Node {
|
||||||
return h.Div(
|
return h.Div(
|
||||||
h.Class("bg-gray-600 dark:bg-gray-400 px-4 py-2 rounded-lg shadow-lg w-64 animate-notification"),
|
h.Class("p-2 sm:p-4 animate-notification"),
|
||||||
h.P(g.Text(n.Content)),
|
h.Div(
|
||||||
|
h.Class("bg-gray-600 dark:bg-gray-400 px-4 py-2 rounded-lg shadow-lg w-64"),
|
||||||
|
g.Text(n.Content),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
153
web/pages/admin.go
Normal file
153
web/pages/admin.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package pages
|
||||||
|
|
||||||
|
import (
|
||||||
|
g "maragu.dev/gomponents"
|
||||||
|
h "maragu.dev/gomponents/html"
|
||||||
|
|
||||||
|
"reichard.io/antholume/web/components/ui"
|
||||||
|
"reichard.io/antholume/web/models"
|
||||||
|
"reichard.io/antholume/web/pages/layout"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Page = (*AdminGeneral)(nil)
|
||||||
|
|
||||||
|
type AdminGeneral struct{}
|
||||||
|
|
||||||
|
func (p *AdminGeneral) Generate(ctx models.PageContext) (g.Node, error) {
|
||||||
|
return layout.Layout(
|
||||||
|
ctx.WithRoute(models.AdminGeneralPage),
|
||||||
|
h.Div(
|
||||||
|
h.Class("w-full flex flex-col gap-4 grow"),
|
||||||
|
backupAndRestoreSection(),
|
||||||
|
tasksSection(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupAndRestoreSection() g.Node {
|
||||||
|
return h.Div(
|
||||||
|
h.Class("flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"),
|
||||||
|
h.P(
|
||||||
|
h.Class("text-lg font-semibold mb-2"),
|
||||||
|
g.Text("Backup & Restore"),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex flex-col gap-4"),
|
||||||
|
backupForm(),
|
||||||
|
restoreForm(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupForm() g.Node {
|
||||||
|
return h.Form(
|
||||||
|
h.Class("flex justify-between"),
|
||||||
|
h.Action("./admin"),
|
||||||
|
h.Method("POST"),
|
||||||
|
h.Input(
|
||||||
|
h.Type("text"),
|
||||||
|
h.Name("action"),
|
||||||
|
h.Value("BACKUP"),
|
||||||
|
h.Class("hidden"),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex gap-8"),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex gap-2 items-center"),
|
||||||
|
h.Input(
|
||||||
|
h.Type("checkbox"),
|
||||||
|
h.ID("backup_covers"),
|
||||||
|
h.Name("backup_types"),
|
||||||
|
h.Value("COVERS"),
|
||||||
|
),
|
||||||
|
h.Label(
|
||||||
|
h.For("backup_covers"),
|
||||||
|
g.Text("Covers"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex gap-2 items-center"),
|
||||||
|
h.Input(
|
||||||
|
h.Type("checkbox"),
|
||||||
|
h.ID("backup_documents"),
|
||||||
|
h.Name("backup_types"),
|
||||||
|
h.Value("DOCUMENTS"),
|
||||||
|
),
|
||||||
|
h.Label(
|
||||||
|
h.For("backup_documents"),
|
||||||
|
g.Text("Documents"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("h-10 w-40"),
|
||||||
|
ui.FormButton(g.Text("Backup"), "", ui.ButtonConfig{Variant: ui.ButtonVariantSecondary}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreForm() g.Node {
|
||||||
|
return h.Form(
|
||||||
|
h.Class("flex justify-between"),
|
||||||
|
h.Action("./admin"),
|
||||||
|
h.Method("POST"),
|
||||||
|
g.Attr("enctype", "multipart/form-data"),
|
||||||
|
h.Input(
|
||||||
|
h.Type("text"),
|
||||||
|
h.Name("action"),
|
||||||
|
h.Value("RESTORE"),
|
||||||
|
h.Class("hidden"),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("flex items-center"),
|
||||||
|
h.Input(
|
||||||
|
h.Type("file"),
|
||||||
|
h.Accept(".zip"),
|
||||||
|
h.Name("restore_file"),
|
||||||
|
h.Class("w-full"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("h-10 w-40"),
|
||||||
|
ui.FormButton(g.Text("Restore"), "", ui.ButtonConfig{Variant: ui.ButtonVariantSecondary}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tasksSection() g.Node {
|
||||||
|
return h.Div(
|
||||||
|
h.Class("flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"),
|
||||||
|
h.P(
|
||||||
|
h.Class("text-lg font-semibold mb-4"),
|
||||||
|
g.Text("Tasks"),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("grid grid-cols-[1fr_auto] gap-x-4 gap-y-3 items-center"),
|
||||||
|
g.Group(taskItem("Metadata Matching", "METADATA_MATCH")),
|
||||||
|
g.Group(taskItem("Cache Tables", "CACHE_TABLES")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func taskItem(name, action string) []g.Node {
|
||||||
|
return []g.Node{
|
||||||
|
h.P(
|
||||||
|
h.Class("text-black dark:text-white"),
|
||||||
|
g.Text(name),
|
||||||
|
),
|
||||||
|
h.Form(
|
||||||
|
h.Action("./admin"),
|
||||||
|
h.Method("POST"),
|
||||||
|
h.Input(
|
||||||
|
h.Type("text"),
|
||||||
|
h.Name("action"),
|
||||||
|
h.Value(action),
|
||||||
|
h.Class("hidden"),
|
||||||
|
),
|
||||||
|
h.Div(
|
||||||
|
h.Class("h-10 w-40"),
|
||||||
|
ui.FormButton(g.Text("Run"), "", ui.ButtonConfig{Variant: ui.ButtonVariantSecondary}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,7 @@ func Navigation(ctx models.PageContext) g.Node {
|
|||||||
return h.Div(
|
return h.Div(
|
||||||
g.Attr("class", "flex items-center justify-between w-full h-16"),
|
g.Attr("class", "flex items-center justify-between w-full h-16"),
|
||||||
Sidebar(ctx),
|
Sidebar(ctx),
|
||||||
h.H1(g.Attr("class", "text-xl font-bold px-6 lg:ml-44"), g.Text(ctx.Route.Title())),
|
h.H1(g.Attr("class", "text-xl font-bold whitespace-nowrap px-6 lg:ml-44"), g.Text(ctx.Route.Title())),
|
||||||
Dropdown(ctx.UserInfo.Username),
|
Dropdown(ctx.UserInfo.Username),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user