feat: implement system statistics tracking, commit management controllers, and associated database migrations
All checks were successful
Build and Release / release (push) Successful in 1m49s
All checks were successful
Build and Release / release (push) Successful in 1m49s
This commit is contained in:
@@ -136,3 +136,35 @@ func (h *CommitController) GetProjectCommits(c fiber.Ctx) error {
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// GetCommitByID godoc
|
||||
// @Summary Get commit by ID
|
||||
// @Description Retrieve a specific commit by its ID
|
||||
// @Tags Commits
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param commitId path string true "Commit ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 404 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /projects/commits/{commitId} [get]
|
||||
func (h *CommitController) GetCommitByID(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
commitID := c.Params("commitId")
|
||||
res, err := h.service.GetCommitByID(ctx, commitID)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
96
internal/controllers/statisticController.go
Normal file
96
internal/controllers/statisticController.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/services"
|
||||
"history-api/pkg/validator"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type StatisticController struct {
|
||||
statService services.StatisticService
|
||||
}
|
||||
|
||||
func NewStatisticController(statService services.StatisticService) *StatisticController {
|
||||
return &StatisticController{
|
||||
statService: statService,
|
||||
}
|
||||
}
|
||||
|
||||
// SearchStatistics godoc
|
||||
// @Summary Search system statistics
|
||||
// @Description Fetch daily system statistics with optional date range filtering
|
||||
// @Tags Statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param start_date query string false "Start date in YYYY-MM-DD format"
|
||||
// @Param end_date query string false "End date in YYYY-MM-DD format"
|
||||
// @Success 200 {object} response.CommonResponse{data=[]response.StatisticResponse}
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 401 {object} response.CommonResponse
|
||||
// @Failure 403 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /statistics [get]
|
||||
// @Security BearerAuth
|
||||
func (c *StatisticController) SearchStatistics(ctx fiber.Ctx) error {
|
||||
dto := new(request.SearchStatisticDto)
|
||||
if err := validator.ValidateQueryDto(ctx, dto); err != nil {
|
||||
return ctx.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Errors: err,
|
||||
})
|
||||
}
|
||||
|
||||
res, err := c.statService.Search(ctx.Context(), dto)
|
||||
if err != nil {
|
||||
return ctx.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStatisticByDate godoc
|
||||
// @Summary Get system statistics by date
|
||||
// @Description Fetch system statistics for a specific date
|
||||
// @Tags Statistics
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param date path string true "Date in YYYY-MM-DD format"
|
||||
// @Success 200 {object} response.CommonResponse{data=response.StatisticResponse}
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 401 {object} response.CommonResponse
|
||||
// @Failure 403 {object} response.CommonResponse
|
||||
// @Failure 404 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /statistics/{date} [get]
|
||||
// @Security BearerAuth
|
||||
func (c *StatisticController) GetStatisticByDate(ctx fiber.Ctx) error {
|
||||
dateStr := ctx.Params("date")
|
||||
if dateStr == "" {
|
||||
return ctx.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "Date parameter is required",
|
||||
})
|
||||
}
|
||||
|
||||
res, err := c.statService.GetByDate(ctx.Context(), dateStr)
|
||||
if err != nil {
|
||||
return ctx.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
|
||||
return ctx.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
@@ -526,3 +526,80 @@ func (h *UserController) GetProjectByUserID(c fiber.Ctx) error {
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminUpdateProfile godoc
|
||||
// @Summary Update user profile (Admin/Mod only)
|
||||
// @Description Update the profile details of any user
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param request body request.UpdateProfileDto true "Update Profile request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id} [put]
|
||||
func (h *UserController) AdminUpdateProfile(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
userId := c.Params("id")
|
||||
dto := &request.UpdateProfileDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Errors: err,
|
||||
})
|
||||
}
|
||||
|
||||
res, err := h.service.UpdateProfile(ctx, userId, dto)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminResetPassword godoc
|
||||
// @Summary Reset user password (Admin/Mod only)
|
||||
// @Description Reset the password for any user without requiring the old password
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "User ID"
|
||||
// @Param request body request.ResetPasswordDto true "Reset Password request"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id}/password [patch]
|
||||
func (h *UserController) AdminResetPassword(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
userId := c.Params("id")
|
||||
dto := &request.ResetPasswordDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Errors: err,
|
||||
})
|
||||
}
|
||||
err := h.service.AdminResetPassword(ctx, userId, dto)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Message: "Password reset successfully",
|
||||
})
|
||||
}
|
||||
|
||||
6
internal/dtos/request/statistic.go
Normal file
6
internal/dtos/request/statistic.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package request
|
||||
|
||||
type SearchStatisticDto struct {
|
||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||
}
|
||||
@@ -45,3 +45,8 @@ type CreateUserDto struct {
|
||||
DisplayName string `json:"display_name" validate:"required,min=2,max=50"`
|
||||
Roles []string `json:"role_ids" validate:"required,min=1,dive,required,uuid"`
|
||||
}
|
||||
|
||||
type ResetPasswordDto struct {
|
||||
NewPassword string `json:"new_password" validate:"required,min=8,max=64"`
|
||||
IsSendEmail bool `json:"is_send_email"`
|
||||
}
|
||||
|
||||
27
internal/dtos/response/statistic.go
Normal file
27
internal/dtos/response/statistic.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package response
|
||||
|
||||
import "time"
|
||||
|
||||
type StatisticResponse struct {
|
||||
ID string `json:"id"`
|
||||
Date string `json:"date"`
|
||||
TotalUsers int32 `json:"total_users"`
|
||||
TotalProjects int32 `json:"total_projects"`
|
||||
TotalCommits int32 `json:"total_commits"`
|
||||
TotalSubmissions int32 `json:"total_submissions"`
|
||||
TotalMedias int32 `json:"total_medias"`
|
||||
TotalWikis int32 `json:"total_wikis"`
|
||||
TotalEntities int32 `json:"total_entities"`
|
||||
TotalGeometries int32 `json:"total_geometries"`
|
||||
TotalStorageBytes int64 `json:"total_storage_bytes"`
|
||||
NewUsers int32 `json:"new_users"`
|
||||
NewProjects int32 `json:"new_projects"`
|
||||
NewCommits int32 `json:"new_commits"`
|
||||
NewSubmissions int32 `json:"new_submissions"`
|
||||
NewMedias int32 `json:"new_medias"`
|
||||
NewWikis int32 `json:"new_wikis"`
|
||||
NewEntities int32 `json:"new_entities"`
|
||||
NewGeometries int32 `json:"new_geometries"`
|
||||
NewStorageBytes int64 `json:"new_storage_bytes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
@@ -129,6 +129,30 @@ type Submission struct {
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type SystemStatistic struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Date pgtype.Date `json:"date"`
|
||||
TotalUsers int32 `json:"total_users"`
|
||||
TotalProjects int32 `json:"total_projects"`
|
||||
TotalCommits int32 `json:"total_commits"`
|
||||
TotalSubmissions int32 `json:"total_submissions"`
|
||||
TotalMedias int32 `json:"total_medias"`
|
||||
TotalWikis int32 `json:"total_wikis"`
|
||||
TotalEntities int32 `json:"total_entities"`
|
||||
TotalGeometries int32 `json:"total_geometries"`
|
||||
TotalStorageBytes int64 `json:"total_storage_bytes"`
|
||||
NewUsers int32 `json:"new_users"`
|
||||
NewProjects int32 `json:"new_projects"`
|
||||
NewCommits int32 `json:"new_commits"`
|
||||
NewSubmissions int32 `json:"new_submissions"`
|
||||
NewMedias int32 `json:"new_medias"`
|
||||
NewWikis int32 `json:"new_wikis"`
|
||||
NewEntities int32 `json:"new_entities"`
|
||||
NewGeometries int32 `json:"new_geometries"`
|
||||
NewStorageBytes int64 `json:"new_storage_bytes"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
|
||||
224
internal/gen/sqlc/statistic.sql.go
Normal file
224
internal/gen/sqlc/statistic.sql.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: statistic.sql
|
||||
|
||||
package sqlc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const getSystemStatisticsByDate = `-- name: GetSystemStatisticsByDate :one
|
||||
SELECT id, date, total_users, total_projects, total_commits, total_submissions, total_medias, total_wikis, total_entities, total_geometries, total_storage_bytes, new_users, new_projects, new_commits, new_submissions, new_medias, new_wikis, new_entities, new_geometries, new_storage_bytes, created_at FROM system_statistics WHERE date = $1 LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSystemStatisticsByDate(ctx context.Context, date pgtype.Date) (SystemStatistic, error) {
|
||||
row := q.db.QueryRow(ctx, getSystemStatisticsByDate, date)
|
||||
var i SystemStatistic
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Date,
|
||||
&i.TotalUsers,
|
||||
&i.TotalProjects,
|
||||
&i.TotalCommits,
|
||||
&i.TotalSubmissions,
|
||||
&i.TotalMedias,
|
||||
&i.TotalWikis,
|
||||
&i.TotalEntities,
|
||||
&i.TotalGeometries,
|
||||
&i.TotalStorageBytes,
|
||||
&i.NewUsers,
|
||||
&i.NewProjects,
|
||||
&i.NewCommits,
|
||||
&i.NewSubmissions,
|
||||
&i.NewMedias,
|
||||
&i.NewWikis,
|
||||
&i.NewEntities,
|
||||
&i.NewGeometries,
|
||||
&i.NewStorageBytes,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSystemStatisticsByIDs = `-- name: GetSystemStatisticsByIDs :many
|
||||
SELECT id, date, total_users, total_projects, total_commits, total_submissions, total_medias, total_wikis, total_entities, total_geometries, total_storage_bytes, new_users, new_projects, new_commits, new_submissions, new_medias, new_wikis, new_entities, new_geometries, new_storage_bytes, created_at FROM system_statistics WHERE id = ANY($1::UUID[])
|
||||
`
|
||||
|
||||
func (q *Queries) GetSystemStatisticsByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]SystemStatistic, error) {
|
||||
rows, err := q.db.Query(ctx, getSystemStatisticsByIDs, dollar_1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SystemStatistic{}
|
||||
for rows.Next() {
|
||||
var i SystemStatistic
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Date,
|
||||
&i.TotalUsers,
|
||||
&i.TotalProjects,
|
||||
&i.TotalCommits,
|
||||
&i.TotalSubmissions,
|
||||
&i.TotalMedias,
|
||||
&i.TotalWikis,
|
||||
&i.TotalEntities,
|
||||
&i.TotalGeometries,
|
||||
&i.TotalStorageBytes,
|
||||
&i.NewUsers,
|
||||
&i.NewProjects,
|
||||
&i.NewCommits,
|
||||
&i.NewSubmissions,
|
||||
&i.NewMedias,
|
||||
&i.NewWikis,
|
||||
&i.NewEntities,
|
||||
&i.NewGeometries,
|
||||
&i.NewStorageBytes,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const searchSystemStatistics = `-- name: SearchSystemStatistics :many
|
||||
SELECT id, date, total_users, total_projects, total_commits, total_submissions, total_medias, total_wikis, total_entities, total_geometries, total_storage_bytes, new_users, new_projects, new_commits, new_submissions, new_medias, new_wikis, new_entities, new_geometries, new_storage_bytes, created_at FROM system_statistics
|
||||
WHERE
|
||||
($1::DATE IS NULL OR date >= $1::DATE) AND
|
||||
($2::DATE IS NULL OR date <= $2::DATE)
|
||||
ORDER BY date DESC
|
||||
`
|
||||
|
||||
type SearchSystemStatisticsParams struct {
|
||||
StartDate pgtype.Date `json:"start_date"`
|
||||
EndDate pgtype.Date `json:"end_date"`
|
||||
}
|
||||
|
||||
func (q *Queries) SearchSystemStatistics(ctx context.Context, arg SearchSystemStatisticsParams) ([]SystemStatistic, error) {
|
||||
rows, err := q.db.Query(ctx, searchSystemStatistics, arg.StartDate, arg.EndDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SystemStatistic{}
|
||||
for rows.Next() {
|
||||
var i SystemStatistic
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Date,
|
||||
&i.TotalUsers,
|
||||
&i.TotalProjects,
|
||||
&i.TotalCommits,
|
||||
&i.TotalSubmissions,
|
||||
&i.TotalMedias,
|
||||
&i.TotalWikis,
|
||||
&i.TotalEntities,
|
||||
&i.TotalGeometries,
|
||||
&i.TotalStorageBytes,
|
||||
&i.NewUsers,
|
||||
&i.NewProjects,
|
||||
&i.NewCommits,
|
||||
&i.NewSubmissions,
|
||||
&i.NewMedias,
|
||||
&i.NewWikis,
|
||||
&i.NewEntities,
|
||||
&i.NewGeometries,
|
||||
&i.NewStorageBytes,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertSystemStatistics = `-- name: UpsertSystemStatistics :one
|
||||
INSERT INTO system_statistics (
|
||||
date,
|
||||
total_users, total_projects, total_commits, total_submissions, total_medias, total_wikis, total_entities, total_geometries, total_storage_bytes,
|
||||
new_users, new_projects, new_commits, new_submissions, new_medias, new_wikis, new_entities, new_geometries, new_storage_bytes
|
||||
) VALUES (
|
||||
$1,
|
||||
(SELECT COUNT(*)::INT FROM users),
|
||||
(SELECT COUNT(*)::INT FROM projects),
|
||||
(SELECT COUNT(*)::INT FROM commits),
|
||||
(SELECT COUNT(*)::INT FROM submissions),
|
||||
(SELECT COUNT(*)::INT FROM medias),
|
||||
(SELECT COUNT(*)::INT FROM wikis),
|
||||
(SELECT COUNT(*)::INT FROM entities),
|
||||
(SELECT COUNT(*)::INT FROM geometries),
|
||||
COALESCE((SELECT SUM(size)::BIGINT FROM medias), 0),
|
||||
|
||||
(SELECT COUNT(*)::INT FROM users WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'),
|
||||
(SELECT COUNT(*)::INT FROM projects WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'),
|
||||
(SELECT COUNT(*)::INT FROM commits WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'),
|
||||
(SELECT COUNT(*)::INT FROM submissions WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'),
|
||||
(SELECT COUNT(*)::INT FROM medias WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'),
|
||||
(SELECT COUNT(*)::INT FROM wikis WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'),
|
||||
(SELECT COUNT(*)::INT FROM entities WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'),
|
||||
(SELECT COUNT(*)::INT FROM geometries WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'),
|
||||
COALESCE((SELECT SUM(size)::BIGINT FROM medias WHERE created_at >= $1::DATE AND created_at < $1::DATE + INTERVAL '1 day'), 0)
|
||||
)
|
||||
ON CONFLICT (date) DO UPDATE SET
|
||||
total_users = EXCLUDED.total_users,
|
||||
total_projects = EXCLUDED.total_projects,
|
||||
total_commits = EXCLUDED.total_commits,
|
||||
total_submissions = EXCLUDED.total_submissions,
|
||||
total_medias = EXCLUDED.total_medias,
|
||||
total_wikis = EXCLUDED.total_wikis,
|
||||
total_entities = EXCLUDED.total_entities,
|
||||
total_geometries = EXCLUDED.total_geometries,
|
||||
total_storage_bytes = EXCLUDED.total_storage_bytes,
|
||||
new_users = EXCLUDED.new_users,
|
||||
new_projects = EXCLUDED.new_projects,
|
||||
new_commits = EXCLUDED.new_commits,
|
||||
new_submissions = EXCLUDED.new_submissions,
|
||||
new_medias = EXCLUDED.new_medias,
|
||||
new_wikis = EXCLUDED.new_wikis,
|
||||
new_entities = EXCLUDED.new_entities,
|
||||
new_geometries = EXCLUDED.new_geometries,
|
||||
new_storage_bytes = EXCLUDED.new_storage_bytes
|
||||
RETURNING id, date, total_users, total_projects, total_commits, total_submissions, total_medias, total_wikis, total_entities, total_geometries, total_storage_bytes, new_users, new_projects, new_commits, new_submissions, new_medias, new_wikis, new_entities, new_geometries, new_storage_bytes, created_at
|
||||
`
|
||||
|
||||
func (q *Queries) UpsertSystemStatistics(ctx context.Context, date pgtype.Date) (SystemStatistic, error) {
|
||||
row := q.db.QueryRow(ctx, upsertSystemStatistics, date)
|
||||
var i SystemStatistic
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Date,
|
||||
&i.TotalUsers,
|
||||
&i.TotalProjects,
|
||||
&i.TotalCommits,
|
||||
&i.TotalSubmissions,
|
||||
&i.TotalMedias,
|
||||
&i.TotalWikis,
|
||||
&i.TotalEntities,
|
||||
&i.TotalGeometries,
|
||||
&i.TotalStorageBytes,
|
||||
&i.NewUsers,
|
||||
&i.NewProjects,
|
||||
&i.NewCommits,
|
||||
&i.NewSubmissions,
|
||||
&i.NewMedias,
|
||||
&i.NewWikis,
|
||||
&i.NewEntities,
|
||||
&i.NewGeometries,
|
||||
&i.NewStorageBytes,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
66
internal/models/statistic.go
Normal file
66
internal/models/statistic.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"history-api/internal/dtos/response"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StatisticEntity struct {
|
||||
ID string `json:"id"`
|
||||
Date time.Time `json:"date"`
|
||||
TotalUsers int32 `json:"total_users"`
|
||||
TotalProjects int32 `json:"total_projects"`
|
||||
TotalCommits int32 `json:"total_commits"`
|
||||
TotalSubmissions int32 `json:"total_submissions"`
|
||||
TotalMedias int32 `json:"total_medias"`
|
||||
TotalWikis int32 `json:"total_wikis"`
|
||||
TotalEntities int32 `json:"total_entities"`
|
||||
TotalGeometries int32 `json:"total_geometries"`
|
||||
TotalStorageBytes int64 `json:"total_storage_bytes"`
|
||||
NewUsers int32 `json:"new_users"`
|
||||
NewProjects int32 `json:"new_projects"`
|
||||
NewCommits int32 `json:"new_commits"`
|
||||
NewSubmissions int32 `json:"new_submissions"`
|
||||
NewMedias int32 `json:"new_medias"`
|
||||
NewWikis int32 `json:"new_wikis"`
|
||||
NewEntities int32 `json:"new_entities"`
|
||||
NewGeometries int32 `json:"new_geometries"`
|
||||
NewStorageBytes int64 `json:"new_storage_bytes"`
|
||||
CreatedAt *time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (e *StatisticEntity) ToResponse() *response.StatisticResponse {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
dateStr := e.Date.Format("2006-01-02")
|
||||
var createdAt time.Time
|
||||
if e.CreatedAt != nil {
|
||||
createdAt = *e.CreatedAt
|
||||
}
|
||||
|
||||
return &response.StatisticResponse{
|
||||
ID: e.ID,
|
||||
Date: dateStr,
|
||||
TotalUsers: e.TotalUsers,
|
||||
TotalProjects: e.TotalProjects,
|
||||
TotalCommits: e.TotalCommits,
|
||||
TotalSubmissions: e.TotalSubmissions,
|
||||
TotalMedias: e.TotalMedias,
|
||||
TotalWikis: e.TotalWikis,
|
||||
TotalEntities: e.TotalEntities,
|
||||
TotalGeometries: e.TotalGeometries,
|
||||
TotalStorageBytes: e.TotalStorageBytes,
|
||||
NewUsers: e.NewUsers,
|
||||
NewProjects: e.NewProjects,
|
||||
NewCommits: e.NewCommits,
|
||||
NewSubmissions: e.NewSubmissions,
|
||||
NewMedias: e.NewMedias,
|
||||
NewWikis: e.NewWikis,
|
||||
NewEntities: e.NewEntities,
|
||||
NewGeometries: e.NewGeometries,
|
||||
NewStorageBytes: e.NewStorageBytes,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
}
|
||||
@@ -88,3 +88,9 @@ func UsersEntityToResponse(users []*UserEntity) []*response.UserResponse {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type AdminUserActionPayload struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
241
internal/repositories/statisticRepo.go
Normal file
241
internal/repositories/statisticRepo.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type StatisticRepository interface {
|
||||
Search(ctx context.Context, params sqlc.SearchSystemStatisticsParams) ([]*models.StatisticEntity, error)
|
||||
GetByDate(ctx context.Context, date time.Time) (*models.StatisticEntity, error)
|
||||
GetByID(ctx context.Context, id pgtype.UUID) (*models.StatisticEntity, error)
|
||||
Upsert(ctx context.Context, date time.Time) (*models.StatisticEntity, error)
|
||||
WithTx(tx pgx.Tx) StatisticRepository
|
||||
}
|
||||
|
||||
type statisticRepository struct {
|
||||
q *sqlc.Queries
|
||||
c cache.Cache
|
||||
db sqlc.DBTX
|
||||
}
|
||||
|
||||
func NewStatisticRepository(db sqlc.DBTX, c cache.Cache) StatisticRepository {
|
||||
return &statisticRepository{
|
||||
q: sqlc.New(db),
|
||||
c: c,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statisticRepository) WithTx(tx pgx.Tx) StatisticRepository {
|
||||
return &statisticRepository{
|
||||
q: r.q.WithTx(tx),
|
||||
c: r.c,
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statisticRepository) generateQueryKey(prefix string, params any) string {
|
||||
b, _ := json.Marshal(params)
|
||||
hash := fmt.Sprintf("%x", md5.Sum(b))
|
||||
return fmt.Sprintf("%s:%s", prefix, hash)
|
||||
}
|
||||
|
||||
func mapToEntity(row sqlc.SystemStatistic) *models.StatisticEntity {
|
||||
return &models.StatisticEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
Date: row.Date.Time,
|
||||
TotalUsers: row.TotalUsers,
|
||||
TotalProjects: row.TotalProjects,
|
||||
TotalCommits: row.TotalCommits,
|
||||
TotalSubmissions: row.TotalSubmissions,
|
||||
TotalMedias: row.TotalMedias,
|
||||
TotalWikis: row.TotalWikis,
|
||||
TotalEntities: row.TotalEntities,
|
||||
TotalGeometries: row.TotalGeometries,
|
||||
TotalStorageBytes: row.TotalStorageBytes,
|
||||
NewUsers: row.NewUsers,
|
||||
NewProjects: row.NewProjects,
|
||||
NewCommits: row.NewCommits,
|
||||
NewSubmissions: row.NewSubmissions,
|
||||
NewMedias: row.NewMedias,
|
||||
NewWikis: row.NewWikis,
|
||||
NewEntities: row.NewEntities,
|
||||
NewGeometries: row.NewGeometries,
|
||||
NewStorageBytes: row.NewStorageBytes,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statisticRepository) getByIDsWithFallback(ctx context.Context, ids []string) ([]*models.StatisticEntity, error) {
|
||||
if len(ids) == 0 {
|
||||
return []*models.StatisticEntity{}, nil
|
||||
}
|
||||
keys := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
keys[i] = fmt.Sprintf("statistic:id:%s", id)
|
||||
}
|
||||
raws := r.c.MGet(ctx, keys...)
|
||||
|
||||
var stats []*models.StatisticEntity
|
||||
missingStatsToCache := make(map[string]any)
|
||||
|
||||
var missingPgIds []pgtype.UUID
|
||||
for i, b := range raws {
|
||||
if len(b) == 0 {
|
||||
pgId := pgtype.UUID{}
|
||||
err := pgId.Scan(ids[i])
|
||||
if err == nil {
|
||||
missingPgIds = append(missingPgIds, pgId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbMap := make(map[string]*models.StatisticEntity)
|
||||
if len(missingPgIds) > 0 {
|
||||
dbRows, err := r.q.GetSystemStatisticsByIDs(ctx, missingPgIds)
|
||||
if err == nil {
|
||||
for _, row := range dbRows {
|
||||
entity := mapToEntity(row)
|
||||
dbMap[entity.ID] = entity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, b := range raws {
|
||||
if len(b) > 0 {
|
||||
var s models.StatisticEntity
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
stats = append(stats, &s)
|
||||
}
|
||||
} else {
|
||||
if item, ok := dbMap[ids[i]]; ok {
|
||||
stats = append(stats, item)
|
||||
missingStatsToCache[keys[i]] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingStatsToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, missingStatsToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (r *statisticRepository) Search(ctx context.Context, params sqlc.SearchSystemStatisticsParams) ([]*models.StatisticEntity, error) {
|
||||
queryKey := r.generateQueryKey("statistic:search", params)
|
||||
|
||||
var cachedIDs []string
|
||||
if err := r.c.Get(ctx, queryKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||
return r.getByIDsWithFallback(ctx, cachedIDs)
|
||||
}
|
||||
|
||||
rows, err := r.q.SearchSystemStatistics(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ids []string
|
||||
statsToCache := make(map[string]any)
|
||||
var stats []*models.StatisticEntity
|
||||
|
||||
for _, row := range rows {
|
||||
entity := mapToEntity(row)
|
||||
ids = append(ids, entity.ID)
|
||||
stats = append(stats, entity)
|
||||
statsToCache[fmt.Sprintf("statistic:id:%s", entity.ID)] = entity
|
||||
}
|
||||
|
||||
if len(statsToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, statsToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (r *statisticRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.StatisticEntity, error) {
|
||||
cacheId := fmt.Sprintf("statistic:id:%s", convert.UUIDToString(id))
|
||||
var stat models.StatisticEntity
|
||||
err := r.c.Get(ctx, cacheId, &stat)
|
||||
if err == nil {
|
||||
return &stat, nil
|
||||
}
|
||||
|
||||
rows, err := r.q.GetSystemStatisticsByIDs(ctx, []pgtype.UUID{id})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
entity := mapToEntity(rows[0])
|
||||
_ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration)
|
||||
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func (r *statisticRepository) GetByDate(ctx context.Context, date time.Time) (*models.StatisticEntity, error) {
|
||||
dateStr := date.Format("2006-01-02")
|
||||
cacheId := fmt.Sprintf("statistic:date:%s", dateStr)
|
||||
|
||||
var stat models.StatisticEntity
|
||||
err := r.c.Get(ctx, cacheId, &stat)
|
||||
if err == nil {
|
||||
return &stat, nil
|
||||
}
|
||||
|
||||
row, err := r.q.GetSystemStatisticsByDate(ctx, pgtype.Date{Time: date, Valid: true})
|
||||
if err != nil {
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entity := mapToEntity(row)
|
||||
|
||||
_ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration)
|
||||
_ = r.c.Set(ctx, fmt.Sprintf("statistic:id:%s", entity.ID), entity, constants.NormalCacheDuration)
|
||||
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
func (r *statisticRepository) Upsert(ctx context.Context, date time.Time) (*models.StatisticEntity, error) {
|
||||
row, err := r.q.UpsertSystemStatistics(ctx, pgtype.Date{Time: date, Valid: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entity := mapToEntity(row)
|
||||
|
||||
// Clear search cache and the specific date cache
|
||||
go func() {
|
||||
bgCtx := context.Background()
|
||||
_ = r.c.DelByPattern(bgCtx, "statistic:search*")
|
||||
_ = r.c.Del(
|
||||
bgCtx,
|
||||
fmt.Sprintf("statistic:id:%s", entity.ID),
|
||||
fmt.Sprintf("statistic:date:%s", date.Format("2006-01-02")),
|
||||
)
|
||||
}()
|
||||
|
||||
return entity, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,12 @@ func ProjectRoutes(
|
||||
userRepo repositories.UserRepository,
|
||||
) {
|
||||
route := app.Group("/projects")
|
||||
|
||||
route.Get(
|
||||
"/commits/:commitId",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
commitController.GetCommitByID,
|
||||
)
|
||||
|
||||
route.Post(
|
||||
"/:id/commits",
|
||||
@@ -35,6 +41,7 @@ func ProjectRoutes(
|
||||
commitController.GetProjectCommits,
|
||||
)
|
||||
|
||||
|
||||
route.Post(
|
||||
"/:id/members",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
|
||||
22
internal/routes/statisticRoute.go
Normal file
22
internal/routes/statisticRoute.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
"history-api/internal/middlewares"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/constants"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func StatisticRoutes(app *fiber.App, statController *controllers.StatisticController, userRepo repositories.UserRepository) {
|
||||
statGroup := app.Group(
|
||||
"/statistics",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
|
||||
)
|
||||
|
||||
statGroup.Get("/", statController.SearchStatistics)
|
||||
statGroup.Get("/:date", statController.GetStatisticByDate)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.GetUserCurrent,
|
||||
)
|
||||
|
||||
|
||||
route.Put(
|
||||
"/current",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
@@ -61,6 +61,13 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
||||
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
|
||||
controller.DeleteUser,
|
||||
)
|
||||
|
||||
route.Put(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
|
||||
controller.AdminUpdateProfile,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/:id/media",
|
||||
@@ -90,6 +97,13 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
||||
controller.RestoreUser,
|
||||
)
|
||||
|
||||
route.Patch(
|
||||
"/:id/password",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.RoleTypeAdmin, constants.RoleTypeMod),
|
||||
controller.AdminResetPassword,
|
||||
)
|
||||
|
||||
route.Patch(
|
||||
"/:id/role",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
|
||||
@@ -22,6 +22,7 @@ type CommitService interface {
|
||||
CreateCommit(ctx context.Context, userID string, projectID string, dto *request.CreateCommitDto) (*response.CommitResponse, *fiber.Error)
|
||||
RestoreCommit(ctx context.Context, userID string, projectID string, dto *request.RestoreCommitDto) *fiber.Error
|
||||
GetProjectCommits(ctx context.Context, projectID string) ([]*response.CommitResponse, *fiber.Error)
|
||||
GetCommitByID(ctx context.Context, commitID string) (*response.CommitResponse, *fiber.Error)
|
||||
}
|
||||
|
||||
type commitService struct {
|
||||
@@ -188,3 +189,17 @@ func (s *commitService) GetProjectCommits(ctx context.Context, projectID string)
|
||||
|
||||
return models.CommitsEntityToResponse(commits), nil
|
||||
}
|
||||
|
||||
func (s *commitService) GetCommitByID(ctx context.Context, commitID string) (*response.CommitResponse, *fiber.Error) {
|
||||
commitUUID, err := convert.StringToUUID(commitID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid commit ID")
|
||||
}
|
||||
|
||||
commit, err := s.commitRepo.GetByID(ctx, commitUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Commit not found")
|
||||
}
|
||||
|
||||
return commit.ToResponse(), nil
|
||||
}
|
||||
|
||||
75
internal/services/statisticService.go
Normal file
75
internal/services/statisticService.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/repositories"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type StatisticService interface {
|
||||
Search(ctx context.Context, dto *request.SearchStatisticDto) ([]*response.StatisticResponse, *fiber.Error)
|
||||
GetByDate(ctx context.Context, dateStr string) (*response.StatisticResponse, *fiber.Error)
|
||||
}
|
||||
|
||||
type statisticService struct {
|
||||
repo repositories.StatisticRepository
|
||||
}
|
||||
|
||||
func NewStatisticService(repo repositories.StatisticRepository) StatisticService {
|
||||
return &statisticService{repo: repo}
|
||||
}
|
||||
|
||||
func (s *statisticService) Search(ctx context.Context, dto *request.SearchStatisticDto) ([]*response.StatisticResponse, *fiber.Error) {
|
||||
params := sqlc.SearchSystemStatisticsParams{}
|
||||
|
||||
if dto.StartDate != "" {
|
||||
parsedDate, err := time.Parse("2006-01-02", dto.StartDate)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid start_date format, expected YYYY-MM-DD")
|
||||
}
|
||||
params.StartDate = pgtype.Date{Time: parsedDate, Valid: true}
|
||||
}
|
||||
|
||||
if dto.EndDate != "" {
|
||||
parsedDate, err := time.Parse("2006-01-02", dto.EndDate)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid end_date format, expected YYYY-MM-DD")
|
||||
}
|
||||
params.EndDate = pgtype.Date{Time: parsedDate, Valid: true}
|
||||
}
|
||||
|
||||
stats, err := s.repo.Search(ctx, params)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search statistics")
|
||||
}
|
||||
|
||||
responses := make([]*response.StatisticResponse, 0, len(stats))
|
||||
for _, stat := range stats {
|
||||
responses = append(responses, stat.ToResponse())
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *statisticService) GetByDate(ctx context.Context, dateStr string) (*response.StatisticResponse, *fiber.Error) {
|
||||
parsedDate, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format, expected YYYY-MM-DD")
|
||||
}
|
||||
|
||||
stat, err := s.repo.GetByDate(ctx, parsedDate)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get statistics")
|
||||
}
|
||||
if stat == nil {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Statistics not found for the given date")
|
||||
}
|
||||
|
||||
return stat.ToResponse(), nil
|
||||
}
|
||||
@@ -34,6 +34,7 @@ type UserService interface {
|
||||
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error)
|
||||
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, *fiber.Error)
|
||||
SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, *fiber.Error)
|
||||
AdminResetPassword(ctx context.Context, userId string, dto *request.ResetPasswordDto) *fiber.Error
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
@@ -121,7 +122,13 @@ func (u *userService) CreateUser(ctx context.Context, dto *request.CreateUserDto
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
|
||||
}
|
||||
|
||||
|
||||
_ = u.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeAdminUserAction, models.AdminUserActionPayload{
|
||||
Email: dto.Email,
|
||||
Password: dto.Password,
|
||||
Action: "create",
|
||||
})
|
||||
|
||||
finalUser, err := u.userRepo.GetByID(ctx, userUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created user")
|
||||
@@ -189,6 +196,63 @@ func (u *userService) ChangePassword(ctx context.Context, userId string, dto *re
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *userService) AdminResetPassword(ctx context.Context, userId string, dto *request.ResetPasswordDto) *fiber.Error {
|
||||
tx, err := u.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
uRepo := u.userRepo.WithTx(tx)
|
||||
|
||||
pgID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Invalid user ID")
|
||||
}
|
||||
user, err := u.userRepo.GetByID(ctx, pgID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Failed to fetch user")
|
||||
}
|
||||
if user == nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, "User not found")
|
||||
}
|
||||
|
||||
hashPassword, err := bcrypt.GenerateFromPassword([]byte(dto.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to hash new password")
|
||||
}
|
||||
|
||||
err = uRepo.UpdatePassword(ctx, sqlc.UpdateUserPasswordParams{
|
||||
ID: pgID,
|
||||
PasswordHash: pgtype.Text{String: string(hashPassword), Valid: true},
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update password")
|
||||
}
|
||||
|
||||
err = uRepo.UpdateTokenVersion(ctx, sqlc.UpdateTokenVersionParams{
|
||||
ID: pgID,
|
||||
TokenVersion: user.TokenVersion + 1,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update token version")
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
|
||||
}
|
||||
|
||||
if dto.IsSendEmail {
|
||||
_ = u.c.PublishTask(ctx, constants.StreamEmailName, constants.TaskTypeAdminUserAction, models.AdminUserActionPayload{
|
||||
Email: user.Email,
|
||||
Password: dto.NewPassword,
|
||||
Action: "reset",
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *userService) ChangeRoleUser(ctx context.Context, userId string, claims *response.JWTClaims, dto *request.ChangeRoleDto) (*response.UserResponse, *fiber.Error) {
|
||||
tx, err := u.db.Begin(ctx)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user