feat(admin): adding user & importing
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,14 +13,19 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
argon2 "github.com/alexedwards/argon2id"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/itchyny/gojq"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/metadata"
|
||||
"reichard.io/antholume/utils"
|
||||
)
|
||||
|
||||
type adminAction string
|
||||
@@ -59,12 +65,13 @@ type operationType string
|
||||
const (
|
||||
opUpdate operationType = "UPDATE"
|
||||
opCreate operationType = "CREATE"
|
||||
opDelete operationType = "DELETE"
|
||||
)
|
||||
|
||||
type requestAdminUpdateUser struct {
|
||||
User string `form:"user"`
|
||||
Password string `form:"password"`
|
||||
isAdmin bool `form:"is_admin"`
|
||||
Password *string `form:"password"`
|
||||
isAdmin *bool `form:"is_admin"`
|
||||
Operation operationType `form:"operation"`
|
||||
}
|
||||
|
||||
@@ -72,6 +79,22 @@ type requestAdminLogs struct {
|
||||
Filter string `form:"filter"`
|
||||
}
|
||||
|
||||
type importStatus string
|
||||
|
||||
const (
|
||||
importFailed importStatus = "FAILED"
|
||||
importSuccess importStatus = "SUCCESS"
|
||||
importExists importStatus = "EXISTS"
|
||||
)
|
||||
|
||||
type importResult struct {
|
||||
ID string
|
||||
Name string
|
||||
Path string
|
||||
Status importStatus
|
||||
Error error
|
||||
}
|
||||
|
||||
func (api *API) appPerformAdminAction(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("admin", c)
|
||||
|
||||
@@ -82,15 +105,18 @@ func (api *API) appPerformAdminAction(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - Messages
|
||||
switch rAdminAction.Action {
|
||||
case adminMetadataMatch:
|
||||
// TODO
|
||||
// 1. Documents xref most recent metadata table?
|
||||
// 2. Select all / deselect?
|
||||
case adminCacheTables:
|
||||
go api.db.CacheTempTables()
|
||||
// TODO - Message
|
||||
go func() {
|
||||
err := api.db.CacheTempTables()
|
||||
if err != nil {
|
||||
log.Error("Unable to cache temp tables: ", err)
|
||||
}
|
||||
}()
|
||||
case adminRestore:
|
||||
api.processRestoreFile(rAdminAction, c)
|
||||
return
|
||||
@@ -252,17 +278,18 @@ func (api *API) appUpdateAdminUsers(c *gin.Context) {
|
||||
var err error
|
||||
switch rAdminUserUpdate.Operation {
|
||||
case opCreate:
|
||||
err = api.createUser(rAdminUserUpdate.User, rAdminUserUpdate.Password)
|
||||
err = api.createUser(rAdminUserUpdate)
|
||||
case opUpdate:
|
||||
err = fmt.Errorf("unimplemented")
|
||||
err = api.updateUser(rAdminUserUpdate)
|
||||
case opDelete:
|
||||
err = api.deleteUser(rAdminUserUpdate)
|
||||
default:
|
||||
appErrorPage(c, http.StatusNotFound, "Unknown user operation")
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unable to create user: %v", err))
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unable to create or update user: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -338,46 +365,157 @@ func (api *API) appPerformAdminImport(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - Store results for approval?
|
||||
|
||||
// Walk import directory & copy or import files
|
||||
// Get import directory
|
||||
importDirectory := filepath.Clean(rAdminImport.Directory)
|
||||
_ = filepath.WalkDir(importDirectory, func(currentPath string, f fs.DirEntry, err error) error {
|
||||
|
||||
// Get data directory
|
||||
absoluteDataPath, _ := filepath.Abs(filepath.Join(api.cfg.DataPath, "documents"))
|
||||
|
||||
// Validate different path
|
||||
if absoluteDataPath == importDirectory {
|
||||
appErrorPage(c, http.StatusBadRequest, "Directory is the same as data path")
|
||||
return
|
||||
}
|
||||
|
||||
// Do Transaction
|
||||
tx, err := api.db.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("Transaction Begin DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Unknown error")
|
||||
return
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Track imports
|
||||
importResults := make([]importResult, 0)
|
||||
|
||||
// Walk Directory & Import
|
||||
err = filepath.WalkDir(importDirectory, func(importPath string, f fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get metadata
|
||||
fileMeta, err := metadata.GetMetadata(currentPath)
|
||||
// Get relative path
|
||||
basePath := importDirectory
|
||||
relFilePath, err := filepath.Rel(importDirectory, importPath)
|
||||
if err != nil {
|
||||
fmt.Printf("metadata error: %v\n", err)
|
||||
log.Warnf("path error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only needed if copying
|
||||
newName := deriveBaseFileName(fileMeta)
|
||||
// Track imports
|
||||
iResult := importResult{
|
||||
Path: relFilePath,
|
||||
Status: importFailed,
|
||||
}
|
||||
defer func() {
|
||||
importResults = append(importResults, iResult)
|
||||
}()
|
||||
|
||||
// Open File on Disk
|
||||
// file, err := os.Open(currentPath)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer file.Close()
|
||||
// Get metadata
|
||||
fileMeta, err := metadata.GetMetadata(importPath)
|
||||
if err != nil {
|
||||
log.Errorf("metadata error: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
iResult.ID = *fileMeta.PartialMD5
|
||||
iResult.Name = fmt.Sprintf("%s - %s", *fileMeta.Author, *fileMeta.Title)
|
||||
|
||||
// TODO - BasePath in DB
|
||||
// TODO - Copy / Import
|
||||
// Check already exists
|
||||
_, err = qtx.GetDocument(api.db.Ctx, *fileMeta.PartialMD5)
|
||||
if err == nil {
|
||||
log.Warnf("document already exists: %s", *fileMeta.PartialMD5)
|
||||
iResult.Status = importExists
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("New File Metadata: %s\n", newName)
|
||||
// Import Copy
|
||||
if rAdminImport.Type == importCopy {
|
||||
// Derive & Sanitize File Name
|
||||
relFilePath = deriveBaseFileName(fileMeta)
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", relFilePath)
|
||||
|
||||
// Open Source File
|
||||
srcFile, err := os.Open(importPath)
|
||||
if err != nil {
|
||||
log.Errorf("unable to open current file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Open Destination File
|
||||
destFile, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Errorf("unable to open destination file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Copy File
|
||||
if _, err = io.Copy(destFile, srcFile); err != nil {
|
||||
log.Errorf("unable to save file: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update Base & Path
|
||||
basePath = filepath.Join(api.cfg.DataPath, "documents")
|
||||
iResult.Path = relFilePath
|
||||
}
|
||||
|
||||
// Upsert document
|
||||
if _, err = qtx.UpsertDocument(api.db.Ctx, database.UpsertDocumentParams{
|
||||
ID: *fileMeta.PartialMD5,
|
||||
Title: fileMeta.Title,
|
||||
Author: fileMeta.Author,
|
||||
Description: fileMeta.Description,
|
||||
Md5: fileMeta.MD5,
|
||||
Words: fileMeta.WordCount,
|
||||
Filepath: &relFilePath,
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Errorf("UpsertDocument DB Error: %v", err)
|
||||
iResult.Error = err
|
||||
return nil
|
||||
}
|
||||
|
||||
iResult.Status = importSuccess
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Import Failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["CurrentPath"] = filepath.Clean(rAdminImport.Directory)
|
||||
// Commit transaction
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.Error("Transaction Commit DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Import DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "page/admin-import", templateVars)
|
||||
// Sort import results
|
||||
sort.Slice(importResults, func(i int, j int) bool {
|
||||
return importStatusPriority(importResults[i].Status) <
|
||||
importStatusPriority(importResults[j].Status)
|
||||
})
|
||||
|
||||
templateVars["Data"] = importResults
|
||||
c.HTML(http.StatusOK, "page/admin-import-results", templateVars)
|
||||
}
|
||||
|
||||
func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Context) {
|
||||
@@ -521,7 +659,6 @@ func (api *API) processRestoreFile(rAdminAction requestAdminAction, c *gin.Conte
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
// Restore all data
|
||||
func (api *API) restoreData(zipReader *zip.Reader) error {
|
||||
// Ensure Directories
|
||||
api.cfg.EnsureDirectories()
|
||||
@@ -552,7 +689,6 @@ func (api *API) restoreData(zipReader *zip.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove all data
|
||||
func (api *API) removeData() error {
|
||||
allPaths := []string{
|
||||
"covers",
|
||||
@@ -575,7 +711,6 @@ func (api *API) removeData() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Backup all data
|
||||
func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||
ar := zip.NewWriter(w)
|
||||
|
||||
@@ -628,7 +763,11 @@ func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
io.Copy(newDbFile, dbFile)
|
||||
|
||||
_, err = io.Copy(newDbFile, dbFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Backup Covers & Documents
|
||||
for _, dir := range directories {
|
||||
@@ -641,3 +780,118 @@ func (api *API) createBackup(w io.Writer, directories []string) error {
|
||||
ar.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) createUser(createRequest requestAdminUpdateUser) error {
|
||||
// Validate Necessary Parameters
|
||||
if createRequest.User == "" {
|
||||
return fmt.Errorf("username can't be empty")
|
||||
}
|
||||
if createRequest.Password == nil || *createRequest.Password == "" {
|
||||
return fmt.Errorf("password can't be empty")
|
||||
}
|
||||
|
||||
// Base Params
|
||||
createParams := database.CreateUserParams{
|
||||
ID: createRequest.User,
|
||||
}
|
||||
|
||||
// Handle Admin (Explicit or False)
|
||||
if createRequest.isAdmin != nil {
|
||||
createParams.Admin = *createRequest.isAdmin
|
||||
} else {
|
||||
createParams.Admin = false
|
||||
}
|
||||
|
||||
// Parse Password
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*createRequest.Password)))
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create hashed password")
|
||||
}
|
||||
createParams.Pass = &hashedPassword
|
||||
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create token for user")
|
||||
}
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
createParams.AuthHash = &authHash
|
||||
|
||||
// Create user in DB
|
||||
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, createParams); err != nil {
|
||||
log.Error("CreateUser DB Error:", err)
|
||||
return fmt.Errorf("unable to create user")
|
||||
} else if rows == 0 {
|
||||
log.Warn("User Already Exists:", createParams.ID)
|
||||
return fmt.Errorf("user already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) updateUser(updateRequest requestAdminUpdateUser) error {
|
||||
// Validate Necessary Parameters
|
||||
if updateRequest.User == "" {
|
||||
return fmt.Errorf("username can't be empty")
|
||||
}
|
||||
if updateRequest.Password == nil && updateRequest.isAdmin == nil {
|
||||
return fmt.Errorf("nothing to update")
|
||||
}
|
||||
|
||||
// Base Params
|
||||
updateParams := database.UpdateUserParams{
|
||||
UserID: updateRequest.User,
|
||||
}
|
||||
|
||||
// Handle Admin (Update or Existing)
|
||||
if updateRequest.isAdmin != nil {
|
||||
updateParams.Admin = *updateRequest.isAdmin
|
||||
} else {
|
||||
user, err := api.db.Queries.GetUser(api.db.Ctx, updateRequest.User)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("GetUser DB Error: %v", err))
|
||||
}
|
||||
updateParams.Admin = user.Admin
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - Validate Not Last Admin
|
||||
|
||||
// Handle Password
|
||||
if updateRequest.Password != nil {
|
||||
if *updateRequest.Password == "" {
|
||||
return fmt.Errorf("password can't be empty")
|
||||
}
|
||||
|
||||
// Parse Password
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(*updateRequest.Password)))
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create hashed password")
|
||||
}
|
||||
updateParams.Password = &hashedPassword
|
||||
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create token for user")
|
||||
}
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
updateParams.AuthHash = &authHash
|
||||
}
|
||||
|
||||
// Update User
|
||||
_, err := api.db.Queries.UpdateUser(api.db.Ctx, updateParams)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("UpdateUser DB Error: %v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) deleteUser(updateRequest requestAdminUpdateUser) error {
|
||||
// TODO:
|
||||
// - Validate Not Last Admin
|
||||
return errors.New("unimplemented")
|
||||
}
|
||||
|
||||
@@ -314,7 +314,11 @@ func (api *API) appGetSearch(c *gin.Context) {
|
||||
templateVars, _ := api.getBaseTemplateVars("search", c)
|
||||
|
||||
var sParams searchParams
|
||||
c.BindQuery(&sParams)
|
||||
err := c.BindQuery(&sParams)
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Invalid Form Bind: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Only Handle Query
|
||||
if sParams.Query != nil && sParams.Source != nil {
|
||||
@@ -369,10 +373,9 @@ func (api *API) appGetDocumentProgress(c *gin.Context) {
|
||||
DocumentID: rDoc.DocumentID,
|
||||
UserID: auth.UserName,
|
||||
})
|
||||
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
log.Error("UpsertDocument DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
log.Error("GetDocumentProgress DB Error: ", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentProgress DB Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -461,7 +464,8 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
||||
|
||||
// Derive & Sanitize File Name
|
||||
fileName := deriveBaseFileName(metadataInfo)
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
safePath := filepath.Join(basePath, fileName)
|
||||
|
||||
// Open Destination File
|
||||
destFile, err := os.Create(safePath)
|
||||
@@ -488,9 +492,7 @@ func (api *API) appUploadNewDocument(c *gin.Context) {
|
||||
Md5: metadataInfo.MD5,
|
||||
Words: metadataInfo.WordCount,
|
||||
Filepath: &fileName,
|
||||
|
||||
// TODO (BasePath):
|
||||
// - Should be current config directory
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Errorf("UpsertDocument DB Error: %v", err)
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpsertDocument DB Error: %v", err))
|
||||
@@ -595,7 +597,6 @@ func (api *API) appEditDocument(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "./")
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) appDeleteDocument(c *gin.Context) {
|
||||
@@ -764,6 +765,11 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
||||
|
||||
// Derive Extension on MIME
|
||||
fileMime, err := mimetype.DetectFile(tempFilePath)
|
||||
if err != nil {
|
||||
log.Warn("MIME Detect Error: ", err)
|
||||
sendDownloadMessage("Unable to download file", gin.H{"Error": true})
|
||||
return
|
||||
}
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
// Derive Filename
|
||||
@@ -797,7 +803,9 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
||||
defer sourceFile.Close()
|
||||
|
||||
// Generate Storage Path & Open File
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
safePath := filepath.Join(basePath, fileName)
|
||||
|
||||
destFile, err := os.Create(safePath)
|
||||
if err != nil {
|
||||
log.Error("Dest File Error: ", err)
|
||||
@@ -844,8 +852,9 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
|
||||
Title: rDocAdd.Title,
|
||||
Author: rDocAdd.Author,
|
||||
Md5: fileHash,
|
||||
Filepath: &fileName,
|
||||
Words: wordCount,
|
||||
Filepath: &fileName,
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Error("UpsertDocument DB Error: ", err)
|
||||
sendDownloadMessage("Unable to save to database", gin.H{"Error": true})
|
||||
@@ -951,7 +960,11 @@ func (api *API) getDocumentsWordCount(documents []database.GetDocumentsWithStats
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
for _, item := range documents {
|
||||
@@ -1000,7 +1013,11 @@ func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) (gin.H, au
|
||||
|
||||
func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams {
|
||||
var qParams queryParams
|
||||
c.BindQuery(&qParams)
|
||||
err := c.BindQuery(&qParams)
|
||||
if err != nil {
|
||||
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Invalid Form Bind: %v", err))
|
||||
return qParams
|
||||
}
|
||||
|
||||
if qParams.Limit == nil {
|
||||
qParams.Limit = &defaultLimit
|
||||
|
||||
93
api/auth.go
93
api/auth.go
@@ -28,18 +28,13 @@ type authKOHeader struct {
|
||||
AuthKey string `header:"x-auth-key"`
|
||||
}
|
||||
|
||||
// OPDS Auth Headers
|
||||
type authOPDSHeader struct {
|
||||
Authorization string `header:"authorization"`
|
||||
}
|
||||
|
||||
func (api *API) authorizeCredentials(username string, password string) (auth *authData) {
|
||||
user, err := api.db.Queries.GetUser(api.db.Ctx, username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || match != true {
|
||||
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || !match {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,7 +52,7 @@ func (api *API) authKOMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Check Session First
|
||||
if auth, ok := api.getSession(session); ok == true {
|
||||
if auth, ok := api.getSession(session); ok {
|
||||
c.Set("Authorization", auth)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
@@ -98,7 +93,7 @@ func (api *API) authOPDSMiddleware(c *gin.Context) {
|
||||
user, rawPassword, hasAuth := c.Request.BasicAuth()
|
||||
|
||||
// Validate Auth Fields
|
||||
if hasAuth != true || user == "" || rawPassword == "" {
|
||||
if !hasAuth || user == "" || rawPassword == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
|
||||
return
|
||||
}
|
||||
@@ -120,7 +115,7 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Check Session
|
||||
if auth, ok := api.getSession(session); ok == true {
|
||||
if auth, ok := api.getSession(session); ok {
|
||||
c.Set("Authorization", auth)
|
||||
c.Header("Cache-Control", "private")
|
||||
c.Next()
|
||||
@@ -129,13 +124,12 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
|
||||
if data, _ := c.Get("Authorization"); data != nil {
|
||||
auth := data.(authData)
|
||||
if auth.IsAdmin == true {
|
||||
if auth.IsAdmin {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
@@ -143,7 +137,6 @@ func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
|
||||
|
||||
appErrorPage(c, http.StatusUnauthorized, "Admin Permissions Required")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
func (api *API) appAuthLogin(c *gin.Context) {
|
||||
@@ -276,7 +269,10 @@ func (api *API) appAuthRegister(c *gin.Context) {
|
||||
func (api *API) appAuthLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Save()
|
||||
if err := session.Save(); err != nil {
|
||||
log.Error("unable to save session")
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
@@ -377,7 +373,10 @@ func (api *API) getSession(session sessions.Session) (auth authData, ok bool) {
|
||||
// Refresh
|
||||
if expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
|
||||
log.Info("Refreshing Session")
|
||||
api.setSession(session, auth)
|
||||
if err := api.setSession(session, auth); err != nil {
|
||||
log.Error("unable to get session")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Authorized
|
||||
@@ -422,7 +421,11 @@ func (api *API) rotateAllAuthHashes() error {
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
users, err := qtx.GetUsers(api.db.Ctx)
|
||||
@@ -430,7 +433,8 @@ func (api *API) rotateAllAuthHashes() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update users
|
||||
// Update Users
|
||||
newAuthHashCache := make(map[string]string, 0)
|
||||
for _, user := range users {
|
||||
// Generate Auth Hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
@@ -448,8 +452,8 @@ func (api *API) rotateAllAuthHashes() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update Cache
|
||||
api.userAuthCache[user.ID] = fmt.Sprintf("%x", rawAuthHash)
|
||||
// Save New Hash Cache
|
||||
newAuthHashCache[user.ID] = fmt.Sprintf("%x", rawAuthHash)
|
||||
}
|
||||
|
||||
// Commit Transaction
|
||||
@@ -458,56 +462,9 @@ func (api *API) rotateAllAuthHashes() error {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *API) createUser(username string, rawPassword string) error {
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||
|
||||
if username == "" {
|
||||
return fmt.Errorf("username can't be empty")
|
||||
}
|
||||
|
||||
if rawPassword == "" {
|
||||
return fmt.Errorf("password can't be empty")
|
||||
}
|
||||
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create hashed password")
|
||||
}
|
||||
|
||||
// Generate auth hash
|
||||
rawAuthHash, err := utils.GenerateToken(64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create token for user")
|
||||
}
|
||||
|
||||
// Get current users
|
||||
currentUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get current users")
|
||||
}
|
||||
|
||||
// Determine if we should be admin
|
||||
isAdmin := false
|
||||
if len(currentUsers) == 0 {
|
||||
isAdmin = true
|
||||
}
|
||||
|
||||
// Create user in DB
|
||||
authHash := fmt.Sprintf("%x", rawAuthHash)
|
||||
if rows, err := api.db.Queries.CreateUser(api.db.Ctx, database.CreateUserParams{
|
||||
ID: username,
|
||||
Pass: &hashedPassword,
|
||||
AuthHash: &authHash,
|
||||
Admin: isAdmin,
|
||||
}); err != nil {
|
||||
log.Error("CreateUser DB Error:", err)
|
||||
return fmt.Errorf("unable to create user")
|
||||
} else if rows == 0 {
|
||||
log.Warn("User Already Exists:", username)
|
||||
return fmt.Errorf("user already exists")
|
||||
// Transaction Succeeded -> Update Cache
|
||||
for user, hash := range newAuthHashCache {
|
||||
api.userAuthCache[user] = hash
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,11 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/antholume/database"
|
||||
"reichard.io/antholume/metadata"
|
||||
)
|
||||
@@ -34,8 +35,14 @@ func (api *API) createDownloadDocumentHandler(errorFunc func(*gin.Context, int,
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Basepath
|
||||
basepath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
if document.Basepath != nil && *document.Basepath != "" {
|
||||
basepath = *document.Basepath
|
||||
}
|
||||
|
||||
// Derive Storage Location
|
||||
filePath := filepath.Join(api.cfg.DataPath, "documents", *document.Filepath)
|
||||
filePath := filepath.Join(basepath, *document.Filepath)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(filePath)
|
||||
|
||||
@@ -193,7 +193,11 @@ func (api *API) koAddActivities(c *gin.Context) {
|
||||
allDocuments := getKeys(allDocumentsMap)
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Upsert Documents
|
||||
@@ -316,7 +320,11 @@ func (api *API) koAddDocuments(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
defer func() {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
log.Error("DB Rollback Error:", err)
|
||||
}
|
||||
}()
|
||||
qtx := api.db.Queries.WithTx(tx)
|
||||
|
||||
// Upsert Documents
|
||||
@@ -375,11 +383,8 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
missingDocs := []database.Document{}
|
||||
deletedDocIDs := []string{}
|
||||
|
||||
// Get Missing Documents
|
||||
missingDocs, err = api.db.Queries.GetMissingDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
missingDocs, err := api.db.Queries.GetMissingDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("GetMissingDocuments DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
@@ -387,7 +392,7 @@ func (api *API) koCheckDocumentsSync(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Get Deleted Documents
|
||||
deletedDocIDs, err = api.db.Queries.GetDeletedDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
deletedDocIDs, err := api.db.Queries.GetDeletedDocuments(api.db.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("GetDeletedDocuments DB Error", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Invalid Request")
|
||||
@@ -494,7 +499,8 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
||||
})
|
||||
|
||||
// Generate Storage Path
|
||||
safePath := filepath.Join(api.cfg.DataPath, "documents", fileName)
|
||||
basePath := filepath.Join(api.cfg.DataPath, "documents")
|
||||
safePath := filepath.Join(basePath, fileName)
|
||||
|
||||
// Save & Prevent Overwrites
|
||||
_, err = os.Stat(safePath)
|
||||
@@ -521,6 +527,7 @@ func (api *API) koUploadExistingDocument(c *gin.Context) {
|
||||
Md5: metadataInfo.MD5,
|
||||
Words: metadataInfo.WordCount,
|
||||
Filepath: &fileName,
|
||||
Basepath: &basePath,
|
||||
}); err != nil {
|
||||
log.Error("UpsertDocument DB Error:", err)
|
||||
apiErrorPage(c, http.StatusBadRequest, "Document Error")
|
||||
|
||||
11
api/utils.go
11
api/utils.go
@@ -155,3 +155,14 @@ func deriveBaseFileName(metadataInfo *metadata.MetadataInfo) string {
|
||||
fileName := strings.ReplaceAll(newFileName, "/", "")
|
||||
return "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, *metadataInfo.PartialMD5, metadataInfo.Type))
|
||||
}
|
||||
|
||||
func importStatusPriority(status importStatus) int {
|
||||
switch status {
|
||||
case importFailed:
|
||||
return 1
|
||||
case importExists:
|
||||
return 2
|
||||
default:
|
||||
return 3
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user