Files
AnthoLume/api/v1/handlers.go
2026-03-22 17:21:32 -04:00

294 lines
7.9 KiB
Go

package v1
import (
"context"
"net/http"
"strconv"
"strings"
"reichard.io/antholume/database"
)
// DocumentRequest represents a request for a single document
type DocumentRequest struct {
ID string
}
// DocumentListRequest represents a request for listing documents
type DocumentListRequest struct {
Page int64
Limit int64
Search *string
}
// ProgressRequest represents a request for document progress
type ProgressRequest struct {
ID string
}
// ActivityRequest represents a request for activity data
type ActivityRequest struct {
DocFilter bool
DocumentID string
Offset int64
Limit int64
}
// SettingsRequest represents a request for settings data
type SettingsRequest struct{}
// GetDocument handles GET /api/v1/documents/:id
func (s *Server) GetDocument(ctx context.Context, req DocumentRequest) (DocumentResponse, error) {
auth := getAuthFromContext(ctx)
if auth == nil {
return DocumentResponse{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"}
}
doc, err := s.db.Queries.GetDocument(ctx, req.ID)
if err != nil {
return DocumentResponse{}, &apiError{status: http.StatusNotFound, message: "Document not found"}
}
progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{
UserID: auth.UserName,
DocumentID: req.ID,
})
var progress *Progress
if err == nil {
progress = &Progress{
UserID: progressRow.UserID,
DocumentID: progressRow.DocumentID,
DeviceID: progressRow.DeviceID,
Percentage: progressRow.Percentage,
Progress: progressRow.Progress,
CreatedAt: progressRow.CreatedAt,
}
}
return DocumentResponse{
Document: doc,
User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin},
Progress: progress,
}, nil
}
// GetDocuments handles GET /api/v1/documents
func (s *Server) GetDocuments(ctx context.Context, req DocumentListRequest) (DocumentsResponse, error) {
auth := getAuthFromContext(ctx)
if auth == nil {
return DocumentsResponse{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"}
}
rows, err := s.db.Queries.GetDocumentsWithStats(
ctx,
database.GetDocumentsWithStatsParams{
UserID: auth.UserName,
Query: req.Search,
Deleted: ptrOf(false),
Offset: (req.Page - 1) * req.Limit,
Limit: req.Limit,
},
)
if err != nil {
return DocumentsResponse{}, &apiError{status: http.StatusInternalServerError, message: err.Error()}
}
total := int64(len(rows))
var nextPage *int64
var previousPage *int64
if req.Page*req.Limit < total {
nextPage = ptrOf(req.Page + 1)
}
if req.Page > 1 {
previousPage = ptrOf(req.Page - 1)
}
wordCounts := make([]WordCount, 0, len(rows))
for _, row := range rows {
if row.Words != nil {
wordCounts = append(wordCounts, WordCount{
DocumentID: row.ID,
Count: *row.Words,
})
}
}
return DocumentsResponse{
Documents: rows,
Total: total,
Page: req.Page,
Limit: req.Limit,
NextPage: nextPage,
PreviousPage: previousPage,
Search: req.Search,
User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin},
WordCounts: wordCounts,
}, nil
}
// GetProgress handles GET /api/v1/progress/:id
func (s *Server) GetProgress(ctx context.Context, req ProgressRequest) (Progress, error) {
auth := getAuthFromContext(ctx)
if auth == nil {
return Progress{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"}
}
if req.ID == "" {
return Progress{}, &apiError{status: http.StatusBadRequest, message: "Document ID required"}
}
progressRow, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{
UserID: auth.UserName,
DocumentID: req.ID,
})
if err != nil {
return Progress{}, &apiError{status: http.StatusNotFound, message: "Progress not found"}
}
return Progress{
UserID: progressRow.UserID,
DocumentID: progressRow.DocumentID,
DeviceID: progressRow.DeviceID,
Percentage: progressRow.Percentage,
Progress: progressRow.Progress,
CreatedAt: progressRow.CreatedAt,
}, nil
}
// GetActivity handles GET /api/v1/activity
func (s *Server) GetActivity(ctx context.Context, req ActivityRequest) (ActivityResponse, error) {
auth := getAuthFromContext(ctx)
if auth == nil {
return ActivityResponse{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"}
}
activities, err := s.db.Queries.GetActivity(ctx, database.GetActivityParams{
UserID: auth.UserName,
DocFilter: req.DocFilter,
DocumentID: req.DocumentID,
Offset: req.Offset,
Limit: req.Limit,
})
if err != nil {
return ActivityResponse{}, &apiError{status: http.StatusInternalServerError, message: err.Error()}
}
return ActivityResponse{
Activities: activities,
User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin},
}, nil
}
// GetSettings handles GET /api/v1/settings
func (s *Server) GetSettings(ctx context.Context, req SettingsRequest) (SettingsResponse, error) {
auth := getAuthFromContext(ctx)
if auth == nil {
return SettingsResponse{}, &apiError{status: http.StatusUnauthorized, message: "Unauthorized"}
}
user, err := s.db.Queries.GetUser(ctx, auth.UserName)
if err != nil {
return SettingsResponse{}, &apiError{status: http.StatusInternalServerError, message: err.Error()}
}
return SettingsResponse{
Settings: []database.Setting{},
User: UserData{Username: auth.UserName, IsAdmin: auth.IsAdmin},
Timezone: user.Timezone,
}, nil
}
// getAuthFromContext extracts authData from context
func getAuthFromContext(ctx context.Context) *authData {
auth, ok := ctx.Value("auth").(authData)
if !ok {
return nil
}
return &auth
}
// apiError represents an API error with status code
type apiError struct {
status int
message string
}
// Error implements error interface
func (e *apiError) Error() string {
return e.message
}
// handlerFunc is a generic API handler function
type handlerFunc[T, R any] func(context.Context, T) (R, error)
// requestParser parses an HTTP request into a request struct
type requestParser[T any] func(*http.Request) T
// handle wraps an API handler function with HTTP response writing
func handle[T, R any](fn handlerFunc[T, R], parser requestParser[T]) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
req := parser(r)
resp, err := fn(r.Context(), req)
if err != nil {
if apiErr, ok := err.(*apiError); ok {
writeJSONError(w, apiErr.status, apiErr.message)
} else {
writeJSONError(w, http.StatusInternalServerError, err.Error())
}
return
}
writeJSON(w, http.StatusOK, resp)
}
}
// parseDocumentRequest extracts document request from HTTP request
func parseDocumentRequest(r *http.Request) DocumentRequest {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/documents/")
id := strings.TrimPrefix(path, "/")
return DocumentRequest{ID: id}
}
// parseDocumentListRequest extracts document list request from URL query
func parseDocumentListRequest(r *http.Request) DocumentListRequest {
query := r.URL.Query()
page, _ := strconv.ParseInt(query.Get("page"), 10, 64)
if page == 0 {
page = 1
}
limit, _ := strconv.ParseInt(query.Get("limit"), 10, 64)
if limit == 0 {
limit = 9
}
search := query.Get("search")
var searchPtr *string
if search != "" {
searchPtr = ptrOf("%" + search + "%")
}
return DocumentListRequest{
Page: page,
Limit: limit,
Search: searchPtr,
}
}
// parseProgressRequest extracts progress request from HTTP request
func parseProgressRequest(r *http.Request) ProgressRequest {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/progress/")
id := strings.TrimPrefix(path, "/")
return ProgressRequest{ID: id}
}
// parseActivityRequest extracts activity request from HTTP request
func parseActivityRequest(r *http.Request) ActivityRequest {
return ActivityRequest{
DocFilter: false,
DocumentID: "",
Offset: 0,
Limit: 100,
}
}
// parseSettingsRequest extracts settings request from HTTP request
func parseSettingsRequest(r *http.Request) SettingsRequest {
return SettingsRequest{}
}