cloud convert integration + more advanced activity log + issue reporting + video file management fixes
This commit is contained in:
parent
97c4f3d28f
commit
0f44e63692
15
cmd/main.go
15
cmd/main.go
|
|
@ -22,6 +22,8 @@ import (
|
||||||
"Yimaru-Backend/internal/services/settings"
|
"Yimaru-Backend/internal/services/settings"
|
||||||
"Yimaru-Backend/internal/services/subscriptions"
|
"Yimaru-Backend/internal/services/subscriptions"
|
||||||
"Yimaru-Backend/internal/services/team"
|
"Yimaru-Backend/internal/services/team"
|
||||||
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
|
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
||||||
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
|
@ -364,6 +366,15 @@ func main() {
|
||||||
courseSvc.SetVimeoService(vimeoSvc)
|
courseSvc.SetVimeoService(vimeoSvc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloudConvert service for video compression
|
||||||
|
if cfg.CloudConvert.Enabled && cfg.CloudConvert.APIKey != "" {
|
||||||
|
ccSvc := cloudconvertservice.NewService(cfg.CloudConvert.APIKey, domain.MongoDBLogger)
|
||||||
|
courseSvc.SetCloudConvertService(ccSvc)
|
||||||
|
logger.Info("CloudConvert service initialized")
|
||||||
|
} else {
|
||||||
|
logger.Info("CloudConvert service disabled (CLOUDCONVERT_ENABLED not set or missing API key)")
|
||||||
|
}
|
||||||
|
|
||||||
// Questions service (unified questions system)
|
// Questions service (unified questions system)
|
||||||
questionsSvc := questions.NewService(store)
|
questionsSvc := questions.NewService(store)
|
||||||
|
|
||||||
|
|
@ -386,6 +397,9 @@ func main() {
|
||||||
// santimpaySvc := santimpay.NewSantimPayService(santimpayClient, cfg, transferStore)
|
// santimpaySvc := santimpay.NewSantimPayService(santimpayClient, cfg, transferStore)
|
||||||
// telebirrSvc := telebirr.NewTelebirrService(cfg, transferStore)
|
// telebirrSvc := telebirr.NewTelebirrService(cfg, transferStore)
|
||||||
|
|
||||||
|
// Activity Log service
|
||||||
|
activityLogSvc := activitylogservice.NewService(store, domain.MongoDBLogger)
|
||||||
|
|
||||||
// Initialize and start HTTP server
|
// Initialize and start HTTP server
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
|
|
@ -396,6 +410,7 @@ func main() {
|
||||||
issueReportingSvc,
|
issueReportingSvc,
|
||||||
vimeoSvc,
|
vimeoSvc,
|
||||||
teamSvc,
|
teamSvc,
|
||||||
|
activityLogSvc,
|
||||||
cfg.Port,
|
cfg.Port,
|
||||||
v,
|
v,
|
||||||
settingSvc,
|
settingSvc,
|
||||||
|
|
|
||||||
31
db/data/004_activity_logs_seed.sql
Normal file
31
db/data/004_activity_logs_seed.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
INSERT INTO activity_logs (actor_id, actor_role, action, resource_type, resource_id, message, metadata, ip_address, user_agent, created_at) VALUES
|
||||||
|
(1, 'SUPER_ADMIN', 'CATEGORY_CREATED', 'CATEGORY', 1, 'Created course category: Mathematics', '{"name": "Mathematics"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '30 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'CATEGORY_CREATED', 'CATEGORY', 2, 'Created course category: Science', '{"name": "Science"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '29 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'CATEGORY_CREATED', 'CATEGORY', 3, 'Created course category: Language Arts', '{"name": "Language Arts"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '28 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'COURSE_CREATED', 'COURSE', 1, 'Created course: Algebra Fundamentals', '{"title": "Algebra Fundamentals", "category_id": 1}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '27 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'COURSE_CREATED', 'COURSE', 2, 'Created course: Biology 101', '{"title": "Biology 101", "category_id": 2}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '26 days'),
|
||||||
|
(2, 'ADMIN', 'COURSE_CREATED', 'COURSE', 3, 'Created course: English Grammar', '{"title": "English Grammar", "category_id": 3}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '25 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'SUB_COURSE_CREATED', 'SUB_COURSE', 1, 'Created sub-course: Linear Equations', '{"title": "Linear Equations", "course_id": 1, "level": "BEGINNER"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '24 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'SUB_COURSE_CREATED', 'SUB_COURSE', 2, 'Created sub-course: Quadratic Equations', '{"title": "Quadratic Equations", "course_id": 1, "level": "INTERMEDIATE"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '23 days'),
|
||||||
|
(2, 'ADMIN', 'SUB_COURSE_CREATED', 'SUB_COURSE', 3, 'Created sub-course: Cell Biology', '{"title": "Cell Biology", "course_id": 2, "level": "BEGINNER"}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '22 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'VIDEO_CREATED', 'VIDEO', 1, 'Created video: Introduction to Algebra', '{"title": "Introduction to Algebra", "sub_course_id": 1}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '21 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'VIDEO_UPLOADED', 'VIDEO', 1, 'Uploaded video to Vimeo: Introduction to Algebra', '{"title": "Introduction to Algebra", "vimeo_id": "987654321", "file_size": 52428800}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '21 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'VIDEO_PUBLISHED', 'VIDEO', 1, 'Published video: Introduction to Algebra', '{"title": "Introduction to Algebra"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '20 days'),
|
||||||
|
(2, 'ADMIN', 'VIDEO_CREATED', 'VIDEO', 2, 'Created video: Solving for X', '{"title": "Solving for X", "sub_course_id": 1}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '19 days'),
|
||||||
|
(2, 'ADMIN', 'VIDEO_UPLOADED', 'VIDEO', 2, 'Uploaded video to Vimeo: Solving for X', '{"title": "Solving for X", "vimeo_id": "987654322", "file_size": 41943040}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '19 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'COURSE_UPDATED', 'COURSE', 1, 'Updated course: Algebra Fundamentals', '{"title": "Algebra Fundamentals", "changed_fields": ["description", "thumbnail"]}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '18 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'CATEGORY_UPDATED', 'CATEGORY', 1, 'Updated course category: Mathematics & Statistics', '{"name": "Mathematics & Statistics"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '17 days'),
|
||||||
|
(2, 'ADMIN', 'VIDEO_CREATED', 'VIDEO', 3, 'Created video: Cell Structure Overview', '{"title": "Cell Structure Overview", "sub_course_id": 3}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '15 days'),
|
||||||
|
(2, 'ADMIN', 'VIDEO_UPLOADED', 'VIDEO', 3, 'Uploaded video to Vimeo: Cell Structure Overview', '{"title": "Cell Structure Overview", "vimeo_id": "987654323", "file_size": 73400320}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '15 days'),
|
||||||
|
(2, 'ADMIN', 'VIDEO_PUBLISHED', 'VIDEO', 2, 'Published video: Solving for X', '{"title": "Solving for X"}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '14 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'SUB_COURSE_UPDATED', 'SUB_COURSE', 2, 'Updated sub-course: Quadratic Equations', '{"title": "Quadratic Equations", "changed_fields": ["description"]}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '12 days'),
|
||||||
|
(2, 'ADMIN', 'VIDEO_UPDATED', 'VIDEO', 3, 'Updated video: Cell Structure Overview', '{"title": "Cell Structure Overview", "changed_fields": ["thumbnail", "resolution"]}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '10 days'),
|
||||||
|
(2, 'ADMIN', 'VIDEO_PUBLISHED', 'VIDEO', 3, 'Published video: Cell Structure Overview', '{"title": "Cell Structure Overview"}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '9 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'VIDEO_ARCHIVED', 'VIDEO', 4, 'Archived video ID: 4', '{"id": 4}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '7 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'SETTINGS_UPDATED', 'SETTINGS', NULL, 'Updated global settings', '{"keys": ["site_name", "maintenance_mode"]}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '5 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'TEAM_MEMBER_CREATED', 'TEAM_MEMBER', 3, 'Created team member: John Doe', '{"name": "John Doe", "role": "instructor"}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '4 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'COURSE_CREATED', 'COURSE', 4, 'Created course: Advanced Physics', '{"title": "Advanced Physics", "category_id": 2}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '3 days'),
|
||||||
|
(2, 'ADMIN', 'CATEGORY_DELETED', 'CATEGORY', 5, 'Deleted category ID: 5', '{"id": 5}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '2 days'),
|
||||||
|
(1, 'SUPER_ADMIN', 'SUB_COURSE_DELETED', 'SUB_COURSE', 6, 'Deleted sub-course ID: 6', '{"id": 6}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '1 day'),
|
||||||
|
(2, 'ADMIN', 'VIDEO_DELETED', 'VIDEO', 5, 'Deleted video ID: 5', '{"id": 5}', '10.0.0.55', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', now() - interval '6 hours'),
|
||||||
|
(1, 'SUPER_ADMIN', 'TEAM_MEMBER_UPDATED', 'TEAM_MEMBER', 3, 'Updated team member: John Doe', '{"name": "John Doe", "changed_fields": ["role"]}', '192.168.1.10', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', now() - interval '2 hours');
|
||||||
1
db/migrations/000014_activity_logs.down.sql
Normal file
1
db/migrations/000014_activity_logs.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS activity_logs;
|
||||||
18
db/migrations/000014_activity_logs.up.sql
Normal file
18
db/migrations/000014_activity_logs.up.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_logs (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
actor_id BIGINT NULL,
|
||||||
|
actor_role TEXT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
resource_type TEXT NOT NULL,
|
||||||
|
resource_id BIGINT NULL,
|
||||||
|
message TEXT NULL,
|
||||||
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
ip_address TEXT NULL,
|
||||||
|
user_agent TEXT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs (created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_logs_actor ON activity_logs (actor_id, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_logs_resource ON activity_logs (resource_type, resource_id, created_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_activity_logs_action ON activity_logs (action, created_at DESC);
|
||||||
39
db/query/activity_logs.sql
Normal file
39
db/query/activity_logs.sql
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
-- name: CreateActivityLog :one
|
||||||
|
INSERT INTO activity_logs (
|
||||||
|
actor_id, actor_role, action, resource_type, resource_id,
|
||||||
|
message, metadata, ip_address, user_agent
|
||||||
|
) VALUES (
|
||||||
|
sqlc.narg('actor_id'), sqlc.narg('actor_role'),
|
||||||
|
@action, @resource_type, sqlc.narg('resource_id'),
|
||||||
|
sqlc.narg('message'), COALESCE(sqlc.narg('metadata'), '{}'::jsonb),
|
||||||
|
sqlc.narg('ip_address'), sqlc.narg('user_agent')
|
||||||
|
)
|
||||||
|
RETURNING *;
|
||||||
|
|
||||||
|
-- name: ListActivityLogs :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER() AS total_count,
|
||||||
|
al.id,
|
||||||
|
al.actor_id,
|
||||||
|
al.actor_role,
|
||||||
|
al.action,
|
||||||
|
al.resource_type,
|
||||||
|
al.resource_id,
|
||||||
|
al.message,
|
||||||
|
al.metadata,
|
||||||
|
al.ip_address,
|
||||||
|
al.user_agent,
|
||||||
|
al.created_at
|
||||||
|
FROM activity_logs al
|
||||||
|
WHERE
|
||||||
|
(sqlc.narg('filter_actor_id')::bigint IS NULL OR al.actor_id = sqlc.narg('filter_actor_id'))
|
||||||
|
AND (sqlc.narg('filter_action')::text IS NULL OR al.action = sqlc.narg('filter_action'))
|
||||||
|
AND (sqlc.narg('filter_resource_type')::text IS NULL OR al.resource_type = sqlc.narg('filter_resource_type'))
|
||||||
|
AND (sqlc.narg('filter_resource_id')::bigint IS NULL OR al.resource_id = sqlc.narg('filter_resource_id'))
|
||||||
|
AND (sqlc.narg('filter_after')::timestamptz IS NULL OR al.created_at >= sqlc.narg('filter_after'))
|
||||||
|
AND (sqlc.narg('filter_before')::timestamptz IS NULL OR al.created_at <= sqlc.narg('filter_before'))
|
||||||
|
ORDER BY al.created_at DESC
|
||||||
|
LIMIT @log_limit OFFSET @log_offset;
|
||||||
|
|
||||||
|
-- name: GetActivityLogByID :one
|
||||||
|
SELECT * FROM activity_logs WHERE id = $1;
|
||||||
|
|
@ -35,3 +35,6 @@ WHERE id = $1;
|
||||||
-- name: DeleteReportedIssue :exec
|
-- name: DeleteReportedIssue :exec
|
||||||
DELETE FROM reported_issues
|
DELETE FROM reported_issues
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: GetReportedIssueByID :one
|
||||||
|
SELECT * FROM reported_issues WHERE id = $1;
|
||||||
183
gen/db/activity_logs.sql.go
Normal file
183
gen/db/activity_logs.sql.go
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: activity_logs.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CreateActivityLog = `-- name: CreateActivityLog :one
|
||||||
|
INSERT INTO activity_logs (
|
||||||
|
actor_id, actor_role, action, resource_type, resource_id,
|
||||||
|
message, metadata, ip_address, user_agent
|
||||||
|
) VALUES (
|
||||||
|
$1, $2,
|
||||||
|
$3, $4, $5,
|
||||||
|
$6, COALESCE($7, '{}'::jsonb),
|
||||||
|
$8, $9
|
||||||
|
)
|
||||||
|
RETURNING id, actor_id, actor_role, action, resource_type, resource_id, message, metadata, ip_address, user_agent, created_at
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateActivityLogParams struct {
|
||||||
|
ActorID pgtype.Int8 `json:"actor_id"`
|
||||||
|
ActorRole pgtype.Text `json:"actor_role"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
ResourceID pgtype.Int8 `json:"resource_id"`
|
||||||
|
Message pgtype.Text `json:"message"`
|
||||||
|
Metadata interface{} `json:"metadata"`
|
||||||
|
IpAddress pgtype.Text `json:"ip_address"`
|
||||||
|
UserAgent pgtype.Text `json:"user_agent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateActivityLog(ctx context.Context, arg CreateActivityLogParams) (ActivityLog, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CreateActivityLog,
|
||||||
|
arg.ActorID,
|
||||||
|
arg.ActorRole,
|
||||||
|
arg.Action,
|
||||||
|
arg.ResourceType,
|
||||||
|
arg.ResourceID,
|
||||||
|
arg.Message,
|
||||||
|
arg.Metadata,
|
||||||
|
arg.IpAddress,
|
||||||
|
arg.UserAgent,
|
||||||
|
)
|
||||||
|
var i ActivityLog
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ActorID,
|
||||||
|
&i.ActorRole,
|
||||||
|
&i.Action,
|
||||||
|
&i.ResourceType,
|
||||||
|
&i.ResourceID,
|
||||||
|
&i.Message,
|
||||||
|
&i.Metadata,
|
||||||
|
&i.IpAddress,
|
||||||
|
&i.UserAgent,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetActivityLogByID = `-- name: GetActivityLogByID :one
|
||||||
|
SELECT id, actor_id, actor_role, action, resource_type, resource_id, message, metadata, ip_address, user_agent, created_at FROM activity_logs WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetActivityLogByID(ctx context.Context, id int64) (ActivityLog, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetActivityLogByID, id)
|
||||||
|
var i ActivityLog
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ActorID,
|
||||||
|
&i.ActorRole,
|
||||||
|
&i.Action,
|
||||||
|
&i.ResourceType,
|
||||||
|
&i.ResourceID,
|
||||||
|
&i.Message,
|
||||||
|
&i.Metadata,
|
||||||
|
&i.IpAddress,
|
||||||
|
&i.UserAgent,
|
||||||
|
&i.CreatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListActivityLogs = `-- name: ListActivityLogs :many
|
||||||
|
SELECT
|
||||||
|
COUNT(*) OVER() AS total_count,
|
||||||
|
al.id,
|
||||||
|
al.actor_id,
|
||||||
|
al.actor_role,
|
||||||
|
al.action,
|
||||||
|
al.resource_type,
|
||||||
|
al.resource_id,
|
||||||
|
al.message,
|
||||||
|
al.metadata,
|
||||||
|
al.ip_address,
|
||||||
|
al.user_agent,
|
||||||
|
al.created_at
|
||||||
|
FROM activity_logs al
|
||||||
|
WHERE
|
||||||
|
($1::bigint IS NULL OR al.actor_id = $1)
|
||||||
|
AND ($2::text IS NULL OR al.action = $2)
|
||||||
|
AND ($3::text IS NULL OR al.resource_type = $3)
|
||||||
|
AND ($4::bigint IS NULL OR al.resource_id = $4)
|
||||||
|
AND ($5::timestamptz IS NULL OR al.created_at >= $5)
|
||||||
|
AND ($6::timestamptz IS NULL OR al.created_at <= $6)
|
||||||
|
ORDER BY al.created_at DESC
|
||||||
|
LIMIT $8 OFFSET $7
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListActivityLogsParams struct {
|
||||||
|
FilterActorID pgtype.Int8 `json:"filter_actor_id"`
|
||||||
|
FilterAction pgtype.Text `json:"filter_action"`
|
||||||
|
FilterResourceType pgtype.Text `json:"filter_resource_type"`
|
||||||
|
FilterResourceID pgtype.Int8 `json:"filter_resource_id"`
|
||||||
|
FilterAfter pgtype.Timestamptz `json:"filter_after"`
|
||||||
|
FilterBefore pgtype.Timestamptz `json:"filter_before"`
|
||||||
|
LogOffset int32 `json:"log_offset"`
|
||||||
|
LogLimit int32 `json:"log_limit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListActivityLogsRow struct {
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ActorID pgtype.Int8 `json:"actor_id"`
|
||||||
|
ActorRole pgtype.Text `json:"actor_role"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
ResourceID pgtype.Int8 `json:"resource_id"`
|
||||||
|
Message pgtype.Text `json:"message"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
|
IpAddress pgtype.Text `json:"ip_address"`
|
||||||
|
UserAgent pgtype.Text `json:"user_agent"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListActivityLogs(ctx context.Context, arg ListActivityLogsParams) ([]ListActivityLogsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListActivityLogs,
|
||||||
|
arg.FilterActorID,
|
||||||
|
arg.FilterAction,
|
||||||
|
arg.FilterResourceType,
|
||||||
|
arg.FilterResourceID,
|
||||||
|
arg.FilterAfter,
|
||||||
|
arg.FilterBefore,
|
||||||
|
arg.LogOffset,
|
||||||
|
arg.LogLimit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ListActivityLogsRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListActivityLogsRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.TotalCount,
|
||||||
|
&i.ID,
|
||||||
|
&i.ActorID,
|
||||||
|
&i.ActorRole,
|
||||||
|
&i.Action,
|
||||||
|
&i.ResourceType,
|
||||||
|
&i.ResourceID,
|
||||||
|
&i.Message,
|
||||||
|
&i.Metadata,
|
||||||
|
&i.IpAddress,
|
||||||
|
&i.UserAgent,
|
||||||
|
&i.CreatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
@ -91,6 +91,28 @@ func (q *Queries) DeleteReportedIssue(ctx context.Context, id int64) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetReportedIssueByID = `-- name: GetReportedIssueByID :one
|
||||||
|
SELECT id, user_id, user_role, subject, description, issue_type, status, metadata, created_at, updated_at FROM reported_issues WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetReportedIssueByID(ctx context.Context, id int64) (ReportedIssue, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetReportedIssueByID, id)
|
||||||
|
var i ReportedIssue
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.UserID,
|
||||||
|
&i.UserRole,
|
||||||
|
&i.Subject,
|
||||||
|
&i.Description,
|
||||||
|
&i.IssueType,
|
||||||
|
&i.Status,
|
||||||
|
&i.Metadata,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const ListReportedIssues = `-- name: ListReportedIssues :many
|
const ListReportedIssues = `-- name: ListReportedIssues :many
|
||||||
SELECT id, user_id, user_role, subject, description, issue_type, status, metadata, created_at, updated_at
|
SELECT id, user_id, user_role, subject, description, issue_type, status, metadata, created_at, updated_at
|
||||||
FROM reported_issues
|
FROM reported_issues
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,20 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ActivityLog struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ActorID pgtype.Int8 `json:"actor_id"`
|
||||||
|
ActorRole pgtype.Text `json:"actor_role"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
ResourceID pgtype.Int8 `json:"resource_id"`
|
||||||
|
Message pgtype.Text `json:"message"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
|
IpAddress pgtype.Text `json:"ip_address"`
|
||||||
|
UserAgent pgtype.Text `json:"user_agent"`
|
||||||
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
type Course struct {
|
type Course struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CategoryID int64 `json:"category_id"`
|
CategoryID int64 `json:"category_id"`
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -46,11 +46,8 @@ require (
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
|
||||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
|
||||||
github.com/zeebo/errs v1.4.0 // indirect
|
github.com/zeebo/errs v1.4.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||||
|
|
|
||||||
28
go.sum
28
go.sum
|
|
@ -12,12 +12,16 @@ cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDh
|
||||||
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
|
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
|
||||||
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
|
||||||
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
|
||||||
|
cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=
|
||||||
|
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
|
||||||
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
cloud.google.com/go/longrunning v0.6.7 h1:IGtfDWHhQCgCjwQjV9iiLnUta9LBCo8R9QmAFsS/PrE=
|
||||||
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
|
||||||
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
|
cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=
|
||||||
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
|
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
|
||||||
cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
|
cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw=
|
||||||
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
|
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
|
||||||
|
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
|
||||||
|
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
|
||||||
firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=
|
firebase.google.com/go/v4 v4.19.0 h1:f5NMlC2YHFsncz00c2+ecBr+ZYlRMhKIhj1z8Iz0lD8=
|
||||||
firebase.google.com/go/v4 v4.19.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
|
firebase.google.com/go/v4 v4.19.0/go.mod h1:P7UfBpzc8+Z3MckX79+zsWzKVfpGryr6HLbAe7gCWfs=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
|
@ -25,6 +29,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI=
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0=
|
||||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
|
@ -41,7 +47,6 @@ github.com/amanuelabay/afrosms-go v1.0.6/go.mod h1:5mzzZtWSCDdvQsA0OyYf5CtbdGpl9
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||||
|
|
@ -57,11 +62,15 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1Ig
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
|
||||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
|
github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=
|
||||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
|
||||||
|
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||||
|
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
|
@ -100,7 +109,6 @@ github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAre
|
||||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
|
|
@ -113,6 +121,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||||
|
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||||
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
|
@ -172,18 +182,17 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
|
||||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/resend/resend-go/v2 v2.28.0 h1:ttM1/VZR4fApBv3xI1TneSKi1pbfFsVrq7fXFlHKtj4=
|
github.com/resend/resend-go/v2 v2.28.0 h1:ttM1/VZR4fApBv3xI1TneSKi1pbfFsVrq7fXFlHKtj4=
|
||||||
github.com/resend/resend-go/v2 v2.28.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
|
github.com/resend/resend-go/v2 v2.28.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
|
@ -214,8 +223,6 @@ github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9J
|
||||||
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
|
||||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||||
github.com/twilio/twilio-go v1.28.8 h1:wbFz7Wt4S5mCEaes6FcM/ddcJGIhdjwp/9CHb9e+4fk=
|
|
||||||
github.com/twilio/twilio-go v1.28.8/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw=
|
|
||||||
github.com/twilio/twilio-go v1.30.0 h1:86FBso7jFqpSZ0XC0GKJcEY2KOeUNOFh6zLhTbUMlnc=
|
github.com/twilio/twilio-go v1.30.0 h1:86FBso7jFqpSZ0XC0GKJcEY2KOeUNOFh6zLhTbUMlnc=
|
||||||
github.com/twilio/twilio-go v1.30.0/go.mod h1:QbitvbvtkV77Jn4BABAKVmxabYSjMyQG4tHey9gfPqg=
|
github.com/twilio/twilio-go v1.30.0/go.mod h1:QbitvbvtkV77Jn4BABAKVmxabYSjMyQG4tHey9gfPqg=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
|
@ -254,6 +261,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6h
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
|
@ -339,9 +348,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
|
google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=
|
||||||
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
|
||||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
|
||||||
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
|
||||||
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,18 @@ type VimeoConfig struct {
|
||||||
Enabled bool `mapstructure:"vimeo_enabled"`
|
Enabled bool `mapstructure:"vimeo_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CloudConvertConfig struct {
|
||||||
|
APIKey string `mapstructure:"cloudconvert_api_key"`
|
||||||
|
Enabled bool `mapstructure:"cloudconvert_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
GoogleOAuthClientID string
|
GoogleOAuthClientID string
|
||||||
GoogleOAuthClientSecret string
|
GoogleOAuthClientSecret string
|
||||||
GoogleOAuthRedirectURL string
|
GoogleOAuthRedirectURL string
|
||||||
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
|
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
|
||||||
Vimeo VimeoConfig `mapstructure:"vimeo_config"`
|
Vimeo VimeoConfig `mapstructure:"vimeo_config"`
|
||||||
|
CloudConvert CloudConvertConfig `mapstructure:"cloudconvert_config"`
|
||||||
APP_VERSION string
|
APP_VERSION string
|
||||||
FIXER_API_KEY string
|
FIXER_API_KEY string
|
||||||
FIXER_BASE_URL string
|
FIXER_BASE_URL string
|
||||||
|
|
@ -483,6 +489,13 @@ func (c *Config) loadEnv() error {
|
||||||
}
|
}
|
||||||
c.Vimeo.AccessToken = os.Getenv("VIMEO_ACCESS_TOKEN")
|
c.Vimeo.AccessToken = os.Getenv("VIMEO_ACCESS_TOKEN")
|
||||||
|
|
||||||
|
// CloudConvert configuration
|
||||||
|
cloudConvertEnabled := os.Getenv("CLOUDCONVERT_ENABLED")
|
||||||
|
if cloudConvertEnabled == "true" || cloudConvertEnabled == "1" {
|
||||||
|
c.CloudConvert.Enabled = true
|
||||||
|
}
|
||||||
|
c.CloudConvert.APIKey = os.Getenv("CLOUDCONVERT_API_KEY")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
90
internal/domain/activity_log.go
Normal file
90
internal/domain/activity_log.go
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionCourseCreated ActivityAction = "COURSE_CREATED"
|
||||||
|
ActionCourseUpdated ActivityAction = "COURSE_UPDATED"
|
||||||
|
ActionCourseDeleted ActivityAction = "COURSE_DELETED"
|
||||||
|
ActionSubCourseCreated ActivityAction = "SUB_COURSE_CREATED"
|
||||||
|
ActionSubCourseUpdated ActivityAction = "SUB_COURSE_UPDATED"
|
||||||
|
ActionSubCourseDeleted ActivityAction = "SUB_COURSE_DELETED"
|
||||||
|
ActionVideoCreated ActivityAction = "VIDEO_CREATED"
|
||||||
|
ActionVideoUploaded ActivityAction = "VIDEO_UPLOADED"
|
||||||
|
ActionVideoPublished ActivityAction = "VIDEO_PUBLISHED"
|
||||||
|
ActionVideoUpdated ActivityAction = "VIDEO_UPDATED"
|
||||||
|
ActionVideoArchived ActivityAction = "VIDEO_ARCHIVED"
|
||||||
|
ActionVideoDeleted ActivityAction = "VIDEO_DELETED"
|
||||||
|
ActionUserCreated ActivityAction = "USER_CREATED"
|
||||||
|
ActionUserUpdated ActivityAction = "USER_UPDATED"
|
||||||
|
ActionUserDeleted ActivityAction = "USER_DELETED"
|
||||||
|
ActionSettingsUpdated ActivityAction = "SETTINGS_UPDATED"
|
||||||
|
ActionTeamMemberCreated ActivityAction = "TEAM_MEMBER_CREATED"
|
||||||
|
ActionTeamMemberUpdated ActivityAction = "TEAM_MEMBER_UPDATED"
|
||||||
|
ActionTeamMemberDeleted ActivityAction = "TEAM_MEMBER_DELETED"
|
||||||
|
ActionCategoryCreated ActivityAction = "CATEGORY_CREATED"
|
||||||
|
ActionCategoryUpdated ActivityAction = "CATEGORY_UPDATED"
|
||||||
|
ActionCategoryDeleted ActivityAction = "CATEGORY_DELETED"
|
||||||
|
ActionAdminCreated ActivityAction = "ADMIN_CREATED"
|
||||||
|
ActionAdminUpdated ActivityAction = "ADMIN_UPDATED"
|
||||||
|
ActionSubscriptionPlanCreated ActivityAction = "SUBSCRIPTION_PLAN_CREATED"
|
||||||
|
ActionSubscriptionPlanUpdated ActivityAction = "SUBSCRIPTION_PLAN_UPDATED"
|
||||||
|
ActionSubscriptionPlanDeleted ActivityAction = "SUBSCRIPTION_PLAN_DELETED"
|
||||||
|
ActionQuestionCreated ActivityAction = "QUESTION_CREATED"
|
||||||
|
ActionQuestionUpdated ActivityAction = "QUESTION_UPDATED"
|
||||||
|
ActionQuestionDeleted ActivityAction = "QUESTION_DELETED"
|
||||||
|
ActionQuestionSetCreated ActivityAction = "QUESTION_SET_CREATED"
|
||||||
|
ActionQuestionSetUpdated ActivityAction = "QUESTION_SET_UPDATED"
|
||||||
|
ActionQuestionSetDeleted ActivityAction = "QUESTION_SET_DELETED"
|
||||||
|
ActionSubCourseDeactivated ActivityAction = "SUB_COURSE_DEACTIVATED"
|
||||||
|
ActionIssueCreated ActivityAction = "ISSUE_CREATED"
|
||||||
|
ActionIssueStatusUpdated ActivityAction = "ISSUE_STATUS_UPDATED"
|
||||||
|
ActionIssueDeleted ActivityAction = "ISSUE_DELETED"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResourceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResourceCourse ResourceType = "COURSE"
|
||||||
|
ResourceSubCourse ResourceType = "SUB_COURSE"
|
||||||
|
ResourceVideo ResourceType = "VIDEO"
|
||||||
|
ResourceUser ResourceType = "USER"
|
||||||
|
ResourceSettings ResourceType = "SETTINGS"
|
||||||
|
ResourceTeamMember ResourceType = "TEAM_MEMBER"
|
||||||
|
ResourceCategory ResourceType = "CATEGORY"
|
||||||
|
ResourceAdmin ResourceType = "ADMIN"
|
||||||
|
ResourceSubscriptionPlan ResourceType = "SUBSCRIPTION_PLAN"
|
||||||
|
ResourceQuestion ResourceType = "QUESTION"
|
||||||
|
ResourceQuestionSet ResourceType = "QUESTION_SET"
|
||||||
|
ResourceIssue ResourceType = "ISSUE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLog struct {
|
||||||
|
ID int64
|
||||||
|
ActorID *int64
|
||||||
|
ActorRole *string
|
||||||
|
Action string
|
||||||
|
ResourceType string
|
||||||
|
ResourceID *int64
|
||||||
|
Message *string
|
||||||
|
Metadata json.RawMessage
|
||||||
|
IPAddress *string
|
||||||
|
UserAgent *string
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogFilter struct {
|
||||||
|
ActorID *int64
|
||||||
|
Action *string
|
||||||
|
ResourceType *string
|
||||||
|
ResourceID *int64
|
||||||
|
After *time.Time
|
||||||
|
Before *time.Time
|
||||||
|
Limit int32
|
||||||
|
Offset int32
|
||||||
|
}
|
||||||
|
|
@ -5,19 +5,17 @@ import "time"
|
||||||
type ReportedIssueType string
|
type ReportedIssueType string
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ISSUE_TYPE_DEPOSIT ReportedIssueType = "deposit"
|
ISSUE_TYPE_COURSE ReportedIssueType = "course"
|
||||||
ISSUE_TYPE_WITHDRAWAL ReportedIssueType = "withdrawal"
|
ISSUE_TYPE_VIDEO ReportedIssueType = "video"
|
||||||
ISSUE_TYPE_BET ReportedIssueType = "bet"
|
ISSUE_TYPE_SUBSCRIPTION ReportedIssueType = "subscription"
|
||||||
ISSUE_TYPE_CASHOUT ReportedIssueType = "cashout"
|
ISSUE_TYPE_PAYMENT ReportedIssueType = "payment"
|
||||||
ISSUE_TYPE_ODDS ReportedIssueType = "odds"
|
ISSUE_TYPE_ACCOUNT ReportedIssueType = "account"
|
||||||
ISSUE_TYPE_EVENTS ReportedIssueType = "events"
|
|
||||||
ISSUE_TYPE_BRANCH ReportedIssueType = "branch"
|
|
||||||
ISSUE_TYPE_USER ReportedIssueType = "user"
|
|
||||||
ISSUE_TYPE_LOGIN ReportedIssueType = "login"
|
ISSUE_TYPE_LOGIN ReportedIssueType = "login"
|
||||||
ISSUE_TYPE_REGISTER ReportedIssueType = "register"
|
ISSUE_TYPE_CONTENT ReportedIssueType = "content"
|
||||||
ISSUE_TYPE_RESET_PASSWORD ReportedIssueType = "reset_password"
|
ISSUE_TYPE_PERFORMANCE ReportedIssueType = "performance"
|
||||||
ISSUE_TYPE_WALLET ReportedIssueType = "wallet"
|
ISSUE_TYPE_ACCESSIBILITY ReportedIssueType = "accessibility"
|
||||||
ISSUE_TYPE_VIRTUAL ReportedIssueType = "virtual games"
|
ISSUE_TYPE_FEATURE_REQUEST ReportedIssueType = "feature_request"
|
||||||
|
ISSUE_TYPE_BUG ReportedIssueType = "bug"
|
||||||
ISSUE_TYPE_OTHER ReportedIssueType = "other"
|
ISSUE_TYPE_OTHER ReportedIssueType = "other"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
273
internal/pkgs/cloudconvert/client.go
Normal file
273
internal/pkgs/cloudconvert/client.go
Normal file
|
|
@ -0,0 +1,273 @@
|
||||||
|
package cloudconvert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const BaseURL = "https://api.cloudconvert.com/v2"
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
apiKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(apiKey string) *Client {
|
||||||
|
return &Client{
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
},
|
||||||
|
apiKey: apiKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobRequest struct {
|
||||||
|
Tasks map[string]interface{} `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobResponse struct {
|
||||||
|
Data Job `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Job struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Tasks []Task `json:"tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Operation string `json:"operation"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Result *TaskResult `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskResult struct {
|
||||||
|
Form *UploadForm `json:"form,omitempty"`
|
||||||
|
Files []ExportFile `json:"files,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadForm struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
Parameters map[string]interface{} `json:"parameters"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportFile struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Size int64 `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doRequest(ctx context.Context, method, url string, body interface{}) (*http.Response, error) {
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
jsonBytes, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewReader(jsonBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||||
|
if body != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.httpClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateJob(ctx context.Context, jobReq *JobRequest) (*Job, error) {
|
||||||
|
resp, err := c.doRequest(ctx, http.MethodPost, BaseURL+"/jobs", jobReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
return nil, fmt.Errorf("failed to create job: status %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobResp JobResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&jobResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode job response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &jobResp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetJob(ctx context.Context, jobID string) (*Job, error) {
|
||||||
|
resp, err := c.doRequest(ctx, http.MethodGet, BaseURL+"/jobs/"+jobID, 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 get job: status %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobResp JobResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&jobResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode job response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &jobResp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UploadFile(ctx context.Context, form *UploadForm, filename string, fileData io.Reader) error {
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
for key, val := range form.Parameters {
|
||||||
|
var strVal string
|
||||||
|
switch v := val.(type) {
|
||||||
|
case string:
|
||||||
|
strVal = v
|
||||||
|
case float64:
|
||||||
|
strVal = fmt.Sprintf("%.0f", v)
|
||||||
|
default:
|
||||||
|
strVal = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
if err := writer.WriteField(key, strVal); err != nil {
|
||||||
|
return fmt.Errorf("failed to write form field %s: %w", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile("file", filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create form file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(part, fileData); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy file data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close multipart writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadClient := &http.Client{
|
||||||
|
Timeout: 30 * time.Minute,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, form.URL, body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create upload request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
resp, err := uploadClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload file: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("upload failed: status %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) WaitForJob(ctx context.Context, jobID string, pollInterval time.Duration, maxWait time.Duration) (*Job, error) {
|
||||||
|
deadline := time.Now().Add(maxWait)
|
||||||
|
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
job, err := c.GetJob(ctx, jobID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch job.Status {
|
||||||
|
case "finished":
|
||||||
|
return job, nil
|
||||||
|
case "error":
|
||||||
|
for _, task := range job.Tasks {
|
||||||
|
if task.Status == "error" {
|
||||||
|
return nil, fmt.Errorf("job failed: task '%s' error: %s", task.Name, task.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("job failed with unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(pollInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("job timed out after %v", maxWait)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DownloadFile(ctx context.Context, url string) (io.ReadCloser, int64, error) {
|
||||||
|
downloadClient := &http.Client{
|
||||||
|
Timeout: 30 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("failed to create download request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := downloadClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, fmt.Errorf("failed to download file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, 0, fmt.Errorf("download failed: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Body, resp.ContentLength, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateVideoCompressionJob(ctx context.Context) (*Job, error) {
|
||||||
|
jobReq := &JobRequest{
|
||||||
|
Tasks: map[string]interface{}{
|
||||||
|
"import-video": map[string]interface{}{
|
||||||
|
"operation": "import/upload",
|
||||||
|
},
|
||||||
|
"convert-video": map[string]interface{}{
|
||||||
|
"operation": "convert",
|
||||||
|
"input": "import-video",
|
||||||
|
"output_format": "mp4",
|
||||||
|
"video_codec": "x264",
|
||||||
|
"crf": 28,
|
||||||
|
"preset": "medium",
|
||||||
|
"height": 720,
|
||||||
|
"fit": "max",
|
||||||
|
"audio_codec": "aac",
|
||||||
|
"audio_bitrate": 128,
|
||||||
|
},
|
||||||
|
"export-video": map[string]interface{}{
|
||||||
|
"operation": "export/url",
|
||||||
|
"input": "convert-video",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.CreateJob(ctx, jobReq)
|
||||||
|
}
|
||||||
|
|
@ -238,6 +238,43 @@ func (c *Client) CreateTusUpload(ctx context.Context, name, description string,
|
||||||
return c.CreateUpload(ctx, req)
|
return c.CreateUpload(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) UploadTusVideoFile(ctx context.Context, uploadLink string, fileData io.Reader, fileSize int64) error {
|
||||||
|
uploadClient := &http.Client{
|
||||||
|
Timeout: 30 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, uploadLink, fileData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create TUS upload request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Tus-Resumable", "1.0.0")
|
||||||
|
req.Header.Set("Upload-Offset", "0")
|
||||||
|
req.Header.Set("Content-Type", "application/offset+octet-stream")
|
||||||
|
req.ContentLength = fileSize
|
||||||
|
|
||||||
|
resp, err := uploadClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to upload video file to Vimeo: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
return fmt.Errorf("TUS upload failed: status %d, body: %s", resp.StatusCode, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetStr := resp.Header.Get("Upload-Offset")
|
||||||
|
if offsetStr != "" {
|
||||||
|
offset, err := strconv.ParseInt(offsetStr, 10, 64)
|
||||||
|
if err == nil && offset < fileSize {
|
||||||
|
return fmt.Errorf("incomplete upload: uploaded %d of %d bytes", offset, fileSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) UpdateVideo(ctx context.Context, videoID string, req *UpdateVideoRequest) (*Video, error) {
|
func (c *Client) UpdateVideo(ctx context.Context, videoID string, req *UpdateVideoRequest) (*Video, error) {
|
||||||
resp, err := c.doRequest(ctx, http.MethodPatch, "/videos/"+videoID, req)
|
resp, err := c.doRequest(ctx, http.MethodPatch, "/videos/"+videoID, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
12
internal/ports/activity_log.go
Normal file
12
internal/ports/activity_log.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
package ports
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLogStore interface {
|
||||||
|
CreateActivityLog(ctx context.Context, log domain.ActivityLog) (domain.ActivityLog, error)
|
||||||
|
ListActivityLogs(ctx context.Context, filter domain.ActivityLogFilter) ([]domain.ActivityLog, int64, error)
|
||||||
|
GetActivityLogByID(ctx context.Context, id int64) (domain.ActivityLog, error)
|
||||||
|
}
|
||||||
147
internal/repository/activity_logs.go
Normal file
147
internal/repository/activity_logs.go
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Store) CreateActivityLog(ctx context.Context, log domain.ActivityLog) (domain.ActivityLog, error) {
|
||||||
|
var actorID, resourceID pgtype.Int8
|
||||||
|
var actorRole, message, ipAddress, userAgent pgtype.Text
|
||||||
|
|
||||||
|
if log.ActorID != nil {
|
||||||
|
actorID = pgtype.Int8{Int64: *log.ActorID, Valid: true}
|
||||||
|
}
|
||||||
|
if log.ResourceID != nil {
|
||||||
|
resourceID = pgtype.Int8{Int64: *log.ResourceID, Valid: true}
|
||||||
|
}
|
||||||
|
if log.ActorRole != nil {
|
||||||
|
actorRole = pgtype.Text{String: *log.ActorRole, Valid: true}
|
||||||
|
}
|
||||||
|
if log.Message != nil {
|
||||||
|
message = pgtype.Text{String: *log.Message, Valid: true}
|
||||||
|
}
|
||||||
|
if log.IPAddress != nil {
|
||||||
|
ipAddress = pgtype.Text{String: *log.IPAddress, Valid: true}
|
||||||
|
}
|
||||||
|
if log.UserAgent != nil {
|
||||||
|
userAgent = pgtype.Text{String: *log.UserAgent, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata interface{}
|
||||||
|
if log.Metadata != nil {
|
||||||
|
metadata = log.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
row, err := s.queries.CreateActivityLog(ctx, dbgen.CreateActivityLogParams{
|
||||||
|
ActorID: actorID,
|
||||||
|
ActorRole: actorRole,
|
||||||
|
Action: log.Action,
|
||||||
|
ResourceType: log.ResourceType,
|
||||||
|
ResourceID: resourceID,
|
||||||
|
Message: message,
|
||||||
|
Metadata: metadata,
|
||||||
|
IpAddress: ipAddress,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return domain.ActivityLog{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapActivityLogRow(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) ListActivityLogs(ctx context.Context, filter domain.ActivityLogFilter) ([]domain.ActivityLog, int64, error) {
|
||||||
|
var filterActorID, filterResourceID pgtype.Int8
|
||||||
|
var filterAction, filterResourceType pgtype.Text
|
||||||
|
var filterAfter, filterBefore pgtype.Timestamptz
|
||||||
|
|
||||||
|
if filter.ActorID != nil {
|
||||||
|
filterActorID = pgtype.Int8{Int64: *filter.ActorID, Valid: true}
|
||||||
|
}
|
||||||
|
if filter.ResourceID != nil {
|
||||||
|
filterResourceID = pgtype.Int8{Int64: *filter.ResourceID, Valid: true}
|
||||||
|
}
|
||||||
|
if filter.Action != nil {
|
||||||
|
filterAction = pgtype.Text{String: *filter.Action, Valid: true}
|
||||||
|
}
|
||||||
|
if filter.ResourceType != nil {
|
||||||
|
filterResourceType = pgtype.Text{String: *filter.ResourceType, Valid: true}
|
||||||
|
}
|
||||||
|
if filter.After != nil {
|
||||||
|
filterAfter = pgtype.Timestamptz{Time: *filter.After, Valid: true}
|
||||||
|
}
|
||||||
|
if filter.Before != nil {
|
||||||
|
filterBefore = pgtype.Timestamptz{Time: *filter.Before, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := s.queries.ListActivityLogs(ctx, dbgen.ListActivityLogsParams{
|
||||||
|
FilterActorID: filterActorID,
|
||||||
|
FilterAction: filterAction,
|
||||||
|
FilterResourceType: filterResourceType,
|
||||||
|
FilterResourceID: filterResourceID,
|
||||||
|
FilterAfter: filterAfter,
|
||||||
|
FilterBefore: filterBefore,
|
||||||
|
LogOffset: filter.Offset,
|
||||||
|
LogLimit: filter.Limit,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
logs []domain.ActivityLog
|
||||||
|
totalCount int64
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, row := range rows {
|
||||||
|
if i == 0 {
|
||||||
|
totalCount = row.TotalCount
|
||||||
|
}
|
||||||
|
|
||||||
|
logs = append(logs, domain.ActivityLog{
|
||||||
|
ID: row.ID,
|
||||||
|
ActorID: ptrInt64(row.ActorID),
|
||||||
|
ActorRole: ptrString(row.ActorRole),
|
||||||
|
Action: row.Action,
|
||||||
|
ResourceType: row.ResourceType,
|
||||||
|
ResourceID: ptrInt64(row.ResourceID),
|
||||||
|
Message: ptrString(row.Message),
|
||||||
|
Metadata: json.RawMessage(row.Metadata),
|
||||||
|
IPAddress: ptrString(row.IpAddress),
|
||||||
|
UserAgent: ptrString(row.UserAgent),
|
||||||
|
CreatedAt: row.CreatedAt.Time,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs, totalCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetActivityLogByID(ctx context.Context, id int64) (domain.ActivityLog, error) {
|
||||||
|
row, err := s.queries.GetActivityLogByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return domain.ActivityLog{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapActivityLogRow(row), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapActivityLogRow(row dbgen.ActivityLog) domain.ActivityLog {
|
||||||
|
return domain.ActivityLog{
|
||||||
|
ID: row.ID,
|
||||||
|
ActorID: ptrInt64(row.ActorID),
|
||||||
|
ActorRole: ptrString(row.ActorRole),
|
||||||
|
Action: row.Action,
|
||||||
|
ResourceType: row.ResourceType,
|
||||||
|
ResourceID: ptrInt64(row.ResourceID),
|
||||||
|
Message: ptrString(row.Message),
|
||||||
|
Metadata: json.RawMessage(row.Metadata),
|
||||||
|
IPAddress: ptrString(row.IpAddress),
|
||||||
|
UserAgent: ptrString(row.UserAgent),
|
||||||
|
CreatedAt: row.CreatedAt.Time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,3 +19,17 @@ func ptrTimestamptz(t pgtype.Timestamptz) *time.Time {
|
||||||
}
|
}
|
||||||
return &t.Time
|
return &t.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ptrString(t pgtype.Text) *string {
|
||||||
|
if !t.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &t.String
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrInt64(v pgtype.Int8) *int64 {
|
||||||
|
if !v.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &v.Int64
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type ReportedIssueRepository interface {
|
||||||
CountReportedIssuesByUser(ctx context.Context, userID int64) (int64, error)
|
CountReportedIssuesByUser(ctx context.Context, userID int64) (int64, error)
|
||||||
UpdateReportedIssueStatus(ctx context.Context, id int64, status string) error
|
UpdateReportedIssueStatus(ctx context.Context, id int64, status string) error
|
||||||
DeleteReportedIssue(ctx context.Context, id int64) error
|
DeleteReportedIssue(ctx context.Context, id int64) error
|
||||||
|
GetReportedIssueByID(ctx context.Context, id int64) (dbgen.ReportedIssue, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReportedIssueRepo struct {
|
type ReportedIssueRepo struct {
|
||||||
|
|
@ -62,3 +63,7 @@ func (s *ReportedIssueRepo) UpdateReportedIssueStatus(ctx context.Context, id in
|
||||||
func (s *ReportedIssueRepo) DeleteReportedIssue(ctx context.Context, id int64) error {
|
func (s *ReportedIssueRepo) DeleteReportedIssue(ctx context.Context, id int64) error {
|
||||||
return s.store.queries.DeleteReportedIssue(ctx, id)
|
return s.store.queries.DeleteReportedIssue(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ReportedIssueRepo) GetReportedIssueByID(ctx context.Context, id int64) (dbgen.ReportedIssue, error) {
|
||||||
|
return s.store.queries.GetReportedIssueByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,9 +233,4 @@ func (s *Store) DeleteSubCourse(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ptrString(t pgtype.Text) *string {
|
|
||||||
if !t.Valid {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &t.String
|
|
||||||
}
|
|
||||||
|
|
|
||||||
54
internal/services/activity_log/service.go
Normal file
54
internal/services/activity_log/service.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package activity_log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"Yimaru-Backend/internal/ports"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
store ports.ActivityLogStore
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(store ports.ActivityLogStore, logger *zap.Logger) *Service {
|
||||||
|
return &Service{
|
||||||
|
store: store,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Record(ctx context.Context, log domain.ActivityLog) {
|
||||||
|
if _, err := s.store.CreateActivityLog(ctx, log); err != nil {
|
||||||
|
s.logger.Warn("failed to record activity log",
|
||||||
|
zap.String("action", log.Action),
|
||||||
|
zap.String("resource_type", log.ResourceType),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) List(ctx context.Context, filter domain.ActivityLogFilter) ([]domain.ActivityLog, int64, error) {
|
||||||
|
return s.store.ListActivityLogs(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(ctx context.Context, id int64) (domain.ActivityLog, error) {
|
||||||
|
return s.store.GetActivityLogByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) RecordAction(ctx context.Context, actorID *int64, actorRole *string, action domain.ActivityAction, resourceType domain.ResourceType, resourceID *int64, message string, metadata json.RawMessage, ip *string, userAgent *string) {
|
||||||
|
s.Record(ctx, domain.ActivityLog{
|
||||||
|
ActorID: actorID,
|
||||||
|
ActorRole: actorRole,
|
||||||
|
Action: string(action),
|
||||||
|
ResourceType: string(resourceType),
|
||||||
|
ResourceID: resourceID,
|
||||||
|
Message: &message,
|
||||||
|
Metadata: metadata,
|
||||||
|
IPAddress: ip,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
})
|
||||||
|
}
|
||||||
110
internal/services/cloudconvert/service.go
Normal file
110
internal/services/cloudconvert/service.go
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
package cloudconvert
|
||||||
|
|
||||||
|
import (
|
||||||
|
cc "Yimaru-Backend/internal/pkgs/cloudconvert"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client *cc.Client
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(apiKey string, logger *zap.Logger) *Service {
|
||||||
|
return &Service{
|
||||||
|
client: cc.NewClient(apiKey),
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CompressResult struct {
|
||||||
|
Data io.ReadCloser
|
||||||
|
FileSize int64
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CompressVideo(ctx context.Context, filename string, fileData io.Reader, fileSize int64) (*CompressResult, error) {
|
||||||
|
s.logger.Info("Creating CloudConvert compression job", zap.String("filename", filename), zap.Int64("original_size", fileSize))
|
||||||
|
|
||||||
|
job, err := s.client.CreateVideoCompressionJob(ctx)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to create CloudConvert job", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("failed to create compression job: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadForm *cc.UploadForm
|
||||||
|
for _, task := range job.Tasks {
|
||||||
|
if task.Name == "import-video" && task.Result != nil && task.Result.Form != nil {
|
||||||
|
uploadForm = task.Result.Form
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploadForm == nil {
|
||||||
|
return nil, fmt.Errorf("no upload form found in job response")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Uploading video to CloudConvert", zap.String("job_id", job.ID))
|
||||||
|
|
||||||
|
if err := s.client.UploadFile(ctx, uploadForm, filename, fileData); err != nil {
|
||||||
|
s.logger.Error("Failed to upload file to CloudConvert", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("failed to upload file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Waiting for CloudConvert job to complete", zap.String("job_id", job.ID))
|
||||||
|
|
||||||
|
completedJob, err := s.client.WaitForJob(ctx, job.ID, 5*time.Second, 30*time.Minute)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("CloudConvert job failed", zap.String("job_id", job.ID), zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("compression job failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exportURL string
|
||||||
|
var exportFilename string
|
||||||
|
for _, task := range completedJob.Tasks {
|
||||||
|
if task.Name == "export-video" && task.Result != nil && len(task.Result.Files) > 0 {
|
||||||
|
exportURL = task.Result.Files[0].URL
|
||||||
|
exportFilename = task.Result.Files[0].Filename
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exportURL == "" {
|
||||||
|
return nil, fmt.Errorf("no export URL found in completed job")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Downloading compressed video from CloudConvert", zap.String("job_id", job.ID), zap.String("filename", exportFilename))
|
||||||
|
|
||||||
|
body, contentLength, err := s.client.DownloadFile(ctx, exportURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to download compressed file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentLength <= 0 {
|
||||||
|
data, err := io.ReadAll(body)
|
||||||
|
body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read compressed file: %w", err)
|
||||||
|
}
|
||||||
|
contentLength = int64(len(data))
|
||||||
|
body = io.NopCloser(bytes.NewReader(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("Video compression complete",
|
||||||
|
zap.Int64("original_size", fileSize),
|
||||||
|
zap.Int64("compressed_size", contentLength),
|
||||||
|
zap.String("filename", exportFilename),
|
||||||
|
)
|
||||||
|
|
||||||
|
return &CompressResult{
|
||||||
|
Data: body,
|
||||||
|
FileSize: contentLength,
|
||||||
|
Filename: exportFilename,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package course_management
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
"Yimaru-Backend/internal/ports"
|
"Yimaru-Backend/internal/ports"
|
||||||
|
cloudconvertservice "Yimaru-Backend/internal/services/cloudconvert"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
||||||
)
|
)
|
||||||
|
|
@ -12,6 +13,7 @@ type Service struct {
|
||||||
courseStore ports.CourseStore
|
courseStore ports.CourseStore
|
||||||
notificationSvc *notificationservice.Service
|
notificationSvc *notificationservice.Service
|
||||||
vimeoSvc *vimeoservice.Service
|
vimeoSvc *vimeoservice.Service
|
||||||
|
cloudConvertSvc *cloudconvertservice.Service
|
||||||
config *config.Config
|
config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,3 +38,11 @@ func (s *Service) SetVimeoService(vimeoSvc *vimeoservice.Service) {
|
||||||
func (s *Service) HasVimeoService() bool {
|
func (s *Service) HasVimeoService() bool {
|
||||||
return s.vimeoSvc != nil
|
return s.vimeoSvc != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) SetCloudConvertService(ccSvc *cloudconvertservice.Service) {
|
||||||
|
s.cloudConvertSvc = ccSvc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) HasCloudConvertService() bool {
|
||||||
|
return s.cloudConvertSvc != nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,13 @@ package course_management
|
||||||
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"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) CreateSubCourseVideo(
|
func (s *Service) CreateSubCourseVideo(
|
||||||
|
|
@ -49,36 +54,132 @@ func (s *Service) CreateSubCourseVideoWithVimeo(
|
||||||
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create pull upload to Vimeo
|
|
||||||
descStr := ""
|
descStr := ""
|
||||||
if description != nil {
|
if description != nil {
|
||||||
descStr = *description
|
descStr = *description
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadResult, err := s.vimeoSvc.CreatePullUpload(ctx, title, descStr, sourceURL, fileSize)
|
var uploadResult *vimeoservice.UploadResult
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if s.cloudConvertSvc != nil {
|
||||||
|
httpClient := &http.Client{Timeout: 30 * time.Minute}
|
||||||
|
req, reqErr := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
|
||||||
|
if reqErr != nil {
|
||||||
|
return domain.SubCourseVideo{}, fmt.Errorf("failed to create download request: %w", reqErr)
|
||||||
|
}
|
||||||
|
resp, dlErr := httpClient.Do(req)
|
||||||
|
if dlErr != nil {
|
||||||
|
return domain.SubCourseVideo{}, fmt.Errorf("failed to download source video: %w", dlErr)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return domain.SubCourseVideo{}, fmt.Errorf("failed to download source video: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
dlSize := resp.ContentLength
|
||||||
|
if dlSize <= 0 {
|
||||||
|
dlSize = fileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := path.Base(sourceURL)
|
||||||
|
if filename == "" || filename == "." || filename == "/" {
|
||||||
|
filename = "video.mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
result, compErr := s.cloudConvertSvc.CompressVideo(ctx, filename, resp.Body, dlSize)
|
||||||
|
if compErr != nil {
|
||||||
|
return domain.SubCourseVideo{}, fmt.Errorf("failed to compress video: %w", compErr)
|
||||||
|
}
|
||||||
|
defer result.Data.Close()
|
||||||
|
|
||||||
|
uploadResult, err = s.vimeoSvc.UploadVideoFile(ctx, title, descStr, result.Data, result.FileSize)
|
||||||
|
} else {
|
||||||
|
uploadResult, err = s.vimeoSvc.CreatePullUpload(ctx, title, descStr, sourceURL, fileSize)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.SubCourseVideo{}, fmt.Errorf("failed to upload to Vimeo: %w", err)
|
return domain.SubCourseVideo{}, fmt.Errorf("failed to upload to Vimeo: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate embed URL
|
|
||||||
embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{
|
embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{
|
||||||
Title: true,
|
Title: true,
|
||||||
Byline: true,
|
Byline: true,
|
||||||
Portrait: true,
|
Portrait: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Generate embed HTML
|
|
||||||
embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil)
|
embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil)
|
||||||
|
|
||||||
// Set values for Vimeo fields
|
|
||||||
provider := string(domain.VideoHostProviderVimeo)
|
provider := string(domain.VideoHostProviderVimeo)
|
||||||
vimeoStatus := "uploading"
|
vimeoStatus := "uploading"
|
||||||
status := "DRAFT"
|
status := "DRAFT"
|
||||||
|
|
||||||
// Create the video record with Vimeo info
|
|
||||||
return s.courseStore.CreateSubCourseVideo(
|
return s.courseStore.CreateSubCourseVideo(
|
||||||
ctx, subCourseID, title, description,
|
ctx, subCourseID, title, description,
|
||||||
uploadResult.Link, // Use Vimeo link as video URL
|
uploadResult.Link,
|
||||||
|
duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status,
|
||||||
|
&uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateSubCourseVideoWithFileUpload(
|
||||||
|
ctx context.Context,
|
||||||
|
subCourseID int64,
|
||||||
|
title string,
|
||||||
|
description *string,
|
||||||
|
filename string,
|
||||||
|
fileData io.Reader,
|
||||||
|
fileSize int64,
|
||||||
|
duration int32,
|
||||||
|
resolution *string,
|
||||||
|
instructorID *string,
|
||||||
|
thumbnail *string,
|
||||||
|
visibility *string,
|
||||||
|
displayOrder *int32,
|
||||||
|
) (domain.SubCourseVideo, error) {
|
||||||
|
if s.vimeoSvc == nil {
|
||||||
|
return domain.SubCourseVideo{}, fmt.Errorf("vimeo service is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
descStr := ""
|
||||||
|
if description != nil {
|
||||||
|
descStr = *description
|
||||||
|
}
|
||||||
|
|
||||||
|
videoReader := fileData
|
||||||
|
videoSize := fileSize
|
||||||
|
|
||||||
|
if s.cloudConvertSvc != nil {
|
||||||
|
result, err := s.cloudConvertSvc.CompressVideo(ctx, filename, fileData, fileSize)
|
||||||
|
if err != nil {
|
||||||
|
return domain.SubCourseVideo{}, fmt.Errorf("failed to compress video: %w", err)
|
||||||
|
}
|
||||||
|
defer result.Data.Close()
|
||||||
|
videoReader = result.Data
|
||||||
|
videoSize = result.FileSize
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadResult, err := s.vimeoSvc.UploadVideoFile(ctx, title, descStr, videoReader, videoSize)
|
||||||
|
if err != nil {
|
||||||
|
return domain.SubCourseVideo{}, fmt.Errorf("failed to upload video file to Vimeo: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
embedURL := vimeo.GenerateEmbedURL(uploadResult.VimeoID, &vimeo.EmbedOptions{
|
||||||
|
Title: true,
|
||||||
|
Byline: true,
|
||||||
|
Portrait: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
embedHTML := vimeo.GenerateIframeEmbed(uploadResult.VimeoID, 640, 360, nil)
|
||||||
|
|
||||||
|
provider := string(domain.VideoHostProviderVimeo)
|
||||||
|
vimeoStatus := "uploading"
|
||||||
|
status := "DRAFT"
|
||||||
|
|
||||||
|
return s.courseStore.CreateSubCourseVideo(
|
||||||
|
ctx, subCourseID, title, description,
|
||||||
|
uploadResult.Link,
|
||||||
duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status,
|
duration, resolution, instructorID, thumbnail, visibility, displayOrder, &status,
|
||||||
&uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider,
|
&uploadResult.VimeoID, &embedURL, &embedHTML, &vimeoStatus, &provider,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package issuereporting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
dbgen "Yimaru-Backend/gen/db"
|
||||||
|
|
@ -18,38 +19,27 @@ func New(repo repository.ReportedIssueRepository) *Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) CreateReportedIssue(ctx context.Context, issue domain.ReportedIssueReq, userID int64, role domain.Role) (domain.ReportedIssue, error) {
|
func (s *Service) CreateReportedIssue(ctx context.Context, issue domain.ReportedIssueReq, userID int64, role domain.Role) (domain.ReportedIssue, error) {
|
||||||
|
var metadata []byte
|
||||||
// metadata, err := json.Marshal(issue.Metadata)
|
if issue.Metadata != nil {
|
||||||
// if err != nil {
|
var err error
|
||||||
// return domain.ReportedIssue{}, err
|
metadata, err = json.Marshal(issue.Metadata)
|
||||||
// }
|
if err != nil {
|
||||||
|
return domain.ReportedIssue{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
params := dbgen.CreateReportedIssueParams{
|
params := dbgen.CreateReportedIssueParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
UserRole: string(role),
|
UserRole: string(role),
|
||||||
Subject: issue.Subject,
|
Subject: issue.Subject,
|
||||||
Description: issue.Description,
|
Description: issue.Description,
|
||||||
IssueType: string(issue.IssueType),
|
IssueType: string(issue.IssueType),
|
||||||
// Metadata: metadata,
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
dbIssue, err := s.repo.CreateReportedIssue(ctx, params)
|
dbIssue, err := s.repo.CreateReportedIssue(ctx, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ReportedIssue{}, err
|
return domain.ReportedIssue{}, err
|
||||||
}
|
}
|
||||||
// Map dbgen.ReportedIssue to domain.ReportedIssue
|
return mapDBIssueToDomain(dbIssue), nil
|
||||||
reportedIssue := domain.ReportedIssue{
|
|
||||||
ID: dbIssue.ID,
|
|
||||||
Subject: dbIssue.Subject,
|
|
||||||
Description: dbIssue.Description,
|
|
||||||
UserID: dbIssue.UserID,
|
|
||||||
UserRole: domain.Role(dbIssue.UserRole),
|
|
||||||
Status: domain.ReportedIssueStatus(dbIssue.Status),
|
|
||||||
IssueType: domain.ReportedIssueType(dbIssue.IssueType),
|
|
||||||
CreatedAt: dbIssue.CreatedAt.Time,
|
|
||||||
UpdatedAt: dbIssue.UpdatedAt.Time,
|
|
||||||
|
|
||||||
// Add other fields as necessary
|
|
||||||
}
|
|
||||||
return reportedIssue, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetIssuesForUser(ctx context.Context, userID int64, limit, offset int) ([]domain.ReportedIssue, error) {
|
func (s *Service) GetIssuesForUser(ctx context.Context, userID int64, limit, offset int) ([]domain.ReportedIssue, error) {
|
||||||
|
|
@ -57,26 +47,23 @@ func (s *Service) GetIssuesForUser(ctx context.Context, userID int64, limit, off
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
reportedIssues := make([]domain.ReportedIssue, len(dbIssues))
|
return mapDBIssuesToDomain(dbIssues), nil
|
||||||
for i, dbIssue := range dbIssues {
|
|
||||||
reportedIssues[i] = domain.ReportedIssue{
|
|
||||||
ID: dbIssue.ID,
|
|
||||||
Subject: dbIssue.Subject,
|
|
||||||
Description: dbIssue.Description,
|
|
||||||
UserID: dbIssue.UserID,
|
|
||||||
UserRole: domain.Role(dbIssue.UserRole),
|
|
||||||
Status: domain.ReportedIssueStatus(dbIssue.Status),
|
|
||||||
IssueType: domain.ReportedIssueType(dbIssue.IssueType),
|
|
||||||
CreatedAt: dbIssue.CreatedAt.Time,
|
|
||||||
UpdatedAt: dbIssue.UpdatedAt.Time,
|
|
||||||
// Add other fields as necessary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return reportedIssues, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetAllIssues(ctx context.Context, limit, offset int) ([]dbgen.ReportedIssue, error) {
|
func (s *Service) GetAllIssues(ctx context.Context, limit, offset int) ([]domain.ReportedIssue, error) {
|
||||||
return s.repo.ListReportedIssues(ctx, int32(limit), int32(offset))
|
dbIssues, err := s.repo.ListReportedIssues(ctx, int32(limit), int32(offset))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mapDBIssuesToDomain(dbIssues), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetIssueByID(ctx context.Context, issueID int64) (domain.ReportedIssue, error) {
|
||||||
|
dbIssue, err := s.repo.GetReportedIssueByID(ctx, issueID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.ReportedIssue{}, err
|
||||||
|
}
|
||||||
|
return mapDBIssueToDomain(dbIssue), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateIssueStatus(ctx context.Context, issueID int64, status string) error {
|
func (s *Service) UpdateIssueStatus(ctx context.Context, issueID int64, status string) error {
|
||||||
|
|
@ -87,6 +74,40 @@ func (s *Service) UpdateIssueStatus(ctx context.Context, issueID int64, status s
|
||||||
return s.repo.UpdateReportedIssueStatus(ctx, issueID, status)
|
return s.repo.UpdateReportedIssueStatus(ctx, issueID, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) CountAllIssues(ctx context.Context) (int64, error) {
|
||||||
|
return s.repo.CountReportedIssues(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) CountIssuesByUser(ctx context.Context, userID int64) (int64, error) {
|
||||||
|
return s.repo.CountReportedIssuesByUser(ctx, userID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) DeleteIssue(ctx context.Context, issueID int64) error {
|
func (s *Service) DeleteIssue(ctx context.Context, issueID int64) error {
|
||||||
return s.repo.DeleteReportedIssue(ctx, issueID)
|
return s.repo.DeleteReportedIssue(ctx, issueID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapDBIssueToDomain(dbIssue dbgen.ReportedIssue) domain.ReportedIssue {
|
||||||
|
issue := domain.ReportedIssue{
|
||||||
|
ID: dbIssue.ID,
|
||||||
|
Subject: dbIssue.Subject,
|
||||||
|
Description: dbIssue.Description,
|
||||||
|
UserID: dbIssue.UserID,
|
||||||
|
UserRole: domain.Role(dbIssue.UserRole),
|
||||||
|
Status: domain.ReportedIssueStatus(dbIssue.Status),
|
||||||
|
IssueType: domain.ReportedIssueType(dbIssue.IssueType),
|
||||||
|
CreatedAt: dbIssue.CreatedAt.Time,
|
||||||
|
UpdatedAt: dbIssue.UpdatedAt.Time,
|
||||||
|
}
|
||||||
|
if len(dbIssue.Metadata) > 0 {
|
||||||
|
_ = json.Unmarshal(dbIssue.Metadata, &issue.Metadata)
|
||||||
|
}
|
||||||
|
return issue
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapDBIssuesToDomain(dbIssues []dbgen.ReportedIssue) []domain.ReportedIssue {
|
||||||
|
issues := make([]domain.ReportedIssue, len(dbIssues))
|
||||||
|
for i, dbIssue := range dbIssues {
|
||||||
|
issues[i] = mapDBIssueToDomain(dbIssue)
|
||||||
|
}
|
||||||
|
return issues
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"Yimaru-Backend/internal/pkgs/vimeo"
|
"Yimaru-Backend/internal/pkgs/vimeo"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
@ -120,6 +121,29 @@ func (s *Service) CreateTusUpload(ctx context.Context, name, description string,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) UploadVideoFile(ctx context.Context, name, description string, fileData io.Reader, fileSize int64) (*UploadResult, error) {
|
||||||
|
resp, err := s.client.CreateTusUpload(ctx, name, description, fileSize)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to create TUS upload slot", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("failed to create TUS upload slot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.client.UploadTusVideoFile(ctx, resp.Upload.UploadLink, fileData, fileSize); err != nil {
|
||||||
|
s.logger.Error("Failed to upload video file to Vimeo", zap.Error(err))
|
||||||
|
return nil, fmt.Errorf("failed to upload video file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
videoID := vimeo.ExtractVideoID(resp.URI)
|
||||||
|
|
||||||
|
return &UploadResult{
|
||||||
|
VimeoID: videoID,
|
||||||
|
URI: resp.URI,
|
||||||
|
Link: resp.Link,
|
||||||
|
UploadLink: resp.Upload.UploadLink,
|
||||||
|
Status: "uploading",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) UpdateVideoMetadata(ctx context.Context, videoID string, name, description *string) (*VideoInfo, error) {
|
func (s *Service) UpdateVideoMetadata(ctx context.Context, videoID string, name, description *string) (*VideoInfo, error) {
|
||||||
req := &vimeo.UpdateVideoRequest{
|
req := &vimeo.UpdateVideoRequest{
|
||||||
Name: name,
|
Name: name,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package httpserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
|
|
@ -38,6 +39,7 @@ type App struct {
|
||||||
issueReportingSvc *issuereporting.Service
|
issueReportingSvc *issuereporting.Service
|
||||||
vimeoSvc *vimeoservice.Service
|
vimeoSvc *vimeoservice.Service
|
||||||
teamSvc *team.Service
|
teamSvc *team.Service
|
||||||
|
activityLogSvc *activitylogservice.Service
|
||||||
fiber *fiber.App
|
fiber *fiber.App
|
||||||
recommendationSvc recommendation.RecommendationService
|
recommendationSvc recommendation.RecommendationService
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
|
|
@ -63,6 +65,7 @@ func NewApp(
|
||||||
issueReportingSvc *issuereporting.Service,
|
issueReportingSvc *issuereporting.Service,
|
||||||
vimeoSvc *vimeoservice.Service,
|
vimeoSvc *vimeoservice.Service,
|
||||||
teamSvc *team.Service,
|
teamSvc *team.Service,
|
||||||
|
activityLogSvc *activitylogservice.Service,
|
||||||
port int, validator *customvalidator.CustomValidator,
|
port int, validator *customvalidator.CustomValidator,
|
||||||
settingSvc *settings.Service,
|
settingSvc *settings.Service,
|
||||||
authSvc *authentication.Service,
|
authSvc *authentication.Service,
|
||||||
|
|
@ -80,6 +83,7 @@ func NewApp(
|
||||||
DisableHeaderNormalizing: true,
|
DisableHeaderNormalizing: true,
|
||||||
JSONEncoder: sonic.Marshal,
|
JSONEncoder: sonic.Marshal,
|
||||||
JSONDecoder: sonic.Unmarshal,
|
JSONDecoder: sonic.Unmarshal,
|
||||||
|
BodyLimit: 500 * 1024 * 1024, // 500 MB
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Use(cors.New(cors.Config{
|
app.Use(cors.New(cors.Config{
|
||||||
|
|
@ -99,7 +103,8 @@ func NewApp(
|
||||||
arifpaySvc: arifpaySvc,
|
arifpaySvc: arifpaySvc,
|
||||||
vimeoSvc: vimeoSvc,
|
vimeoSvc: vimeoSvc,
|
||||||
teamSvc: teamSvc,
|
teamSvc: teamSvc,
|
||||||
// issueReportingSvc: issueReportingSvc,
|
activityLogSvc: activityLogSvc,
|
||||||
|
issueReportingSvc: issueReportingSvc,
|
||||||
fiber: app,
|
fiber: app,
|
||||||
port: port,
|
port: port,
|
||||||
settingSvc: settingSvc,
|
settingSvc: settingSvc,
|
||||||
|
|
|
||||||
208
internal/web_server/handlers/activity_logs.go
Normal file
208
internal/web_server/handlers/activity_logs.go
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type activityLogRes struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ActorID *int64 `json:"actor_id,omitempty"`
|
||||||
|
ActorRole *string `json:"actor_role,omitempty"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
ResourceID *int64 `json:"resource_id,omitempty"`
|
||||||
|
Message *string `json:"message,omitempty"`
|
||||||
|
Metadata json.RawMessage `json:"metadata"`
|
||||||
|
IPAddress *string `json:"ip_address,omitempty"`
|
||||||
|
UserAgent *string `json:"user_agent,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type activityLogListRes struct {
|
||||||
|
Logs []activityLogRes `json:"logs"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
Limit int32 `json:"limit"`
|
||||||
|
Offset int32 `json:"offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActivityLogs godoc
|
||||||
|
// @Summary Get activity logs
|
||||||
|
// @Description Returns a filtered, paginated list of activity logs
|
||||||
|
// @Tags activity-logs
|
||||||
|
// @Produce json
|
||||||
|
// @Param actor_id query int false "Filter by actor ID"
|
||||||
|
// @Param action query string false "Filter by action"
|
||||||
|
// @Param resource_type query string false "Filter by resource type"
|
||||||
|
// @Param resource_id query int false "Filter by resource ID"
|
||||||
|
// @Param after query string false "Filter logs after this RFC3339 timestamp"
|
||||||
|
// @Param before query string false "Filter logs before this RFC3339 timestamp"
|
||||||
|
// @Param limit query int false "Limit" default(20)
|
||||||
|
// @Param offset query int false "Offset" default(0)
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/activity-logs [get]
|
||||||
|
func (h *Handler) GetActivityLogs(c *fiber.Ctx) error {
|
||||||
|
var filter domain.ActivityLogFilter
|
||||||
|
|
||||||
|
if v := c.Query("actor_id"); v != "" {
|
||||||
|
id, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid actor_id parameter",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
filter.ActorID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.Query("action"); v != "" {
|
||||||
|
filter.Action = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.Query("resource_type"); v != "" {
|
||||||
|
filter.ResourceType = &v
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.Query("resource_id"); v != "" {
|
||||||
|
id, err := strconv.ParseInt(v, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid resource_id parameter",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
filter.ResourceID = &id
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.Query("after"); v != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, v)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid after parameter, expected RFC3339 format",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
filter.After = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := c.Query("before"); v != "" {
|
||||||
|
t, err := time.Parse(time.RFC3339, v)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid before parameter, expected RFC3339 format",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
filter.Before = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
limitStr := c.Query("limit", "20")
|
||||||
|
limit, err := strconv.Atoi(limitStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid limit parameter",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
filter.Limit = int32(limit)
|
||||||
|
|
||||||
|
offsetStr := c.Query("offset", "0")
|
||||||
|
offset, err := strconv.Atoi(offsetStr)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid offset parameter",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
filter.Offset = int32(offset)
|
||||||
|
|
||||||
|
logs, totalCount, err := h.activityLogSvc.List(c.Context(), filter)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to retrieve activity logs",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var logResponses []activityLogRes
|
||||||
|
for _, l := range logs {
|
||||||
|
logResponses = append(logResponses, activityLogRes{
|
||||||
|
ID: l.ID,
|
||||||
|
ActorID: l.ActorID,
|
||||||
|
ActorRole: l.ActorRole,
|
||||||
|
Action: l.Action,
|
||||||
|
ResourceType: l.ResourceType,
|
||||||
|
ResourceID: l.ResourceID,
|
||||||
|
Message: l.Message,
|
||||||
|
Metadata: l.Metadata,
|
||||||
|
IPAddress: l.IPAddress,
|
||||||
|
UserAgent: l.UserAgent,
|
||||||
|
CreatedAt: l.CreatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Activity logs retrieved successfully",
|
||||||
|
Data: activityLogListRes{
|
||||||
|
Logs: logResponses,
|
||||||
|
TotalCount: totalCount,
|
||||||
|
Limit: int32(limit),
|
||||||
|
Offset: int32(offset),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetActivityLogByID godoc
|
||||||
|
// @Summary Get activity log by ID
|
||||||
|
// @Description Returns a single activity log entry by its ID
|
||||||
|
// @Tags activity-logs
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Activity Log ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/activity-logs/{id} [get]
|
||||||
|
func (h *Handler) GetActivityLogByID(c *fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid activity log ID",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := h.activityLogSvc.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Activity log not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Activity log retrieved successfully",
|
||||||
|
Data: activityLogRes{
|
||||||
|
ID: l.ID,
|
||||||
|
ActorID: l.ActorID,
|
||||||
|
ActorRole: l.ActorRole,
|
||||||
|
Action: l.Action,
|
||||||
|
ResourceType: l.ResourceType,
|
||||||
|
ResourceID: l.ResourceID,
|
||||||
|
Message: l.Message,
|
||||||
|
Metadata: l.Metadata,
|
||||||
|
IPAddress: l.IPAddress,
|
||||||
|
UserAgent: l.UserAgent,
|
||||||
|
CreatedAt: l.CreatedAt,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
"Yimaru-Backend/internal/web_server/response"
|
"Yimaru-Backend/internal/web_server/response"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -127,6 +129,13 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"email": req.Email, "admin_id": newUser.ID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionAdminCreated, domain.ResourceAdmin, &newUser.ID, "Created admin: "+req.Email, meta, &ip, &ua)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Admin created successfully", nil, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Admin created successfully", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -359,5 +368,12 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update admin: "+err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update admin: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"admin_id": adminID, "first_name": req.FirstName, "last_name": req.LastName})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionAdminUpdated, domain.ResourceAdmin, &adminID, fmt.Sprintf("Updated admin ID: %d", adminID), meta, &ip, &ua)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Admin updated successfully", nil, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Admin updated successfully", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
|
@ -49,6 +51,13 @@ func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"name": category.Name})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCategoryCreated, domain.ResourceCategory, &category.ID, "Created course category: "+category.Name, meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Course category created successfully",
|
Message: "Course category created successfully",
|
||||||
Data: courseCategoryRes{
|
Data: courseCategoryRes{
|
||||||
|
|
@ -206,6 +215,13 @@ func (h *Handler) UpdateCourseCategory(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id, "name": req.Name, "is_active": req.IsActive})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCategoryUpdated, domain.ResourceCategory, &id, fmt.Sprintf("Updated course category ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Course category updated successfully",
|
Message: "Course category updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -239,6 +255,13 @@ func (h *Handler) DeleteCourseCategory(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCategoryDeleted, domain.ResourceCategory, &id, fmt.Sprintf("Deleted category ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Course category deleted successfully",
|
Message: "Course category deleted successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -290,6 +313,13 @@ func (h *Handler) CreateCourse(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"title": course.Title, "category_id": course.CategoryID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseCreated, domain.ResourceCourse, &course.ID, "Created course: "+course.Title, meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Course created successfully",
|
Message: "Course created successfully",
|
||||||
Data: courseRes{
|
Data: courseRes{
|
||||||
|
|
@ -465,6 +495,13 @@ func (h *Handler) UpdateCourse(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id, "title": req.Title, "description": req.Description, "thumbnail": req.Thumbnail, "is_active": req.IsActive})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseUpdated, domain.ResourceCourse, &id, fmt.Sprintf("Updated course ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Course updated successfully",
|
Message: "Course updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -498,6 +535,13 @@ func (h *Handler) DeleteCourse(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionCourseDeleted, domain.ResourceCourse, &id, fmt.Sprintf("Deleted course ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Course deleted successfully",
|
Message: "Course deleted successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -553,6 +597,13 @@ func (h *Handler) CreateSubCourse(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"title": subCourse.Title, "course_id": subCourse.CourseID, "level": subCourse.Level})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubCourseCreated, domain.ResourceSubCourse, &subCourse.ID, "Created sub-course: "+subCourse.Title, meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Sub-course created successfully",
|
Message: "Sub-course created successfully",
|
||||||
Data: subCourseRes{
|
Data: subCourseRes{
|
||||||
|
|
@ -800,6 +851,13 @@ func (h *Handler) UpdateSubCourse(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id, "title": req.Title, "description": req.Description, "level": req.Level, "is_active": req.IsActive})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubCourseUpdated, domain.ResourceSubCourse, &id, fmt.Sprintf("Updated sub-course ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Sub-course updated successfully",
|
Message: "Sub-course updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -833,6 +891,13 @@ func (h *Handler) DeactivateSubCourse(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubCourseDeactivated, domain.ResourceSubCourse, &id, fmt.Sprintf("Deactivated sub-course ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Sub-course deactivated successfully",
|
Message: "Sub-course deactivated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -866,6 +931,13 @@ func (h *Handler) DeleteSubCourse(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubCourseDeleted, domain.ResourceSubCourse, &id, fmt.Sprintf("Deleted sub-course ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Sub-course deleted successfully",
|
Message: "Sub-course deleted successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -959,6 +1031,13 @@ func (h *Handler) CreateSubCourseVideo(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"title": video.Title, "sub_course_id": video.SubCourseID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoCreated, domain.ResourceVideo, &video.ID, "Created video: "+video.Title, meta, &ip, &ua)
|
||||||
|
|
||||||
var publishDate *string
|
var publishDate *string
|
||||||
if video.PublishDate != nil {
|
if video.PublishDate != nil {
|
||||||
pd := video.PublishDate.String()
|
pd := video.PublishDate.String()
|
||||||
|
|
@ -1197,6 +1276,13 @@ func (h *Handler) PublishSubCourseVideo(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoPublished, domain.ResourceVideo, &id, fmt.Sprintf("Published video ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Video published successfully",
|
Message: "Video published successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -1252,6 +1338,13 @@ func (h *Handler) UpdateSubCourseVideo(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id, "title": req.Title})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoUpdated, domain.ResourceVideo, &id, fmt.Sprintf("Updated video ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Video updated successfully",
|
Message: "Video updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -1285,6 +1378,13 @@ func (h *Handler) DeleteSubCourseVideo(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoArchived, domain.ResourceVideo, &id, fmt.Sprintf("Archived video ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Video deleted successfully",
|
Message: "Video deleted successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -1319,6 +1419,143 @@ func (h *Handler) GetFullLearningTree(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UploadSubCourseVideo godoc
|
||||||
|
// @Summary Upload a video file and create sub-course video
|
||||||
|
// @Description Accepts a video file upload, uploads it to Vimeo via TUS, and creates a sub-course video record
|
||||||
|
// @Tags sub-course-videos
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Produce json
|
||||||
|
// @Param file formData file true "Video file"
|
||||||
|
// @Param sub_course_id formData int true "Sub-course ID"
|
||||||
|
// @Param title formData string true "Video title"
|
||||||
|
// @Param description formData string false "Video description"
|
||||||
|
// @Param duration formData int false "Duration in seconds"
|
||||||
|
// @Param resolution formData string false "Video resolution"
|
||||||
|
// @Param instructor_id formData string false "Instructor ID"
|
||||||
|
// @Param thumbnail formData string false "Thumbnail URL"
|
||||||
|
// @Param visibility formData string false "Visibility"
|
||||||
|
// @Param display_order formData int false "Display order"
|
||||||
|
// @Success 201 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/course-management/videos/upload [post]
|
||||||
|
func (h *Handler) UploadSubCourseVideo(c *fiber.Ctx) error {
|
||||||
|
subCourseIDStr := c.FormValue("sub_course_id")
|
||||||
|
if subCourseIDStr == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "sub_course_id is required",
|
||||||
|
Error: "sub_course_id form field is empty",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
subCourseID, err := strconv.ParseInt(subCourseIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid sub_course_id",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
title := c.FormValue("title")
|
||||||
|
if title == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "title is required",
|
||||||
|
Error: "title form field is empty",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Video file is required",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxSize = 500 * 1024 * 1024 // 500 MB
|
||||||
|
if fileHeader.Size > maxSize {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "File too large",
|
||||||
|
Error: "Video file must be <= 500MB",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to read uploaded file",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var description *string
|
||||||
|
if desc := c.FormValue("description"); desc != "" {
|
||||||
|
description = &desc
|
||||||
|
}
|
||||||
|
|
||||||
|
var duration int32
|
||||||
|
if durStr := c.FormValue("duration"); durStr != "" {
|
||||||
|
durVal, err := strconv.ParseInt(durStr, 10, 32)
|
||||||
|
if err == nil {
|
||||||
|
duration = int32(durVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolution *string
|
||||||
|
if res := c.FormValue("resolution"); res != "" {
|
||||||
|
resolution = &res
|
||||||
|
}
|
||||||
|
|
||||||
|
var instructorID *string
|
||||||
|
if iid := c.FormValue("instructor_id"); iid != "" {
|
||||||
|
instructorID = &iid
|
||||||
|
}
|
||||||
|
|
||||||
|
var thumbnail *string
|
||||||
|
if thumb := c.FormValue("thumbnail"); thumb != "" {
|
||||||
|
thumbnail = &thumb
|
||||||
|
}
|
||||||
|
|
||||||
|
var visibility *string
|
||||||
|
if vis := c.FormValue("visibility"); vis != "" {
|
||||||
|
visibility = &vis
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayOrder *int32
|
||||||
|
if doStr := c.FormValue("display_order"); doStr != "" {
|
||||||
|
doVal, err := strconv.ParseInt(doStr, 10, 32)
|
||||||
|
if err == nil {
|
||||||
|
do := int32(doVal)
|
||||||
|
displayOrder = &do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video, err := h.courseMgmtSvc.CreateSubCourseVideoWithFileUpload(
|
||||||
|
c.Context(), subCourseID, title, description,
|
||||||
|
fileHeader.Filename, file, fileHeader.Size, duration, resolution,
|
||||||
|
instructorID, thumbnail, visibility, displayOrder,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to upload video",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"title": video.Title, "sub_course_id": subCourseID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoUploaded, domain.ResourceVideo, &video.ID, "Uploaded video: "+video.Title, meta, &ip, &ua)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
|
Message: "Video uploaded and created successfully",
|
||||||
|
Data: mapVideoToResponse(video),
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// CreateSubCourseVideoWithVimeo godoc
|
// CreateSubCourseVideoWithVimeo godoc
|
||||||
// @Summary Create a new sub-course video with Vimeo upload
|
// @Summary Create a new sub-course video with Vimeo upload
|
||||||
// @Description Creates a video by uploading to Vimeo from a source URL
|
// @Description Creates a video by uploading to Vimeo from a source URL
|
||||||
|
|
@ -1358,6 +1595,13 @@ func (h *Handler) CreateSubCourseVideoWithVimeo(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"title": video.Title, "sub_course_id": video.SubCourseID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoCreated, domain.ResourceVideo, &video.ID, "Created video with Vimeo: "+video.Title, meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Video created and uploaded to Vimeo successfully",
|
Message: "Video created and uploaded to Vimeo successfully",
|
||||||
Data: mapVideoToResponse(video),
|
Data: mapVideoToResponse(video),
|
||||||
|
|
@ -1403,6 +1647,13 @@ func (h *Handler) CreateSubCourseVideoFromVimeoID(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"title": video.Title, "sub_course_id": video.SubCourseID, "vimeo_video_id": req.VimeoVideoID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionVideoCreated, domain.ResourceVideo, &video.ID, "Imported video from Vimeo: "+video.Title, meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Video imported from Vimeo successfully",
|
Message: "Video imported from Vimeo successfully",
|
||||||
Data: mapVideoToResponse(video),
|
Data: mapVideoToResponse(video),
|
||||||
|
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
// @Summary Get supported currencies
|
|
||||||
// @Description Returns list of supported currencies
|
|
||||||
// @Tags Multi-Currency
|
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} domain.Response{data=[]domain.Currency}
|
|
||||||
// @Router /api/v1/currencies [get]
|
|
||||||
// func (h *Handler) GetSupportedCurrencies(c *fiber.Ctx) error {
|
|
||||||
// currencies, err := h.currSvc.GetSupportedCurrencies(c.Context())
|
|
||||||
// if err != nil {
|
|
||||||
// return domain.UnExpectedErrorResponse(c)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
||||||
// Success: true,
|
|
||||||
// Message: "Supported currencies retrieved successfully",
|
|
||||||
// Data: currencies,
|
|
||||||
// StatusCode: fiber.StatusOK,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @Summary Convert currency
|
|
||||||
// @Description Converts amount from one currency to another
|
|
||||||
// @Tags Multi-Currency
|
|
||||||
// @Produce json
|
|
||||||
// @Param from query string true "Source currency code (e.g., USD)"
|
|
||||||
// @Param to query string true "Target currency code (e.g., ETB)"
|
|
||||||
// @Param amount query number true "Amount to convert"
|
|
||||||
// @Success 200 {object} domain.Response{data=float64}
|
|
||||||
// @Failure 400 {object} domain.ErrorResponse
|
|
||||||
// @Router /api/v1/currencies/convert [get]
|
|
||||||
// func (h *Handler) ConvertCurrency(c *fiber.Ctx) error {
|
|
||||||
// from := domain.IntCurrency(c.Query("from"))
|
|
||||||
// to := domain.IntCurrency(c.Query("to"))
|
|
||||||
// amount := c.QueryFloat("amount", 0)
|
|
||||||
// // if err != nil {
|
|
||||||
// // return domain.BadRequestResponse(c)
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// converted, err := h.currSvc.Convert(c.Context(), amount, from, to)
|
|
||||||
// if err != nil {
|
|
||||||
// return domain.UnExpectedErrorResponse(c)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
||||||
// Success: true,
|
|
||||||
// Message: "Currency converted successfully",
|
|
||||||
// Data: converted,
|
|
||||||
// StatusCode: fiber.StatusOK,
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
@ -2,7 +2,9 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/config"
|
"Yimaru-Backend/internal/config"
|
||||||
|
activitylogservice "Yimaru-Backend/internal/services/activity_log"
|
||||||
"Yimaru-Backend/internal/services/arifpay"
|
"Yimaru-Backend/internal/services/arifpay"
|
||||||
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
"Yimaru-Backend/internal/services/assessment"
|
"Yimaru-Backend/internal/services/assessment"
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
course_management "Yimaru-Backend/internal/services/course_management"
|
course_management "Yimaru-Backend/internal/services/course_management"
|
||||||
|
|
@ -41,6 +43,8 @@ type Handler struct {
|
||||||
authSvc *authentication.Service
|
authSvc *authentication.Service
|
||||||
vimeoSvc *vimeoservice.Service
|
vimeoSvc *vimeoservice.Service
|
||||||
teamSvc *team.Service
|
teamSvc *team.Service
|
||||||
|
activityLogSvc *activitylogservice.Service
|
||||||
|
issueReportingSvc *issuereporting.Service
|
||||||
jwtConfig jwtutil.JwtConfig
|
jwtConfig jwtutil.JwtConfig
|
||||||
validator *customvalidator.CustomValidator
|
validator *customvalidator.CustomValidator
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
|
|
@ -63,6 +67,8 @@ func New(
|
||||||
authSvc *authentication.Service,
|
authSvc *authentication.Service,
|
||||||
vimeoSvc *vimeoservice.Service,
|
vimeoSvc *vimeoservice.Service,
|
||||||
teamSvc *team.Service,
|
teamSvc *team.Service,
|
||||||
|
activityLogSvc *activitylogservice.Service,
|
||||||
|
issueReportingSvc *issuereporting.Service,
|
||||||
jwtConfig jwtutil.JwtConfig,
|
jwtConfig jwtutil.JwtConfig,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
mongoLoggerSvc *zap.Logger,
|
mongoLoggerSvc *zap.Logger,
|
||||||
|
|
@ -83,6 +89,8 @@ func New(
|
||||||
authSvc: authSvc,
|
authSvc: authSvc,
|
||||||
vimeoSvc: vimeoSvc,
|
vimeoSvc: vimeoSvc,
|
||||||
teamSvc: teamSvc,
|
teamSvc: teamSvc,
|
||||||
|
activityLogSvc: activityLogSvc,
|
||||||
|
issueReportingSvc: issueReportingSvc,
|
||||||
jwtConfig: jwtConfig,
|
jwtConfig: jwtConfig,
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
mongoLoggerSvc: mongoLoggerSvc,
|
mongoLoggerSvc: mongoLoggerSvc,
|
||||||
|
|
|
||||||
400
internal/web_server/handlers/issue_reporting.go
Normal file
400
internal/web_server/handlers/issue_reporting.go
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type createIssueReq struct {
|
||||||
|
Subject string `json:"subject" validate:"required"`
|
||||||
|
Description string `json:"description" validate:"required"`
|
||||||
|
IssueType string `json:"issue_type" validate:"required"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type issueRes struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
UserRole string `json:"user_role"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
IssueType string `json:"issue_type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type updateIssueStatusReq struct {
|
||||||
|
Status string `json:"status" validate:"required,oneof=pending in_progress resolved rejected"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type issueListRes struct {
|
||||||
|
Issues []issueRes `json:"issues"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapIssueToRes(issue domain.ReportedIssue) issueRes {
|
||||||
|
return issueRes{
|
||||||
|
ID: issue.ID,
|
||||||
|
UserID: issue.UserID,
|
||||||
|
UserRole: string(issue.UserRole),
|
||||||
|
Subject: issue.Subject,
|
||||||
|
Description: issue.Description,
|
||||||
|
IssueType: string(issue.IssueType),
|
||||||
|
Status: string(issue.Status),
|
||||||
|
Metadata: issue.Metadata,
|
||||||
|
CreatedAt: issue.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
UpdatedAt: issue.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateIssue godoc
|
||||||
|
// @Summary Report an issue
|
||||||
|
// @Description Allows any authenticated user to report an issue they encountered
|
||||||
|
// @Tags issues
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param body body createIssueReq true "Issue report payload"
|
||||||
|
// @Success 201 {object} domain.Response{data=issueRes}
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 401 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/issues [post]
|
||||||
|
func (h *Handler) CreateIssue(c *fiber.Ctx) error {
|
||||||
|
var req createIssueReq
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
var errMsg string
|
||||||
|
for field, msg := range valErrs {
|
||||||
|
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Validation failed",
|
||||||
|
Error: errMsg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := c.Locals("user_id").(int64)
|
||||||
|
role := c.Locals("role").(domain.Role)
|
||||||
|
|
||||||
|
issueReq := domain.ReportedIssueReq{
|
||||||
|
Subject: req.Subject,
|
||||||
|
Description: req.Description,
|
||||||
|
IssueType: domain.ReportedIssueType(req.IssueType),
|
||||||
|
Metadata: req.Metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := h.issueReportingSvc.CreateReportedIssue(c.Context(), issueReq, userID, role)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to create issue report",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
actorRole := string(role)
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"subject": issue.Subject, "issue_type": string(issue.IssueType)})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &userID, &actorRole, domain.ActionIssueCreated, domain.ResourceIssue, &issue.ID, "Reported issue: "+issue.Subject, meta, &ip, &ua)
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
|
Message: "Issue reported successfully",
|
||||||
|
Data: mapIssueToRes(issue),
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIssueByID godoc
|
||||||
|
// @Summary Get issue by ID
|
||||||
|
// @Description Returns a single issue report by its ID (admin only)
|
||||||
|
// @Tags issues
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path int true "Issue ID"
|
||||||
|
// @Success 200 {object} domain.Response{data=issueRes}
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/issues/{id} [get]
|
||||||
|
func (h *Handler) GetIssueByID(c *fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid issue ID",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := h.issueReportingSvc.GetIssueByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Issue not found",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Issue retrieved successfully",
|
||||||
|
Data: mapIssueToRes(issue),
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserIssues godoc
|
||||||
|
// @Summary Get issues for a specific user
|
||||||
|
// @Description Returns paginated issues reported by a specific user (admin only)
|
||||||
|
// @Tags issues
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param user_id path int true "User ID"
|
||||||
|
// @Param limit query int false "Limit" default(20)
|
||||||
|
// @Param offset query int false "Offset" default(0)
|
||||||
|
// @Success 200 {object} domain.Response{data=issueListRes}
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/issues/user/{user_id} [get]
|
||||||
|
func (h *Handler) GetUserIssues(c *fiber.Ctx) error {
|
||||||
|
userIDStr := c.Params("user_id")
|
||||||
|
userID, err := strconv.ParseInt(userIDStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid user ID",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, err := h.issueReportingSvc.GetIssuesForUser(c.Context(), userID, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to retrieve user issues",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCount, _ := h.issueReportingSvc.CountIssuesByUser(c.Context(), userID)
|
||||||
|
|
||||||
|
var issueResponses []issueRes
|
||||||
|
for _, issue := range issues {
|
||||||
|
issueResponses = append(issueResponses, mapIssueToRes(issue))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "User issues retrieved successfully",
|
||||||
|
Data: issueListRes{
|
||||||
|
Issues: issueResponses,
|
||||||
|
TotalCount: totalCount,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMyIssues godoc
|
||||||
|
// @Summary Get my reported issues
|
||||||
|
// @Description Returns paginated issues reported by the authenticated user
|
||||||
|
// @Tags issues
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param limit query int false "Limit" default(20)
|
||||||
|
// @Param offset query int false "Offset" default(0)
|
||||||
|
// @Success 200 {object} domain.Response{data=issueListRes}
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/issues/me [get]
|
||||||
|
func (h *Handler) GetMyIssues(c *fiber.Ctx) error {
|
||||||
|
userID := c.Locals("user_id").(int64)
|
||||||
|
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, err := h.issueReportingSvc.GetIssuesForUser(c.Context(), userID, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to retrieve issues",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCount, _ := h.issueReportingSvc.CountIssuesByUser(c.Context(), userID)
|
||||||
|
|
||||||
|
var issueResponses []issueRes
|
||||||
|
for _, issue := range issues {
|
||||||
|
issueResponses = append(issueResponses, mapIssueToRes(issue))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Issues retrieved successfully",
|
||||||
|
Data: issueListRes{
|
||||||
|
Issues: issueResponses,
|
||||||
|
TotalCount: totalCount,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllIssues godoc
|
||||||
|
// @Summary Get all issues
|
||||||
|
// @Description Returns all reported issues with pagination (admin only)
|
||||||
|
// @Tags issues
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param limit query int false "Limit" default(20)
|
||||||
|
// @Param offset query int false "Offset" default(0)
|
||||||
|
// @Success 200 {object} domain.Response{data=issueListRes}
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/issues [get]
|
||||||
|
func (h *Handler) GetAllIssues(c *fiber.Ctx) error {
|
||||||
|
limit, _ := strconv.Atoi(c.Query("limit", "20"))
|
||||||
|
offset, _ := strconv.Atoi(c.Query("offset", "0"))
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
issues, err := h.issueReportingSvc.GetAllIssues(c.Context(), limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to retrieve issues",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
totalCount, _ := h.issueReportingSvc.CountAllIssues(c.Context())
|
||||||
|
|
||||||
|
var issueResponses []issueRes
|
||||||
|
for _, issue := range issues {
|
||||||
|
issueResponses = append(issueResponses, mapIssueToRes(issue))
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Issues retrieved successfully",
|
||||||
|
Data: issueListRes{
|
||||||
|
Issues: issueResponses,
|
||||||
|
TotalCount: totalCount,
|
||||||
|
},
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateIssueStatus godoc
|
||||||
|
// @Summary Update issue status
|
||||||
|
// @Description Updates the status of an issue (admin only)
|
||||||
|
// @Tags issues
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path int true "Issue ID"
|
||||||
|
// @Param body body updateIssueStatusReq true "Status update payload"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/issues/{id}/status [patch]
|
||||||
|
func (h *Handler) UpdateIssueStatus(c *fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid issue ID",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var req updateIssueStatusReq
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid request body",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
var errMsg string
|
||||||
|
for field, msg := range valErrs {
|
||||||
|
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Validation failed",
|
||||||
|
Error: errMsg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.issueReportingSvc.UpdateIssueStatus(c.Context(), id, req.Status); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to update issue status",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"issue_id": id, "new_status": req.Status})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionIssueStatusUpdated, domain.ResourceIssue, &id, fmt.Sprintf("Updated issue %d status to %s", id, req.Status), meta, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Issue status updated successfully",
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteIssue godoc
|
||||||
|
// @Summary Delete an issue
|
||||||
|
// @Description Deletes an issue report (admin only)
|
||||||
|
// @Tags issues
|
||||||
|
// @Produce json
|
||||||
|
// @Security Bearer
|
||||||
|
// @Param id path int true "Issue ID"
|
||||||
|
// @Success 200 {object} domain.Response
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse
|
||||||
|
// @Router /api/v1/issues/{id} [delete]
|
||||||
|
func (h *Handler) DeleteIssue(c *fiber.Ctx) error {
|
||||||
|
idStr := c.Params("id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Invalid issue ID",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.issueReportingSvc.DeleteIssue(c.Context(), id); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to delete issue",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"issue_id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionIssueDeleted, domain.ResourceIssue, &id, fmt.Sprintf("Deleted issue ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
|
return c.JSON(domain.Response{
|
||||||
|
Message: "Issue deleted successfully",
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ChapaSecret string
|
|
||||||
ChapaBaseURL string
|
|
||||||
)
|
|
||||||
|
|
||||||
type InitPaymentRequest struct {
|
|
||||||
Amount string `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
TxRef string `json:"tx_ref"`
|
|
||||||
CallbackURL string `json:"callback_url"`
|
|
||||||
ReturnURL string `json:"return_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransferRequest struct {
|
|
||||||
AccountNumber string `json:"account_number"`
|
|
||||||
BankCode string `json:"bank_code"`
|
|
||||||
Amount string `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
Reason string `json:"reason"`
|
|
||||||
RecipientName string `json:"recipient_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaSupportedBank struct {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Slug string `json:"slug"`
|
|
||||||
Swift string `json:"swift"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
AcctLength int `json:"acct_length"`
|
|
||||||
AcctNumberRegex string `json:"acct_number_regex"`
|
|
||||||
ExampleValue string `json:"example_value"`
|
|
||||||
CountryId int `json:"country_id"`
|
|
||||||
IsMobilemoney *int `json:"is_mobilemoney"`
|
|
||||||
|
|
||||||
IsActive int `json:"is_active"`
|
|
||||||
IsRtgs *int `json:"is_rtgs"`
|
|
||||||
Active int `json:"active"`
|
|
||||||
Is24Hrs *int `json:"is_24hrs"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChapaSupportedBanksResponse struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data []ChapaSupportedBank `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitPaymentData struct {
|
|
||||||
TxRef string `json:"tx_ref"`
|
|
||||||
CheckoutURL string `json:"checkout_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitPaymentResponse struct {
|
|
||||||
Status string `json:"status"` // "success"
|
|
||||||
Message string `json:"message"` // e.g., "Payment initialized"
|
|
||||||
Data InitPaymentData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebhookPayload map[string]interface{}
|
|
||||||
|
|
||||||
type TransactionData struct {
|
|
||||||
TxRef string `json:"tx_ref"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Amount string `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
CustomerEmail string `json:"email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type VerifyTransactionResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data TransactionData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransferData struct {
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Amount string `json:"amount"`
|
|
||||||
Currency string `json:"currency"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CreateTransferResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data TransferData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransferVerificationData struct {
|
|
||||||
Reference string `json:"reference"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
BankCode string `json:"bank_code"`
|
|
||||||
AccountName string `json:"account_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type VerifyTransferResponse struct {
|
|
||||||
Status string `json:"status"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data TransferVerificationData `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,9 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
@ -129,6 +132,13 @@ func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"question_type": question.QuestionType})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionCreated, domain.ResourceQuestion, &question.ID, "Created question: "+question.QuestionText, meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Question created successfully",
|
Message: "Question created successfully",
|
||||||
Data: questionRes{
|
Data: questionRes{
|
||||||
|
|
@ -429,6 +439,13 @@ func (h *Handler) UpdateQuestion(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"question_id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionUpdated, domain.ResourceQuestion, &id, fmt.Sprintf("Updated question ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question updated successfully",
|
Message: "Question updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -462,6 +479,13 @@ func (h *Handler) DeleteQuestion(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"question_id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionDeleted, domain.ResourceQuestion, &id, fmt.Sprintf("Deleted question ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question deleted successfully",
|
Message: "Question deleted successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -550,6 +574,13 @@ func (h *Handler) CreateQuestionSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
qsMeta, _ := json.Marshal(map[string]interface{}{"title": set.Title, "set_type": set.SetType})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetCreated, domain.ResourceQuestionSet, &set.ID, "Created question set: "+set.Title, qsMeta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Question set created successfully",
|
Message: "Question set created successfully",
|
||||||
Data: questionSetRes{
|
Data: questionSetRes{
|
||||||
|
|
@ -815,6 +846,13 @@ func (h *Handler) UpdateQuestionSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"question_set_id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetUpdated, domain.ResourceQuestionSet, &id, fmt.Sprintf("Updated question set ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question set updated successfully",
|
Message: "Question set updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -848,6 +886,13 @@ func (h *Handler) DeleteQuestionSet(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"question_set_id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionQuestionSetDeleted, domain.ResourceQuestionSet, &id, fmt.Sprintf("Deleted question set ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Question set deleted successfully",
|
Message: "Question set deleted successfully",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
"Yimaru-Backend/internal/web_server/response"
|
"Yimaru-Backend/internal/web_server/response"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -105,6 +107,14 @@ func (h *Handler) UpdateGlobalSettingList(c *fiber.Ctx) error {
|
||||||
|
|
||||||
res := domain.ConvertSettingListRes(settingsList)
|
res := domain.ConvertSettingListRes(settingsList)
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
settingID := int64(0)
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"updated": true})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSettingsUpdated, domain.ResourceSettings, &settingID, "Updated global settings", meta, &ip, &ua)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "setting updated", res, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "setting updated", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
|
@ -121,6 +124,13 @@ func (h *Handler) CreateSubscriptionPlan(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"name": plan.Name, "price": plan.Price})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubscriptionPlanCreated, domain.ResourceSubscriptionPlan, &plan.ID, "Created subscription plan: "+plan.Name, meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Subscription plan created successfully",
|
Message: "Subscription plan created successfully",
|
||||||
Data: planToRes(plan),
|
Data: planToRes(plan),
|
||||||
|
|
@ -232,6 +242,13 @@ func (h *Handler) UpdateSubscriptionPlan(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"plan_id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubscriptionPlanUpdated, domain.ResourceSubscriptionPlan, &id, fmt.Sprintf("Updated subscription plan ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Subscription plan updated successfully",
|
Message: "Subscription plan updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -262,6 +279,13 @@ func (h *Handler) DeleteSubscriptionPlan(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"plan_id": id})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionSubscriptionPlanDeleted, domain.ResourceSubscriptionPlan, &id, fmt.Sprintf("Deleted subscription plan ID: %d", id), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.JSON(domain.Response{
|
return c.JSON(domain.Response{
|
||||||
Message: "Subscription plan deleted successfully",
|
Message: "Subscription plan deleted successfully",
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"Yimaru-Backend/internal/domain"
|
"Yimaru-Backend/internal/domain"
|
||||||
jwtutil "Yimaru-Backend/internal/web_server/jwt"
|
jwtutil "Yimaru-Backend/internal/web_server/jwt"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -237,6 +239,12 @@ func (h *Handler) CreateTeamMember(c *fiber.Ctx) error {
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"email": member.Email, "team_role": string(member.TeamRole)})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &creatorID, &actorRole, domain.ActionTeamMemberCreated, domain.ResourceTeamMember, &member.ID, "Created team member: "+member.Email, meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
||||||
Message: "Team member created successfully",
|
Message: "Team member created successfully",
|
||||||
Data: toTeamMemberResponse(&member),
|
Data: toTeamMemberResponse(&member),
|
||||||
|
|
@ -464,6 +472,12 @@ func (h *Handler) UpdateTeamMember(c *fiber.Ctx) error {
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"member_id": memberID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &updaterID, &actorRole, domain.ActionTeamMemberUpdated, domain.ResourceTeamMember, &memberID, fmt.Sprintf("Updated team member ID: %d", memberID), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
Message: "Team member updated successfully",
|
Message: "Team member updated successfully",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
|
@ -560,6 +574,12 @@ func (h *Handler) UpdateTeamMemberStatus(c *fiber.Ctx) error {
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"member_id": memberID, "new_status": req.Status})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &updaterID, &actorRole, domain.ActionTeamMemberUpdated, domain.ResourceTeamMember, &memberID, fmt.Sprintf("Updated team member status ID: %d to %s", memberID, req.Status), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
Message: "Team member status updated successfully",
|
Message: "Team member status updated successfully",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
|
@ -616,6 +636,13 @@ func (h *Handler) DeleteTeamMember(c *fiber.Ctx) error {
|
||||||
zap.Time("timestamp", time.Now()),
|
zap.Time("timestamp", time.Now()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"member_id": memberID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionTeamMemberDeleted, domain.ResourceTeamMember, &memberID, fmt.Sprintf("Deleted team member ID: %d", memberID), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
Message: "Team member deleted successfully",
|
Message: "Team member deleted successfully",
|
||||||
Success: true,
|
Success: true,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"Yimaru-Backend/internal/services/authentication"
|
"Yimaru-Backend/internal/services/authentication"
|
||||||
jwtutil "Yimaru-Backend/internal/web_server/jwt"
|
jwtutil "Yimaru-Backend/internal/web_server/jwt"
|
||||||
"Yimaru-Backend/internal/web_server/response"
|
"Yimaru-Backend/internal/web_server/response"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -140,6 +142,12 @@ func (h *Handler) UpdateUser(c *fiber.Ctx) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"user_id": userID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &userID, &actorRole, domain.ActionUserUpdated, domain.ResourceUser, &userID, fmt.Sprintf("Updated user ID: %d", userID), meta, &ip, &ua)
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
Message: "User updated successfully",
|
Message: "User updated successfully",
|
||||||
})
|
})
|
||||||
|
|
@ -1594,6 +1602,13 @@ func (h *Handler) DeleteUser(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete user:"+err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete user:"+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actorID := c.Locals("user_id").(int64)
|
||||||
|
actorRole := string(c.Locals("role").(domain.Role))
|
||||||
|
ip := c.IP()
|
||||||
|
ua := c.Get("User-Agent")
|
||||||
|
meta, _ := json.Marshal(map[string]interface{}{"deleted_user_id": userID})
|
||||||
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionUserDeleted, domain.ResourceUser, &userID, fmt.Sprintf("Deleted user ID: %d", userID), meta, &ip, &ua)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "User deleted successfully", nil, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "User deleted successfully", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ func (a *App) initAppRoutes() {
|
||||||
a.authSvc,
|
a.authSvc,
|
||||||
a.vimeoSvc,
|
a.vimeoSvc,
|
||||||
a.teamSvc,
|
a.teamSvc,
|
||||||
|
a.activityLogSvc,
|
||||||
|
a.issueReportingSvc,
|
||||||
a.JwtConfig,
|
a.JwtConfig,
|
||||||
a.cfg,
|
a.cfg,
|
||||||
a.mongoLoggerSvc,
|
a.mongoLoggerSvc,
|
||||||
|
|
@ -160,6 +162,7 @@ func (a *App) initAppRoutes() {
|
||||||
// Sub-course Videos
|
// Sub-course Videos
|
||||||
groupV1.Post("/course-management/videos", a.authMiddleware, h.CreateSubCourseVideo)
|
groupV1.Post("/course-management/videos", a.authMiddleware, h.CreateSubCourseVideo)
|
||||||
groupV1.Post("/course-management/videos/vimeo", a.authMiddleware, h.CreateSubCourseVideoWithVimeo)
|
groupV1.Post("/course-management/videos/vimeo", a.authMiddleware, h.CreateSubCourseVideoWithVimeo)
|
||||||
|
groupV1.Post("/course-management/videos/upload", a.authMiddleware, h.UploadSubCourseVideo)
|
||||||
groupV1.Post("/course-management/videos/vimeo/import", a.authMiddleware, h.CreateSubCourseVideoFromVimeoID)
|
groupV1.Post("/course-management/videos/vimeo/import", a.authMiddleware, h.CreateSubCourseVideoFromVimeoID)
|
||||||
groupV1.Get("/course-management/videos/:id", a.authMiddleware, h.GetSubCourseVideoByID)
|
groupV1.Get("/course-management/videos/:id", a.authMiddleware, h.GetSubCourseVideoByID)
|
||||||
groupV1.Get("/course-management/sub-courses/:subCourseId/videos", a.authMiddleware, h.GetVideosBySubCourse)
|
groupV1.Get("/course-management/sub-courses/:subCourseId/videos", a.authMiddleware, h.GetVideosBySubCourse)
|
||||||
|
|
@ -313,6 +316,10 @@ func (a *App) initAppRoutes() {
|
||||||
//mongoDB logs
|
//mongoDB logs
|
||||||
groupV1.Get("/logs", a.authMiddleware, a.OnlyAdminAndAbove, handlers.GetLogsHandler(context.Background()))
|
groupV1.Get("/logs", a.authMiddleware, a.OnlyAdminAndAbove, handlers.GetLogsHandler(context.Background()))
|
||||||
|
|
||||||
|
// Activity Logs
|
||||||
|
groupV1.Get("/activity-logs", a.authMiddleware, a.OnlyAdminAndAbove, h.GetActivityLogs)
|
||||||
|
groupV1.Get("/activity-logs/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.GetActivityLogByID)
|
||||||
|
|
||||||
// groupV1.Get("/shop/transaction", a.authMiddleware, a.CompanyOnly, h.GetAllTransactions)
|
// groupV1.Get("/shop/transaction", a.authMiddleware, a.CompanyOnly, h.GetAllTransactions)
|
||||||
// groupV1.Get("/shop/transaction/:id", a.authMiddleware, a.CompanyOnly, h.GetTransactionByID)
|
// groupV1.Get("/shop/transaction/:id", a.authMiddleware, a.CompanyOnly, h.GetTransactionByID)
|
||||||
// groupV1.Get("/shop/transaction/:id/bet", a.authMiddleware, a.CompanyOnly, h.GetShopBetByTransactionID)
|
// groupV1.Get("/shop/transaction/:id/bet", a.authMiddleware, a.CompanyOnly, h.GetShopBetByTransactionID)
|
||||||
|
|
@ -327,12 +334,14 @@ func (a *App) initAppRoutes() {
|
||||||
groupV1.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications)
|
groupV1.Get("/notifications/unread", a.authMiddleware, h.CountUnreadNotifications)
|
||||||
groupV1.Post("/notifications/create", a.authMiddleware, h.CreateAndSendNotification)
|
groupV1.Post("/notifications/create", a.authMiddleware, h.CreateAndSendNotification)
|
||||||
|
|
||||||
//Issue Reporting Routes
|
// Issue Reporting Routes
|
||||||
// groupV1.Post("/issues", a.authMiddleware, h.CreateIssue) //anyone who has logged can report a
|
groupV1.Post("/issues", a.authMiddleware, h.CreateIssue)
|
||||||
// groupV1.Get("/issues/customer/:customer_id", a.authMiddleware, a.OnlyAdminAndAbove, h.GetUserIssues)
|
groupV1.Get("/issues/me", a.authMiddleware, h.GetMyIssues)
|
||||||
// groupV1.Get("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllIssues)
|
groupV1.Get("/issues/user/:user_id", a.authMiddleware, a.OnlyAdminAndAbove, h.GetUserIssues)
|
||||||
// groupV1.Patch("/issues/:issue_id/status", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateIssueStatus)
|
groupV1.Get("/issues", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllIssues)
|
||||||
// groupV1.Delete("/issues/:issue_id", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteIssue)
|
groupV1.Get("/issues/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.GetIssueByID)
|
||||||
|
groupV1.Patch("/issues/:id/status", a.authMiddleware, a.OnlyAdminAndAbove, h.UpdateIssueStatus)
|
||||||
|
groupV1.Delete("/issues/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteIssue)
|
||||||
|
|
||||||
// Device Token Registration
|
// Device Token Registration
|
||||||
groupV1.Post("/devices/register", a.authMiddleware, h.RegisterDeviceToken)
|
groupV1.Post("/devices/register", a.authMiddleware, h.RegisterDeviceToken)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user