[add] admin panel, [add] better logging
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
d3d89b36f6
commit
c5b181dda4
26
api/api.go
26
api/api.go
@ -9,6 +9,7 @@ import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/multitemplate"
|
||||
"github.com/gin-contrib/sessions"
|
||||
@ -32,12 +33,15 @@ type API struct {
|
||||
func NewApi(db *database.DBManager, c *config.Config, assets *embed.FS) *API {
|
||||
api := &API{
|
||||
HTMLPolicy: bluemonday.StrictPolicy(),
|
||||
Router: gin.Default(),
|
||||
Router: gin.New(),
|
||||
Config: c,
|
||||
DB: db,
|
||||
Assets: assets,
|
||||
}
|
||||
|
||||
// Add Logger
|
||||
api.Router.Use(apiLogger())
|
||||
|
||||
// Assets & Web App Templates
|
||||
assetsDir, _ := fs.Sub(assets, "assets")
|
||||
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("/register", api.appGetRegister)
|
||||
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("/register", api.appAuthFormRegister)
|
||||
|
||||
@ -228,6 +235,23 @@ func (api *API) generateTemplates() *multitemplate.Renderer {
|
||||
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) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
|
@ -1,14 +1,17 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@ -24,6 +27,29 @@ import (
|
||||
"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 {
|
||||
Page *int64 `form:"page"`
|
||||
Limit *int64 `form:"limit"`
|
||||
@ -51,6 +77,20 @@ type requestDocumentEdit struct {
|
||||
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 {
|
||||
Title *string `form:"title"`
|
||||
Author *string `form:"author"`
|
||||
@ -93,7 +133,7 @@ func (api *API) appDocumentReader(c *gin.Context) {
|
||||
|
||||
func (api *API) appGetDocuments(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("documents", c)
|
||||
userID := templateVars["User"].(string)
|
||||
auth := templateVars["Authorization"].(authData)
|
||||
qParams := bindQueryParams(c, 9)
|
||||
|
||||
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{
|
||||
UserID: userID,
|
||||
UserID: auth.UserName,
|
||||
Query: query,
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
@ -145,7 +185,7 @@ func (api *API) appGetDocuments(c *gin.Context) {
|
||||
|
||||
func (api *API) appGetDocument(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("document", c)
|
||||
userID := templateVars["User"].(string)
|
||||
auth := templateVars["Authorization"].(authData)
|
||||
|
||||
var rDocID requestDocumentID
|
||||
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{
|
||||
UserID: userID,
|
||||
UserID: auth.UserName,
|
||||
DocumentID: rDocID.DocumentID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -172,11 +212,12 @@ func (api *API) appGetDocument(c *gin.Context) {
|
||||
|
||||
func (api *API) appGetProgress(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("progress", c)
|
||||
userID := templateVars["User"].(string)
|
||||
auth := templateVars["Authorization"].(authData)
|
||||
|
||||
qParams := bindQueryParams(c, 15)
|
||||
|
||||
progressFilter := database.GetProgressParams{
|
||||
UserID: userID,
|
||||
UserID: auth.UserName,
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
}
|
||||
@ -200,11 +241,11 @@ func (api *API) appGetProgress(c *gin.Context) {
|
||||
|
||||
func (api *API) appGetActivity(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("activity", c)
|
||||
userID := templateVars["User"].(string)
|
||||
auth := templateVars["Authorization"].(authData)
|
||||
qParams := bindQueryParams(c, 15)
|
||||
|
||||
activityFilter := database.GetActivityParams{
|
||||
UserID: userID,
|
||||
UserID: auth.UserName,
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
}
|
||||
@ -228,17 +269,17 @@ func (api *API) appGetActivity(c *gin.Context) {
|
||||
|
||||
func (api *API) appGetHome(c *gin.Context) {
|
||||
templateVars := api.getBaseTemplateVars("home", c)
|
||||
userID := templateVars["User"].(string)
|
||||
auth := templateVars["Authorization"].(authData)
|
||||
|
||||
start := time.Now()
|
||||
graphData, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, userID)
|
||||
log.Info("GetDailyReadStats Performance: ", time.Since(start))
|
||||
graphData, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, auth.UserName)
|
||||
log.Debug("[appGetHome] GetDailyReadStats Performance: ", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
databaseInfo, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, userID)
|
||||
log.Info("GetDatabaseInfo Performance: ", time.Since(start))
|
||||
databaseInfo, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, auth.UserName)
|
||||
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)
|
||||
|
||||
templateVars["Data"] = gin.H{
|
||||
@ -253,16 +294,16 @@ func (api *API) appGetHome(c *gin.Context) {
|
||||
|
||||
func (api *API) appGetSettings(c *gin.Context) {
|
||||
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 {
|
||||
log.Error("[appGetSettings] GetUser DB Error:", err)
|
||||
errorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||
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 {
|
||||
log.Error("[appGetSettings] GetDevices DB Error:", 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
templateVars := api.getBaseTemplateVars("search", c)
|
||||
|
||||
@ -320,7 +481,10 @@ func (api *API) appGetRegister(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
|
||||
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{
|
||||
DocumentID: rDoc.DocumentID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
})
|
||||
|
||||
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{
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DocumentID: rDoc.DocumentID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -361,9 +525,12 @@ func (api *API) appGetDocumentProgress(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 {
|
||||
log.Error("[appGetDevices] GetDevices DB Error:", err)
|
||||
@ -677,7 +844,7 @@ func (api *API) appIdentifyDocument(c *gin.Context) {
|
||||
|
||||
// Get Template Variables
|
||||
templateVars := api.getBaseTemplateVars("document", c)
|
||||
userID := templateVars["User"].(string)
|
||||
auth := templateVars["Authorization"].(authData)
|
||||
|
||||
// Get Metadata
|
||||
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{
|
||||
UserID: userID,
|
||||
UserID: auth.UserName,
|
||||
DocumentID: rDocID.DocumentID,
|
||||
})
|
||||
if err != nil {
|
||||
@ -896,17 +1063,19 @@ func (api *API) appEditSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
templateVars := api.getBaseTemplateVars("settings", c)
|
||||
userID := templateVars["User"].(string)
|
||||
auth := templateVars["Authorization"].(authData)
|
||||
|
||||
newUserSettings := database.UpdateUserParams{
|
||||
UserID: userID,
|
||||
UserID: auth.UserName,
|
||||
}
|
||||
|
||||
// Set New Password
|
||||
if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
|
||||
authorized := api.authorizeCredentials(userID, password)
|
||||
if authorized == true {
|
||||
data := api.authorizeCredentials(auth.UserName, password)
|
||||
if data == nil {
|
||||
templateVars["PasswordErrorMessage"] = "Invalid Password"
|
||||
} else {
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.NewPassword)))
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
@ -915,8 +1084,6 @@ func (api *API) appEditSettings(c *gin.Context) {
|
||||
templateVars["PasswordMessage"] = "Password Updated"
|
||||
newUserSettings.Password = &hashedPassword
|
||||
}
|
||||
} else {
|
||||
templateVars["PasswordErrorMessage"] = "Invalid Password"
|
||||
}
|
||||
}
|
||||
|
||||
@ -935,7 +1102,7 @@ func (api *API) appEditSettings(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 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 {
|
||||
log.Error("[appEditSettings] GetUser DB Error:", 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
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, userID)
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, auth.UserName)
|
||||
if err != nil {
|
||||
log.Error("[appEditSettings] GetDevices DB Error:", 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 {
|
||||
var userID string
|
||||
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
||||
userID = rUser.(string)
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
"User": userID,
|
||||
"Authorization": auth,
|
||||
"RouteName": routeName,
|
||||
"Config": gin.H{
|
||||
"Version": api.Config.Version,
|
||||
|
98
api/auth.go
98
api/auth.go
@ -14,6 +14,12 @@ import (
|
||||
"reichard.io/bbank/database"
|
||||
)
|
||||
|
||||
// Authorization Data
|
||||
type authData struct {
|
||||
UserName string
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
// KOSync API Auth Headers
|
||||
type authKOHeader struct {
|
||||
AuthUser string `header:"x-auth-user"`
|
||||
@ -25,25 +31,28 @@ type authOPDSHeader struct {
|
||||
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)
|
||||
if err != nil {
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
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) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Check Session First
|
||||
if user, ok := getSession(session); ok == true {
|
||||
c.Set("AuthorizedUser", user)
|
||||
if auth, ok := getSession(session); ok == true {
|
||||
c.Set("Authorization", auth)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
return
|
||||
@ -61,17 +70,18 @@ func (api *API) authKOMiddleware(c *gin.Context) {
|
||||
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"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := setSession(session, rHeader.AuthUser); err != nil {
|
||||
if err := setSession(session, *authData); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("AuthorizedUser", rHeader.AuthUser)
|
||||
c.Set("Authorization", *authData)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
}
|
||||
@ -89,12 +99,13 @@ func (api *API) authOPDSMiddleware(c *gin.Context) {
|
||||
|
||||
// Validate Auth
|
||||
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"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("AuthorizedUser", user)
|
||||
c.Set("Authorization", *authData)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
}
|
||||
@ -103,8 +114,8 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Check Session
|
||||
if user, ok := getSession(session); ok == true {
|
||||
c.Set("AuthorizedUser", user)
|
||||
if auth, ok := getSession(session); ok == true {
|
||||
c.Set("Authorization", auth)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
return
|
||||
@ -115,6 +126,20 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
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) {
|
||||
templateVars := api.getBaseTemplateVars("login", c)
|
||||
|
||||
@ -129,7 +154,8 @@ func (api *API) appAuthFormLogin(c *gin.Context) {
|
||||
|
||||
// MD5 - KOSync Compatiblity
|
||||
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"
|
||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||
return
|
||||
@ -137,7 +163,7 @@ func (api *API) appAuthFormLogin(c *gin.Context) {
|
||||
|
||||
// Set Session
|
||||
session := sessions.Default(c)
|
||||
if err := setSession(session, username); err != nil {
|
||||
if err := setSession(session, *authData); err != nil {
|
||||
templateVars["Error"] = "Invalid Credentials"
|
||||
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
|
||||
return
|
||||
@ -194,9 +220,22 @@ func (api *API) appAuthFormRegister(c *gin.Context) {
|
||||
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
|
||||
auth := authData{
|
||||
UserName: user.ID,
|
||||
IsAdmin: user.Admin,
|
||||
}
|
||||
session := sessions.Default(c)
|
||||
if err := setSession(session, username); err != nil {
|
||||
if err := setSession(session, auth); err != nil {
|
||||
errorPage(c, http.StatusUnauthorized, "Unauthorized.")
|
||||
return
|
||||
}
|
||||
@ -212,26 +251,35 @@ func (api *API) appAuthLogout(c *gin.Context) {
|
||||
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
|
||||
authorizedUser := session.Get("authorizedUser")
|
||||
if authorizedUser == nil {
|
||||
return "", false
|
||||
isAdmin := session.Get("isAdmin")
|
||||
expiresAt := session.Get("expiresAt")
|
||||
if authorizedUser == nil || isAdmin == nil || expiresAt == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Create Auth Object
|
||||
auth = authData{
|
||||
UserName: authorizedUser.(string),
|
||||
IsAdmin: isAdmin.(bool),
|
||||
}
|
||||
|
||||
// Refresh
|
||||
expiresAt := session.Get("expiresAt")
|
||||
if expiresAt != nil && expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
|
||||
if expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
|
||||
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
|
||||
session.Set("authorizedUser", user)
|
||||
session.Set("authorizedUser", auth.UserName)
|
||||
session.Set("isAdmin", auth.IsAdmin)
|
||||
session.Set("expiresAt", time.Now().Unix()+(60*60*24*7))
|
||||
return session.Save()
|
||||
}
|
||||
|
@ -129,7 +129,10 @@ func (api *API) koCreateUser(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
|
||||
if err := c.ShouldBindJSON(&rPosition); err != nil {
|
||||
@ -141,7 +144,7 @@ func (api *API) koSetProgress(c *gin.Context) {
|
||||
// Upsert Device
|
||||
if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
ID: rPosition.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceName: rPosition.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
@ -160,7 +163,7 @@ func (api *API) koSetProgress(c *gin.Context) {
|
||||
Percentage: rPosition.Percentage,
|
||||
DocumentID: rPosition.DocumentID,
|
||||
DeviceID: rPosition.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
Progress: rPosition.Progress,
|
||||
})
|
||||
if err != nil {
|
||||
@ -171,7 +174,7 @@ func (api *API) koSetProgress(c *gin.Context) {
|
||||
|
||||
// Update Statistic
|
||||
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.Info("[koSetProgress] UpdateDocumentUserStatistic Complete")
|
||||
@ -183,7 +186,10 @@ func (api *API) koSetProgress(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
|
||||
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{
|
||||
DocumentID: rDocID.DocumentID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
})
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@ -217,7 +223,10 @@ func (api *API) koGetProgress(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
|
||||
if err := c.ShouldBindJSON(&rActivity); err != nil {
|
||||
@ -259,7 +268,7 @@ func (api *API) koAddActivities(c *gin.Context) {
|
||||
// Upsert Device
|
||||
if _, err = qtx.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
ID: rActivity.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceName: rActivity.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
@ -271,7 +280,7 @@ func (api *API) koAddActivities(c *gin.Context) {
|
||||
// Add All Activity
|
||||
for _, item := range rActivity.Activity {
|
||||
if _, err := qtx.AddActivity(api.DB.Ctx, database.AddActivityParams{
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DocumentID: item.DocumentID,
|
||||
DeviceID: rActivity.DeviceID,
|
||||
StartTime: time.Unix(int64(item.StartTime), 0).UTC().Format(time.RFC3339),
|
||||
@ -295,7 +304,7 @@ func (api *API) koAddActivities(c *gin.Context) {
|
||||
// Update Statistic
|
||||
for _, doc := range allDocuments {
|
||||
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.Info("[koAddActivities] UpdateDocumentUserStatistic Complete")
|
||||
@ -307,7 +316,10 @@ func (api *API) koAddActivities(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
|
||||
if err := c.ShouldBindJSON(&rCheckActivity); err != nil {
|
||||
@ -319,7 +331,7 @@ func (api *API) koCheckActivitySync(c *gin.Context) {
|
||||
// Upsert Device
|
||||
if _, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
ID: rCheckActivity.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceName: rCheckActivity.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
}); err != nil {
|
||||
@ -330,7 +342,7 @@ func (api *API) koCheckActivitySync(c *gin.Context) {
|
||||
|
||||
// Get Last Device Activity
|
||||
lastActivity, err := api.DB.Queries.GetLastActivity(api.DB.Ctx, database.GetLastActivityParams{
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceID: rCheckActivity.DeviceID,
|
||||
})
|
||||
if err == sql.ErrNoRows {
|
||||
@ -405,7 +417,10 @@ func (api *API) koAddDocuments(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
|
||||
if err := c.ShouldBindJSON(&rCheckDocs); err != nil {
|
||||
@ -417,7 +432,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
||||
// Upsert Device
|
||||
_, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
ID: rCheckDocs.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
UserID: auth.UserName,
|
||||
DeviceName: rCheckDocs.Device,
|
||||
LastSynced: time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
|
@ -61,9 +61,9 @@ func (api *API) opdsEntry(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (api *API) opdsDocuments(c *gin.Context) {
|
||||
var userID string
|
||||
if rUser, _ := c.Get("AuthorizedUser"); rUser != nil {
|
||||
userID = rUser.(string)
|
||||
var auth authData
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth = data.(authData)
|
||||
}
|
||||
|
||||
// Potential URL Parameters (Default Pagination - 100)
|
||||
@ -78,7 +78,7 @@ func (api *API) opdsDocuments(c *gin.Context) {
|
||||
|
||||
// Get Documents
|
||||
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
|
||||
UserID: userID,
|
||||
UserID: auth.UserName,
|
||||
Query: query,
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
|
File diff suppressed because one or more lines are too long
@ -2,9 +2,11 @@ package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/snowzach/rotatefilehook"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
@ -32,6 +34,15 @@ type Config struct {
|
||||
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 {
|
||||
c := &Config{
|
||||
Version: "0.0.1",
|
||||
@ -50,11 +61,31 @@ func Load() *Config {
|
||||
}
|
||||
|
||||
// Log Level
|
||||
ll, err := log.ParseLevel(c.LogLevel)
|
||||
logLevel, err := log.ParseLevel(c.LogLevel)
|
||||
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
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@ -47,6 +47,7 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/ugorji/go/codec v1.2.11 // 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/tools v0.13.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
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
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/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
11
main.go
11
main.go
@ -15,18 +15,7 @@ import (
|
||||
//go:embed templates/* assets/*
|
||||
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() {
|
||||
log.SetFormatter(UTCFormatter{&log.TextFormatter{FullTimestamp: true}})
|
||||
|
||||
app := &cli.App{
|
||||
Name: "AnthoLume",
|
||||
Usage: "A self hosted e-book progress tracker.",
|
||||
|
@ -231,6 +231,19 @@
|
||||
aria-orientation="vertical"
|
||||
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
|
||||
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"
|
||||
@ -265,7 +278,7 @@
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
</label>
|
||||
|
183
templates/pages/admin.html
Normal file
183
templates/pages/admin.html
Normal 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}}
|
@ -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"
|
||||
>
|
||||
{{ template "svg/user" (dict "Size" 60) }}
|
||||
<p class="text-lg">{{ .User }}</p>
|
||||
<p class="text-lg">{{ .Authorization.UserName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
13
templates/svgs/import.svg
Normal file
13
templates/svgs/import.svg
Normal 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 |
Loading…
Reference in New Issue
Block a user