Add paginated Vimeo video list API (GET /me/videos).

Exposes the Vimeo account library for admin workflows and syncs swagger docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yared Yemane 2026-05-17 22:23:50 -07:00
parent 9afc9a4392
commit 7f8ef3373c
8 changed files with 1722 additions and 34 deletions

View File

@ -702,6 +702,64 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/analytics/dashboard": {
"get": {
"description": "Platform analytics with optional date filters: all-time (default), year, year+month, or custom from/to range.",
"produces": [
"application/json"
],
"tags": [
"analytics"
],
"summary": "Analytics dashboard",
"parameters": [
{
"type": "integer",
"description": "Calendar year (e.g. 2025)",
"name": "year",
"in": "query"
},
{
"type": "integer",
"description": "Calendar month 1-12 (requires year)",
"name": "month",
"in": "query"
},
{
"type": "string",
"description": "Custom range start (YYYY-MM-DD or RFC3339)",
"name": "from",
"in": "query"
},
{
"type": "string",
"description": "Custom range end (YYYY-MM-DD or RFC3339, inclusive)",
"name": "to",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.AnalyticsDashboard"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/assessment/questions": { "/api/v1/assessment/questions": {
"get": { "get": {
"description": "Returns all active assessment questions from the initial assessment set", "description": "Returns all active assessment questions from the initial assessment set",
@ -8701,6 +8759,105 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/vimeo/videos": {
"get": {
"description": "Returns a paginated list of videos for the Vimeo API token (GET https://api.vimeo.com/me/videos)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Vimeo"
],
"summary": "List videos stored in the Vimeo account",
"parameters": [
{
"type": "integer",
"default": 1,
"description": "Page number (starts at 1)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 25,
"description": "Page size (Vimeo max 100)",
"name": "per_page",
"in": "query"
},
{
"type": "string",
"description": "Search query",
"name": "query",
"in": "query"
},
{
"type": "string",
"description": "Sort field (e.g. date, alphabetical, plays, likes, comments, duration, relevance)",
"name": "sort",
"in": "query"
},
{
"type": "string",
"description": "asc or desc",
"name": "direction",
"in": "query"
},
{
"type": "string",
"description": "Vimeo filter (e.g. embeddable, playable)",
"name": "filter",
"in": "query"
},
{
"type": "string",
"description": "Vimeo filter_type when using filter",
"name": "filter_type",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.VimeoVideoResponse"
}
},
"metadata": {
"$ref": "#/definitions/handlers.VimeoVideosListMetadata"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/vimeo/videos/{video_id}": { "/api/v1/vimeo/videos/{video_id}": {
"get": { "get": {
"description": "Retrieves video details from Vimeo by video ID", "description": "Retrieves video details from Vimeo by video ID",
@ -9455,6 +9612,375 @@ const docTemplate = `{
"Age55Plus" "Age55Plus"
] ]
}, },
"domain.AnalyticsContentSection": {
"type": "object",
"properties": {
"question_sets_by_type": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"questions_by_type": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"total_question_sets": {
"type": "integer"
},
"total_questions": {
"type": "integer"
}
}
},
"domain.AnalyticsCoursesSection": {
"type": "object",
"properties": {
"total_categories": {
"type": "integer"
},
"total_courses": {
"type": "integer"
},
"total_sub_courses": {
"type": "integer"
},
"total_videos": {
"type": "integer"
}
}
},
"domain.AnalyticsDashboard": {
"type": "object",
"properties": {
"content": {
"$ref": "#/definitions/domain.AnalyticsContentSection"
},
"courses": {
"$ref": "#/definitions/domain.AnalyticsCoursesSection"
},
"date_filter": {
"$ref": "#/definitions/domain.AnalyticsDateFilter"
},
"generated_at": {
"type": "string"
},
"issues": {
"$ref": "#/definitions/domain.AnalyticsIssuesSection"
},
"notifications": {
"$ref": "#/definitions/domain.AnalyticsNotificationsSection"
},
"payments": {
"$ref": "#/definitions/domain.AnalyticsPaymentsSection"
},
"subscriptions": {
"$ref": "#/definitions/domain.AnalyticsSubscriptionsSection"
},
"team": {
"$ref": "#/definitions/domain.AnalyticsTeamSection"
},
"users": {
"$ref": "#/definitions/domain.AnalyticsUsersSection"
}
}
},
"domain.AnalyticsDateFilter": {
"type": "object",
"properties": {
"from": {
"type": "string"
},
"mode": {
"type": "string"
},
"month": {
"type": "integer"
},
"range_end": {
"type": "string"
},
"range_start": {
"type": "string"
},
"ref_date": {
"type": "string"
},
"series_end": {
"type": "string"
},
"series_start": {
"type": "string"
},
"to": {
"type": "string"
},
"year": {
"type": "integer"
}
}
},
"domain.AnalyticsIssuesSection": {
"type": "object",
"properties": {
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_type": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"resolution_rate": {
"type": "number"
},
"resolved_issues": {
"type": "integer"
},
"total_issues": {
"type": "integer"
}
}
},
"domain.AnalyticsLabelAmount": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"count": {
"type": "integer"
},
"label": {
"type": "string"
}
}
},
"domain.AnalyticsLabelCount": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"label": {
"type": "string"
}
}
},
"domain.AnalyticsNotificationsSection": {
"type": "object",
"properties": {
"by_channel": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_type": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"read_count": {
"type": "integer"
},
"total_sent": {
"type": "integer"
},
"unread_count": {
"type": "integer"
}
}
},
"domain.AnalyticsPaymentsSection": {
"type": "object",
"properties": {
"avg_transaction_value": {
"type": "number"
},
"by_method": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelAmount"
}
},
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelAmount"
}
},
"revenue_last_30_days": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsRevenueTimePoint"
}
},
"successful_payments": {
"type": "integer"
},
"total_payments": {
"type": "integer"
},
"total_revenue": {
"type": "number"
}
}
},
"domain.AnalyticsRevenueByPlan": {
"type": "object",
"properties": {
"currency": {
"type": "string"
},
"plan_name": {
"type": "string"
},
"revenue": {
"type": "number"
}
}
},
"domain.AnalyticsRevenueTimePoint": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"revenue": {
"type": "number"
}
}
},
"domain.AnalyticsSubscriptionsSection": {
"type": "object",
"properties": {
"active_subscriptions": {
"type": "integer"
},
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"new_month": {
"type": "integer"
},
"new_subscriptions_last_30_days": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsTimePoint"
}
},
"new_today": {
"type": "integer"
},
"new_week": {
"type": "integer"
},
"revenue_by_plan": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsRevenueByPlan"
}
},
"total_subscriptions": {
"type": "integer"
}
}
},
"domain.AnalyticsTeamSection": {
"type": "object",
"properties": {
"by_role": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"total_members": {
"type": "integer"
}
}
},
"domain.AnalyticsTimePoint": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"date": {
"type": "string"
}
}
},
"domain.AnalyticsUsersSection": {
"type": "object",
"properties": {
"by_age_group": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_knowledge_level": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_region": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_role": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"new_month": {
"type": "integer"
},
"new_today": {
"type": "integer"
},
"new_week": {
"type": "integer"
},
"registrations_last_30_days": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsTimePoint"
}
},
"total_users": {
"type": "integer"
}
}
},
"domain.CreateCourseInput": { "domain.CreateCourseInput": {
"type": "object", "type": "object",
"required": [ "required": [
@ -11198,6 +11724,35 @@ const docTemplate = `{
} }
} }
}, },
"handlers.VimeoVideosListMetadata": {
"type": "object",
"properties": {
"current_page": {
"type": "integer"
},
"first": {
"type": "string"
},
"last": {
"type": "string"
},
"limit": {
"type": "integer"
},
"next": {
"type": "string"
},
"previous": {
"type": "string"
},
"total": {
"type": "integer"
},
"total_pages": {
"type": "integer"
}
}
},
"handlers.addQuestionToSetReq": { "handlers.addQuestionToSetReq": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -694,6 +694,64 @@
} }
} }
}, },
"/api/v1/analytics/dashboard": {
"get": {
"description": "Platform analytics with optional date filters: all-time (default), year, year+month, or custom from/to range.",
"produces": [
"application/json"
],
"tags": [
"analytics"
],
"summary": "Analytics dashboard",
"parameters": [
{
"type": "integer",
"description": "Calendar year (e.g. 2025)",
"name": "year",
"in": "query"
},
{
"type": "integer",
"description": "Calendar month 1-12 (requires year)",
"name": "month",
"in": "query"
},
{
"type": "string",
"description": "Custom range start (YYYY-MM-DD or RFC3339)",
"name": "from",
"in": "query"
},
{
"type": "string",
"description": "Custom range end (YYYY-MM-DD or RFC3339, inclusive)",
"name": "to",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.AnalyticsDashboard"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/assessment/questions": { "/api/v1/assessment/questions": {
"get": { "get": {
"description": "Returns all active assessment questions from the initial assessment set", "description": "Returns all active assessment questions from the initial assessment set",
@ -8693,6 +8751,105 @@
} }
} }
}, },
"/api/v1/vimeo/videos": {
"get": {
"description": "Returns a paginated list of videos for the Vimeo API token (GET https://api.vimeo.com/me/videos)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Vimeo"
],
"summary": "List videos stored in the Vimeo account",
"parameters": [
{
"type": "integer",
"default": 1,
"description": "Page number (starts at 1)",
"name": "page",
"in": "query"
},
{
"type": "integer",
"default": 25,
"description": "Page size (Vimeo max 100)",
"name": "per_page",
"in": "query"
},
{
"type": "string",
"description": "Search query",
"name": "query",
"in": "query"
},
{
"type": "string",
"description": "Sort field (e.g. date, alphabetical, plays, likes, comments, duration, relevance)",
"name": "sort",
"in": "query"
},
{
"type": "string",
"description": "asc or desc",
"name": "direction",
"in": "query"
},
{
"type": "string",
"description": "Vimeo filter (e.g. embeddable, playable)",
"name": "filter",
"in": "query"
},
{
"type": "string",
"description": "Vimeo filter_type when using filter",
"name": "filter_type",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"allOf": [
{
"$ref": "#/definitions/domain.Response"
},
{
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handlers.VimeoVideoResponse"
}
},
"metadata": {
"$ref": "#/definitions/handlers.VimeoVideosListMetadata"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/vimeo/videos/{video_id}": { "/api/v1/vimeo/videos/{video_id}": {
"get": { "get": {
"description": "Retrieves video details from Vimeo by video ID", "description": "Retrieves video details from Vimeo by video ID",
@ -9447,6 +9604,375 @@
"Age55Plus" "Age55Plus"
] ]
}, },
"domain.AnalyticsContentSection": {
"type": "object",
"properties": {
"question_sets_by_type": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"questions_by_type": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"total_question_sets": {
"type": "integer"
},
"total_questions": {
"type": "integer"
}
}
},
"domain.AnalyticsCoursesSection": {
"type": "object",
"properties": {
"total_categories": {
"type": "integer"
},
"total_courses": {
"type": "integer"
},
"total_sub_courses": {
"type": "integer"
},
"total_videos": {
"type": "integer"
}
}
},
"domain.AnalyticsDashboard": {
"type": "object",
"properties": {
"content": {
"$ref": "#/definitions/domain.AnalyticsContentSection"
},
"courses": {
"$ref": "#/definitions/domain.AnalyticsCoursesSection"
},
"date_filter": {
"$ref": "#/definitions/domain.AnalyticsDateFilter"
},
"generated_at": {
"type": "string"
},
"issues": {
"$ref": "#/definitions/domain.AnalyticsIssuesSection"
},
"notifications": {
"$ref": "#/definitions/domain.AnalyticsNotificationsSection"
},
"payments": {
"$ref": "#/definitions/domain.AnalyticsPaymentsSection"
},
"subscriptions": {
"$ref": "#/definitions/domain.AnalyticsSubscriptionsSection"
},
"team": {
"$ref": "#/definitions/domain.AnalyticsTeamSection"
},
"users": {
"$ref": "#/definitions/domain.AnalyticsUsersSection"
}
}
},
"domain.AnalyticsDateFilter": {
"type": "object",
"properties": {
"from": {
"type": "string"
},
"mode": {
"type": "string"
},
"month": {
"type": "integer"
},
"range_end": {
"type": "string"
},
"range_start": {
"type": "string"
},
"ref_date": {
"type": "string"
},
"series_end": {
"type": "string"
},
"series_start": {
"type": "string"
},
"to": {
"type": "string"
},
"year": {
"type": "integer"
}
}
},
"domain.AnalyticsIssuesSection": {
"type": "object",
"properties": {
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_type": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"resolution_rate": {
"type": "number"
},
"resolved_issues": {
"type": "integer"
},
"total_issues": {
"type": "integer"
}
}
},
"domain.AnalyticsLabelAmount": {
"type": "object",
"properties": {
"amount": {
"type": "number"
},
"count": {
"type": "integer"
},
"label": {
"type": "string"
}
}
},
"domain.AnalyticsLabelCount": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"label": {
"type": "string"
}
}
},
"domain.AnalyticsNotificationsSection": {
"type": "object",
"properties": {
"by_channel": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_type": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"read_count": {
"type": "integer"
},
"total_sent": {
"type": "integer"
},
"unread_count": {
"type": "integer"
}
}
},
"domain.AnalyticsPaymentsSection": {
"type": "object",
"properties": {
"avg_transaction_value": {
"type": "number"
},
"by_method": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelAmount"
}
},
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelAmount"
}
},
"revenue_last_30_days": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsRevenueTimePoint"
}
},
"successful_payments": {
"type": "integer"
},
"total_payments": {
"type": "integer"
},
"total_revenue": {
"type": "number"
}
}
},
"domain.AnalyticsRevenueByPlan": {
"type": "object",
"properties": {
"currency": {
"type": "string"
},
"plan_name": {
"type": "string"
},
"revenue": {
"type": "number"
}
}
},
"domain.AnalyticsRevenueTimePoint": {
"type": "object",
"properties": {
"date": {
"type": "string"
},
"revenue": {
"type": "number"
}
}
},
"domain.AnalyticsSubscriptionsSection": {
"type": "object",
"properties": {
"active_subscriptions": {
"type": "integer"
},
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"new_month": {
"type": "integer"
},
"new_subscriptions_last_30_days": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsTimePoint"
}
},
"new_today": {
"type": "integer"
},
"new_week": {
"type": "integer"
},
"revenue_by_plan": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsRevenueByPlan"
}
},
"total_subscriptions": {
"type": "integer"
}
}
},
"domain.AnalyticsTeamSection": {
"type": "object",
"properties": {
"by_role": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"total_members": {
"type": "integer"
}
}
},
"domain.AnalyticsTimePoint": {
"type": "object",
"properties": {
"count": {
"type": "integer"
},
"date": {
"type": "string"
}
}
},
"domain.AnalyticsUsersSection": {
"type": "object",
"properties": {
"by_age_group": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_knowledge_level": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_region": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_role": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"by_status": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsLabelCount"
}
},
"new_month": {
"type": "integer"
},
"new_today": {
"type": "integer"
},
"new_week": {
"type": "integer"
},
"registrations_last_30_days": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AnalyticsTimePoint"
}
},
"total_users": {
"type": "integer"
}
}
},
"domain.CreateCourseInput": { "domain.CreateCourseInput": {
"type": "object", "type": "object",
"required": [ "required": [
@ -11190,6 +11716,35 @@
} }
} }
}, },
"handlers.VimeoVideosListMetadata": {
"type": "object",
"properties": {
"current_page": {
"type": "integer"
},
"first": {
"type": "string"
},
"last": {
"type": "string"
},
"limit": {
"type": "integer"
},
"next": {
"type": "string"
},
"previous": {
"type": "string"
},
"total": {
"type": "integer"
},
"total_pages": {
"type": "integer"
}
}
},
"handlers.addQuestionToSetReq": { "handlers.addQuestionToSetReq": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -17,6 +17,247 @@ definitions:
- Age35To44 - Age35To44
- Age45To54 - Age45To54
- Age55Plus - Age55Plus
domain.AnalyticsContentSection:
properties:
question_sets_by_type:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
questions_by_type:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
total_question_sets:
type: integer
total_questions:
type: integer
type: object
domain.AnalyticsCoursesSection:
properties:
total_categories:
type: integer
total_courses:
type: integer
total_sub_courses:
type: integer
total_videos:
type: integer
type: object
domain.AnalyticsDashboard:
properties:
content:
$ref: '#/definitions/domain.AnalyticsContentSection'
courses:
$ref: '#/definitions/domain.AnalyticsCoursesSection'
date_filter:
$ref: '#/definitions/domain.AnalyticsDateFilter'
generated_at:
type: string
issues:
$ref: '#/definitions/domain.AnalyticsIssuesSection'
notifications:
$ref: '#/definitions/domain.AnalyticsNotificationsSection'
payments:
$ref: '#/definitions/domain.AnalyticsPaymentsSection'
subscriptions:
$ref: '#/definitions/domain.AnalyticsSubscriptionsSection'
team:
$ref: '#/definitions/domain.AnalyticsTeamSection'
users:
$ref: '#/definitions/domain.AnalyticsUsersSection'
type: object
domain.AnalyticsDateFilter:
properties:
from:
type: string
mode:
type: string
month:
type: integer
range_end:
type: string
range_start:
type: string
ref_date:
type: string
series_end:
type: string
series_start:
type: string
to:
type: string
year:
type: integer
type: object
domain.AnalyticsIssuesSection:
properties:
by_status:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
by_type:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
resolution_rate:
type: number
resolved_issues:
type: integer
total_issues:
type: integer
type: object
domain.AnalyticsLabelAmount:
properties:
amount:
type: number
count:
type: integer
label:
type: string
type: object
domain.AnalyticsLabelCount:
properties:
count:
type: integer
label:
type: string
type: object
domain.AnalyticsNotificationsSection:
properties:
by_channel:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
by_type:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
read_count:
type: integer
total_sent:
type: integer
unread_count:
type: integer
type: object
domain.AnalyticsPaymentsSection:
properties:
avg_transaction_value:
type: number
by_method:
items:
$ref: '#/definitions/domain.AnalyticsLabelAmount'
type: array
by_status:
items:
$ref: '#/definitions/domain.AnalyticsLabelAmount'
type: array
revenue_last_30_days:
items:
$ref: '#/definitions/domain.AnalyticsRevenueTimePoint'
type: array
successful_payments:
type: integer
total_payments:
type: integer
total_revenue:
type: number
type: object
domain.AnalyticsRevenueByPlan:
properties:
currency:
type: string
plan_name:
type: string
revenue:
type: number
type: object
domain.AnalyticsRevenueTimePoint:
properties:
date:
type: string
revenue:
type: number
type: object
domain.AnalyticsSubscriptionsSection:
properties:
active_subscriptions:
type: integer
by_status:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
new_month:
type: integer
new_subscriptions_last_30_days:
items:
$ref: '#/definitions/domain.AnalyticsTimePoint'
type: array
new_today:
type: integer
new_week:
type: integer
revenue_by_plan:
items:
$ref: '#/definitions/domain.AnalyticsRevenueByPlan'
type: array
total_subscriptions:
type: integer
type: object
domain.AnalyticsTeamSection:
properties:
by_role:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
by_status:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
total_members:
type: integer
type: object
domain.AnalyticsTimePoint:
properties:
count:
type: integer
date:
type: string
type: object
domain.AnalyticsUsersSection:
properties:
by_age_group:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
by_knowledge_level:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
by_region:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
by_role:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
by_status:
items:
$ref: '#/definitions/domain.AnalyticsLabelCount'
type: array
new_month:
type: integer
new_today:
type: integer
new_week:
type: integer
registrations_last_30_days:
items:
$ref: '#/definitions/domain.AnalyticsTimePoint'
type: array
total_users:
type: integer
type: object
domain.CreateCourseInput: domain.CreateCourseInput:
properties: properties:
description: description:
@ -1190,6 +1431,25 @@ definitions:
width: width:
type: integer type: integer
type: object type: object
handlers.VimeoVideosListMetadata:
properties:
current_page:
type: integer
first:
type: string
last:
type: string
limit:
type: integer
next:
type: string
previous:
type: string
total:
type: integer
total_pages:
type: integer
type: object
handlers.addQuestionToSetReq: handlers.addQuestionToSetReq:
properties: properties:
display_order: display_order:
@ -2602,6 +2862,45 @@ paths:
summary: List account deletion requests summary: List account deletion requests
tags: tags:
- user - user
/api/v1/analytics/dashboard:
get:
description: 'Platform analytics with optional date filters: all-time (default),
year, year+month, or custom from/to range.'
parameters:
- description: Calendar year (e.g. 2025)
in: query
name: year
type: integer
- description: Calendar month 1-12 (requires year)
in: query
name: month
type: integer
- description: Custom range start (YYYY-MM-DD or RFC3339)
in: query
name: from
type: string
- description: Custom range end (YYYY-MM-DD or RFC3339, inclusive)
in: query
name: to
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.AnalyticsDashboard'
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Analytics dashboard
tags:
- analytics
/api/v1/assessment/questions: /api/v1/assessment/questions:
get: get:
description: Returns all active assessment questions from the initial assessment description: Returns all active assessment questions from the initial assessment
@ -7824,6 +8123,71 @@ paths:
summary: Create a TUS resumable upload to Vimeo summary: Create a TUS resumable upload to Vimeo
tags: tags:
- Vimeo - Vimeo
/api/v1/vimeo/videos:
get:
consumes:
- application/json
description: Returns a paginated list of videos for the Vimeo API token (GET
https://api.vimeo.com/me/videos)
parameters:
- default: 1
description: Page number (starts at 1)
in: query
name: page
type: integer
- default: 25
description: Page size (Vimeo max 100)
in: query
name: per_page
type: integer
- description: Search query
in: query
name: query
type: string
- description: Sort field (e.g. date, alphabetical, plays, likes, comments,
duration, relevance)
in: query
name: sort
type: string
- description: asc or desc
in: query
name: direction
type: string
- description: Vimeo filter (e.g. embeddable, playable)
in: query
name: filter
type: string
- description: Vimeo filter_type when using filter
in: query
name: filter_type
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/domain.Response'
- properties:
data:
items:
$ref: '#/definitions/handlers.VimeoVideoResponse'
type: array
metadata:
$ref: '#/definitions/handlers.VimeoVideosListMetadata'
type: object
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
"503":
description: Service Unavailable
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List videos stored in the Vimeo account
tags:
- Vimeo
/api/v1/vimeo/videos/{video_id}: /api/v1/vimeo/videos/{video_id}:
delete: delete:
consumes: consumes:

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"time" "time"
) )
@ -141,6 +142,35 @@ type UpdateVideoRequest struct {
Privacy *PrivacyParams `json:"privacy,omitempty"` Privacy *PrivacyParams `json:"privacy,omitempty"`
} }
// ListVideosParams configures GET /me/videos (authenticated users library).
// See https://developer.vimeo.com/api/reference/videos#get_videos
type ListVideosParams struct {
Page int // 1-based; omitted when 0
PerPage int // max 100; omitted when 0
Query string // optional search filter
Sort string // e.g. date, alphabetical, plays, likes, comments, duration, relevance
Direction string // asc or desc
Filter string // optional: embeddable, playable, playable_in_subscription, etc.
FilterType string // optional: 8 for staff picks (when using filter)
}
// ListVideosResponse is the JSON envelope Vimeo returns for list endpoints.
type ListVideosResponse struct {
Total int `json:"total"`
Page int `json:"page"`
PerPage int `json:"per_page"`
Paging PagingLinks `json:"paging"`
Data []Video `json:"data"`
}
// PagingLinks contains cursor URLs for the next/previous page from Vimeo.
type PagingLinks struct {
Next string `json:"next"`
Previous string `json:"previous"`
First string `json:"first"`
Last string `json:"last"`
}
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) { func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
var reqBody io.Reader var reqBody io.Reader
if body != nil { if body != nil {
@ -185,6 +215,55 @@ func (c *Client) GetVideo(ctx context.Context, videoID string) (*Video, error) {
return &video, nil return &video, nil
} }
// ListMyVideos calls GET /me/videos for the tokens Vimeo account.
func (c *Client) ListMyVideos(ctx context.Context, params ListVideosParams) (*ListVideosResponse, error) {
q := url.Values{}
if params.Page > 0 {
q.Set("page", strconv.Itoa(params.Page))
}
if params.PerPage > 0 {
q.Set("per_page", strconv.Itoa(params.PerPage))
}
if params.Query != "" {
q.Set("query", params.Query)
}
if params.Sort != "" {
q.Set("sort", params.Sort)
}
if params.Direction != "" {
q.Set("direction", params.Direction)
}
if params.Filter != "" {
q.Set("filter", params.Filter)
}
if params.FilterType != "" {
q.Set("filter_type", params.FilterType)
}
path := "/me/videos"
if enc := q.Encode(); enc != "" {
path += "?" + enc
}
resp, err := c.doRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to list videos: status %d, body: %s", resp.StatusCode, string(bodyBytes))
}
var out ListVideosResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("failed to decode list videos response: %w", err)
}
return &out, nil
}
func (c *Client) CreateUpload(ctx context.Context, req *UploadRequest) (*UploadResponse, error) { func (c *Client) CreateUpload(ctx context.Context, req *UploadRequest) (*UploadResponse, error) {
resp, err := c.doRequest(ctx, http.MethodPost, "/me/videos", req) resp, err := c.doRequest(ctx, http.MethodPost, "/me/videos", req)
if err != nil { if err != nil {

View File

@ -243,6 +243,7 @@ var AllPermissions = []domain.PermissionSeed{
{Key: "analytics.dashboard", Name: "View Dashboard", Description: "View analytics dashboard", GroupName: "Analytics"}, {Key: "analytics.dashboard", Name: "View Dashboard", Description: "View analytics dashboard", GroupName: "Analytics"},
// Vimeo // Vimeo
{Key: "vimeo.videos.list", Name: "List Vimeo Videos", Description: "List videos in the Vimeo account", GroupName: "Vimeo"},
{Key: "vimeo.videos.get", Name: "Get Vimeo Video", Description: "Get Vimeo video details", GroupName: "Vimeo"}, {Key: "vimeo.videos.get", Name: "Get Vimeo Video", Description: "Get Vimeo video details", GroupName: "Vimeo"},
{Key: "vimeo.videos.embed", Name: "Get Embed Code", Description: "Get Vimeo embed code", GroupName: "Vimeo"}, {Key: "vimeo.videos.embed", Name: "Get Embed Code", Description: "Get Vimeo embed code", GroupName: "Vimeo"},
{Key: "vimeo.videos.status", Name: "Get Transcode Status", Description: "Get Vimeo transcode status", GroupName: "Vimeo"}, {Key: "vimeo.videos.status", Name: "Get Transcode Status", Description: "Get Vimeo transcode status", GroupName: "Vimeo"},
@ -380,7 +381,7 @@ var DefaultRolePermissions = map[string][]string{
"analytics.dashboard", "analytics.dashboard",
// Vimeo // Vimeo
"vimeo.videos.get", "vimeo.videos.embed", "vimeo.videos.status", "vimeo.videos.delete", "vimeo.videos.list", "vimeo.videos.get", "vimeo.videos.embed", "vimeo.videos.status", "vimeo.videos.delete",
"vimeo.uploads.pull", "vimeo.uploads.tus", "vimeo.uploads.pull", "vimeo.uploads.tus",
// Team (full access) // Team (full access)

View File

@ -45,6 +45,15 @@ type UploadResult struct {
Status string Status string
} }
// ListVideosPage is the service result for a paginated Vimeo library query.
type ListVideosPage struct {
Total int
Page int
PerPage int
Paging vimeo.PagingLinks
Videos []*VideoInfo
}
func (s *Service) GetVideoInfo(ctx context.Context, videoID string) (*VideoInfo, error) { func (s *Service) GetVideoInfo(ctx context.Context, videoID string) (*VideoInfo, error) {
video, err := s.client.GetVideo(ctx, videoID) video, err := s.client.GetVideo(ctx, videoID)
if err != nil { if err != nil {
@ -52,6 +61,18 @@ func (s *Service) GetVideoInfo(ctx context.Context, videoID string) (*VideoInfo,
return nil, fmt.Errorf("failed to get video: %w", err) return nil, fmt.Errorf("failed to get video: %w", err)
} }
return s.videoModelToInfo(video, videoID), nil
}
func (s *Service) videoModelToInfo(video *vimeo.Video, fallbackID string) *VideoInfo {
videoID := fallbackID
if videoID == "" {
videoID = vimeo.ExtractVideoID(video.URI)
}
if videoID == "" {
videoID = vimeo.ExtractVideoID(video.Link)
}
info := &VideoInfo{ info := &VideoInfo{
VimeoID: videoID, VimeoID: videoID,
URI: video.URI, URI: video.URI,
@ -66,7 +87,7 @@ func (s *Service) GetVideoInfo(ctx context.Context, videoID string) (*VideoInfo,
if video.PlayerEmbedURL != "" { if video.PlayerEmbedURL != "" {
info.EmbedURL = video.PlayerEmbedURL info.EmbedURL = video.PlayerEmbedURL
} else { } else if videoID != "" {
info.EmbedURL = vimeo.GenerateEmbedURL(videoID, nil) info.EmbedURL = vimeo.GenerateEmbedURL(videoID, nil)
} }
@ -82,7 +103,28 @@ func (s *Service) GetVideoInfo(ctx context.Context, videoID string) (*VideoInfo,
info.TranscodeStatus = video.Transcode.Status info.TranscodeStatus = video.Transcode.Status
} }
return info, nil return info
}
func (s *Service) ListVideos(ctx context.Context, params vimeo.ListVideosParams) (*ListVideosPage, error) {
raw, err := s.client.ListMyVideos(ctx, params)
if err != nil {
s.logger.Error("Failed to list Vimeo videos", zap.Error(err))
return nil, fmt.Errorf("failed to list videos: %w", err)
}
out := &ListVideosPage{
Total: raw.Total,
Page: raw.Page,
PerPage: raw.PerPage,
Paging: raw.Paging,
Videos: make([]*VideoInfo, 0, len(raw.Data)),
}
for i := range raw.Data {
out.Videos = append(out.Videos, s.videoModelToInfo(&raw.Data[i], ""))
}
return out, nil
} }
func (s *Service) CreatePullUpload(ctx context.Context, name, description, sourceURL string, fileSize int64) (*UploadResult, error) { func (s *Service) CreatePullUpload(ctx context.Context, name, description, sourceURL string, fileSize int64) (*UploadResult, error) {

View File

@ -3,6 +3,7 @@ package handlers
import ( import (
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/pkgs/vimeo" "Yimaru-Backend/internal/pkgs/vimeo"
vimeoservice "Yimaru-Backend/internal/services/vimeo"
"fmt" "fmt"
"strconv" "strconv"
@ -62,6 +63,124 @@ type VimeoEmbedResponse struct {
EmbedHTML string `json:"embed_html"` EmbedHTML string `json:"embed_html"`
} }
type VimeoVideosListMetadata struct {
domain.Pagination
Next string `json:"next,omitempty"`
Previous string `json:"previous,omitempty"`
First string `json:"first,omitempty"`
Last string `json:"last,omitempty"`
}
func vimeoVideoInfoToResponse(info *vimeoservice.VideoInfo) VimeoVideoResponse {
return 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,
}
}
// ListVimeoVideos godoc
// @Summary List videos stored in the Vimeo account
// @Description Returns a paginated list of videos for the Vimeo API token (GET https://api.vimeo.com/me/videos)
// @Tags Vimeo
// @Accept json
// @Produce json
// @Param page query int false "Page number (starts at 1)" default(1)
// @Param per_page query int false "Page size (Vimeo max 100)" default(25)
// @Param query query string false "Search query"
// @Param sort query string false "Sort field (e.g. date, alphabetical, plays, likes, comments, duration, relevance)"
// @Param direction query string false "asc or desc"
// @Param filter query string false "Vimeo filter (e.g. embeddable, playable)"
// @Param filter_type query string false "Vimeo filter_type when using filter"
// @Success 200 {object} domain.Response{data=[]handlers.VimeoVideoResponse,metadata=handlers.VimeoVideosListMetadata}
// @Failure 503 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/vimeo/videos [get]
func (h *Handler) ListVimeoVideos(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",
})
}
page, _ := strconv.Atoi(c.Query("page", "1"))
perPage, _ := strconv.Atoi(c.Query("per_page", "25"))
if page < 1 {
page = 1
}
if perPage < 1 {
perPage = 25
}
if perPage > 100 {
perPage = 100
}
params := vimeo.ListVideosParams{
Page: page,
PerPage: perPage,
Query: c.Query("query"),
Sort: c.Query("sort"),
Direction: c.Query("direction"),
Filter: c.Query("filter"),
FilterType: c.Query("filter_type"),
}
pageResult, err := h.vimeoSvc.ListVideos(c.Context(), params)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to list Vimeo videos",
Error: err.Error(),
})
}
items := make([]VimeoVideoResponse, 0, len(pageResult.Videos))
for _, info := range pageResult.Videos {
if info == nil {
continue
}
items = append(items, vimeoVideoInfoToResponse(info))
}
totalPages := 0
if pageResult.PerPage > 0 && pageResult.Total > 0 {
totalPages = (pageResult.Total + pageResult.PerPage - 1) / pageResult.PerPage
}
currentPage := pageResult.Page
if currentPage < 1 {
currentPage = page
}
return c.JSON(domain.Response{
Message: "Vimeo videos listed successfully",
Data: items,
MetaData: VimeoVideosListMetadata{
Pagination: domain.Pagination{
Total: pageResult.Total,
TotalPages: totalPages,
CurrentPage: currentPage,
Limit: pageResult.PerPage,
},
Next: pageResult.Paging.Next,
Previous: pageResult.Paging.Previous,
First: pageResult.Paging.First,
Last: pageResult.Paging.Last,
},
Success: true,
StatusCode: fiber.StatusOK,
})
}
// GetVimeoVideo godoc // GetVimeoVideo godoc
// @Summary Get video information from Vimeo // @Summary Get video information from Vimeo
// @Description Retrieves video details from Vimeo by video ID // @Description Retrieves video details from Vimeo by video ID
@ -99,21 +218,7 @@ func (h *Handler) GetVimeoVideo(c *fiber.Ctx) error {
return c.JSON(domain.Response{ return c.JSON(domain.Response{
Message: "Video retrieved successfully", Message: "Video retrieved successfully",
Data: VimeoVideoResponse{ Data: vimeoVideoInfoToResponse(info),
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,
},
Success: true, Success: true,
StatusCode: fiber.StatusOK, StatusCode: fiber.StatusOK,
}) })
@ -413,21 +518,7 @@ func (h *Handler) GetSampleVideo(c *fiber.Ctx) error {
return c.JSON(domain.Response{ return c.JSON(domain.Response{
Message: "Sample video retrieved successfully", Message: "Sample video retrieved successfully",
Data: fiber.Map{ Data: fiber.Map{
"video": VimeoVideoResponse{ "video": vimeoVideoInfoToResponse(info),
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, "iframe": iframe,
}, },
Success: true, Success: true,

View File

@ -357,6 +357,7 @@ func (a *App) initAppRoutes() {
// Vimeo // Vimeo
vimeoGroup := groupV1.Group("/vimeo") vimeoGroup := groupV1.Group("/vimeo")
vimeoGroup.Get("/videos", a.authMiddleware, a.RequirePermission("vimeo.videos.list"), h.ListVimeoVideos)
vimeoGroup.Get("/videos/:video_id", a.authMiddleware, a.RequirePermission("vimeo.videos.get"), h.GetVimeoVideo) vimeoGroup.Get("/videos/:video_id", a.authMiddleware, a.RequirePermission("vimeo.videos.get"), h.GetVimeoVideo)
vimeoGroup.Get("/videos/:video_id/embed", a.authMiddleware, a.RequirePermission("vimeo.videos.embed"), h.GetEmbedCode) vimeoGroup.Get("/videos/:video_id/embed", a.authMiddleware, a.RequirePermission("vimeo.videos.embed"), h.GetEmbedCode)
vimeoGroup.Get("/videos/:video_id/status", a.authMiddleware, a.RequirePermission("vimeo.videos.status"), h.GetTranscodeStatus) vimeoGroup.Get("/videos/:video_id/status", a.authMiddleware, a.RequirePermission("vimeo.videos.status"), h.GetTranscodeStatus)