Initial Commit
This commit is contained in:
137
api/api.go
Normal file
137
api/api.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/multitemplate"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"reichard.io/bbank/config"
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/bbank/graph"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
Router *gin.Engine
|
||||
Config *config.Config
|
||||
DB *database.DBManager
|
||||
}
|
||||
|
||||
func NewApi(db *database.DBManager, c *config.Config) *API {
|
||||
api := &API{
|
||||
Router: gin.Default(),
|
||||
Config: c,
|
||||
DB: db,
|
||||
}
|
||||
|
||||
// Assets & Web App Templates
|
||||
api.Router.Static("/assets", "./assets")
|
||||
|
||||
// Generate Secure Token
|
||||
newToken, err := generateToken(64)
|
||||
if err != nil {
|
||||
panic("Unable to generate secure token")
|
||||
}
|
||||
|
||||
// Configure Cookie Session Store
|
||||
store := cookie.NewStore(newToken)
|
||||
store.Options(sessions.Options{
|
||||
MaxAge: 60 * 60 * 24,
|
||||
Secure: true,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
api.Router.Use(sessions.Sessions("token", store))
|
||||
|
||||
// Register Web App Route
|
||||
api.registerWebAppRoutes()
|
||||
|
||||
// Register API Routes
|
||||
apiGroup := api.Router.Group("/api")
|
||||
api.registerKOAPIRoutes(apiGroup)
|
||||
api.registerWebAPIRoutes(apiGroup)
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
func (api *API) registerWebAppRoutes() {
|
||||
// Define Templates & Helper Functions
|
||||
render := multitemplate.NewRenderer()
|
||||
helperFuncs := template.FuncMap{
|
||||
"GetSVGGraphData": graph.GetSVGGraphData,
|
||||
}
|
||||
|
||||
render.AddFromFilesFuncs("login", helperFuncs, "templates/login.html")
|
||||
render.AddFromFilesFuncs("home", helperFuncs, "templates/base.html", "templates/home.html")
|
||||
render.AddFromFilesFuncs("graphs", helperFuncs, "templates/base.html", "templates/graphs.html")
|
||||
render.AddFromFilesFuncs("activity", helperFuncs, "templates/base.html", "templates/activity.html")
|
||||
render.AddFromFilesFuncs("documents", helperFuncs, "templates/base.html", "templates/documents.html")
|
||||
|
||||
api.Router.HTMLRender = render
|
||||
|
||||
api.Router.GET("/login", api.createAppResourcesRoute("login"))
|
||||
api.Router.GET("/register", api.createAppResourcesRoute("login", gin.H{"Register": true}))
|
||||
api.Router.GET("/logout", api.authWebAppMiddleware, api.authLogout)
|
||||
api.Router.POST("/login", api.authFormLogin)
|
||||
api.Router.POST("/register", api.authFormRegister)
|
||||
|
||||
api.Router.GET("/", api.authWebAppMiddleware, api.createAppResourcesRoute("home"))
|
||||
api.Router.GET("/documents", api.authWebAppMiddleware, api.createAppResourcesRoute("documents"))
|
||||
api.Router.GET("/documents/:document/file", api.authWebAppMiddleware, api.downloadDocumentFile)
|
||||
api.Router.GET("/documents/:document/cover", api.authWebAppMiddleware, api.getDocumentCover)
|
||||
|
||||
// TODO
|
||||
api.Router.GET("/activity", api.authWebAppMiddleware, baseResourceRoute("activity"))
|
||||
api.Router.GET("/graphs", api.authWebAppMiddleware, baseResourceRoute("graphs"))
|
||||
|
||||
}
|
||||
|
||||
func (api *API) registerKOAPIRoutes(apiGroup *gin.RouterGroup) {
|
||||
koGroup := apiGroup.Group("/ko")
|
||||
|
||||
koGroup.GET("/info", api.serverInfo)
|
||||
|
||||
koGroup.POST("/users/create", api.createUser)
|
||||
koGroup.GET("/users/auth", api.authAPIMiddleware, api.authorizeUser)
|
||||
|
||||
koGroup.PUT("/syncs/progress", api.authAPIMiddleware, api.setProgress)
|
||||
koGroup.GET("/syncs/progress/:document", api.authAPIMiddleware, api.getProgress)
|
||||
|
||||
koGroup.POST("/documents", api.authAPIMiddleware, api.addDocuments)
|
||||
koGroup.POST("/syncs/documents", api.authAPIMiddleware, api.checkDocumentsSync)
|
||||
koGroup.PUT("/documents/:document/file", api.authAPIMiddleware, api.uploadDocumentFile)
|
||||
koGroup.GET("/documents/:document/file", api.authAPIMiddleware, api.downloadDocumentFile)
|
||||
|
||||
koGroup.POST("/activity", api.authAPIMiddleware, api.addActivities)
|
||||
koGroup.POST("/syncs/activity", api.authAPIMiddleware, api.checkActivitySync)
|
||||
}
|
||||
|
||||
func (api *API) registerWebAPIRoutes(apiGroup *gin.RouterGroup) {
|
||||
v1Group := apiGroup.Group("/v1")
|
||||
|
||||
v1Group.GET("/info", api.serverInfo)
|
||||
|
||||
v1Group.POST("/users", api.createUser)
|
||||
v1Group.GET("/users", api.authAPIMiddleware, api.getUsers)
|
||||
|
||||
v1Group.POST("/documents", api.authAPIMiddleware, api.checkDocumentsSync)
|
||||
v1Group.GET("/documents", api.authAPIMiddleware, api.getDocuments)
|
||||
|
||||
v1Group.GET("/documents/:document/file", api.authAPIMiddleware, api.downloadDocumentFile)
|
||||
v1Group.PUT("/documents/:document/file", api.authAPIMiddleware, api.uploadDocumentFile)
|
||||
|
||||
v1Group.GET("/activity", api.authAPIMiddleware, api.getActivity)
|
||||
v1Group.GET("/devices", api.authAPIMiddleware, api.getDevices)
|
||||
}
|
||||
|
||||
func generateToken(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
169
api/app-routes.go
Normal file
169
api/app-routes.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"reichard.io/bbank/database"
|
||||
"reichard.io/bbank/metadata"
|
||||
)
|
||||
|
||||
func baseResourceRoute(template string, args ...map[string]any) func(c *gin.Context) {
|
||||
variables := gin.H{"RouteName": template}
|
||||
if len(args) > 0 {
|
||||
variables = args[0]
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
variables["User"] = rUser
|
||||
c.HTML(http.StatusOK, template, variables)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any) func(*gin.Context) {
|
||||
// Merge Optional Template Data
|
||||
var templateVars = gin.H{}
|
||||
if len(args) > 0 {
|
||||
templateVars = args[0]
|
||||
}
|
||||
templateVars["RouteName"] = routeName
|
||||
|
||||
return func(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
qParams := bindQueryParams(c)
|
||||
templateVars["User"] = rUser
|
||||
|
||||
if routeName == "documents" {
|
||||
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
|
||||
UserID: rUser.(string),
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
log.Info(err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
templateVars["Data"] = documents
|
||||
} else if routeName == "home" {
|
||||
weekly_streak, _ := api.DB.Queries.GetUserWindowStreaks(api.DB.Ctx, database.GetUserWindowStreaksParams{
|
||||
UserID: rUser.(string),
|
||||
Window: "WEEK",
|
||||
})
|
||||
|
||||
daily_streak, _ := api.DB.Queries.GetUserWindowStreaks(api.DB.Ctx, database.GetUserWindowStreaksParams{
|
||||
UserID: rUser.(string),
|
||||
Window: "DAY",
|
||||
})
|
||||
|
||||
database_info, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, rUser.(string))
|
||||
read_graph_data, err := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, rUser.(string))
|
||||
if err != nil {
|
||||
log.Info("HMMMM:", err)
|
||||
}
|
||||
|
||||
templateVars["Data"] = gin.H{
|
||||
"DailyStreak": daily_streak,
|
||||
"WeeklyStreak": weekly_streak,
|
||||
"DatabaseInfo": database_info,
|
||||
"GraphData": read_graph_data,
|
||||
}
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, routeName, templateVars)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *API) getDocumentCover(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Document Exists in DB
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle Identified Document
|
||||
if document.Olid != nil {
|
||||
if *document.Olid == "UNKNOWN" {
|
||||
c.Redirect(http.StatusFound, "/assets/no-cover.jpg")
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Path
|
||||
fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *document.Olid))
|
||||
safePath := filepath.Join(api.Config.DataPath, "covers", fileName)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(safePath)
|
||||
if err != nil {
|
||||
c.Redirect(http.StatusFound, "/assets/no-cover.jpg")
|
||||
return
|
||||
}
|
||||
|
||||
c.File(safePath)
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
This is a bit convoluted because we want to ensure we set the OLID to
|
||||
UNKNOWN if there are any errors. This will ideally prevent us from
|
||||
hitting the OpenLibrary API multiple times in the future.
|
||||
*/
|
||||
|
||||
var coverID string = "UNKNOWN"
|
||||
var coverFilePath *string
|
||||
|
||||
// Identify Documents & Save Covers
|
||||
coverIDs, err := metadata.GetCoverIDs(document.Title, document.Author)
|
||||
if err == nil && len(coverIDs) > 0 {
|
||||
coverFilePath, err = metadata.DownloadAndSaveCover(coverIDs[0], api.Config.DataPath)
|
||||
if err == nil {
|
||||
coverID = coverIDs[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: document.ID,
|
||||
Olid: &coverID,
|
||||
}); err != nil {
|
||||
log.Error("Document Upsert Error")
|
||||
}
|
||||
|
||||
// Return Unknown Cover
|
||||
if coverID == "UNKNOWN" {
|
||||
c.Redirect(http.StatusFound, "/assets/no-cover.jpg")
|
||||
return
|
||||
}
|
||||
|
||||
c.File(*coverFilePath)
|
||||
}
|
||||
|
||||
/*
|
||||
METADATA:
|
||||
- Metadata Match
|
||||
- Update Metadata
|
||||
*/
|
||||
|
||||
/*
|
||||
GRAPHS:
|
||||
- Streaks (Daily, Weekly, Monthly)
|
||||
- Last Week Activity (Daily - Pages & Time)
|
||||
|
||||
|
||||
- Pages Read (Daily, Weekly, Monthly)
|
||||
- Reading Progress
|
||||
- Average Reading Time (Daily, Weekly, Monthly)
|
||||
*/
|
||||
167
api/auth.go
Normal file
167
api/auth.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
argon2 "github.com/alexedwards/argon2id"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"reichard.io/bbank/database"
|
||||
)
|
||||
|
||||
type authHeader struct {
|
||||
AuthUser string `header:"x-auth-user"`
|
||||
AuthKey string `header:"x-auth-key"`
|
||||
}
|
||||
|
||||
func (api *API) authorizeCredentials(username string, password string) (authorized bool) {
|
||||
user, err := api.DB.Queries.GetUser(api.DB.Ctx, username)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if match, err := argon2.ComparePasswordAndHash(password, user.Pass); err != nil || match != true {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (api *API) authAPIMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Utilize Session Token
|
||||
if authorizedUser := session.Get("authorizedUser"); authorizedUser != nil {
|
||||
c.Set("AuthorizedUser", authorizedUser)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
var rHeader authHeader
|
||||
if err := c.ShouldBindHeader(&rHeader); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Incorrect Headers"})
|
||||
return
|
||||
}
|
||||
if rHeader.AuthUser == "" || rHeader.AuthKey == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
|
||||
return
|
||||
}
|
||||
|
||||
if authorized := api.authorizeCredentials(rHeader.AuthUser, rHeader.AuthKey); authorized != true {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Set Session Cookie
|
||||
session.Set("authorizedUser", rHeader.AuthUser)
|
||||
session.Save()
|
||||
|
||||
c.Set("AuthorizedUser", rHeader.AuthUser)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func (api *API) authWebAppMiddleware(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Utilize Session Token
|
||||
if authorizedUser := session.Get("authorizedUser"); authorizedUser != nil {
|
||||
c.Set("AuthorizedUser", authorizedUser)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
func (api *API) authFormLogin(c *gin.Context) {
|
||||
username := strings.TrimSpace(c.PostForm("username"))
|
||||
rawPassword := strings.TrimSpace(c.PostForm("password"))
|
||||
|
||||
if username == "" || rawPassword == "" {
|
||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||
"Error": "Invalid Credentials",
|
||||
})
|
||||
return
|
||||
}
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||
|
||||
if authorized := api.authorizeCredentials(username, password); authorized != true {
|
||||
c.HTML(http.StatusUnauthorized, "login", gin.H{
|
||||
"Error": "Invalid Credentials",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Set Session Cookie
|
||||
session.Set("authorizedUser", username)
|
||||
session.Save()
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
|
||||
func (api *API) authLogout(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Save()
|
||||
c.Redirect(http.StatusFound, "/login")
|
||||
}
|
||||
|
||||
func (api *API) authFormRegister(c *gin.Context) {
|
||||
username := strings.TrimSpace(c.PostForm("username"))
|
||||
rawPassword := strings.TrimSpace(c.PostForm("password"))
|
||||
|
||||
if username == "" || rawPassword == "" {
|
||||
c.HTML(http.StatusBadRequest, "login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
return
|
||||
}
|
||||
password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
|
||||
|
||||
hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
||||
ID: username,
|
||||
Pass: hashedPassword,
|
||||
})
|
||||
|
||||
// SQL Error
|
||||
if err != nil {
|
||||
c.HTML(http.StatusBadRequest, "login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// User Already Exists
|
||||
if rows == 0 {
|
||||
c.HTML(http.StatusBadRequest, "login", gin.H{
|
||||
"Register": true,
|
||||
"Error": "Registration Disabled or User Already Exists",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
|
||||
// Set Session Cookie
|
||||
session.Set("authorizedUser", username)
|
||||
session.Save()
|
||||
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
}
|
||||
593
api/ko-routes.go
Normal file
593
api/ko-routes.go
Normal file
@@ -0,0 +1,593 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
argon2 "github.com/alexedwards/argon2id"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/exp/slices"
|
||||
"reichard.io/bbank/database"
|
||||
)
|
||||
|
||||
type activityItem struct {
|
||||
DocumentID string `json:"document"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
Duration int64 `json:"duration"`
|
||||
CurrentPage int64 `json:"current_page"`
|
||||
TotalPages int64 `json:"total_pages"`
|
||||
}
|
||||
|
||||
type requestActivity struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Device string `json:"device"`
|
||||
Activity []activityItem `json:"activity"`
|
||||
}
|
||||
|
||||
type requestCheckActivitySync struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
|
||||
type requestDocument struct {
|
||||
Documents []database.Document `json:"documents"`
|
||||
}
|
||||
|
||||
type requestPosition struct {
|
||||
DocumentID string `json:"document"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
Progress string `json:"progress"`
|
||||
Device string `json:"device"`
|
||||
DeviceID string `json:"device_id"`
|
||||
}
|
||||
|
||||
type requestUser struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type requestCheckDocumentSync struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Device string `json:"device"`
|
||||
Have []string `json:"have"`
|
||||
}
|
||||
|
||||
type responseCheckDocumentSync struct {
|
||||
Want []string `json:"want"`
|
||||
Give []database.Document `json:"give"`
|
||||
Delete []string `json:"deleted"`
|
||||
}
|
||||
|
||||
type requestDocumentID struct {
|
||||
DocumentID string `uri:"document" binding:"required"`
|
||||
}
|
||||
|
||||
var allowedExtensions []string = []string{".epub", ".html"}
|
||||
|
||||
func (api *API) authorizeUser(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"authorized": "OK",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) createUser(c *gin.Context) {
|
||||
var rUser requestUser
|
||||
if err := c.ShouldBindJSON(&rUser); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
|
||||
if rUser.Username == "" || rUser.Password == "" {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := argon2.CreateHash(rUser.Password, argon2.DefaultParams)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO - Initial User is Admin & Enable / Disable Registration
|
||||
rows, err := api.DB.Queries.CreateUser(api.DB.Ctx, database.CreateUserParams{
|
||||
ID: rUser.Username,
|
||||
Pass: hashedPassword,
|
||||
})
|
||||
|
||||
// SQL Error
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid User Data"})
|
||||
return
|
||||
}
|
||||
|
||||
// User Exists (ON CONFLICT DO NOTHING)
|
||||
if rows == 0 {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "User Already Exists"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Struct -> JSON
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"username": rUser.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) setProgress(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rPosition requestPosition
|
||||
if err := c.ShouldBindJSON(&rPosition); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Progress Data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert Device
|
||||
device, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
ID: rPosition.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
DeviceName: rPosition.Device,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Device Upsert Error:", device, err)
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
document, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: rPosition.DocumentID,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Document Upsert Error:", document, err)
|
||||
}
|
||||
|
||||
// Create or Replace Progress
|
||||
progress, err := api.DB.Queries.UpdateProgress(api.DB.Ctx, database.UpdateProgressParams{
|
||||
Percentage: rPosition.Percentage,
|
||||
DocumentID: rPosition.DocumentID,
|
||||
DeviceID: rPosition.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
Progress: rPosition.Progress,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Struct -> JSON
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"document": progress.DocumentID,
|
||||
"timestamp": progress.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) getProgress(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rDocID requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDocID); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
progress, err := api.DB.Queries.GetProgress(api.DB.Ctx, database.GetProgressParams{
|
||||
DocumentID: rDocID.DocumentID,
|
||||
UserID: rUser.(string),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error("Invalid Progress:", progress, err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Struct -> JSON
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"document": progress.DocumentID,
|
||||
"percentage": progress.Percentage,
|
||||
"progress": progress.Progress,
|
||||
"device": progress.DeviceName,
|
||||
"device_id": progress.DeviceID,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) addActivities(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rActivity requestActivity
|
||||
if err := c.ShouldBindJSON(&rActivity); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})
|
||||
return
|
||||
}
|
||||
|
||||
// Do Transaction
|
||||
tx, err := api.DB.DB.Begin()
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Unique Documents
|
||||
allDocumentsMap := make(map[string]bool)
|
||||
for _, item := range rActivity.Activity {
|
||||
allDocumentsMap[item.DocumentID] = true
|
||||
}
|
||||
allDocuments := getKeys(allDocumentsMap)
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
qtx := api.DB.Queries.WithTx(tx)
|
||||
|
||||
// Upsert Documents
|
||||
for _, doc := range allDocuments {
|
||||
_, err := qtx.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: doc,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Device
|
||||
_, err = qtx.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
ID: rActivity.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
DeviceName: rActivity.Device,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
||||
return
|
||||
}
|
||||
|
||||
// Add All Activity
|
||||
for _, item := range rActivity.Activity {
|
||||
_, err := qtx.AddActivity(api.DB.Ctx, database.AddActivityParams{
|
||||
UserID: rUser.(string),
|
||||
DocumentID: item.DocumentID,
|
||||
DeviceID: rActivity.DeviceID,
|
||||
StartTime: time.Unix(int64(item.StartTime), 0).UTC(),
|
||||
Duration: int64(item.Duration),
|
||||
CurrentPage: int64(item.CurrentPage),
|
||||
TotalPages: int64(item.TotalPages),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Activity"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Commit Transaction
|
||||
tx.Commit()
|
||||
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"added": len(rActivity.Activity),
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) checkActivitySync(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rCheckActivity requestCheckActivitySync
|
||||
if err := c.ShouldBindJSON(&rCheckActivity); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get Last Device Activity
|
||||
lastActivity, err := api.DB.Queries.GetLastActivity(api.DB.Ctx, database.GetLastActivityParams{
|
||||
UserID: rUser.(string),
|
||||
DeviceID: rCheckActivity.DeviceID,
|
||||
})
|
||||
if err == sql.ErrNoRows {
|
||||
lastActivity = time.UnixMilli(0)
|
||||
} else if err != nil {
|
||||
log.Error("GetLastActivity Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"last_sync": lastActivity.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) addDocuments(c *gin.Context) {
|
||||
var rNewDocs requestDocument
|
||||
if err := c.ShouldBindJSON(&rNewDocs); err != nil {
|
||||
log.Error("[addDocuments] Invalid JSON Bind")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document(s)"})
|
||||
return
|
||||
}
|
||||
|
||||
// Do Transaction
|
||||
tx, err := api.DB.DB.Begin()
|
||||
if err != nil {
|
||||
log.Error("[addDocuments] Unknown Transaction Error")
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Defer & Start Transaction
|
||||
defer tx.Rollback()
|
||||
qtx := api.DB.Queries.WithTx(tx)
|
||||
|
||||
// Upsert Documents
|
||||
for _, doc := range rNewDocs.Documents {
|
||||
doc, err := qtx.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: doc.ID,
|
||||
Title: doc.Title,
|
||||
Author: doc.Author,
|
||||
Series: doc.Series,
|
||||
SeriesIndex: doc.SeriesIndex,
|
||||
Lang: doc.Lang,
|
||||
Description: doc.Description,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[addDocuments] UpsertDocument Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
|
||||
_, err = qtx.UpdateDocumentSync(api.DB.Ctx, database.UpdateDocumentSyncParams{
|
||||
ID: doc.ID,
|
||||
Synced: true,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("[addDocuments] UpsertDocumentSync Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Commit Transaction
|
||||
tx.Commit()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"changed": len(rNewDocs.Documents),
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) checkDocumentsSync(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
|
||||
var rCheckDocs requestCheckDocumentSync
|
||||
if err := c.ShouldBindJSON(&rCheckDocs); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert Device
|
||||
device, err := api.DB.Queries.UpsertDevice(api.DB.Ctx, database.UpsertDeviceParams{
|
||||
ID: rCheckDocs.DeviceID,
|
||||
UserID: rUser.(string),
|
||||
DeviceName: rCheckDocs.Device,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Device"})
|
||||
return
|
||||
}
|
||||
|
||||
missingDocs := []database.Document{}
|
||||
deletedDocIDs := []string{}
|
||||
|
||||
if device.Sync == true {
|
||||
// Get Missing Documents
|
||||
missingDocs, err = api.DB.Queries.GetMissingDocuments(api.DB.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("GetMissingDocuments Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get Deleted Documents
|
||||
deletedDocIDs, err = api.DB.Queries.GetDeletedDocuments(api.DB.Ctx, rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("GetDeletedDocuements Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get Wanted Documents
|
||||
jsonHaves, err := json.Marshal(rCheckDocs.Have)
|
||||
if err != nil {
|
||||
log.Error("JSON Marshal Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
wantedDocIDs, err := api.DB.Queries.GetWantedDocuments(api.DB.Ctx, string(jsonHaves))
|
||||
if err != nil {
|
||||
log.Error("GetWantedDocuments Error:", err)
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
rCheckDocSync := responseCheckDocumentSync{
|
||||
Delete: []string{},
|
||||
Want: []string{},
|
||||
Give: []database.Document{},
|
||||
}
|
||||
|
||||
// Ensure Empty Array
|
||||
if wantedDocIDs != nil {
|
||||
rCheckDocSync.Want = wantedDocIDs
|
||||
}
|
||||
if missingDocs != nil {
|
||||
rCheckDocSync.Give = missingDocs
|
||||
}
|
||||
if deletedDocIDs != nil {
|
||||
rCheckDocSync.Delete = deletedDocIDs
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, rCheckDocSync)
|
||||
}
|
||||
|
||||
func (api *API) uploadDocumentFile(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
fileData, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Type & Derive Extension on MIME
|
||||
uploadedFile, err := fileData.Open()
|
||||
fileMime, err := mimetype.DetectReader(uploadedFile)
|
||||
fileExtension := fileMime.Extension()
|
||||
|
||||
if !slices.Contains(allowedExtensions, fileExtension) {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Filetype"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate Document Exists in DB
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Filename
|
||||
var fileName string
|
||||
if document.Author != nil {
|
||||
fileName = fileName + *document.Author
|
||||
} else {
|
||||
fileName = fileName + "Unknown"
|
||||
}
|
||||
|
||||
if document.Title != nil {
|
||||
fileName = fileName + " - " + *document.Title
|
||||
} else {
|
||||
fileName = fileName + " - Unknown"
|
||||
}
|
||||
|
||||
// Derive & Sanitize File Name
|
||||
fileName = "." + filepath.Clean(fmt.Sprintf("/%s [%s]%s", fileName, document.ID, fileExtension))
|
||||
|
||||
// Generate Storage Path
|
||||
safePath := filepath.Join(api.Config.DataPath, "documents", fileName)
|
||||
|
||||
// Save & Prevent Overwrites
|
||||
_, err = os.Stat(safePath)
|
||||
if os.IsNotExist(err) {
|
||||
err = c.SaveUploadedFile(fileData, safePath)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get MD5 Hash
|
||||
fileHash, err := getFileMD5(safePath)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "File Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert Document
|
||||
_, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
||||
ID: document.ID,
|
||||
Md5: fileHash,
|
||||
Filepath: &fileName,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update Document Sync Attribute
|
||||
_, err = api.DB.Queries.UpdateDocumentSync(api.DB.Ctx, database.UpdateDocumentSyncParams{
|
||||
ID: document.ID,
|
||||
Synced: true,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Document"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) downloadDocumentFile(c *gin.Context) {
|
||||
var rDoc requestDocumentID
|
||||
if err := c.ShouldBindUri(&rDoc); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get Document
|
||||
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unknown Document"})
|
||||
return
|
||||
}
|
||||
|
||||
if document.Filepath == nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exist"})
|
||||
return
|
||||
}
|
||||
|
||||
// Derive Storage Location
|
||||
filePath := filepath.Join(api.Config.DataPath, "documents", *document.Filepath)
|
||||
|
||||
// Validate File Exists
|
||||
_, err = os.Stat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Document Doesn't Exists"})
|
||||
return
|
||||
}
|
||||
|
||||
// Force Download (Security)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(*document.Filepath)))
|
||||
c.File(filePath)
|
||||
}
|
||||
|
||||
func getKeys[M ~map[K]V, K comparable, V any](m M) []K {
|
||||
r := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
r = append(r, k)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func getFileMD5(filePath string) (*string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
hash := md5.New()
|
||||
_, err = io.Copy(hash, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileHash := fmt.Sprintf("%x", hash.Sum(nil))
|
||||
|
||||
return &fileHash, nil
|
||||
}
|
||||
163
api/web-routes.go
Normal file
163
api/web-routes.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
argon2 "github.com/alexedwards/argon2id"
|
||||
"github.com/gin-gonic/gin"
|
||||
"reichard.io/bbank/database"
|
||||
)
|
||||
|
||||
type infoResponse struct {
|
||||
Authorized bool `json:"authorized"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type queryParams struct {
|
||||
Page *int64 `form:"page"`
|
||||
Limit *int64 `form:"limit"`
|
||||
Document *string `form:"document"`
|
||||
}
|
||||
|
||||
func bindQueryParams(c *gin.Context) queryParams {
|
||||
var qParams queryParams
|
||||
c.BindQuery(&qParams)
|
||||
|
||||
if qParams.Limit == nil {
|
||||
var defaultValue int64 = 50
|
||||
qParams.Limit = &defaultValue
|
||||
} else if *qParams.Limit < 0 {
|
||||
var zeroValue int64 = 0
|
||||
qParams.Limit = &zeroValue
|
||||
}
|
||||
|
||||
if qParams.Page == nil || *qParams.Page < 1 {
|
||||
var oneValue int64 = 0
|
||||
qParams.Page = &oneValue
|
||||
}
|
||||
|
||||
return qParams
|
||||
}
|
||||
|
||||
func (api *API) serverInfo(c *gin.Context) {
|
||||
respData := infoResponse{
|
||||
Authorized: false,
|
||||
Version: api.Config.Version,
|
||||
}
|
||||
|
||||
var rHeader authHeader
|
||||
if err := c.ShouldBindHeader(&rHeader); err != nil {
|
||||
c.JSON(200, respData)
|
||||
return
|
||||
}
|
||||
if rHeader.AuthUser == "" || rHeader.AuthKey == "" {
|
||||
c.JSON(200, respData)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := api.DB.Queries.GetUser(api.DB.Ctx, rHeader.AuthUser)
|
||||
if err != nil {
|
||||
c.JSON(200, respData)
|
||||
return
|
||||
}
|
||||
|
||||
match, err := argon2.ComparePasswordAndHash(rHeader.AuthKey, user.Pass)
|
||||
if err != nil || match != true {
|
||||
c.JSON(200, respData)
|
||||
return
|
||||
}
|
||||
|
||||
respData.Authorized = true
|
||||
c.JSON(200, respData)
|
||||
}
|
||||
|
||||
func (api *API) getDocuments(c *gin.Context) {
|
||||
qParams := bindQueryParams(c)
|
||||
|
||||
documents, err := api.DB.Queries.GetDocuments(api.DB.Ctx, database.GetDocumentsParams{
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
if documents == nil {
|
||||
documents = []database.Document{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, documents)
|
||||
}
|
||||
|
||||
func (api *API) getUsers(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
qParams := bindQueryParams(c)
|
||||
|
||||
users, err := api.DB.Queries.GetUsers(api.DB.Ctx, database.GetUsersParams{
|
||||
User: rUser.(string),
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
if users == nil {
|
||||
users = []database.User{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
func (api *API) getActivity(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
qParams := bindQueryParams(c)
|
||||
|
||||
dbActivityParams := database.GetActivityParams{
|
||||
UserID: rUser.(string),
|
||||
DocFilter: false,
|
||||
DocumentID: "",
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
}
|
||||
|
||||
if qParams.Document != nil {
|
||||
dbActivityParams.DocFilter = true
|
||||
dbActivityParams.DocumentID = *qParams.Document
|
||||
}
|
||||
|
||||
activity, err := api.DB.Queries.GetActivity(api.DB.Ctx, dbActivityParams)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
if activity == nil {
|
||||
activity = []database.Activity{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, activity)
|
||||
}
|
||||
|
||||
func (api *API) getDevices(c *gin.Context) {
|
||||
rUser, _ := c.Get("AuthorizedUser")
|
||||
qParams := bindQueryParams(c)
|
||||
|
||||
devices, err := api.DB.Queries.GetDevices(api.DB.Ctx, database.GetDevicesParams{
|
||||
UserID: rUser.(string),
|
||||
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
||||
Limit: *qParams.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
||||
return
|
||||
}
|
||||
|
||||
if devices == nil {
|
||||
devices = []database.Device{}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, devices)
|
||||
}
|
||||
Reference in New Issue
Block a user