diff --git a/code/backend/cmd/server/main.go b/code/backend/cmd/server/main.go index becbeee..b8ada40 100644 --- a/code/backend/cmd/server/main.go +++ b/code/backend/cmd/server/main.go @@ -81,6 +81,7 @@ func main() { }) app.POST("/user", apiCfg.HandlerUser) app.POST("/log", auth.MiddlewareAuth(apiCfg.LogHandler)) + app.GET("/logs", auth.MiddlewareAuth(apiCfg.GetUserLogsHandler)) app.GET("/parsers", apiCfg.ListParsersHandler) app.GET("/parsers/:id", apiCfg.GetParserHandler) app.PATCH("/parsers/:id", apiCfg.UpdateParserHandler) diff --git a/code/backend/internal/api/get_user_logs.go b/code/backend/internal/api/get_user_logs.go new file mode 100644 index 0000000..028115a --- /dev/null +++ b/code/backend/internal/api/get_user_logs.go @@ -0,0 +1,84 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/EleventhHour-Projects/GoLogGo/code/backend/internal/auth" + "github.com/nottechdm/notnet/pkg/notnet" +) + +// GetUserLogsHandler returns paginated requests + stats for the authenticated user. +// GET /logs?page=1&limit=25&status=all +func (api *Config) GetUserLogsHandler(req *notnet.Request, res *notnet.Response, claims *auth.CustomClaims) error { + ctx := req.HTTPRequest.Context() + + // Parse query params + query := req.HTTPRequest.URL.Query() + page, _ := strconv.ParseInt(query.Get("page"), 10, 64) + if page < 1 { + page = 1 + } + limit, _ := strconv.ParseInt(query.Get("limit"), 10, 64) + if limit < 1 || limit > 100 { + limit = 25 + } + status := query.Get("status") // e.g. "completed", "pending", "" / "all" + + skip := (page - 1) * limit + + // Fetch stats + stats, err := api.Reqs.GetUserStats(ctx, claims.UserID) + if err != nil { + return res.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to fetch user stats", + }) + } + + // Fetch paginated requests + reqs, total, err := api.Reqs.GetUserRequests(ctx, claims.UserID, limit, skip, status) + if err != nil { + return res.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to fetch requests", + }) + } + + // For completed requests, fetch their parsed logs + type ReqRow struct { + ID string `json:"id"` + Payload string `json:"payload"` + Status string `json:"status"` + Attempts int `json:"attempts"` + CreatedAt interface{} `json:"createdAt"` + UpdatedAt interface{} `json:"updatedAt"` + Logs interface{} `json:"logs,omitempty"` + } + + rows := make([]ReqRow, 0, len(reqs)) + for _, r := range reqs { + row := ReqRow{ + ID: r.ID.Hex(), + Payload: string(r.Payload), + Status: string(r.Status), + Attempts: r.Attempts, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + } + if r.Status == "completed" { + logs, err := api.Reqs.GetLogsByReqID(ctx, r.ID) + if err == nil { + row.Logs = logs + } + } + rows = append(rows, row) + } + + return res.JSON(http.StatusOK, map[string]interface{}{ + "stats": stats, + "requests": rows, + "total": total, + "page": page, + "limit": limit, + "totalPages": (total + limit - 1) / limit, + }) +} diff --git a/code/backend/internal/database/get_user_logs.go b/code/backend/internal/database/get_user_logs.go new file mode 100644 index 0000000..d87a253 --- /dev/null +++ b/code/backend/internal/database/get_user_logs.go @@ -0,0 +1,135 @@ +package database + +import ( + "context" + "time" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +// ReqWithLogs is a joined view of a request and its parsed logs. +type ReqWithLogs struct { + Req Req `json:"req"` + Logs []Log `json:"logs"` +} + +// GetUserRequests returns paginated requests belonging to a user, newest first. +// Pass a zero ObjectID to get requests across all users (admin use). +func (m *MongoDB) GetUserRequests( + ctx context.Context, + userID bson.ObjectID, + limit, skip int64, + status string, +) ([]Req, int64, error) { + col := m.Collections[CollectionRequests] + + filter := bson.M{"userId": userID} + if status != "" && status != "all" { + filter["status"] = ReqestStatus(status) + } + + total, err := col.CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + opts := options.Find(). + SetSort(bson.D{{Key: "createdAt", Value: -1}}). + SetLimit(limit). + SetSkip(skip) + + cursor, err := col.Find(ctx, filter, opts) + if err != nil { + return nil, 0, err + } + defer cursor.Close(ctx) + + var reqs []Req + for cursor.Next(ctx) { + var r Req + if err := cursor.Decode(&r); err != nil { + return nil, 0, err + } + reqs = append(reqs, r) + } + if reqs == nil { + reqs = []Req{} + } + return reqs, total, cursor.Err() +} + +// GetLogsByReqID returns all parsed log entries for a given request ID. +func (m *MongoDB) GetLogsByReqID(ctx context.Context, reqID bson.ObjectID) ([]Log, error) { + col := m.Collections[CollectionLogs] + + cursor, err := col.Find(ctx, bson.M{"reqId": reqID}) + if err != nil { + return nil, err + } + defer cursor.Close(ctx) + + var logs []Log + for cursor.Next(ctx) { + var l Log + if err := cursor.Decode(&l); err != nil { + return nil, err + } + logs = append(logs, l) + } + if logs == nil { + logs = []Log{} + } + return logs, cursor.Err() +} + +// GetUserStats returns counts of requests per status for a given user. +type UserStats struct { + Total int64 `json:"total"` + Pending int64 `json:"pending"` + Processing int64 `json:"processing"` + WaitingParser int64 `json:"waitingParser"` + Completed int64 `json:"completed"` + Failed int64 `json:"failed"` + LastActivityAt time.Time `json:"lastActivityAt"` +} + +func (m *MongoDB) GetUserStats(ctx context.Context, userID bson.ObjectID) (*UserStats, error) { + col := m.Collections[CollectionRequests] + + statuses := []ReqestStatus{ + StatusPending, StatusProcessing, StatusWaitingParser, StatusCompleted, StatusFailed, + } + + stats := &UserStats{} + for _, s := range statuses { + n, err := col.CountDocuments(ctx, bson.M{"userId": userID, "status": s}) + if err != nil { + return nil, err + } + switch s { + case StatusPending: + stats.Pending = n + case StatusProcessing: + stats.Processing = n + case StatusWaitingParser: + stats.WaitingParser = n + case StatusCompleted: + stats.Completed = n + case StatusFailed: + stats.Failed = n + } + stats.Total += n + } + + // Grab the most recent request timestamp + var latest struct { + CreatedAt time.Time `bson:"createdAt"` + } + opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: -1}}) + if err := col.FindOne(ctx, bson.M{"userId": userID}, opts).Decode(&latest); err == nil { + stats.LastActivityAt = latest.CreatedAt + } + + return stats, nil +} diff --git a/code/frontend/gologgo-client/app/api/logs/route.js b/code/frontend/gologgo-client/app/api/logs/route.js index f86422d..f734052 100644 --- a/code/frontend/gologgo-client/app/api/logs/route.js +++ b/code/frontend/gologgo-client/app/api/logs/route.js @@ -56,3 +56,51 @@ export async function POST(request) { ); } } + +export async function GET(request) { + try { + const cookieStore = await cookies(); + const tokenCookie = cookieStore.get('tokenString'); + const token = tokenCookie?.value; + + if (!token) { + return NextResponse.json( + { error: 'Unauthorized: No token found' }, + { status: 401 } + ); + } + + // Forward query params (page, limit, status) to the Go backend + const { searchParams } = new URL(request.url); + const queryString = searchParams.toString(); + + const backendRes = await fetch( + `${BACKEND_URL}/logs${queryString ? `?${queryString}` : ''}`, + { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + }, + // No caching — always fresh + cache: 'no-store', + } + ); + + if (!backendRes.ok) { + const errorText = await backendRes.text(); + return NextResponse.json( + { error: 'Failed to fetch logs from backend', details: errorText }, + { status: backendRes.status } + ); + } + + const data = await backendRes.json(); + return NextResponse.json(data); + } catch (error) { + console.error('Error in GET /api/logs:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/code/frontend/gologgo-client/app/layout.jsx b/code/frontend/gologgo-client/app/layout.jsx index c6296ba..71021c6 100644 --- a/code/frontend/gologgo-client/app/layout.jsx +++ b/code/frontend/gologgo-client/app/layout.jsx @@ -1,4 +1,5 @@ import { Analytics } from '@vercel/analytics/next' +import { Toaster } from 'sonner' import './globals.css' export const metadata = { @@ -40,6 +41,7 @@ export default function RootLayout({ children }) {
{children} +