From f9da45da6226bb0173f7a955cf39697ed260070c Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 6 Mar 2026 06:03:05 -0800 Subject: [PATCH] minor fixes --- db/query/user.sql | 7 ++++ docker-compose.yml | 2 +- gen/db/user.sql.go | 21 ++++++++++ internal/domain/user.go | 6 +++ internal/ports/user.go | 1 + internal/repository/user.go | 13 ++++++ internal/services/user/direct.go | 4 ++ internal/services/vimeo/service.go | 16 ++++++++ internal/web_server/handlers/user.go | 27 +++++++++++++ internal/web_server/handlers/vimeo.go | 57 +++++++++++++++++++++++++++ internal/web_server/routes.go | 2 + 11 files changed, 155 insertions(+), 1 deletion(-) diff --git a/db/query/user.sql b/db/query/user.sql index 88b0c1d..906aa8e 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -376,6 +376,13 @@ SET updated_at = CURRENT_TIMESTAMP WHERE id = $2; +-- name: GetUserSummary :one +SELECT + COUNT(*) AS total_users, + COUNT(*) FILTER (WHERE status = 'ACTIVE') AS active_users, + COUNT(*) FILTER (WHERE created_at >= date_trunc('month', CURRENT_DATE)) AS joined_this_month +FROM users; + -- name: UpdateUserKnowledgeLevel :exec UPDATE users SET diff --git a/docker-compose.yml b/docker-compose.yml index 57075f5..48f375d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: container_name: yimaru-backend-postgres-1 image: postgres:16-alpine ports: - - "5432:5422" + - "5592:5422" environment: - POSTGRES_PASSWORD=secret - POSTGRES_USER=root diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index 4dac35c..a4b0c9d 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -764,6 +764,27 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) { return i, err } +const GetUserSummary = `-- name: GetUserSummary :one +SELECT + COUNT(*) AS total_users, + COUNT(*) FILTER (WHERE status = 'ACTIVE') AS active_users, + COUNT(*) FILTER (WHERE created_at >= date_trunc('month', CURRENT_DATE)) AS joined_this_month +FROM users +` + +type GetUserSummaryRow struct { + TotalUsers int64 `json:"total_users"` + ActiveUsers int64 `json:"active_users"` + JoinedThisMonth int64 `json:"joined_this_month"` +} + +func (q *Queries) GetUserSummary(ctx context.Context) (GetUserSummaryRow, error) { + row := q.db.QueryRow(ctx, GetUserSummary) + var i GetUserSummaryRow + err := row.Scan(&i.TotalUsers, &i.ActiveUsers, &i.JoinedThisMonth) + return i, err +} + const IsUserNameUnique = `-- name: IsUserNameUnique :one SELECT CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique diff --git a/internal/domain/user.go b/internal/domain/user.go index e4e1dd2..53d57cb 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -181,6 +181,12 @@ type UpdateUserStatusReq struct { UserID int64 } +type UserSummary struct { + TotalUsers int64 `json:"total_users"` + ActiveUsers int64 `json:"active_users"` + JoinedThisMonth int64 `json:"joined_this_month"` +} + type UpdateUserReq struct { UserID int64 `json:"-"` diff --git a/internal/ports/user.go b/internal/ports/user.go index 3e8c372..ad2ce6e 100644 --- a/internal/ports/user.go +++ b/internal/ports/user.go @@ -58,6 +58,7 @@ type UserStore interface { limit, offset int32, ) ([]domain.User, int64, error) GetTotalUsers(ctx context.Context, role *string) (int64, error) + GetUserSummary(ctx context.Context) (domain.UserSummary, error) SearchUserByNameOrPhone( ctx context.Context, search string, diff --git a/internal/repository/user.go b/internal/repository/user.go index 8d80475..6fa7e02 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -515,6 +515,19 @@ func (s *Store) GetAllUsers( } // GetTotalUsers counts users with optional filters +func (s *Store) GetUserSummary(ctx context.Context) (domain.UserSummary, error) { + res, err := s.queries.GetUserSummary(ctx) + if err != nil { + return domain.UserSummary{}, err + } + + return domain.UserSummary{ + TotalUsers: res.TotalUsers, + ActiveUsers: res.ActiveUsers, + JoinedThisMonth: res.JoinedThisMonth, + }, nil +} + func (s *Store) GetTotalUsers(ctx context.Context, role *string) (int64, error) { count, err := s.queries.GetTotalUsers(ctx, *role) if err != nil { diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index 4c8895a..e62162d 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -91,6 +91,10 @@ func (s *Service) GetAllUsers( ) } +func (s *Service) GetUserSummary(ctx context.Context) (domain.UserSummary, error) { + return s.userStore.GetUserSummary(ctx) +} + func (s *Service) UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error { return s.userStore.UpdateUserStatus(ctx, req) } diff --git a/internal/services/vimeo/service.go b/internal/services/vimeo/service.go index 25750ef..dc322c7 100644 --- a/internal/services/vimeo/service.go +++ b/internal/services/vimeo/service.go @@ -195,3 +195,19 @@ func (s *Service) GetOEmbed(ctx context.Context, vimeoURL string, width, height func (s *Service) GeneratePlayerURL(videoID string, opts *vimeo.EmbedOptions) string { return vimeo.GenerateEmbedURL(videoID, opts) } + +// GetSampleVideo fetches a public Vimeo video by ID and returns its info along with an embeddable iframe. +func (s *Service) GetSampleVideo(ctx context.Context, videoID string, width, height int) (*VideoInfo, string, error) { + info, err := s.GetVideoInfo(ctx, videoID) + if err != nil { + return nil, "", fmt.Errorf("failed to get sample video: %w", err) + } + + iframe := vimeo.GenerateIframeEmbed(videoID, width, height, &vimeo.EmbedOptions{ + Title: true, + Byline: true, + Portrait: true, + }) + + return info, iframe, nil +} diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 2f80c57..9ae4df4 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -22,6 +22,33 @@ import ( "go.uber.org/zap" ) +// GetUserSummary godoc +// @Summary Get user summary statistics +// @Description Returns total users, active users, and users who joined this month +// @Tags user +// @Accept json +// @Produce json +// @Security Bearer +// @Success 200 {object} domain.Response{data=domain.UserSummary} +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/users/summary [get] +func (h *Handler) GetUserSummary(c *fiber.Ctx) error { + summary, err := h.userSvc.GetUserSummary(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to get user summary", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "User summary retrieved successfully", + Data: summary, + Success: true, + StatusCode: fiber.StatusOK, + }) +} + // CheckProfileCompleted godoc // @Summary Check if user profile is completed // @Description Returns the profile completion status and percentage for the specified user diff --git a/internal/web_server/handlers/vimeo.go b/internal/web_server/handlers/vimeo.go index 72b793b..08351ab 100644 --- a/internal/web_server/handlers/vimeo.go +++ b/internal/web_server/handlers/vimeo.go @@ -378,6 +378,63 @@ func (h *Handler) GetTranscodeStatus(c *fiber.Ctx) error { }) } +// GetSampleVideo godoc +// @Summary Get a sample Vimeo video with iframe embed +// @Description Fetches a sample video from Vimeo and returns video details along with an embeddable iframe for client-side integration +// @Tags Vimeo +// @Accept json +// @Produce json +// @Param video_id query string false "Vimeo Video ID to use as sample" default(76979871) +// @Param width query int false "Player width" default(640) +// @Param height query int false "Player height" default(360) +// @Success 200 {object} domain.Response +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/vimeo/sample [get] +func (h *Handler) GetSampleVideo(c *fiber.Ctx) error { + if h.vimeoSvc == nil { + return c.Status(fiber.StatusServiceUnavailable).JSON(domain.ErrorResponse{ + Message: "Vimeo service is not configured", + Error: "Vimeo service is not enabled or missing access token", + }) + } + + videoID := c.Query("video_id", "76979871") + width, _ := strconv.Atoi(c.Query("width", "640")) + height, _ := strconv.Atoi(c.Query("height", "360")) + + info, iframe, err := h.vimeoSvc.GetSampleVideo(c.Context(), videoID, width, height) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to get sample video", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Sample video retrieved successfully", + Data: fiber.Map{ + "video": VimeoVideoResponse{ + VimeoID: info.VimeoID, + URI: info.URI, + Name: info.Name, + Description: info.Description, + Duration: info.Duration, + Width: info.Width, + Height: info.Height, + Link: info.Link, + EmbedURL: info.EmbedURL, + EmbedHTML: info.EmbedHTML, + ThumbnailURL: info.ThumbnailURL, + Status: info.Status, + TranscodeStatus: info.TranscodeStatus, + }, + "iframe": iframe, + }, + Success: true, + StatusCode: fiber.StatusOK, + }) +} + // GetOEmbed godoc // @Summary Get oEmbed data for a Vimeo URL // @Description Fetches oEmbed metadata for a Vimeo video URL diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 4ae192c..3c47a60 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -209,6 +209,7 @@ func (a *App) initAppRoutes() { // User Routes groupV1.Get("/user/:user_id/is-profile-completed", a.authMiddleware, a.RequirePermission("users.profile_completed"), h.CheckProfileCompleted) groupV1.Get("/users", a.authMiddleware, a.RequirePermission("users.list"), h.GetAllUsers) + groupV1.Get("/users/summary", a.authMiddleware, a.RequirePermission("users.summary"), h.GetUserSummary) groupV1.Put("/user", a.authMiddleware, a.RequirePermission("users.update_self"), h.UpdateUser) groupV1.Patch("/user/status", a.authMiddleware, a.RequirePermission("users.update_status"), h.UpdateUserStatus) groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel) @@ -292,6 +293,7 @@ func (a *App) initAppRoutes() { vimeoGroup.Post("/uploads/pull", a.authMiddleware, a.RequirePermission("vimeo.uploads.pull"), h.CreatePullUpload) vimeoGroup.Post("/uploads/tus", a.authMiddleware, a.RequirePermission("vimeo.uploads.tus"), h.CreateTusUpload) vimeoGroup.Get("/oembed", h.GetOEmbed) + vimeoGroup.Get("/sample", h.GetSampleVideo) // Team Management teamGroup := groupV1.Group("/team")