-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathadmin_handler.go
More file actions
656 lines (573 loc) · 19.2 KB
/
admin_handler.go
File metadata and controls
656 lines (573 loc) · 19.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
package app
import (
"context"
"errors"
"fmt"
"io"
"kubecloud/internal"
"kubecloud/models"
"mime/multipart"
"net/http"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"kubecloud/internal/constants"
"kubecloud/internal/logger"
"kubecloud/internal/notification"
"github.com/gin-gonic/gin"
"github.com/hashicorp/go-multierror"
"gorm.io/gorm"
)
type UserResponse struct {
models.User
Balance float64 `json:"balance"` // USD balance
}
// GenerateVouchersInput holds all data needed when creating vouchers
type GenerateVouchersInput struct {
Count int `json:"count" binding:"required,gt=0"`
Value float64 `json:"value" binding:"required,gt=0"`
ExpireAfter int `json:"expire_after_days" binding:"required,gt=0"`
}
// CreditRequestInput represents a request to credit a user's balance
type CreditRequestInput struct {
AmountUSD float64 `json:"amount" binding:"required,gt=0"`
Memo string `json:"memo" binding:"required,min=3,max=255"`
}
// CreditUserResponse holds the response data after crediting a user
type CreditUserResponse struct {
User string `json:"user"`
AmountUSD float64 `json:"amount"`
Memo string `json:"memo"`
}
type PendingRecordsResponse struct {
models.PendingRecord
USDAmount float64 `json:"usd_amount"`
TransferredUSDAmount float64 `json:"transferred_usd_amount"`
}
// AdminMailInput represents the form data for sending emails to all users
type AdminMailInput struct {
Subject string `form:"subject" binding:"required"`
Body string `form:"body" binding:"required"`
Attachments []*multipart.FileHeader `form:"attachments"`
}
type SendMailResponse struct {
TotalUsers int `json:"total_users"`
SuccessfulEmails int `json:"successful_emails"`
FailedEmailsCount int `json:"failed_emails_count"`
FailedEmails []string `json:"failed_emails,omitempty"`
}
type MaintenanceModeStatus struct {
Enabled bool `json:"enabled"`
}
// @Summary Get all users
// @Description Returns a list of all users
// @Tags admin
// @ID get-all-users
// @Accept json
// @Produce json
// @Success 200 {array} UserResponse
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /users [get]
// ListUsersHandler lists all users
func (h *Handler) ListUsersHandler(c *gin.Context) {
users, err := h.db.ListAllUsers()
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to list all users")
InternalServerError(c)
return
}
var usersWithBalance []UserResponse
const maxConcurrentBalanceFetches = 20
balanceConcurrencyLimiter := make(chan struct{}, maxConcurrentBalanceFetches)
var (
wg sync.WaitGroup
mu sync.Mutex
multiErr *multierror.Error
)
for _, user := range users {
wg.Add(1)
balanceConcurrencyLimiter <- struct{}{}
go func(user models.User) {
defer wg.Done()
defer func() { <-balanceConcurrencyLimiter }()
if len(user.Mnemonic) == 0 || len(user.AccountAddress) == 0 {
mu.Lock()
usersWithBalance = append(usersWithBalance, UserResponse{User: user, Balance: 0})
mu.Unlock()
return
}
decryptedMnemonic, err := h.cryptoManager.Decrypt(user.Mnemonic, user.AccountAddress)
if err != nil {
logger.GetLogger().Error().Err(err).Int("user_id", user.ID).Msg("failed to decrypt user mnemonic")
mu.Lock()
multiErr = multierror.Append(multiErr, fmt.Errorf("user %d: failed to decrypt mnemonic", user.ID))
mu.Unlock()
return
}
balance, err := internal.GetUserBalanceUSDMillicent(h.substrateClient, decryptedMnemonic)
if err != nil {
logger.GetLogger().Error().Err(err).Int("user_id", user.ID).Msg("failed to get user balance")
mu.Lock()
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to get balance for user %d: %w", user.ID, err))
mu.Unlock()
return
}
balanceUSD := internal.FromUSDMilliCentToUSD(balance)
mu.Lock()
usersWithBalance = append(usersWithBalance, UserResponse{
User: user,
Balance: balanceUSD,
})
mu.Unlock()
}(user)
}
wg.Wait()
// Check if there were any errors during balance fetching
if multiErr != nil {
logger.GetLogger().Error().Err(multiErr).Msg("errors occurred while fetching user balances")
InternalServerError(c)
return
}
Success(c, http.StatusOK, "Users are retrieved successfully", map[string]interface{}{
"users": usersWithBalance,
})
}
// @Summary Delete a user
// @Description Deletes a user from the system
// @Tags admin
// @ID delete-user
// @Accept json
// @Produce json
// @Param user_id path string true "User ID"
// @Success 200 {object} APIResponse
// @Failure 400 {object} APIResponse "Invalid user ID"
// @Failure 403 {object} APIResponse "Admins cannot delete their own account"
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /users/{user_id} [delete]
// DeleteUsersHandler deletes user from system
func (h *Handler) DeleteUsersHandler(c *gin.Context) {
userID := c.Param("user_id")
if userID == "" {
Error(c, http.StatusBadRequest, "User ID is required", "")
return
}
id, err := strconv.Atoi(userID)
if err != nil || id == 0 {
logger.GetLogger().Error().Err(err).Send()
Error(c, http.StatusBadRequest, "Invalid user ID", err.Error())
return
}
authUserID := c.GetInt("user_id")
if id == authUserID {
Error(c, http.StatusForbidden, "Admins cannot delete their own account", "")
return
}
err = h.db.DeleteUserByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
Error(c, http.StatusNotFound, "User not found", "")
} else {
InternalServerError(c)
}
return
}
Success(c, http.StatusOK, "User is deleted successfully", nil)
}
// @Summary Generate vouchers
// @Description Generates a bulk of vouchers
// @Tags admin
// @ID generate-vouchers
// @Accept json
// @Produce json
// @Param body body GenerateVouchersInput true "Generate Vouchers Input"
// @Success 201 {array} models.Voucher
// @Failure 400 {object} APIResponse "Invalid request format"
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /vouchers/generate [post]
// GenerateVouchersHandler generates bulk of vouchers
func (h *Handler) GenerateVouchersHandler(c *gin.Context) {
var request GenerateVouchersInput
// check on request format
if err := c.ShouldBindJSON(&request); err != nil {
logger.GetLogger().Error().Err(err).Send()
Error(c, http.StatusBadRequest, "Invalid request format", err.Error())
return
}
var vouchers []models.Voucher
for i := 0; i < request.Count; i++ {
voucherCode := internal.GenerateRandomVoucher(h.config.VoucherNameLength)
timestampPart := fmt.Sprintf("%02d%02d", time.Now().Minute(), time.Now().Second())
fullCode := fmt.Sprintf("%s-%s", voucherCode, timestampPart)
voucher := models.Voucher{
Code: fullCode,
Value: request.Value,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(request.ExpireAfter) * 24 * time.Hour),
}
if err := h.db.CreateVoucher(&voucher); err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to create voucher")
InternalServerError(c)
return
}
vouchers = append(vouchers, voucher)
}
adminID := c.GetInt("user_id")
if h.notificationService != nil && adminID > 0 {
payload := notification.MergePayload(notification.CommonPayload{
Message: fmt.Sprintf("%d vouchers generated successfully.", request.Count),
Subject: "Vouchers Generated",
Status: "succeeded",
}, map[string]string{})
notif := models.NewNotification(
adminID,
models.NotificationTypeBilling,
payload,
models.WithChannels(notification.ChannelUI),
models.WithSeverity(models.NotificationSeveritySuccess),
models.WithNoPersist(),
)
if err := h.notificationService.Send(c.Request.Context(), notif); err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to send UI notification for voucher generation")
}
}
Success(c, http.StatusCreated, "Vouchers are generated successfully", map[string]interface{}{
"vouchers": vouchers,
})
}
// @Summary List vouchers
// @Description Returns all vouchers in the system
// @Tags admin
// @ID list-vouchers
// @Accept json
// @Produce json
// @Success 200 {array} models.Voucher
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /vouchers [get]
// ListVouchersHandler returns all vouchers in system
func (h *Handler) ListVouchersHandler(c *gin.Context) {
vouchers, err := h.db.ListAllVouchers()
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to list all vouchers")
InternalServerError(c)
return
}
Success(c, http.StatusOK, "Vouchers are Retrieved successfully", map[string]interface{}{
"vouchers": vouchers,
})
}
// @Summary Credit user balance
// @Description Credits a specific user's balance
// @Tags admin
// @ID credit-user
// @Accept json
// @Produce json
// @Param user_id path string true "User ID"
// @Param body body CreditRequestInput true "Credit Request Input"
// @Success 202 {object} CreditUserResponse
// @Failure 400 {object} APIResponse "Invalid request format or user ID"
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /users/{user_id}/credit [post]
// CreditUserHandler lets admin credit specific user with money
func (h *Handler) CreditUserHandler(c *gin.Context) {
userID := c.Param("user_id")
if userID == "" {
Error(c, http.StatusBadRequest, "User ID is required", "")
return
}
var request CreditRequestInput
// check on request format
if err := c.ShouldBindJSON(&request); err != nil {
Error(c, http.StatusBadRequest, "Invalid request format", err.Error())
return
}
id, err := strconv.Atoi(userID)
if err != nil || id == 0 {
logger.GetLogger().Error().Err(err).Send()
Error(c, http.StatusBadRequest, "Invalid user ID format", "")
return
}
user, err := h.db.GetUserByID(id)
if err != nil {
logger.GetLogger().Error().Err(err).Send()
InternalServerError(c)
return
}
// get admin ID from middleware context
adminID := c.GetInt("user_id")
transaction := models.Transaction{
UserID: user.ID,
AdminID: adminID,
Amount: request.AmountUSD,
Memo: request.Memo,
CreatedAt: time.Now(),
}
wf, err := h.ewfEngine.NewWorkflow(constants.WorkflowAdminCreditBalance)
if err != nil {
logger.GetLogger().Error().Err(err).Send()
InternalServerError(c)
return
}
if err := h.db.CreateTransaction(&transaction); err != nil {
logger.GetLogger().Error().Err(err).Msg("Failed to create credit transaction")
InternalServerError(c)
return
}
decryptedMnemonic, err := h.cryptoManager.Decrypt(user.Mnemonic, user.AccountAddress)
if err != nil {
logger.GetLogger().Error().Err(err).Send()
InternalServerError(c)
return
}
wf.State = map[string]interface{}{
"user_id": user.ID,
"amount": internal.FromUSDToUSDMillicent(request.AmountUSD),
"mnemonic": decryptedMnemonic,
"username": user.Username,
"transfer_mode": models.AdminCreditMode,
"admin_id": adminID,
}
h.ewfEngine.RunAsync(context.Background(), wf)
Success(c, http.StatusAccepted, "Transaction is created successfully, Money transfer is in progress", CreditUserResponse{
User: user.Email,
AmountUSD: request.AmountUSD,
Memo: request.Memo,
})
}
// @Summary List pending records
// @Description Returns all pending records in the system
// @Tags admin
// @ID list-pending-records
// @Accept json
// @Produce json
// @Success 200 {array} PendingRecordsResponse
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /pending-records [get]
// ListPendingRecordsHandler returns all pending records in the system
func (h *Handler) ListPendingRecordsHandler(c *gin.Context) {
pendingRecords, err := h.db.ListAllPendingRecords()
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to list all pending records")
InternalServerError(c)
return
}
var pendingRecordsResponse []PendingRecordsResponse
for _, record := range pendingRecords {
usdAmount, err := internal.FromTFTtoUSDMillicent(h.substrateClient, record.TFTAmount)
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to convert tft to usd amount")
InternalServerError(c)
return
}
usdTransferredAmount, err := internal.FromTFTtoUSDMillicent(h.substrateClient, record.TransferredTFTAmount)
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to convert tft to usd transferred amount")
InternalServerError(c)
return
}
pendingRecordsResponse = append(pendingRecordsResponse, PendingRecordsResponse{
PendingRecord: record,
USDAmount: internal.FromUSDMilliCentToUSD(usdAmount),
TransferredUSDAmount: internal.FromUSDMilliCentToUSD(usdTransferredAmount),
})
}
Success(c, http.StatusOK, "Pending records are retrieved successfully", map[string]any{
"pending_records": pendingRecordsResponse,
})
}
// Only accessible by admins
// @Summary Send mail to all users
// @Description Allows admin to send a custom email to all users with optional file attachments. Returns detailed statistics about successful and failed email deliveries.
// @Tags admin
// @ID admin-mail-all-users
// @Accept multipart/form-data
// @Produce json
// @Param subject formData string true "Email subject"
// @Param body formData string true "Email body content"
// @Param attachments formData file false "Email attachments (multiple files allowed)"
// @Success 200 {object} APIResponse{data=SendMailResponse} "Email sending results with delivery statistics"
// @Failure 400 {object} APIResponse "Invalid request format"
// @Failure 500 {object} APIResponse "Internal server error"
// @Security AdminMiddleware
// @Router /users/mail [post]
func (h *Handler) SendMailToAllUsersHandler(c *gin.Context) {
var input AdminMailInput
if err := c.ShouldBind(&input); err != nil {
Error(c, http.StatusBadRequest, "Invalid request format", err.Error())
return
}
var attachments []internal.Attachment
if form, err := c.MultipartForm(); err == nil {
if uploaded, ok := form.File["attachments"]; ok {
logger.GetLogger().Info().Int("attachment_count", len(uploaded)).Msg("parsed email attachments")
attachments, err = h.parseAttachments(uploaded)
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to parse attachments")
InternalServerError(c)
return
}
}
}
users, err := h.db.ListAllUsers()
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to list all users")
InternalServerError(c)
return
}
body := h.mailService.SystemAnnouncementMailBody(input.Body)
emailConcurrencyLimiter := make(chan struct{}, h.config.MailSender.MaxConcurrentSends)
var (
wg sync.WaitGroup
mu sync.Mutex
failedEmails []string
)
logger.GetLogger().Info().Int("attachment_count", len(attachments)).Msg("parsed email attachments")
for _, user := range users {
wg.Add(1)
emailConcurrencyLimiter <- struct{}{}
go func(user models.User) {
defer wg.Done()
defer func() { <-emailConcurrencyLimiter }()
err := h.mailService.SendMail(h.config.MailSender.Email, user.Email, input.Subject, body, attachments...)
if err != nil {
logger.GetLogger().Error().Err(err).Str("user_email", user.Email).Msg("failed to send mail to user")
mu.Lock()
failedEmails = append(failedEmails, user.Email)
mu.Unlock()
}
}(user)
}
wg.Wait()
totalUsers := len(users)
responseData := SendMailResponse{
TotalUsers: totalUsers,
SuccessfulEmails: totalUsers - len(failedEmails),
FailedEmailsCount: len(failedEmails),
}
if responseData.SuccessfulEmails == 0 {
Error(c, http.StatusInternalServerError, "failed to send mail to all users", "")
return
}
if responseData.FailedEmailsCount > 0 {
Success(c, http.StatusOK, fmt.Sprintf("Mail sent to %d/%d users successfully", responseData.SuccessfulEmails, responseData.TotalUsers), responseData)
return
}
Success(c, http.StatusOK, "Mail sent successfully to all users", responseData)
}
func (h *Handler) parseAttachments(fileHeaders []*multipart.FileHeader) ([]internal.Attachment, error) {
if len(fileHeaders) == 0 {
return nil, nil
}
allowedTypes := map[string]bool{
".pdf": true, ".doc": true, ".docx": true, ".txt": true,
".jpg": true, ".jpeg": true, ".png": true, ".gif": true, ".zip": true,
}
var (
mu sync.Mutex
multiErr *multierror.Error
results []internal.Attachment
wg sync.WaitGroup
)
wg.Add(len(fileHeaders))
for _, fileHeader := range fileHeaders {
go func(fh *multipart.FileHeader) {
defer wg.Done()
ext := strings.ToLower(filepath.Ext(fh.Filename))
if !allowedTypes[ext] {
mu.Lock()
multiErr = multierror.Append(multiErr, fmt.Errorf("file type %s not allowed for %s", ext, fh.Filename))
mu.Unlock()
return
}
maxFileSizeBytes := h.config.MailSender.MaxAttachmentSizeMB * 1024 * 1024
if fh.Size > maxFileSizeBytes {
mu.Lock()
multiErr = multierror.Append(multiErr, fmt.Errorf("file %s is too large: %d bytes (max %d bytes)", fh.Filename, fh.Size, maxFileSizeBytes))
mu.Unlock()
return
}
file, err := fh.Open()
if err != nil {
logger.GetLogger().Error().Err(err).Str("filename", fh.Filename).Msg("failed to open attachment file")
mu.Lock()
multiErr = multierror.Append(multiErr, err)
mu.Unlock()
return
}
defer file.Close()
fileData, err := io.ReadAll(file)
if err != nil {
logger.GetLogger().Error().Err(err).Str("filename", fh.Filename).Msg("failed to read attachment file")
mu.Lock()
multiErr = multierror.Append(multiErr, err)
mu.Unlock()
return
}
attachment := internal.Attachment{
FileName: fh.Filename,
Data: fileData,
}
mu.Lock()
results = append(results, attachment)
mu.Unlock()
}(fileHeader)
}
wg.Wait()
return results, multiErr.ErrorOrNil()
}
// @Summary Set maintenance mode
// @Description Sets maintenance mode for the system
// @Tags admin
// @ID set-maintenance-mode
// @Accept json
// @Produce json
// @Param body body MaintenanceModeStatus true "Maintenance Mode Status"
// @Success 200 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /system/maintenance/status [put]
// SetMaintenanceModeHandler sets maintenance mode for the system
func (h *Handler) SetMaintenanceModeHandler(c *gin.Context) {
var request MaintenanceModeStatus
// check on request format
if err := c.ShouldBindJSON(&request); err != nil {
logger.GetLogger().Error().Err(err).Send()
Error(c, http.StatusBadRequest, "Invalid request format", err.Error())
return
}
if err := h.redis.SetMaintenanceMode(c.Request.Context(), request.Enabled); err != nil {
logger.GetLogger().Error().Err(err).Send()
InternalServerError(c)
return
}
Success(c, http.StatusOK, "Maintenance mode is set successfully", nil)
}
// @Summary Get maintenance mode
// @Description Gets maintenance mode for the system
// @Tags admin
// @ID get-maintenance-mode
// @Accept json
// @Produce json
// @Success 200 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /system/maintenance/status [get]
// GetMaintenanceModeHandler gets maintenance mode for the system
func (h *Handler) GetMaintenanceModeHandler(c *gin.Context) {
enabled, err := h.redis.GetMaintenanceMode(c.Request.Context())
if err != nil {
logger.GetLogger().Error().Err(err).Send()
InternalServerError(c)
return
}
Success(c, http.StatusOK, "Maintenance mode is retrieved successfully", MaintenanceModeStatus{
Enabled: enabled,
})
}