[add] admin panel, [add] better logging
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Evan Reichard 2024-01-09 21:08:40 -05:00
parent d3d89b36f6
commit c5b181dda4
14 changed files with 588 additions and 99 deletions

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/gin-contrib/multitemplate" "github.com/gin-contrib/multitemplate"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
@ -32,12 +33,15 @@ type API struct {
func NewApi(db *database.DBManager, c *config.Config, assets *embed.FS) *API { func NewApi(db *database.DBManager, c *config.Config, assets *embed.FS) *API {
api := &API{ api := &API{
HTMLPolicy: bluemonday.StrictPolicy(), HTMLPolicy: bluemonday.StrictPolicy(),
Router: gin.Default(), Router: gin.New(),
Config: c, Config: c,
DB: db, DB: db,
Assets: assets, Assets: assets,
} }
// Add Logger
api.Router.Use(apiLogger())
// Assets & Web App Templates // Assets & Web App Templates
assetsDir, _ := fs.Sub(assets, "assets") assetsDir, _ := fs.Sub(assets, "assets")
api.Router.StaticFS("/assets", http.FS(assetsDir)) api.Router.StaticFS("/assets", http.FS(assetsDir))
@ -107,6 +111,9 @@ func (api *API) registerWebAppRoutes() {
api.Router.GET("/logout", api.authWebAppMiddleware, api.appAuthLogout) api.Router.GET("/logout", api.authWebAppMiddleware, api.appAuthLogout)
api.Router.GET("/register", api.appGetRegister) api.Router.GET("/register", api.appGetRegister)
api.Router.GET("/settings", api.authWebAppMiddleware, api.appGetSettings) api.Router.GET("/settings", api.authWebAppMiddleware, api.appGetSettings)
api.Router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs)
api.Router.GET("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdmin)
api.Router.POST("/admin", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminAction)
api.Router.POST("/login", api.appAuthFormLogin) api.Router.POST("/login", api.appAuthFormLogin)
api.Router.POST("/register", api.appAuthFormRegister) api.Router.POST("/register", api.appAuthFormRegister)
@ -228,6 +235,23 @@ func (api *API) generateTemplates() *multitemplate.Renderer {
return &render return &render
} }
func apiLogger() gin.HandlerFunc {
return func(c *gin.Context) {
// Start Timer
startTime := time.Now()
// Process Request
c.Next()
// End Timer
endTime := time.Now()
latency := endTime.Sub(startTime).Round(time.Microsecond)
// Log Result
log.Infof("[HTTPRouter] %-15s (%10s) %d %7s %s", c.ClientIP(), latency, c.Writer.Status(), c.Request.Method, c.Request.URL.Path)
}
}
func generateToken(n int) ([]byte, error) { func generateToken(n int) ([]byte, error) {
b := make([]byte, n) b := make([]byte, n)
_, err := rand.Read(b) _, err := rand.Read(b)

View File

@ -1,14 +1,17 @@
package api package api
import ( import (
"archive/zip"
"crypto/md5" "crypto/md5"
"database/sql" "database/sql"
"fmt" "fmt"
"io" "io"
"io/fs"
"math" "math"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
@ -24,6 +27,29 @@ import (
"reichard.io/bbank/utils" "reichard.io/bbank/utils"
) )
type AdminAction string
const (
AA_IMPORT AdminAction = "IMPORT"
AA_BACKUP AdminAction = "BACKUP"
AA_RESTORE AdminAction = "RESTORE"
AA_METADATA_MATCH AdminAction = "METADATA_MATCH"
)
type ImportType string
const (
IMPORT_TYPE_DIRECT ImportType = "DIRECT"
IMPORT_TYPE_COPY ImportType = "COPY"
)
type BackupType string
const (
BACKUP_TYPE_COVERS BackupType = "COVERS"
BACKUP_TYPE_DOCUMENTS BackupType = "DOCUMENTS"
)
type queryParams struct { type queryParams struct {
Page *int64 `form:"page"` Page *int64 `form:"page"`
Limit *int64 `form:"limit"` Limit *int64 `form:"limit"`
@ -51,6 +77,20 @@ type requestDocumentEdit struct {
CoverFile *multipart.FileHeader `form:"cover_file"` CoverFile *multipart.FileHeader `form:"cover_file"`
} }
type requestAdminAction struct {
Action AdminAction `form:"action"`
// Import Action
ImportDirectory *string `form:"import_directory"`
ImportType *ImportType `form:"import_type"`
// Backup Action
BackupTypes []BackupType `form:"backup_types"`
// Restore Action
RestoreFile *multipart.FileHeader `form:"restore_file"`
}
type requestDocumentIdentify struct { type requestDocumentIdentify struct {
Title *string `form:"title"` Title *string `form:"title"`
Author *string `form:"author"` Author *string `form:"author"`
@ -93,7 +133,7 @@ func (api *API) appDocumentReader(c *gin.Context) {
func (api *API) appGetDocuments(c *gin.Context) { func (api *API) appGetDocuments(c *gin.Context) {
templateVars := api.getBaseTemplateVars("documents", c) templateVars := api.getBaseTemplateVars("documents", c)
userID := templateVars["User"].(string) auth := templateVars["Authorization"].(authData)
qParams := bindQueryParams(c, 9) qParams := bindQueryParams(c, 9)
var query *string var query *string
@ -103,7 +143,7 @@ func (api *API) appGetDocuments(c *gin.Context) {
} }
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{ documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
UserID: userID, UserID: auth.UserName,
Query: query, Query: query,
Offset: (*qParams.Page - 1) * *qParams.Limit, Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit, Limit: *qParams.Limit,
@ -145,7 +185,7 @@ func (api *API) appGetDocuments(c *gin.Context) {
func (api *API) appGetDocument(c *gin.Context) { func (api *API) appGetDocument(c *gin.Context) {
templateVars := api.getBaseTemplateVars("document", c) templateVars := api.getBaseTemplateVars("document", c)
userID := templateVars["User"].(string) auth := templateVars["Authorization"].(authData)
var rDocID requestDocumentID var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil { if err := c.ShouldBindUri(&rDocID); err != nil {
@ -155,7 +195,7 @@ func (api *API) appGetDocument(c *gin.Context) {
} }
document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{ document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
UserID: userID, UserID: auth.UserName,
DocumentID: rDocID.DocumentID, DocumentID: rDocID.DocumentID,
}) })
if err != nil { if err != nil {
@ -172,11 +212,12 @@ func (api *API) appGetDocument(c *gin.Context) {
func (api *API) appGetProgress(c *gin.Context) { func (api *API) appGetProgress(c *gin.Context) {
templateVars := api.getBaseTemplateVars("progress", c) templateVars := api.getBaseTemplateVars("progress", c)
userID := templateVars["User"].(string) auth := templateVars["Authorization"].(authData)
qParams := bindQueryParams(c, 15) qParams := bindQueryParams(c, 15)
progressFilter := database.GetProgressParams{ progressFilter := database.GetProgressParams{
UserID: userID, UserID: auth.UserName,
Offset: (*qParams.Page - 1) * *qParams.Limit, Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit, Limit: *qParams.Limit,
} }
@ -200,11 +241,11 @@ func (api *API) appGetProgress(c *gin.Context) {
func (api *API) appGetActivity(c *gin.Context) { func (api *API) appGetActivity(c *gin.Context) {
templateVars := api.getBaseTemplateVars("activity", c) templateVars := api.getBaseTemplateVars("activity", c)
userID := templateVars["User"].(string) auth := templateVars["Authorization"].(authData)
qParams := bindQueryParams(c, 15) qParams := bindQueryParams(c, 15)
activityFilter := database.GetActivityParams{ activityFilter := database.GetActivityParams{
UserID: userID, UserID: auth.UserName,
Offset: (*qParams.Page - 1) * *qParams.Limit, Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit, Limit: *qParams.Limit,
} }
@ -228,17 +269,17 @@ func (api *API) appGetActivity(c *gin.Context) {
func (api *API) appGetHome(c *gin.Context) { func (api *API) appGetHome(c *gin.Context) {
templateVars := api.getBaseTemplateVars("home", c) templateVars := api.getBaseTemplateVars("home", c)
userID := templateVars["User"].(string) auth := templateVars["Authorization"].(authData)
start := time.Now() start := time.Now()
graphData, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, userID) graphData, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, auth.UserName)
log.Info("GetDailyReadStats Performance: ", time.Since(start)) log.Debug("[appGetHome] GetDailyReadStats Performance: ", time.Since(start))
start = time.Now() start = time.Now()
databaseInfo, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, userID) databaseInfo, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, auth.UserName)
log.Info("GetDatabaseInfo Performance: ", time.Since(start)) log.Debug("[appGetHome] GetDatabaseInfo Performance: ", time.Since(start))
streaks, _ := api.DB.Queries.GetUserStreaks(api.DB.Ctx, userID) streaks, _ := api.DB.Queries.GetUserStreaks(api.DB.Ctx, auth.UserName)
WPMLeaderboard, _ := api.DB.Queries.GetWPMLeaderboard(api.DB.Ctx) WPMLeaderboard, _ := api.DB.Queries.GetWPMLeaderboard(api.DB.Ctx)
templateVars["Data"] = gin.H{ templateVars["Data"] = gin.H{
@ -253,16 +294,16 @@ func (api *API) appGetHome(c *gin.Context) {
func (api *API) appGetSettings(c *gin.Context) { func (api *API) appGetSettings(c *gin.Context) {
templateVars := api.getBaseTemplateVars("settings", c) templateVars := api.getBaseTemplateVars("settings", c)
userID := templateVars["User"].(string) auth := templateVars["Authorization"].(authData)
user, err := api.DB.Queries.GetUser(api.DB.Ctx, userID) user, err := api.DB.Queries.GetUser(api.DB.Ctx, auth.UserName)
if err != nil { if err != nil {
log.Error("[appGetSettings] GetUser DB Error:", err) log.Error("[appGetSettings] GetUser DB Error:", err)
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err)) errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
return return
} }
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, userID) devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, auth.UserName)
if err != nil { if err != nil {
log.Error("[appGetSettings] GetDevices DB Error:", err) log.Error("[appGetSettings] GetDevices DB Error:", err)
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err)) errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
@ -277,6 +318,126 @@ func (api *API) appGetSettings(c *gin.Context) {
c.HTML(http.StatusOK, "page/settings", templateVars) c.HTML(http.StatusOK, "page/settings", templateVars)
} }
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) {
// Open Log File
logPath := path.Join(api.Config.ConfigPath, "logs/antholume.log")
logFile, err := os.Open(logPath)
if err != nil {
errorPage(c, http.StatusBadRequest, "Missing AnthoLume log file.")
return
}
defer logFile.Close()
// Write Log File
c.Stream(func(w io.Writer) bool {
_, err = io.Copy(w, logFile)
if err != nil {
return true
}
return false
})
}
func (api *API) appPerformAdminAction(c *gin.Context) {
templateVars := api.getBaseTemplateVars("admin", c)
var rAdminAction requestAdminAction
if err := c.ShouldBind(&rAdminAction); err != nil {
log.Error("[appPerformAdminAction] Invalid Form Bind")
errorPage(c, http.StatusBadRequest, "Invalid or missing form values.")
return
}
switch rAdminAction.Action {
case AA_IMPORT:
// TODO
case AA_METADATA_MATCH:
// TODO
// 1. Documents xref most recent metadata table?
// 2. Select all / deselect?
case AA_RESTORE:
// TODO
// 1. Consume backup ZIP
// 2. Move existing to "backup" folder (db, wal, shm, covers, documents)
// 3. Extract backup zip
// 4. Restart server?
case AA_BACKUP:
// Get File Paths
fileName := fmt.Sprintf("%s.db", api.Config.DBName)
dbLocation := path.Join(api.Config.ConfigPath, fileName)
c.Header("Content-type", "application/octet-stream")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"AnthoLumeExport_%s.zip\"", time.Now().Format("20060102")))
// Stream Backup ZIP Archive
c.Stream(func(w io.Writer) bool {
ar := zip.NewWriter(w)
exportWalker := func(currentPath string, f fs.DirEntry, err error) error {
if err != nil {
return err
}
if f.IsDir() {
return nil
}
// Open File on Disk
file, err := os.Open(currentPath)
if err != nil {
return err
}
defer file.Close()
// Derive Export Structure
fileName := filepath.Base(currentPath)
folderName := filepath.Base(filepath.Dir(currentPath))
// Create File in Export
newF, err := ar.Create(path.Join(folderName, fileName))
if err != nil {
return err
}
// Copy File in Export
_, err = io.Copy(newF, file)
if err != nil {
return err
}
return nil
}
// Copy Database File
dbFile, _ := os.Open(dbLocation)
newDbFile, _ := ar.Create(fileName)
io.Copy(newDbFile, dbFile)
// Backup Covers & Documents
for _, item := range rAdminAction.BackupTypes {
if item == BACKUP_TYPE_COVERS {
filepath.WalkDir(path.Join(api.Config.ConfigPath, "covers"), exportWalker)
} else if item == BACKUP_TYPE_DOCUMENTS {
filepath.WalkDir(path.Join(api.Config.ConfigPath, "documents"), exportWalker)
}
}
ar.Close()
return false
})
return
}
c.HTML(http.StatusOK, "page/admin", templateVars)
}
func (api *API) appGetSearch(c *gin.Context) { func (api *API) appGetSearch(c *gin.Context) {
templateVars := api.getBaseTemplateVars("search", c) templateVars := api.getBaseTemplateVars("search", c)
@ -320,7 +481,10 @@ func (api *API) appGetRegister(c *gin.Context) {
} }
func (api *API) appGetDocumentProgress(c *gin.Context) { func (api *API) appGetDocumentProgress(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser") var auth authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
}
var rDoc requestDocumentID var rDoc requestDocumentID
if err := c.ShouldBindUri(&rDoc); err != nil { if err := c.ShouldBindUri(&rDoc); err != nil {
@ -331,7 +495,7 @@ func (api *API) appGetDocumentProgress(c *gin.Context) {
progress, err := api.DB.Queries.GetDocumentProgress(api.DB.Ctx, database.GetDocumentProgressParams{ progress, err := api.DB.Queries.GetDocumentProgress(api.DB.Ctx, database.GetDocumentProgressParams{
DocumentID: rDoc.DocumentID, DocumentID: rDoc.DocumentID,
UserID: rUser.(string), UserID: auth.UserName,
}) })
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
@ -341,7 +505,7 @@ func (api *API) appGetDocumentProgress(c *gin.Context) {
} }
document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{ document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
UserID: rUser.(string), UserID: auth.UserName,
DocumentID: rDoc.DocumentID, DocumentID: rDoc.DocumentID,
}) })
if err != nil { if err != nil {
@ -361,9 +525,12 @@ func (api *API) appGetDocumentProgress(c *gin.Context) {
} }
func (api *API) appGetDevices(c *gin.Context) { func (api *API) appGetDevices(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser") var auth authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
}
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, rUser.(string)) devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, auth.UserName)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
log.Error("[appGetDevices] GetDevices DB Error:", err) log.Error("[appGetDevices] GetDevices DB Error:", err)
@ -677,7 +844,7 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
// Get Template Variables // Get Template Variables
templateVars := api.getBaseTemplateVars("document", c) templateVars := api.getBaseTemplateVars("document", c)
userID := templateVars["User"].(string) auth := templateVars["Authorization"].(authData)
// Get Metadata // Get Metadata
metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{ metadataResults, err := metadata.SearchMetadata(metadata.GBOOK, metadata.MetadataInfo{
@ -710,7 +877,7 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
} }
document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{ document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
UserID: userID, UserID: auth.UserName,
DocumentID: rDocID.DocumentID, DocumentID: rDocID.DocumentID,
}) })
if err != nil { if err != nil {
@ -896,17 +1063,19 @@ func (api *API) appEditSettings(c *gin.Context) {
} }
templateVars := api.getBaseTemplateVars("settings", c) templateVars := api.getBaseTemplateVars("settings", c)
userID := templateVars["User"].(string) auth := templateVars["Authorization"].(authData)
newUserSettings := database.UpdateUserParams{ newUserSettings := database.UpdateUserParams{
UserID: userID, UserID: auth.UserName,
} }
// Set New Password // Set New Password
if rUserSettings.Password != nil && rUserSettings.NewPassword != nil { if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password))) password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
authorized := api.authorizeCredentials(userID, password) data := api.authorizeCredentials(auth.UserName, password)
if authorized == true { if data == nil {
templateVars["PasswordErrorMessage"] = "Invalid Password"
} else {
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.NewPassword))) password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.NewPassword)))
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams) hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
if err != nil { if err != nil {
@ -915,8 +1084,6 @@ func (api *API) appEditSettings(c *gin.Context) {
templateVars["PasswordMessage"] = "Password Updated" templateVars["PasswordMessage"] = "Password Updated"
newUserSettings.Password = &hashedPassword newUserSettings.Password = &hashedPassword
} }
} else {
templateVars["PasswordErrorMessage"] = "Invalid Password"
} }
} }
@ -935,7 +1102,7 @@ func (api *API) appEditSettings(c *gin.Context) {
} }
// Get User // Get User
user, err := api.DB.Queries.GetUser(api.DB.Ctx, userID) user, err := api.DB.Queries.GetUser(api.DB.Ctx, auth.UserName)
if err != nil { if err != nil {
log.Error("[appEditSettings] GetUser DB Error:", err) log.Error("[appEditSettings] GetUser DB Error:", err)
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err)) errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
@ -943,7 +1110,7 @@ func (api *API) appEditSettings(c *gin.Context) {
} }
// Get Devices // Get Devices
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, userID) devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, auth.UserName)
if err != nil { if err != nil {
log.Error("[appEditSettings] GetDevices DB Error:", err) log.Error("[appEditSettings] GetDevices DB Error:", err)
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err)) errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
@ -1002,13 +1169,13 @@ func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStats
} }
func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) gin.H { func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) gin.H {
var userID string var auth authData
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil { if data, _ := c.Get("Authorization"); data != nil {
userID = rUser.(string) auth = data.(authData)
} }
return gin.H{ return gin.H{
"User": userID, "Authorization": auth,
"RouteName": routeName, "RouteName": routeName,
"Config": gin.H{ "Config": gin.H{
"Version": api.Config.Version, "Version": api.Config.Version,

View File

@ -14,6 +14,12 @@ import (
"reichard.io/bbank/database" "reichard.io/bbank/database"
) )
// Authorization Data
type authData struct {
UserName string
IsAdmin bool
}
// KOSync API Auth Headers // KOSync API Auth Headers
type authKOHeader struct { type authKOHeader struct {
AuthUser string `header:"x-auth-user"` AuthUser string `header:"x-auth-user"`
@ -25,25 +31,28 @@ type authOPDSHeader struct {
Authorization string `header:"authorization"` Authorization string `header:"authorization"`
} }
func (api *API) authorizeCredentials(username string, password string) (authorized bool) { func (api *API) authorizeCredentials(username string, password string) (auth *authData) {
user, err := api.DB.Queries.GetUser(api.DB.Ctx, username) user, err := api.DB.Queries.GetUser(api.DB.Ctx, username)
if err != nil { if err != nil {
return false return
} }
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || match != true { if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || match != true {
return false return
} }
return true return &authData{
UserName: user.ID,
IsAdmin: user.Admin,
}
} }
func (api *API) authKOMiddleware(c *gin.Context) { func (api *API) authKOMiddleware(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
// Check Session First // Check Session First
if user, ok := getSession(session); ok == true { if auth, ok := getSession(session); ok == true {
c.Set("AuthorizedUser", user) c.Set("Authorization", auth)
c.Header("Cache-Control", "private") c.Header("Cache-Control", "private")
c.Next() c.Next()
return return
@ -61,17 +70,18 @@ func (api *API) authKOMiddleware(c *gin.Context) {
return return
} }
if authorized := api.authorizeCredentials(rHeader.AuthUser, rHeader.AuthKey); authorized != true { authData := api.authorizeCredentials(rHeader.AuthUser, rHeader.AuthKey)
if authData == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return return
} }
if err := setSession(session, rHeader.AuthUser); err != nil { if err := setSession(session, *authData); err != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return return
} }
c.Set("AuthorizedUser", rHeader.AuthUser) c.Set("Authorization", *authData)
c.Header("Cache-Control", "private") c.Header("Cache-Control", "private")
c.Next() c.Next()
} }
@ -89,12 +99,13 @@ func (api *API) authOPDSMiddleware(c *gin.Context) {
// Validate Auth // Validate Auth
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword))) password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
if authorized := api.authorizeCredentials(user, password); authorized != true { authData := api.authorizeCredentials(user, password)
if authData == nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return return
} }
c.Set("AuthorizedUser", user) c.Set("Authorization", *authData)
c.Header("Cache-Control", "private") c.Header("Cache-Control", "private")
c.Next() c.Next()
} }
@ -103,8 +114,8 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
// Check Session // Check Session
if user, ok := getSession(session); ok == true { if auth, ok := getSession(session); ok == true {
c.Set("AuthorizedUser", user) c.Set("Authorization", auth)
c.Header("Cache-Control", "private") c.Header("Cache-Control", "private")
c.Next() c.Next()
return return
@ -115,6 +126,20 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
return return
} }
func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
if data, _ := c.Get("Authorization"); data != nil {
auth := data.(authData)
if auth.IsAdmin == true {
c.Next()
return
}
}
errorPage(c, http.StatusUnauthorized, "Admin Permissions Required")
c.Abort()
return
}
func (api *API) appAuthFormLogin(c *gin.Context) { func (api *API) appAuthFormLogin(c *gin.Context) {
templateVars := api.getBaseTemplateVars("login", c) templateVars := api.getBaseTemplateVars("login", c)
@ -129,7 +154,8 @@ func (api *API) appAuthFormLogin(c *gin.Context) {
// MD5 - KOSync Compatiblity // MD5 - KOSync Compatiblity
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword))) password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
if authorized := api.authorizeCredentials(username, password); authorized != true { authData := api.authorizeCredentials(username, password)
if authData == nil {
templateVars["Error"] = "Invalid Credentials" templateVars["Error"] = "Invalid Credentials"
c.HTML(http.StatusUnauthorized, "page/login", templateVars) c.HTML(http.StatusUnauthorized, "page/login", templateVars)
return return
@ -137,7 +163,7 @@ func (api *API) appAuthFormLogin(c *gin.Context) {
// Set Session // Set Session
session := sessions.Default(c) session := sessions.Default(c)
if err := setSession(session, username); err != nil { if err := setSession(session, *authData); err != nil {
templateVars["Error"] = "Invalid Credentials" templateVars["Error"] = "Invalid Credentials"
c.HTML(http.StatusUnauthorized, "page/login", templateVars) c.HTML(http.StatusUnauthorized, "page/login", templateVars)
return return
@ -194,9 +220,22 @@ func (api *API) appAuthFormRegister(c *gin.Context) {
return return
} }
// Get User
user, err := api.DB.Queries.GetUser(api.DB.Ctx, username)
if err != nil {
log.Error("[appAuthFormRegister] GetUser DB Error:", err)
templateVars["Error"] = "Registration Disabled or User Already Exists"
c.HTML(http.StatusBadRequest, "page/login", templateVars)
return
}
// Set Session // Set Session
auth := authData{
UserName: user.ID,
IsAdmin: user.Admin,
}
session := sessions.Default(c) session := sessions.Default(c)
if err := setSession(session, username); err != nil { if err := setSession(session, auth); err != nil {
errorPage(c, http.StatusUnauthorized, "Unauthorized.") errorPage(c, http.StatusUnauthorized, "Unauthorized.")
return return
} }
@ -212,26 +251,35 @@ func (api *API) appAuthLogout(c *gin.Context) {
c.Redirect(http.StatusFound, "/login") c.Redirect(http.StatusFound, "/login")
} }
func getSession(session sessions.Session) (user string, ok bool) { func getSession(session sessions.Session) (auth authData, ok bool) {
// Check Session // Check Session
authorizedUser := session.Get("authorizedUser") authorizedUser := session.Get("authorizedUser")
if authorizedUser == nil { isAdmin := session.Get("isAdmin")
return "", false expiresAt := session.Get("expiresAt")
if authorizedUser == nil || isAdmin == nil || expiresAt == nil {
return
}
// Create Auth Object
auth = authData{
UserName: authorizedUser.(string),
IsAdmin: isAdmin.(bool),
} }
// Refresh // Refresh
expiresAt := session.Get("expiresAt") if expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
if expiresAt != nil && expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
log.Info("[getSession] Refreshing Session") log.Info("[getSession] Refreshing Session")
setSession(session, authorizedUser.(string)) setSession(session, auth)
} }
return authorizedUser.(string), true // Authorized
return auth, true
} }
func setSession(session sessions.Session, user string) error { func setSession(session sessions.Session, auth authData) error {
// Set Session Cookie // Set Session Cookie
session.Set("authorizedUser", user) session.Set("authorizedUser", auth.UserName)
session.Set("isAdmin", auth.IsAdmin)
session.Set("expiresAt", time.Now().Unix()+(60*60*24*7)) session.Set("expiresAt", time.Now().Unix()+(60*60*24*7))
return session.Save() return session.Save()
} }

View File

@ -129,7 +129,10 @@ func (api *API) koCreateUser(c *gin.Context) {
} }
func (api *API) koSetProgress(c *gin.Context) { func (api *API) koSetProgress(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser") var auth authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
}
var rPosition requestPosition var rPosition requestPosition
if err := c.ShouldBindJSON(&rPosition); err != nil { if err := c.ShouldBindJSON(&rPosition); err != nil {
@ -141,7 +144,7 @@ func (api *API) koSetProgress(c *gin.Context) {
// Upsert Device // Upsert Device
if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{ if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
ID: rPosition.DeviceID, ID: rPosition.DeviceID,
UserID: rUser.(string), UserID: auth.UserName,
DeviceName: rPosition.Device, DeviceName: rPosition.Device,
LastSynced: time.Now().UTC().Format(time.RFC3339), LastSynced: time.Now().UTC().Format(time.RFC3339),
}); err != nil { }); err != nil {
@ -160,7 +163,7 @@ func (api *API) koSetProgress(c *gin.Context) {
Percentage: rPosition.Percentage, Percentage: rPosition.Percentage,
DocumentID: rPosition.DocumentID, DocumentID: rPosition.DocumentID,
DeviceID: rPosition.DeviceID, DeviceID: rPosition.DeviceID,
UserID: rUser.(string), UserID: auth.UserName,
Progress: rPosition.Progress, Progress: rPosition.Progress,
}) })
if err != nil { if err != nil {
@ -171,7 +174,7 @@ func (api *API) koSetProgress(c *gin.Context) {
// Update Statistic // Update Statistic
log.Info("[koSetProgress] UpdateDocumentUserStatistic Running...") log.Info("[koSetProgress] UpdateDocumentUserStatistic Running...")
if err := api.DB.UpdateDocumentUserStatistic(rPosition.DocumentID, rUser.(string)); err != nil { if err := api.DB.UpdateDocumentUserStatistic(rPosition.DocumentID, auth.UserName); err != nil {
log.Error("[koSetProgress] UpdateDocumentUserStatistic Error:", err) log.Error("[koSetProgress] UpdateDocumentUserStatistic Error:", err)
} }
log.Info("[koSetProgress] UpdateDocumentUserStatistic Complete") log.Info("[koSetProgress] UpdateDocumentUserStatistic Complete")
@ -183,7 +186,10 @@ func (api *API) koSetProgress(c *gin.Context) {
} }
func (api *API) koGetProgress(c *gin.Context) { func (api *API) koGetProgress(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser") var auth authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
}
var rDocID requestDocumentID var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil { if err := c.ShouldBindUri(&rDocID); err != nil {
@ -194,7 +200,7 @@ func (api *API) koGetProgress(c *gin.Context) {
progress, err := api.DB.Queries.GetDocumentProgress(api.DB.Ctx, database.GetDocumentProgressParams{ progress, err := api.DB.Queries.GetDocumentProgress(api.DB.Ctx, database.GetDocumentProgressParams{
DocumentID: rDocID.DocumentID, DocumentID: rDocID.DocumentID,
UserID: rUser.(string), UserID: auth.UserName,
}) })
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -217,7 +223,10 @@ func (api *API) koGetProgress(c *gin.Context) {
} }
func (api *API) koAddActivities(c *gin.Context) { func (api *API) koAddActivities(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser") var auth authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
}
var rActivity requestActivity var rActivity requestActivity
if err := c.ShouldBindJSON(&rActivity); err != nil { if err := c.ShouldBindJSON(&rActivity); err != nil {
@ -259,7 +268,7 @@ func (api *API) koAddActivities(c *gin.Context) {
// Upsert Device // Upsert Device
if _, err = qtx.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{ if _, err = qtx.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
ID: rActivity.DeviceID, ID: rActivity.DeviceID,
UserID: rUser.(string), UserID: auth.UserName,
DeviceName: rActivity.Device, DeviceName: rActivity.Device,
LastSynced: time.Now().UTC().Format(time.RFC3339), LastSynced: time.Now().UTC().Format(time.RFC3339),
}); err != nil { }); err != nil {
@ -271,7 +280,7 @@ func (api *API) koAddActivities(c *gin.Context) {
// Add All Activity // Add All Activity
for _, item := range rActivity.Activity { for _, item := range rActivity.Activity {
if _, err := qtx.AddActivity(api.DB.Ctx, database.AddActivityParams{ if _, err := qtx.AddActivity(api.DB.Ctx, database.AddActivityParams{
UserID: rUser.(string), UserID: auth.UserName,
DocumentID: item.DocumentID, DocumentID: item.DocumentID,
DeviceID: rActivity.DeviceID, DeviceID: rActivity.DeviceID,
StartTime: time.Unix(int64(item.StartTime), 0).UTC().Format(time.RFC3339), StartTime: time.Unix(int64(item.StartTime), 0).UTC().Format(time.RFC3339),
@ -295,7 +304,7 @@ func (api *API) koAddActivities(c *gin.Context) {
// Update Statistic // Update Statistic
for _, doc := range allDocuments { for _, doc := range allDocuments {
log.Info("[koAddActivities] UpdateDocumentUserStatistic Running...") log.Info("[koAddActivities] UpdateDocumentUserStatistic Running...")
if err := api.DB.UpdateDocumentUserStatistic(doc, rUser.(string)); err != nil { if err := api.DB.UpdateDocumentUserStatistic(doc, auth.UserName); err != nil {
log.Error("[koAddActivities] UpdateDocumentUserStatistic Error:", err) log.Error("[koAddActivities] UpdateDocumentUserStatistic Error:", err)
} }
log.Info("[koAddActivities] UpdateDocumentUserStatistic Complete") log.Info("[koAddActivities] UpdateDocumentUserStatistic Complete")
@ -307,7 +316,10 @@ func (api *API) koAddActivities(c *gin.Context) {
} }
func (api *API) koCheckActivitySync(c *gin.Context) { func (api *API) koCheckActivitySync(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser") var auth authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
}
var rCheckActivity requestCheckActivitySync var rCheckActivity requestCheckActivitySync
if err := c.ShouldBindJSON(&rCheckActivity); err != nil { if err := c.ShouldBindJSON(&rCheckActivity); err != nil {
@ -319,7 +331,7 @@ func (api *API) koCheckActivitySync(c *gin.Context) {
// Upsert Device // Upsert Device
if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{ if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
ID: rCheckActivity.DeviceID, ID: rCheckActivity.DeviceID,
UserID: rUser.(string), UserID: auth.UserName,
DeviceName: rCheckActivity.Device, DeviceName: rCheckActivity.Device,
LastSynced: time.Now().UTC().Format(time.RFC3339), LastSynced: time.Now().UTC().Format(time.RFC3339),
}); err != nil { }); err != nil {
@ -330,7 +342,7 @@ func (api *API) koCheckActivitySync(c *gin.Context) {
// Get Last Device Activity // Get Last Device Activity
lastActivity, err := api.DB.Queries.GetLastActivity(api.DB.Ctx, database.GetLastActivityParams{ lastActivity, err := api.DB.Queries.GetLastActivity(api.DB.Ctx, database.GetLastActivityParams{
UserID: rUser.(string), UserID: auth.UserName,
DeviceID: rCheckActivity.DeviceID, DeviceID: rCheckActivity.DeviceID,
}) })
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -405,7 +417,10 @@ func (api *API) koAddDocuments(c *gin.Context) {
} }
func (api *API) koCheckDocumentsSync(c *gin.Context) { func (api *API) koCheckDocumentsSync(c *gin.Context) {
rUser, _ := c.Get("AuthorizedUser") var auth authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
}
var rCheckDocs requestCheckDocumentSync var rCheckDocs requestCheckDocumentSync
if err := c.ShouldBindJSON(&rCheckDocs); err != nil { if err := c.ShouldBindJSON(&rCheckDocs); err != nil {
@ -417,7 +432,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
// Upsert Device // Upsert Device
_, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{ _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
ID: rCheckDocs.DeviceID, ID: rCheckDocs.DeviceID,
UserID: rUser.(string), UserID: auth.UserName,
DeviceName: rCheckDocs.Device, DeviceName: rCheckDocs.Device,
LastSynced: time.Now().UTC().Format(time.RFC3339), LastSynced: time.Now().UTC().Format(time.RFC3339),
}) })

View File

@ -61,9 +61,9 @@ func (api *API) opdsEntry(c *gin.Context) {
} }
func (api *API) opdsDocuments(c *gin.Context) { func (api *API) opdsDocuments(c *gin.Context) {
var userID string var auth authData
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil { if data, _ := c.Get("Authorization"); data != nil {
userID = rUser.(string) auth = data.(authData)
} }
// Potential URL Parameters (Default Pagination - 100) // Potential URL Parameters (Default Pagination - 100)
@ -78,7 +78,7 @@ func (api *API) opdsDocuments(c *gin.Context) {
// Get Documents // Get Documents
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{ documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
UserID: userID, UserID: auth.UserName,
Query: query, Query: query,
Offset: (*qParams.Page - 1) * *qParams.Limit, Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit, Limit: *qParams.Limit,

File diff suppressed because one or more lines are too long

View File

@ -2,9 +2,11 @@ package config
import ( import (
"os" "os"
"path"
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/snowzach/rotatefilehook"
) )
type Config struct { type Config struct {
@ -32,6 +34,15 @@ type Config struct {
CookieHTTPOnly bool CookieHTTPOnly bool
} }
type UTCFormatter struct {
log.Formatter
}
func (u UTCFormatter) Format(e *log.Entry) ([]byte, error) {
e.Time = e.Time.UTC()
return u.Formatter.Format(e)
}
func Load() *Config { func Load() *Config {
c := &Config{ c := &Config{
Version: "0.0.1", Version: "0.0.1",
@ -50,11 +61,31 @@ func Load() *Config {
} }
// Log Level // Log Level
ll, err := log.ParseLevel(c.LogLevel) logLevel, err := log.ParseLevel(c.LogLevel)
if err != nil { if err != nil {
ll = log.InfoLevel logLevel = log.InfoLevel
} }
log.SetLevel(ll)
// Log Formatter
ttyLogFormatter := &UTCFormatter{&log.TextFormatter{FullTimestamp: true}}
fileLogFormatter := &UTCFormatter{&log.TextFormatter{FullTimestamp: true, DisableColors: true}}
// Log Rotater
rotateFileHook, err := rotatefilehook.NewRotateFileHook(rotatefilehook.RotateFileConfig{
Filename: path.Join(c.ConfigPath, "logs/antholume.log"),
MaxSize: 50,
MaxBackups: 3,
MaxAge: 30,
Level: logLevel,
Formatter: fileLogFormatter,
})
if err != nil {
log.Fatal("[config.Load] Unable to initialize file rotate hook")
}
log.SetLevel(logLevel)
log.SetFormatter(ttyLogFormatter)
log.AddHook(rotateFileHook)
return c return c
} }

2
go.mod
View File

@ -47,6 +47,7 @@ require (
github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
@ -57,6 +58,7 @@ require (
golang.org/x/text v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.13.0 // indirect golang.org/x/tools v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect

4
go.sum
View File

@ -133,6 +133,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d h1:4660u5vJtsyrn3QwJNfESwCws+TM1CMhRn123xjVyQ8=
github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d/go.mod h1:ZLVe3VfhAuMYLYWliGEydMBoRnfib8EFSqkBYu1ck9E=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -236,6 +238,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

11
main.go
View File

@ -15,18 +15,7 @@ import (
//go:embed templates/* assets/* //go:embed templates/* assets/*
var assets embed.FS var assets embed.FS
type UTCFormatter struct {
log.Formatter
}
func (u UTCFormatter) Format(e *log.Entry) ([]byte, error) {
e.Time = e.Time.UTC()
return u.Formatter.Format(e)
}
func main() { func main() {
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
app := &cli.App{ app := &cli.App{
Name: "AnthoLume", Name: "AnthoLume",
Usage: "A self hosted e-book progress tracker.", Usage: "A self hosted e-book progress tracker.",

View File

@ -231,6 +231,19 @@
aria-orientation="vertical" aria-orientation="vertical"
aria-labelledby="options-menu" aria-labelledby="options-menu"
> >
{{ if .Authorization.IsAdmin }}
<a
href="/admin"
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
role="menuitem"
>
<span class="flex flex-col">
<span>Administration</span>
</span>
</a>
{{ end }}
<a <a
href="/settings" href="/settings"
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600" class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
@ -265,7 +278,7 @@
<div <div
class="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer" class="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer"
> >
<span>{{ .User }}</span> <span>{{ .Authorization.UserName }}</span>
<span class="text-gray-800 dark:text-gray-200">{{ template "svg/dropdown" (dict "Size" 20) }}</span> <span class="text-gray-800 dark:text-gray-200">{{ template "svg/dropdown" (dict "Size" 20) }}</span>
</div> </div>
</label> </label>

183
templates/pages/admin.html Normal file
View File

@ -0,0 +1,183 @@
{{template "base" .}} {{define "title"}}Administration{{end}} {{define
"header"}}
<a href="./admin">Administration</a>
{{end}} {{define "content"}}
<div class="w-full flex flex-col md:flex-row gap-4">
<div>
<div
class="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
{{ template "svg/user" (dict "Size" 60) }}
<p class="text-lg">{{ .Authorization.UserName }}</p>
</div>
</div>
<div class="flex flex-col gap-4 grow">
<div
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
<p class="text-lg font-semibold mb-2">Import Documents</p>
<form class="flex gap-4 flex-col" action="./admin" method="POST">
<input type="text" name="action" value="IMPORT" class="hidden" />
<div class="flex gap-4">
<div class="flex grow relative">
<span
class="inline-flex items-center px-3 border-t bg-white border-l border-b border-gray-300 text-gray-500 shadow-sm text-sm"
>
{{ template "svg/import" (dict "Size" 15) }}
</span>
<input
type="text"
name="import_directory"
class="flex-1 appearance-none rounded-none border border-gray-300 w-full py-2 px-4 bg-white text-gray-700 placeholder-gray-400 shadow-sm text-base focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-transparent"
placeholder="Directory"
/>
</div>
<div class="flex flex-col mr-4">
<div class="inline-flex gap-2">
<input
checked
type="radio"
id="copy"
name="import_type"
value="COPY"
/>
<label for="copy"> Copy</label>
</div>
<div class="inline-flex gap-2">
<input
type="radio"
id="direct"
name="import_type"
value="DIRECT"
/>
<label for="direct"> Direct</label>
</div>
</div>
</div>
<button
type="submit"
class="px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
>
<span class="w-full">Import Directory</span>
</button>
</form>
{{ if .PasswordErrorMessage }}
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
{{ else if .PasswordMessage }}
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
{{ end }}
</div>
<div
class="flex flex-col gap-2 grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
<p class="text-lg font-semibold mb-2">Backup & Restore</p>
<div class="flex flex-col gap-4">
<form class="flex justify-between" action="./admin" method="POST">
<input type="text" name="action" value="BACKUP" class="hidden" />
<div class="flex gap-8 items-center">
<div>
<input
type="checkbox"
id="backup_covers"
name="backup_types"
value="COVERS"
/>
<label for="backup_covers"> Covers</label>
</div>
<div>
<input
type="checkbox"
id="backup_documents"
name="backup_types"
value="DOCUMENTS"
/>
<label for="backup_documents"> Documents</label>
</div>
</div>
<button
type="submit"
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
>
<span class="w-full">Backup</span>
</button>
</form>
<form
method="POST"
enctype="multipart/form-data"
action="./admin"
class="flex justify-between grow"
>
<input type="text" name="action" value="RESTORE" class="hidden" />
<div class="flex items-center w-1/2">
<input
type="file"
accept=".zip"
name="restore_file"
class="w-full"
/>
</div>
<button
type="submit"
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
>
<span class="w-full">Restore</span>
</button>
</form>
</div>
{{ if .PasswordErrorMessage }}
<span class="text-red-400 text-xs">{{ .PasswordErrorMessage }}</span>
{{ else if .PasswordMessage }}
<span class="text-green-400 text-xs">{{ .PasswordMessage }}</span>
{{ end }}
</div>
<div
class="flex flex-col grow p-4 rounded shadow-lg bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
>
<p class="text-lg font-semibold">Tasks</p>
<table class="min-w-full bg-white dark:bg-gray-700 text-sm">
<tbody class="text-black dark:text-white">
<tr>
<td class="pl-0">
<p>Metadata Matching</p>
</td>
<td class="py-2 float-right">
<form action="./admin" method="POST">
<input
type="text"
name="action"
value="METADATA_MATCH"
class="hidden"
/>
<button
type="submit"
class="w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
>
<span class="w-full">Run</span>
</button>
</form>
</td>
</tr>
<tr>
<td>
<p>Logs</p>
</td>
<td class="py-2 float-right">
<a
href="./admin/logs"
target="_blank"
class="inline-block w-40 px-10 py-2 text-base font-semibold text-center text-white transition duration-200 ease-in bg-black shadow-md hover:text-black hover:bg-white focus:outline-none focus:ring-2"
>
<span class="w-full">View</span>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{{end}}

View File

@ -7,7 +7,7 @@
class="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 dark:text-white" class="flex flex-col p-4 items-center rounded shadow-lg md:w-60 lg:w-80 bg-white dark:bg-gray-700 text-gray-500 dark:text-white"
> >
{{ template "svg/user" (dict "Size" 60) }} {{ template "svg/user" (dict "Size" 60) }}
<p class="text-lg">{{ .User }}</p> <p class="text-lg">{{ .Authorization.UserName }}</p>
</div> </div>
</div> </div>

13
templates/svgs/import.svg Normal file
View File

@ -0,0 +1,13 @@
<svg
width="{{ or .Size 24 }}"
height="{{ or .Size 24 }}"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.06935 5.00839C2 5.37595 2 5.81722 2 6.69975V13.75C2 17.5212 2 19.4069 3.17157 20.5784C4.34315 21.75 6.22876 21.75 10 21.75H14C17.7712 21.75 19.6569 21.75 20.8284 20.5784C22 19.4069 22 17.5212 22 13.75V11.5479C22 8.91554 22 7.59935 21.2305 6.74383C21.1598 6.66514 21.0849 6.59024 21.0062 6.51946C20.1506 5.75 18.8345 5.75 16.2021 5.75H15.8284C14.6747 5.75 14.0979 5.75 13.5604 5.59678C13.2651 5.5126 12.9804 5.39471 12.7121 5.24543C12.2237 4.97367 11.8158 4.56578 11 3.75L10.4497 3.19975C10.1763 2.92633 10.0396 2.78961 9.89594 2.67051C9.27652 2.15704 8.51665 1.84229 7.71557 1.76738C7.52976 1.75 7.33642 1.75 6.94975 1.75C6.06722 1.75 5.62595 1.75 5.25839 1.81935C3.64031 2.12464 2.37464 3.39031 2.06935 5.00839ZM12 11C12.4142 11 12.75 11.3358 12.75 11.75V13H14C14.4142 13 14.75 13.3358 14.75 13.75C14.75 14.1642 14.4142 14.5 14 14.5H12.75V15.75C12.75 16.1642 12.4142 16.5 12 16.5C11.5858 16.5 11.25 16.1642 11.25 15.75V14.5H10C9.58579 14.5 9.25 14.1642 9.25 13.75C9.25 13.3358 9.58579 13 10 13H11.25V11.75C11.25 11.3358 11.5858 11 12 11Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB