This commit is contained in:
2025-08-30 20:52:27 -04:00
parent e7ebccd4a9
commit f53959b38f
31 changed files with 789 additions and 479 deletions

View File

@@ -145,23 +145,23 @@ func (api *API) registerWebAppRoutes(router *gin.Engine) {
router.GET("/reader/progress/:document", api.authWebAppMiddleware, api.appGetDocumentProgress)
// Web App - Templates
router.GET("/", api.authWebAppMiddleware, api.appGetHomeNew) // DONE
router.GET("/activity", api.authWebAppMiddleware, api.appGetActivityNew) // DONE
router.GET("/progress", api.authWebAppMiddleware, api.appGetProgressNew) // DONE
router.GET("/documents", api.authWebAppMiddleware, api.appGetDocumentsNew) // DONE
router.GET("/documents/:document", api.authWebAppMiddleware, api.appGetDocumentNew) // DONE
router.GET("/", api.authWebAppMiddleware, api.appGetHome) // DONE
router.GET("/activity", api.authWebAppMiddleware, api.appGetActivity) // DONE
router.GET("/progress", api.authWebAppMiddleware, api.appGetProgress) // DONE
router.GET("/documents", api.authWebAppMiddleware, api.appGetDocuments) // DONE
router.GET("/documents/:document", api.authWebAppMiddleware, api.appGetDocument) // DONE
// Web App - Other Routes
router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.createGetCoverHandler(appErrorPage)) // DONE
router.GET("/documents/:document/file", api.authWebAppMiddleware, api.createDownloadDocumentHandler(appErrorPage)) // DONE
router.GET("/logout", api.authWebAppMiddleware, api.appAuthLogout)
router.POST("/login", api.appAuthLogin) // DONE
router.POST("/register", api.appAuthRegister) // DONE
router.GET("/logout", api.authWebAppMiddleware, api.appAuthLogout) // DONE
router.POST("/login", api.appAuthLogin) // DONE
router.POST("/register", api.appAuthRegister) // DONE
router.GET("/settings", api.authWebAppMiddleware, api.appGetSettings) // DONE
// TODO
router.GET("/login", api.appGetLogin)
router.GET("/register", api.appGetRegister)
router.GET("/settings", api.authWebAppMiddleware, api.appGetSettings)
router.GET("/admin/logs", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminLogs)
router.GET("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appGetAdminImport)
router.POST("/admin/import", api.authWebAppMiddleware, api.authAdminWebAppMiddleware, api.appPerformAdminImport)
@@ -182,12 +182,13 @@ func (api *API) registerWebAppRoutes(router *gin.Engine) {
router.POST("/documents/:document/delete", api.authWebAppMiddleware, api.appDeleteDocument) // DONE
router.POST("/documents/:document/edit", api.authWebAppMiddleware, api.appEditDocument) // DONE
router.POST("/documents/:document/identify", api.authWebAppMiddleware, api.appIdentifyDocumentNew) // DONE
router.POST("/settings", api.authWebAppMiddleware, api.appEditSettings) // TODO
router.POST("/settings", api.authWebAppMiddleware, api.appEditSettings) // DONE
}
// Search enabled configuration
if api.cfg.SearchEnabled {
router.GET("/search", api.authWebAppMiddleware, api.appGetSearchNew) // WIP
router.GET("/search", api.authWebAppMiddleware, api.appGetSearch) // DONE
router.POST("/search", api.authWebAppMiddleware, api.appSaveNewDocument) // TODO
}
}
@@ -358,13 +359,13 @@ func loggingMiddleware(c *gin.Context) {
}
// Get username
var auth authData
var auth *authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
auth = data.(*authData)
}
// Log user
if auth.UserName != "" {
if auth != nil && auth.UserName != "" {
logData["user"] = auth.UserName
}

View File

@@ -2,6 +2,7 @@ package api
import (
"cmp"
"crypto/md5"
"fmt"
"math"
"net/http"
@@ -9,6 +10,7 @@ import (
"strings"
"time"
argon2 "github.com/alexedwards/argon2id"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"reichard.io/antholume/database"
@@ -18,20 +20,19 @@ import (
"reichard.io/antholume/pkg/sliceutils"
"reichard.io/antholume/pkg/utils"
"reichard.io/antholume/search"
"reichard.io/antholume/web/components/layout"
"reichard.io/antholume/web/components/stats"
"reichard.io/antholume/web/models"
"reichard.io/antholume/web/pages"
)
func (api *API) appGetHomeNew(c *gin.Context) {
func (api *API) appGetHome(c *gin.Context) {
_, auth := api.getBaseTemplateVars("home", c)
start := time.Now()
dailyStats, err := api.db.Queries.GetDailyReadStats(c, auth.UserName)
if err != nil {
log.Error("GetDailyReadStats DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDailyReadStats DB Error: %v", err))
log.WithError(err).Error("failed to get daily read stats")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get daily read stats: %s", err))
return
}
log.Debug("GetDailyReadStats DB Performance: ", time.Since(start))
@@ -39,8 +40,8 @@ func (api *API) appGetHomeNew(c *gin.Context) {
start = time.Now()
databaseInfo, err := api.db.Queries.GetDatabaseInfo(c, auth.UserName)
if err != nil {
log.Error("GetDatabaseInfo DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDatabaseInfo DB Error: %v", err))
log.WithError(err).Error("failed to get database info")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get database info: %s", err))
return
}
log.Debug("GetDatabaseInfo DB Performance: ", time.Since(start))
@@ -48,8 +49,8 @@ func (api *API) appGetHomeNew(c *gin.Context) {
start = time.Now()
streaks, err := api.db.Queries.GetUserStreaks(c, auth.UserName)
if err != nil {
log.Error("GetUserStreaks DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUserStreaks DB Error: %v", err))
log.WithError(err).Error("failed to get user streaks")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get user streaks: %s", err))
return
}
log.Debug("GetUserStreaks DB Performance: ", time.Since(start))
@@ -57,35 +58,27 @@ func (api *API) appGetHomeNew(c *gin.Context) {
start = time.Now()
userStatistics, err := api.db.Queries.GetUserStatistics(c)
if err != nil {
log.Error("GetUserStatistics DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUserStatistics DB Error: %v", err))
log.WithError(err).Error("failed to get user statistics")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get user statistics: %s", err))
return
}
log.Debug("GetUserStatistics DB Performance: ", time.Since(start))
err = layout.Layout(
pages.Home{
Leaderboard: arrangeUserStatisticsNew(userStatistics),
Streaks: streaks,
DailyStats: dailyStats,
RecordInfo: &databaseInfo,
},
layout.LayoutOptions{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
SearchEnabled: api.cfg.SearchEnabled,
Version: api.cfg.Version,
},
).Render(c.Writer)
if err != nil {
log.Error("Render Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unknown Error: %v", err))
}
api.renderPage(c, &pages.Home{
Leaderboard: arrangeUserStatistic(userStatistics),
Streaks: streaks,
DailyStats: dailyStats,
RecordInfo: &databaseInfo,
})
}
func (api *API) appGetDocumentsNew(c *gin.Context) {
_, auth := api.getBaseTemplateVars("documents", c)
qParams := bindQueryParams(c, 9)
func (api *API) appGetDocuments(c *gin.Context) {
qParams, err := bindQueryParams(c, 9)
if err != nil {
log.WithError(err).Error("failed to bind query params")
appErrorPage(c, http.StatusBadRequest, fmt.Sprintf("failed to bind query params: %s", err))
return
}
var query *string
if qParams.Search != nil && *qParams.Search != "" {
@@ -93,6 +86,7 @@ func (api *API) appGetDocumentsNew(c *gin.Context) {
query = &search
}
_, auth := api.getBaseTemplateVars("documents", c)
documents, err := api.db.Queries.GetDocumentsWithStats(c, database.GetDocumentsWithStatsParams{
UserID: auth.UserName,
Query: query,
@@ -101,170 +95,114 @@ func (api *API) appGetDocumentsNew(c *gin.Context) {
Limit: *qParams.Limit,
})
if err != nil {
log.Error("GetDocumentsWithStats DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsWithStats DB Error: %v", err))
log.WithError(err).Error("failed to get documents with stats")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get documents with stats: %s", err))
return
}
length, err := api.db.Queries.GetDocumentsSize(c, query)
if err != nil {
log.Error("GetDocumentsSize DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocumentsSize DB Error: %v", err))
log.WithError(err).Error("failed to get document sizes")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get document sizes: %s", err))
return
}
if err = api.getDocumentsWordCount(c, documents); err != nil {
log.Error("Unable to Get Word Counts: ", err)
log.WithError(err).Error("failed to get word counts")
}
totalPages := int64(math.Ceil(float64(length) / float64(*qParams.Limit)))
nextPage := *qParams.Page + 1
previousPage := *qParams.Page - 1
err = layout.Layout(
pages.Documents{
Data: sliceutils.Map(documents, convertDBDocToUI),
Previous: utils.Ternary(previousPage >= 0, int(previousPage), 0),
Next: utils.Ternary(nextPage <= totalPages, int(nextPage), 0),
Limit: int(ptr.Deref(qParams.Limit)),
},
layout.LayoutOptions{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
SearchEnabled: api.cfg.SearchEnabled,
Version: api.cfg.Version,
},
).Render(c.Writer)
if err != nil {
log.Error("Render Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unknown Error: %v", err))
}
api.renderPage(c, pages.Documents{
Data: sliceutils.Map(documents, convertDBDocToUI),
Previous: utils.Ternary(previousPage >= 0, int(previousPage), 0),
Next: utils.Ternary(nextPage <= totalPages, int(nextPage), 0),
Limit: int(ptr.Deref(qParams.Limit)),
})
}
func (api *API) appGetDocumentNew(c *gin.Context) {
_, auth := api.getBaseTemplateVars("document", c)
func (api *API) appGetDocument(c *gin.Context) {
var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil {
log.Error("Invalid URI Bind")
log.WithError(err).Error("failed to bind URI")
appErrorPage(c, http.StatusNotFound, "Invalid document")
return
}
_, auth := api.getBaseTemplateVars("document", c)
document, err := api.db.GetDocument(c, rDocID.DocumentID, auth.UserName)
if err != nil {
log.Error("GetDocument DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
log.WithError(err).Error("failed to get document")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get document: %s", err))
return
}
err = layout.Layout(
pages.Document{
Data: convertDBDocToUI(*document),
},
layout.LayoutOptions{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
SearchEnabled: api.cfg.SearchEnabled,
Version: api.cfg.Version,
},
).Render(c.Writer)
if err != nil {
log.Error("Render Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unknown Error: %v", err))
}
api.renderPage(c, &pages.Document{Data: convertDBDocToUI(*document)})
}
func (api *API) appGetActivityNew(c *gin.Context) {
func (api *API) appGetActivity(c *gin.Context) {
qParams, err := bindQueryParams(c, 15)
if err != nil {
log.WithError(err).Error("failed to bind query params")
appErrorPage(c, http.StatusBadRequest, fmt.Sprintf("failed to bind query params: %s", err))
return
}
_, auth := api.getBaseTemplateVars("activity", c)
qParams := bindQueryParams(c, 15)
activityFilter := database.GetActivityParams{
UserID: auth.UserName,
Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit,
}
if qParams.Document != nil {
activityFilter.DocFilter = true
activityFilter.DocumentID = *qParams.Document
}
activity, err := api.db.Queries.GetActivity(c, activityFilter)
activity, err := api.db.Queries.GetActivity(c, database.GetActivityParams{
UserID: auth.UserName,
Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit,
DocFilter: qParams.Document != nil,
DocumentID: ptr.Deref(qParams.Document),
})
if err != nil {
log.Error("GetActivity DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
log.WithError(err).Error("failed to get activity")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get activity: %s", err))
return
}
err = layout.Layout(
pages.Activity{
Data: sliceutils.Map(activity, convertDBActivityToUI),
},
layout.LayoutOptions{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
SearchEnabled: api.cfg.SearchEnabled,
Version: api.cfg.Version,
},
).Render(c.Writer)
if err != nil {
log.Error("Render Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unknown Error: %v", err))
}
api.renderPage(c, &pages.Activity{Data: sliceutils.Map(activity, convertDBActivityToUI)})
}
func (api *API) appGetProgressNew(c *gin.Context) {
_, auth := api.getBaseTemplateVars("progress", c)
qParams := bindQueryParams(c, 15)
progressFilter := database.GetProgressParams{
UserID: auth.UserName,
Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit,
}
if qParams.Document != nil {
progressFilter.DocFilter = true
progressFilter.DocumentID = *qParams.Document
}
progress, err := api.db.Queries.GetProgress(c, progressFilter)
func (api *API) appGetProgress(c *gin.Context) {
qParams, err := bindQueryParams(c, 15)
if err != nil {
log.Error("GetProgress DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetActivity DB Error: %v", err))
log.WithError(err).Error("failed to bind query params")
appErrorPage(c, http.StatusBadRequest, fmt.Sprintf("failed to bind query params: %s", err))
return
}
err = layout.Layout(
pages.Progress{
Data: sliceutils.Map(progress, convertDBProgressToUI),
},
layout.LayoutOptions{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
SearchEnabled: api.cfg.SearchEnabled,
Version: api.cfg.Version,
},
).Render(c.Writer)
_, auth := api.getBaseTemplateVars("progress", c)
progress, err := api.db.Queries.GetProgress(c, database.GetProgressParams{
UserID: auth.UserName,
Offset: (*qParams.Page - 1) * *qParams.Limit,
Limit: *qParams.Limit,
DocFilter: qParams.Document != nil,
DocumentID: ptr.Deref(qParams.Document),
})
if err != nil {
log.Error("Render Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unknown Error: %v", err))
log.WithError(err).Error("failed to get progress")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get progress: %s", err))
return
}
api.renderPage(c, &pages.Progress{Data: sliceutils.Map(progress, convertDBProgressToUI)})
}
func (api *API) appIdentifyDocumentNew(c *gin.Context) {
var rDocID requestDocumentID
if err := c.ShouldBindUri(&rDocID); err != nil {
log.Error("Invalid URI Bind")
log.WithError(err).Error("failed to bind URI")
appErrorPage(c, http.StatusNotFound, "Invalid document")
return
}
var rDocIdentify requestDocumentIdentify
if err := c.ShouldBind(&rDocIdentify); err != nil {
log.Error("Invalid Form Bind")
log.WithError(err).Error("failed to bind form")
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
return
}
@@ -282,15 +220,14 @@ func (api *API) appIdentifyDocumentNew(c *gin.Context) {
// Validate Values
if rDocIdentify.ISBN == nil && rDocIdentify.Title == nil && rDocIdentify.Author == nil {
log.Error("Invalid Form")
log.Error("invalid or missing form values")
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
return
}
// Get Template Variables
_, auth := api.getBaseTemplateVars("document", c)
// Get Metadata
var searchResult *models.DocumentMetadata
var allNotifications []*models.Notification
metadataResults, err := metadata.SearchMetadata(metadata.SourceGoogleBooks, metadata.MetadataInfo{
Title: rDocIdentify.Title,
Author: rDocIdentify.Author,
@@ -298,14 +235,12 @@ func (api *API) appIdentifyDocumentNew(c *gin.Context) {
ISBN13: rDocIdentify.ISBN,
})
if err != nil {
log.Error("Search Metadata Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Search Metadata Error: %v", err))
log.WithError(err).Error("failed to search metadata")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to search metadata: %s", err))
return
}
} else if firstResult, found := sliceutils.First(metadataResults); found {
searchResult = convertMetaToUI(firstResult)
var errorMsg *string
firstResult, found := sliceutils.First(metadataResults)
if found {
// Store First Metadata Result
if _, err = api.db.Queries.AddMetadata(c, database.AddMetadataParams{
DocumentID: rDocID.DocumentID,
@@ -313,52 +248,42 @@ func (api *API) appIdentifyDocumentNew(c *gin.Context) {
Author: firstResult.Author,
Description: firstResult.Description,
Gbid: firstResult.SourceID,
Olid: nil,
Isbn10: firstResult.ISBN10,
Isbn13: firstResult.ISBN13,
}); err != nil {
log.Error("AddMetadata DB Error: ", err)
log.WithError(err).Error("failed to add metadata")
}
} else {
errorMsg = ptr.Of("No Metadata Found")
allNotifications = append(allNotifications, &models.Notification{
Type: models.NotificationTypeError,
Content: "No Metadata Found",
})
}
// Get Auth
_, auth := api.getBaseTemplateVars("document", c)
document, err := api.db.GetDocument(c, rDocID.DocumentID, auth.UserName)
if err != nil {
log.Error("GetDocument DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDocument DB Error: %v", err))
log.WithError(err).Error("failed to get document")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get document: %s", err))
return
}
err = layout.Layout(
pages.Document{
Data: convertDBDocToUI(*document),
Search: convertMetaToUI(firstResult, errorMsg),
},
layout.LayoutOptions{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
SearchEnabled: api.cfg.SearchEnabled,
Version: api.cfg.Version,
},
).Render(c.Writer)
if err != nil {
log.Error("Render Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unknown Error: %v", err))
}
api.renderPage(c, &pages.Document{
Data: convertDBDocToUI(*document),
Search: searchResult,
}, allNotifications...)
}
// Tabs:
// - General (Import, Backup & Restore, Version (githash?), Stats?)
// - Users
// - Metadata
func (api *API) appGetSearchNew(c *gin.Context) {
_, auth := api.getBaseTemplateVars("search", c)
func (api *API) appGetSearch(c *gin.Context) {
var sParams searchParams
err := c.BindQuery(&sParams)
if err != nil {
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Invalid Form Bind: %v", err))
if err := c.BindQuery(&sParams); err != nil {
log.WithError(err).Error("failed to bind form")
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
return
}
@@ -368,6 +293,7 @@ func (api *API) appGetSearchNew(c *gin.Context) {
if sParams.Query != nil && sParams.Source != nil {
results, err := search.SearchBook(*sParams.Query, *sParams.Source)
if err != nil {
log.WithError(err).Error("failed to search book")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Search Error: %v", err))
return
}
@@ -376,23 +302,159 @@ func (api *API) appGetSearchNew(c *gin.Context) {
searchError = "Invailid Query"
}
err = layout.Layout(
pages.Search{
Results: searchResults,
Source: ptr.Deref(sParams.Source),
Query: ptr.Deref(sParams.Query),
Error: searchError,
},
layout.LayoutOptions{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
SearchEnabled: api.cfg.SearchEnabled,
Version: api.cfg.Version,
},
).Render(c.Writer)
api.renderPage(c, &pages.Search{
Results: searchResults,
Source: ptr.Deref(sParams.Source),
Query: ptr.Deref(sParams.Query),
Error: searchError,
})
}
func (api *API) appGetSettings(c *gin.Context) {
_, auth := api.getBaseTemplateVars("settings", c)
user, err := api.db.Queries.GetUser(c, auth.UserName)
if err != nil {
log.Error("Render Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Unknown Error: %v", err))
log.WithError(err).Error("failed to get user")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get user: %s", err))
return
}
devices, err := api.db.Queries.GetDevices(c, auth.UserName)
if err != nil {
log.WithError(err).Error("failed to get devices")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get devices: %s", err))
return
}
api.renderPage(c, &pages.Settings{
Timezone: ptr.Deref(user.Timezone),
Devices: sliceutils.Map(devices, convertDBDeviceToUI),
})
}
func (api *API) appEditSettings(c *gin.Context) {
var rUserSettings requestSettingsEdit
if err := c.ShouldBind(&rUserSettings); err != nil {
log.WithError(err).Error("failed to bind form")
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
return
}
// Validate Something Exists
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.Timezone == nil {
log.Error("invalid or missing form values")
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
return
}
_, auth := api.getBaseTemplateVars("settings", c)
newUserSettings := database.UpdateUserParams{
UserID: auth.UserName,
Admin: auth.IsAdmin,
}
// Set New Password
var allNotifications []*models.Notification
if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
if _, err := api.authorizeCredentials(c, auth.UserName, password); err != nil {
allNotifications = append(allNotifications, &models.Notification{
Type: models.NotificationTypeError,
Content: "Invalid Password",
})
} else {
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.NewPassword)))
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
if err != nil {
allNotifications = append(allNotifications, &models.Notification{
Type: models.NotificationTypeError,
Content: "Unknown Error",
})
} else {
allNotifications = append(allNotifications, &models.Notification{
Type: models.NotificationTypeSuccess,
Content: "Password Updated",
})
newUserSettings.Password = &hashedPassword
}
}
}
// Set Time Offset
if rUserSettings.Timezone != nil {
allNotifications = append(allNotifications, &models.Notification{
Type: models.NotificationTypeSuccess,
Content: "Time Offset Updated",
})
newUserSettings.Timezone = rUserSettings.Timezone
}
// Update User
_, err := api.db.Queries.UpdateUser(c, newUserSettings)
if err != nil {
log.WithError(err).Error("failed to update user")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to update user: %s", err))
return
}
// Get User
user, err := api.db.Queries.GetUser(c, auth.UserName)
if err != nil {
log.WithError(err).Error("failed to get user")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get user: %s", err))
return
}
// Get Devices
devices, err := api.db.Queries.GetDevices(c, auth.UserName)
if err != nil {
log.WithError(err).Error("failed to get devices")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to get devices: %s", err))
return
}
api.renderPage(c, &pages.Settings{
Devices: sliceutils.Map(devices, convertDBDeviceToUI),
Timezone: ptr.Deref(user.Timezone),
}, allNotifications...)
}
func (api *API) renderPage(c *gin.Context, page pages.Page, notifications ...*models.Notification) {
// Get Authentication Data
auth, err := getAuthData(c)
if err != nil {
log.WithError(err).Error("failed to acquire auth data")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to acquire auth data: %s", err))
return
}
// Generate Page
pageNode, err := page.Generate(models.PageContext{
UserInfo: &models.UserInfo{
Username: auth.UserName,
IsAdmin: auth.IsAdmin,
},
ServerInfo: &models.ServerInfo{
RegistrationEnabled: api.cfg.RegistrationEnabled,
SearchEnabled: api.cfg.SearchEnabled,
Version: api.cfg.Version,
},
Notifications: notifications,
})
if err != nil {
log.WithError(err).Error("failed to generate page")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to generate page: %s", err))
return
}
// Render Page
err = pageNode.Render(c.Writer)
if err != nil {
log.WithError(err).Error("failed to render page")
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("failed to render page: %s", err))
return
}
}
@@ -415,7 +477,7 @@ func sortItem[T cmp.Ordered](
return items
}
func arrangeUserStatisticsNew(data []database.GetUserStatisticsRow) []stats.LeaderboardData {
func arrangeUserStatistic(data []database.GetUserStatisticsRow) []stats.LeaderboardData {
wpmFormatter := func(v float64) string { return fmt.Sprintf("%.2f WPM", v) }
return []stats.LeaderboardData{
{

View File

@@ -2,7 +2,6 @@ package api
import (
"context"
"crypto/md5"
"database/sql"
"fmt"
"io"
@@ -13,7 +12,6 @@ import (
"strings"
"time"
argon2 "github.com/alexedwards/argon2id"
"github.com/gabriel-vasile/mimetype"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
@@ -97,31 +95,6 @@ func (api *API) appDocumentReader(c *gin.Context) {
c.FileFromFS("assets/reader/index.htm", http.FS(api.assets))
}
func (api *API) appGetSettings(c *gin.Context) {
templateVars, auth := api.getBaseTemplateVars("settings", c)
user, err := api.db.Queries.GetUser(c, auth.UserName)
if err != nil {
log.Error("GetUser DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
return
}
devices, err := api.db.Queries.GetDevices(c, auth.UserName)
if err != nil {
log.Error("GetDevices DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
return
}
templateVars["Data"] = gin.H{
"Timezone": *user.Timezone,
"Devices": devices,
}
c.HTML(http.StatusOK, "page/settings", templateVars)
}
func (api *API) appGetLogin(c *gin.Context) {
templateVars, _ := api.getBaseTemplateVars("login", c)
templateVars["RegistrationEnabled"] = api.cfg.RegistrationEnabled
@@ -539,84 +512,6 @@ func (api *API) appSaveNewDocument(c *gin.Context) {
})
}
func (api *API) appEditSettings(c *gin.Context) {
var rUserSettings requestSettingsEdit
if err := c.ShouldBind(&rUserSettings); err != nil {
log.Error("Invalid Form Bind")
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
return
}
// Validate Something Exists
if rUserSettings.Password == nil && rUserSettings.NewPassword == nil && rUserSettings.Timezone == nil {
log.Error("Missing Form Values")
appErrorPage(c, http.StatusBadRequest, "Invalid or missing form values")
return
}
templateVars, auth := api.getBaseTemplateVars("settings", c)
newUserSettings := database.UpdateUserParams{
UserID: auth.UserName,
Admin: auth.IsAdmin,
}
// Set New Password
if rUserSettings.Password != nil && rUserSettings.NewPassword != nil {
password := fmt.Sprintf("%x", md5.Sum([]byte(*rUserSettings.Password)))
data := api.authorizeCredentials(c, 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 {
templateVars["PasswordErrorMessage"] = "Unknown Error"
} else {
templateVars["PasswordMessage"] = "Password Updated"
newUserSettings.Password = &hashedPassword
}
}
}
// Set Time Offset
if rUserSettings.Timezone != nil {
templateVars["TimeOffsetMessage"] = "Time Offset Updated"
newUserSettings.Timezone = rUserSettings.Timezone
}
// Update User
_, err := api.db.Queries.UpdateUser(c, newUserSettings)
if err != nil {
log.Error("UpdateUser DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("UpdateUser DB Error: %v", err))
return
}
// Get User
user, err := api.db.Queries.GetUser(c, auth.UserName)
if err != nil {
log.Error("GetUser DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetUser DB Error: %v", err))
return
}
// Get Devices
devices, err := api.db.Queries.GetDevices(c, auth.UserName)
if err != nil {
log.Error("GetDevices DB Error: ", err)
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("GetDevices DB Error: %v", err))
return
}
templateVars["Data"] = gin.H{
"Timezone": *user.Timezone,
"Devices": devices,
}
c.HTML(http.StatusOK, "page/settings", templateVars)
}
func (api *API) appDemoModeError(c *gin.Context) {
appErrorPage(c, http.StatusUnauthorized, "Not Allowed in Demo Mode")
}
@@ -664,10 +559,10 @@ func (api *API) getDocumentsWordCount(ctx context.Context, documents []database.
return nil
}
func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) (gin.H, authData) {
var auth authData
func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) (gin.H, *authData) {
var auth *authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
auth = data.(*authData)
}
return gin.H{
@@ -681,12 +576,11 @@ func (api *API) getBaseTemplateVars(routeName string, c *gin.Context) (gin.H, au
}, auth
}
func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams {
func bindQueryParams(c *gin.Context, defaultLimit int64) (*queryParams, error) {
var qParams queryParams
err := c.BindQuery(&qParams)
if err != nil {
appErrorPage(c, http.StatusInternalServerError, fmt.Sprintf("Invalid Form Bind: %v", err))
return qParams
return nil, err
}
if qParams.Limit == nil {
@@ -701,7 +595,7 @@ func bindQueryParams(c *gin.Context, defaultLimit int64) queryParams {
qParams.Page = &oneValue
}
return qParams
return &qParams, nil
}
func appErrorPage(c *gin.Context, errorCode int, errorMessage string) {

View File

@@ -30,31 +30,31 @@ type authKOHeader struct {
AuthKey string `header:"x-auth-key"`
}
func (api *API) authorizeCredentials(ctx context.Context, username string, password string) (auth *authData) {
func (api *API) authorizeCredentials(ctx context.Context, username string, password string) (*authData, error) {
user, err := api.db.Queries.GetUser(ctx, username)
if err != nil {
return
return nil, err
}
if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || !match {
return
return nil, err
}
// Update auth cache
// Update Auth Cache
api.userAuthCache[user.ID] = *user.AuthHash
return &authData{
UserName: user.ID,
IsAdmin: user.Admin,
AuthHash: *user.AuthHash,
}
}, nil
}
func (api *API) authKOMiddleware(c *gin.Context) {
session := sessions.Default(c)
// Check Session First
if auth, ok := api.getSession(c, session); ok {
if auth, ok := api.authorizeSession(c, session); ok {
c.Set("Authorization", auth)
c.Header("Cache-Control", "private")
c.Next()
@@ -65,21 +65,25 @@ func (api *API) authKOMiddleware(c *gin.Context) {
var rHeader authKOHeader
if err := c.ShouldBindHeader(&rHeader); err != nil {
log.WithError(err).Error("failed to bind auth headers")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Incorrect Headers"})
return
}
if rHeader.AuthUser == "" || rHeader.AuthKey == "" {
log.Error("invalid authentication headers")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
return
}
authData := api.authorizeCredentials(c, rHeader.AuthUser, rHeader.AuthKey)
if authData == nil {
authData, err := api.authorizeCredentials(c, rHeader.AuthUser, rHeader.AuthKey)
if err != nil {
log.WithField("user", rHeader.AuthUser).WithError(err).Error("failed to authorize credentials")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
if err := api.setSession(session, *authData); err != nil {
if err := api.setSession(session, authData); err != nil {
log.WithField("user", rHeader.AuthUser).WithError(err).Error("failed to set session")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
@@ -96,14 +100,16 @@ func (api *API) authOPDSMiddleware(c *gin.Context) {
// Validate Auth Fields
if !hasAuth || user == "" || rawPassword == "" {
log.Error("invalid authorization headers")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
return
}
// Validate Auth
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
authData := api.authorizeCredentials(c, user, password)
if authData == nil {
authData, err := api.authorizeCredentials(c, user, password)
if err != nil {
log.WithField("user", user).WithError(err).Error("failed to authorize credentials")
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
@@ -117,7 +123,7 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
session := sessions.Default(c)
// Check Session
if auth, ok := api.getSession(c, session); ok {
if auth, ok := api.authorizeSession(c, session); ok {
c.Set("Authorization", auth)
c.Header("Cache-Control", "private")
c.Next()
@@ -130,7 +136,7 @@ func (api *API) authWebAppMiddleware(c *gin.Context) {
func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
if data, _ := c.Get("Authorization"); data != nil {
auth := data.(authData)
auth := data.(*authData)
if auth.IsAdmin {
c.Next()
return
@@ -155,8 +161,9 @@ func (api *API) appAuthLogin(c *gin.Context) {
// MD5 - KOSync Compatiblity
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
authData := api.authorizeCredentials(c, username, password)
if authData == nil {
authData, err := api.authorizeCredentials(c, username, password)
if err != nil {
log.WithField("user", username).WithError(err).Error("failed to authorize credentials")
templateVars["Error"] = "Invalid Credentials"
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
return
@@ -164,7 +171,7 @@ func (api *API) appAuthLogin(c *gin.Context) {
// Set Session
session := sessions.Default(c)
if err := api.setSession(session, *authData); err != nil {
if err := api.setSession(session, authData); err != nil {
templateVars["Error"] = "Invalid Credentials"
c.HTML(http.StatusUnauthorized, "page/login", templateVars)
return
@@ -253,7 +260,7 @@ func (api *API) appAuthRegister(c *gin.Context) {
}
// Set session
auth := authData{
auth := &authData{
UserName: user.ID,
IsAdmin: user.Admin,
AuthHash: *user.AuthHash,
@@ -349,35 +356,40 @@ func (api *API) koAuthRegister(c *gin.Context) {
})
}
func (api *API) getSession(ctx context.Context, session sessions.Session) (auth authData, ok bool) {
func (api *API) authorizeSession(ctx context.Context, session sessions.Session) (*authData, bool) {
// Get Session
authorizedUser := session.Get("authorizedUser")
isAdmin := session.Get("isAdmin")
expiresAt := session.Get("expiresAt")
authHash := session.Get("authHash")
if authorizedUser == nil || isAdmin == nil || expiresAt == nil || authHash == nil {
return
return nil, false
}
// Create Auth Object
auth = authData{
auth := &authData{
UserName: authorizedUser.(string),
IsAdmin: isAdmin.(bool),
AuthHash: authHash.(string),
}
logger := log.WithField("user", auth.UserName)
// Validate Auth Hash
correctAuthHash, err := api.getUserAuthHash(ctx, auth.UserName)
if err != nil || correctAuthHash != auth.AuthHash {
return
if err != nil {
logger.WithError(err).Error("failed to get auth hash")
return nil, false
} else if correctAuthHash != auth.AuthHash {
logger.Warn("user auth hash mismatch")
return nil, false
}
// Refresh
if expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
log.Info("Refreshing Session")
logger.Info("refreshing session")
if err := api.setSession(session, auth); err != nil {
log.Error("unable to get session")
return
logger.WithError(err).Error("failed to refresh session")
return nil, false
}
}
@@ -385,7 +397,7 @@ func (api *API) getSession(ctx context.Context, session sessions.Session) (auth
return auth, true
}
func (api *API) setSession(session sessions.Session, auth authData) error {
func (api *API) setSession(session sessions.Session, auth *authData) error {
// Set Session Cookie
session.Set("authorizedUser", auth.UserName)
session.Set("isAdmin", auth.IsAdmin)

View File

@@ -28,7 +28,7 @@ func convertDBDocToUI(r database.GetDocumentsWithStatsRow) models.Document {
}
}
func convertMetaToUI(m metadata.MetadataInfo, errorMsg *string) *models.DocumentMetadata {
func convertMetaToUI(m metadata.MetadataInfo) *models.DocumentMetadata {
return &models.DocumentMetadata{
SourceID: ptr.Deref(m.SourceID),
ISBN10: ptr.Deref(m.ISBN10),
@@ -37,7 +37,6 @@ func convertMetaToUI(m metadata.MetadataInfo, errorMsg *string) *models.Document
Author: ptr.Deref(m.Author),
Description: ptr.Deref(m.Description),
Source: m.Source,
Error: errorMsg,
}
}
@@ -63,6 +62,14 @@ func convertDBProgressToUI(r database.GetProgressRow) models.Progress {
}
}
func convertDBDeviceToUI(r database.GetDevicesRow) models.Device {
return models.Device{
DeviceName: r.DeviceName,
LastSynced: r.LastSynced,
CreatedAt: r.CreatedAt,
}
}
func convertSearchToUI(r search.SearchItem) models.SearchResult {
return models.SearchResult{
ID: r.ID,

View File

@@ -62,13 +62,19 @@ func (api *API) opdsEntry(c *gin.Context) {
}
func (api *API) opdsDocuments(c *gin.Context) {
var auth authData
if data, _ := c.Get("Authorization"); data != nil {
auth = data.(authData)
auth, err := getAuthData(c)
if err != nil {
log.WithError(err).Error("failed to acquire auth data")
c.AbortWithStatus(http.StatusInternalServerError)
}
// Potential URL Parameters (Default Pagination - 100)
qParams := bindQueryParams(c, 100)
qParams, err := bindQueryParams(c, 100)
if err != nil {
log.WithError(err).Error("failed to bind query params")
c.AbortWithStatus(http.StatusBadRequest)
return
}
// Possible Query
var query *string
@@ -86,7 +92,7 @@ func (api *API) opdsDocuments(c *gin.Context) {
Limit: *qParams.Limit,
})
if err != nil {
log.Error("GetDocumentsWithStats DB Error:", err)
log.WithError(err).Error("failed to get documents with stats")
c.AbortWithStatus(http.StatusBadRequest)
return
}

View File

@@ -8,11 +8,22 @@ import (
"reflect"
"strings"
"github.com/gin-gonic/gin"
"reichard.io/antholume/database"
"reichard.io/antholume/graph"
"reichard.io/antholume/metadata"
)
func getAuthData(ctx *gin.Context) (*authData, error) {
if data, ok := ctx.Get("Authorization"); ok {
var auth *authData
if auth, ok = data.(*authData); ok {
return auth, nil
}
}
return nil, errors.New("could not acquire auth data")
}
// getTimeZones returns a string slice of IANA timezones.
func getTimeZones() []string {
return []string{