diff --git a/server/app/admin_handler.go b/server/app/admin_handler.go index afbeb046..89f5c414 100644 --- a/server/app/admin_handler.go +++ b/server/app/admin_handler.go @@ -204,6 +204,11 @@ func (a *App) SetPricesHandler(req *http.Request) (interface{}, Response) { a.config.PricesPerMonth.PublicIP = input.PublicIP } + if err := a.logVMsPriceUpdate(req, a.config.PricesPerMonth); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "New prices are set", Data: nil, @@ -414,6 +419,11 @@ func (a *App) DeleteAllDeploymentsHandler(req *http.Request) (interface{}, Respo } } + if err := a.logAllDeploymentsDelete(req); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Deployments are deleted successfully", }, Ok() @@ -452,6 +462,11 @@ func (a *App) UpdateMaintenanceHandler(req *http.Request) (interface{}, Response return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logMaintenanceUpdate(req, input.ON); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Maintenance is updated successfully", Data: nil, @@ -513,6 +528,11 @@ func (a *App) SetAdminHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logAdminSet(req, user.ID.String(), input.Admin); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "User is updated successfully", }, Ok() @@ -573,6 +593,11 @@ func (a *App) CreateNewAnnouncementHandler(req *http.Request) (interface{}, Resp } } + if err := a.logAnnouncementCreate(req, adminAnnouncement.Subject); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "new announcement is sent successfully", }, Created() @@ -634,6 +659,11 @@ func (a *App) SendEmailHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logEmailSent(req, user.ID.String(), emailUser.Subject); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "new email is sent successfully", }, Created() @@ -672,6 +702,11 @@ func (a *App) UpdateNextLaunchHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logNextLaunchUpdate(req, input.Launched); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Next Launch is updated successfully", Data: nil, diff --git a/server/app/app.go b/server/app/app.go index 77ca1311..0e3b3692 100644 --- a/server/app/app.go +++ b/server/app/app.go @@ -118,6 +118,8 @@ func (a *App) registerHandlers() { userRouter := authRouter.PathPrefix("/user").Subrouter() invoiceRouter := authRouter.PathPrefix("/invoice").Subrouter() cardRouter := userRouter.PathPrefix("/card").Subrouter() + logRouter := userRouter.PathPrefix("/log").Subrouter() + eventRouter := userRouter.PathPrefix("/event").Subrouter() notificationRouter := authRouter.PathPrefix("/notification").Subrouter() vmRouter := authRouter.PathPrefix("/vm").Subrouter() k8sRouter := authRouter.PathPrefix("/k8s").Subrouter() @@ -156,6 +158,10 @@ func (a *App) registerHandlers() { cardRouter.HandleFunc("", WrapFunc(a.ListCardHandler)).Methods("GET", "OPTIONS") cardRouter.HandleFunc("/default", WrapFunc(a.SetDefaultCardHandler)).Methods("PUT", "OPTIONS") + logRouter.HandleFunc("", WrapFunc(a.ListLogsHandler)).Methods("GET", "OPTIONS") + + eventRouter.HandleFunc("", WrapFunc(a.ListEventsHandler)).Methods("GET", "OPTIONS") + invoiceRouter.HandleFunc("", WrapFunc(a.ListInvoicesHandler)).Methods("GET", "OPTIONS") invoiceRouter.HandleFunc("/{id}", WrapFunc(a.GetInvoiceHandler)).Methods("GET", "OPTIONS") invoiceRouter.HandleFunc("/download/{id}", WrapFunc(a.DownloadInvoiceHandler)).Methods("GET", "OPTIONS") @@ -205,10 +211,10 @@ func (a *App) registerHandlers() { voucherRouter.HandleFunc("/all/reset", WrapFunc(a.ResetUsersVoucherBalanceHandler)).Methods("PUT", "OPTIONS") // middlewares - r.Use(middlewares.LoggingMW) r.Use(middlewares.EnableCors) authRouter.Use(middlewares.Authorization(a.db, a.config.Token.Secret, a.config.Token.Timeout)) + authRouter.Use(middlewares.AuditLogMiddleware(a.db)) adminRouter.Use(middlewares.AdminAccess(a.db)) // prometheus registration diff --git a/server/app/audit_handler.go b/server/app/audit_handler.go new file mode 100644 index 00000000..6c49ef83 --- /dev/null +++ b/server/app/audit_handler.go @@ -0,0 +1,78 @@ +package app + +import ( + "errors" + "net/http" + + "github.com/codescalers/cloud4students/middlewares" + "github.com/rs/zerolog/log" + "gorm.io/gorm" +) + +// Example endpoint: List user's logs +// @Summary List user's logs +// @Description List user's logs +// @Tags Audit +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.AuditLog +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/log [get] +func (a *App) ListLogsHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + logs, err := a.db.GetUserLogs(userID) + if err == gorm.ErrRecordNotFound || len(logs) == 0 { + return ResponseMsg{ + Message: "no logs found", + Data: logs, + }, Ok() + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Logs are found", + Data: logs, + }, Ok() +} + +// Example endpoint: List user's events +// @Summary List user's events +// @Description List user's events +// @Tags Audit +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} []models.AuditEvent +// @Failure 400 {object} Response +// @Failure 401 {object} Response +// @Failure 404 {object} Response +// @Failure 500 {object} Response +// @Router /user/event [get] +func (a *App) ListEventsHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + events, err := a.db.GetUserEvents(userID) + if err == gorm.ErrRecordNotFound || len(events) == 0 { + return ResponseMsg{ + Message: "no events found", + Data: events, + }, Ok() + } + if err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + + return ResponseMsg{ + Message: "Events are found", + Data: events, + }, Ok() +} diff --git a/server/app/events.go b/server/app/events.go new file mode 100644 index 00000000..c8e6123a --- /dev/null +++ b/server/app/events.go @@ -0,0 +1,536 @@ +package app + +import ( + "fmt" + "net/http" + "time" + + "github.com/codescalers/cloud4students/internal" + "github.com/codescalers/cloud4students/middlewares" + "github.com/codescalers/cloud4students/models" + "github.com/pkg/errors" +) + +type role string + +var ( + userRole role = "User" + adminRole role = "Admin" + systemRole role = "System" +) + +func (a *App) logUserDelete(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is deleted", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logBalanceCharge(userID, currency string, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_balance", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Balance is charged with %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserVoucherActivate(userID, currency, voucher string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "apply_voucher", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User activated a voucher %v with %v %v", voucher, balance, currency), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserVoucherApply(userID, currency string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "apply_voucher", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User applied for a voucher with %v %v", balance, currency), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserUpdate(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: "User data is updated successfully", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserPasswordUpdate(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_user_password", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: "Password is updated successfully", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserSignedIn(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "signin_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is signed in successfully", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logUserCreated(userID string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_user", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("User %v is created", userID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherReset(userID string, voucherBalance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "reset_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher balance %v is reset", voucherBalance), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherUpdate(userID, voucher string, balance uint64, approved bool) error { + state := "Approved" + if approved { + state = "Rejected" + } + + event := models.AuditEvent{ + UserID: userID, + Action: "update_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher `%v` with balance %v, is %v", voucher, balance, state), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherCreate(userID, voucher string, balance uint64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_voucher", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Voucher `%v` with balance %v, is created successfully", voucher, balance), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardDelete(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is deleted", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardDefaultSet(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "set_default_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is set as default", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logCardAdded(userID, last4digits string) error { + event := models.AuditEvent{ + UserID: userID, + Action: "add_card", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Card ending in %v is added", last4digits), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVoucherBalanceUpdate(userID, currency string, role role, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_voucher_balance", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Voucher balance is updated to %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logBalanceUpdate(userID, currency string, role role, balance float64) error { + event := models.AuditEvent{ + UserID: userID, + Action: "update_balance", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Balance is updated to %v %v", + balance, currency, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logK8sDelete(userID string, role role, k8sID int, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_k8s", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Kubernetes %v which created at %v, is deleted", k8sID, createdAt.Format("January 2, 2006"), + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVMDelete(userID string, role role, vmID int, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "delete_vm", + Role: string(role), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Virtual machine %v which created at %v, is deleted", + vmID, createdAt.Format("January 2, 2006"), + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoiceCreate(userID, currency string, invoiceID int, invoiceTotal float64, createdAt time.Time) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_invoice", + Role: string(systemRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Invoice %v with value: %v %v is created at %v", + invoiceID, invoiceTotal, currency, createdAt.Format("January 2, 2006"), + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoicePayment(userID, currency string, invoiceTotal float64, paymentDetails models.PaymentDetails) error { + event := models.AuditEvent{ + UserID: userID, + Action: "pay_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Invoice %v with value: %v %v is paid using: {balance: %v, vouchers: %v, card:%v}", + paymentDetails.InvoiceID, invoiceTotal, currency, + paymentDetails.Balance, paymentDetails.VoucherBalance, paymentDetails.Card, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoicePDFUpdate(req *http.Request, invoiceID int) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Invoice %v pdf data is updated", invoiceID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logInvoiceDownload(req *http.Request, invoiceID int) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "download_invoice", + Role: string(userRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Invoice %v is downloaded", invoiceID), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logEmailSent(req *http.Request, targetUserID, subject string) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "send_email", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("An email is sent to: %v, with subject: %v", targetUserID, subject), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAnnouncementCreate(req *http.Request, subject string) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "create_announcement", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("An announcement is created with subject: %v", subject), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAdminSet(req *http.Request, adminID string, admin bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + metaData := fmt.Sprintf("A new admin %v is added", adminID) + if !admin { + metaData = fmt.Sprintf("An admin %v is removed", adminID) + } + + event := models.AuditEvent{ + UserID: userID, + Action: "set_admin", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: metaData, + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logNextLaunchUpdate(req *http.Request, on bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_next_launch", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Next launch value is updated to: %v", on), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logMaintenanceUpdate(req *http.Request, on bool) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_maintenance", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Maintenance value is updated to: %v", on), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logAllDeploymentsDelete(req *http.Request) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "delete_all_deployments", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: "All virtual machines are deleted", + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func (a *App) logVMsPriceUpdate(req *http.Request, prices internal.Prices) error { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + + event := models.AuditEvent{ + UserID: userID, + Action: "update_vms_prices", + Role: string(adminRole), + Timestamp: time.Now(), + Metadata: fmt.Sprintf( + "Virtual machines prices are updated {small: %v, medium: %v, large: %v, public IPs: %v}", + prices.SmallVM, prices.MediumVM, prices.LargeVM, prices.PublicIP, + ), + } + + if err := a.db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} diff --git a/server/app/invoice_handler.go b/server/app/invoice_handler.go index 362fb2fb..2a96c4e6 100644 --- a/server/app/invoice_handler.go +++ b/server/app/invoice_handler.go @@ -174,12 +174,22 @@ func (a *App) DownloadInvoiceHandler(req *http.Request) (interface{}, Response) log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logInvoicePDFUpdate(req, invoice.ID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } if userID != invoice.UserID { return nil, NotFound(errors.New("invoice is not found")) } + if err := a.logInvoiceDownload(req, invoice.ID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return invoice.FileData, Ok(). WithHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%s", fmt.Sprintf("invoice-%s-%d.pdf", invoice.UserID, invoice.ID))). WithHeader("Content-Type", "application/pdf") @@ -430,6 +440,10 @@ func (a *App) createInvoice(user models.User, now time.Time) error { if err = a.db.CreateInvoice(&in); err != nil { return err } + + if err := a.logInvoiceCreate(user.ID.String(), a.config.Currency, in.ID, in.Total, in.CreatedAt); err != nil { + return err + } } return nil @@ -451,12 +465,20 @@ func (a *App) deleteInvoiceDeploymentsNotPaidSince3Months(userID string, now tim if err = a.db.DeleteVMByID(dl.DeploymentID); err != nil { log.Error().Err(err).Send() } + + if err := a.logVMDelete(userID, systemRole, dl.DeploymentID, dl.DeploymentCreatedAt); err != nil { + log.Error().Err(err).Send() + } } if dl.DeploymentType == "k8s" { if err = a.db.DeleteK8s(dl.DeploymentID); err != nil { log.Error().Err(err).Send() } + + if err := a.logK8sDelete(userID, systemRole, dl.DeploymentID, dl.DeploymentCreatedAt); err != nil { + log.Error().Err(err).Send() + } } } } @@ -669,6 +691,11 @@ func (a *App) payInvoice(user *models.User, cardPaymentID string, method method, log.Error().Err(err).Send() return InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVoucherBalanceUpdate(user.ID.String(), a.config.Currency, systemRole, user.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } } // invoice used balance @@ -677,6 +704,11 @@ func (a *App) payInvoice(user *models.User, cardPaymentID string, method method, log.Error().Err(err).Send() return InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logBalanceUpdate(user.ID.String(), a.config.Currency, systemRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } } paymentDetails.InvoiceID = invoiceID @@ -689,6 +721,11 @@ func (a *App) payInvoice(user *models.User, cardPaymentID string, method method, return InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logInvoicePayment(user.ID.String(), a.config.Currency, invoiceTotal, paymentDetails); err != nil { + log.Error().Err(err).Send() + return InternalServerError(errors.New(internalServerErrorMsg)) + } + return nil } diff --git a/server/app/k8s_handler.go b/server/app/k8s_handler.go index 70f59942..ea2c4a51 100644 --- a/server/app/k8s_handler.go +++ b/server/app/k8s_handler.go @@ -324,6 +324,11 @@ func (a *App) K8sDeleteHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logK8sDelete(userID, userRole, cluster.ID, cluster.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + // metrics middlewares.Deletions.WithLabelValues(userID, "k8s").Inc() @@ -377,6 +382,11 @@ func (a *App) K8sDeleteAllHandler(req *http.Request) (interface{}, Response) { } for _, c := range clusters { + if err := a.logK8sDelete(userID, userRole, c.ID, c.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(c.UserID, "k8s").Inc() } diff --git a/server/app/payments_handler.go b/server/app/payments_handler.go index 9aaa84c8..1481e6c7 100644 --- a/server/app/payments_handler.go +++ b/server/app/payments_handler.go @@ -128,6 +128,11 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logCardAdded(userID, paymentMethod.Card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + // if no payment is added before then we update the user payment ID with it as a default if len(strings.TrimSpace(user.StripeDefaultPaymentID)) == 0 { // Update the default payment method for future payments @@ -146,6 +151,11 @@ func (a *App) AddCardHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logCardDefaultSet(userID, paymentMethod.Card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } // try to settle old invoices using the card @@ -229,6 +239,11 @@ func (a *App) SetDefaultCardHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logCardDefaultSet(userID, card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Card is set as default successfully", Data: nil, @@ -378,6 +393,11 @@ func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logCardDefaultSet(userID, card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } // If user has another cards or no active deployments, so can delete @@ -392,6 +412,11 @@ func (a *App) DeleteCardHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logCardDelete(userID, card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Card is deleted successfully", Data: nil, diff --git a/server/app/user_handler.go b/server/app/user_handler.go index af2aa17c..93750aea 100644 --- a/server/app/user_handler.go +++ b/server/app/user_handler.go @@ -250,6 +250,11 @@ func (a *App) VerifySignUpCodeHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserCreated(user.ID.String()); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Account is created successfully.", }, Created() @@ -301,6 +306,11 @@ func (a *App) SignInHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserSignedIn(user.ID.String()); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "You are signed in successfully", Data: AccessTokenResponse{Token: token, Timeout: a.config.Token.Timeout}, @@ -501,6 +511,8 @@ func (a *App) VerifyForgetPasswordCodeHandler(req *http.Request) (interface{}, R // @Failure 500 {object} Response // @Router /user/change_password [put] func (a *App) ChangePasswordHandler(req *http.Request) (interface{}, Response) { + userID := req.Context().Value(middlewares.UserIDKey("UserID")).(string) + var data ChangePasswordInput err := json.NewDecoder(req.Body).Decode(&data) if err != nil { @@ -533,6 +545,11 @@ func (a *App) ChangePasswordHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserPasswordUpdate(userID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Password is updated successfully", Data: nil, @@ -632,6 +649,11 @@ func (a *App) UpdateUserHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserUpdate(userID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "User is updated successfully", }, Ok() @@ -721,6 +743,11 @@ func (a *App) ApplyForVoucherHandler(req *http.Request) (interface{}, Response) } middlewares.VoucherApplied.WithLabelValues(userID, voucher.Voucher, fmt.Sprint(voucher.Balance)).Inc() + if err := a.logUserVoucherApply(userID, a.config.Currency, a.config.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Voucher request is being reviewed, you'll receive a confirmation mail soon", Data: nil, @@ -804,18 +831,33 @@ func (a *App) ActivateVoucherHandler(req *http.Request) (interface{}, Response) } } + if err := a.logUserVoucherActivate(userID, a.config.Currency, voucherBalance.Voucher, voucherBalance.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + err = a.db.UpdateUserVoucherBalance(user.ID.String(), user.VoucherBalance) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherBalanceUpdate(userID, a.config.Currency, userRole, user.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + err = a.db.UpdateUserBalance(user.ID.String(), user.Balance) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.VoucherActivated.WithLabelValues(userID, voucherBalance.Voucher, fmt.Sprint(voucherBalance.Balance)).Inc() return ResponseMsg{ @@ -871,6 +913,11 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { user.Balance += float64(input.Amount) + if err := a.logBalanceCharge(userID, a.config.Currency, input.Amount); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + // try to settle old invoices invoices, err := a.db.ListUnpaidInvoices(user.ID.String()) if err != nil { @@ -891,12 +938,22 @@ func (a *App) ChargeBalance(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + err = a.db.UpdateUserVoucherBalance(user.ID.String(), user.VoucherBalance) if err != nil { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherBalanceUpdate(userID, a.config.Currency, userRole, user.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Balance is charged successfully", Data: clientSecretResponse{ClientSecret: intent.ClientSecret}, @@ -995,6 +1052,11 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } err = a.db.DeleteAllVms(userID) @@ -1016,6 +1078,11 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logK8sDelete(userID, userRole, cluster.ID, cluster.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } if len(clusters) > 0 { @@ -1039,6 +1106,11 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logCardDelete(userID, card.Last4); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } err = a.db.DeleteAllCards(userID) @@ -1059,6 +1131,11 @@ func (a *App) DeleteUserHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logUserDelete(userID); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "User is deleted successfully", }, Ok() diff --git a/server/app/vm_handler.go b/server/app/vm_handler.go index 551293f0..5d894c4f 100644 --- a/server/app/vm_handler.go +++ b/server/app/vm_handler.go @@ -294,6 +294,11 @@ func (a *App) DeleteVMHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(userID, "vms").Inc() return ResponseMsg{ Message: "Virtual machine is deleted successfully", @@ -345,6 +350,11 @@ func (a *App) DeleteAllVMsHandler(req *http.Request) (interface{}, Response) { // metrics for _, vm := range vms { + if err := a.logVMDelete(userID, userRole, vm.ID, vm.CreatedAt); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + middlewares.Deletions.WithLabelValues(vm.UserID, "vms").Inc() } diff --git a/server/app/voucher_handler.go b/server/app/voucher_handler.go index eb6a58fb..92051728 100644 --- a/server/app/voucher_handler.go +++ b/server/app/voucher_handler.go @@ -68,6 +68,11 @@ func (a *App) GenerateVoucherHandler(req *http.Request) (interface{}, Response) return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherCreate(v.UserID, v.Voucher, v.Balance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Voucher is generated successfully", Data: map[string]string{"voucher": voucher}, @@ -182,6 +187,11 @@ func (a *App) UpdateVoucherHandler(req *http.Request) (interface{}, Response) { return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + if err := a.logVoucherUpdate(voucher.UserID, voucher.Voucher, voucher.Balance, input.Approved); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } + return ResponseMsg{ Message: "Update mail has been sent to the user", Data: nil, @@ -233,6 +243,11 @@ func (a *App) ApproveAllVouchersHandler(req *http.Request) (interface{}, Respons log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVoucherUpdate(v.UserID, v.Voucher, v.Balance, true); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } return ResponseMsg{ @@ -269,6 +284,11 @@ func (a *App) ResetUsersVoucherBalanceHandler(req *http.Request) (interface{}, R log.Error().Err(err).Send() return nil, InternalServerError(errors.New(internalServerErrorMsg)) } + + if err := a.logVoucherReset(user.ID.String(), user.VoucherBalance); err != nil { + log.Error().Err(err).Send() + return nil, InternalServerError(errors.New(internalServerErrorMsg)) + } } return ResponseMsg{ diff --git a/server/deployer/deployer.go b/server/deployer/deployer.go index 1410b3dd..51d9b43c 100644 --- a/server/deployer/deployer.go +++ b/server/deployer/deployer.go @@ -333,3 +333,35 @@ func UsagePercentageInMonth(start time.Time, end time.Time) (float64, error) { return usedHours / totalHoursInMonth, nil } + +func logVMCreate(db models.DB, userID string, vmID int) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_vm", + Role: "User", + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Virtual machine %v is created successfully", vmID), + } + + if err := db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} + +func logK8sCreate(db models.DB, userID string, k8sID int) error { + event := models.AuditEvent{ + UserID: userID, + Action: "create_k8s", + Role: "User", + Timestamp: time.Now(), + Metadata: fmt.Sprintf("Kubernetes %v is created successfully", k8sID), + } + + if err := db.CreateAuditEvent(&event); err != nil { + return errors.Wrapf(err, "Failed to log audit event: %s", event.Action) + } + + return nil +} diff --git a/server/deployer/k8s_deployer.go b/server/deployer/k8s_deployer.go index 08abd22a..ab6fb980 100644 --- a/server/deployer/k8s_deployer.go +++ b/server/deployer/k8s_deployer.go @@ -170,6 +170,11 @@ func (d *Deployer) loadK8s( return models.K8sCluster{}, err } + if err := logK8sCreate(d.db, k8s.UserID, k8s.ID); err != nil { + log.Error().Err(err).Send() + return models.K8sCluster{}, err + } + return k8s, nil } diff --git a/server/deployer/vms_deployer.go b/server/deployer/vms_deployer.go index 37970bdc..8cff1a91 100644 --- a/server/deployer/vms_deployer.go +++ b/server/deployer/vms_deployer.go @@ -137,6 +137,10 @@ func (d *Deployer) deployVMRequest(ctx context.Context, user models.User, vm mod return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) } + if err := logVMCreate(d.db, vm.UserID, vm.ID); err != nil { + return http.StatusInternalServerError, err, errors.New(internalServerErrorMsg) + } + middlewares.Deployments.WithLabelValues(user.ID.String(), vm.Resources, "vm").Inc() return 0, nil, nil } diff --git a/server/docs/docs.go b/server/docs/docs.go index 584bfe93..423c89a5 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -1897,6 +1897,53 @@ const docTemplate = `{ } } }, + "/user/event": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List user's events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "List user's events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AuditEvent" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/user/forget_password/verify_email": { "post": { "description": "Verify user's email to reset password", @@ -1997,6 +2044,53 @@ const docTemplate = `{ } } }, + "/user/log": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List user's logs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Audit" + ], + "summary": "List user's logs", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AuditLog" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/user/refresh_token": { "post": { "security": [ @@ -3205,6 +3299,55 @@ const docTemplate = `{ "voucherAndBalanceAndCard" ] }, + "models.AuditEvent": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "metadata": { + "type": "string" + }, + "role": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.AuditLog": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "method": { + "type": "string" + }, + "status_code": { + "type": "integer" + }, + "success": { + "type": "boolean" + }, + "timestamp": { + "type": "string" + }, + "url": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, "models.Card": { "type": "object", "required": [ diff --git a/server/docs/swagger.yaml b/server/docs/swagger.yaml index f3bf687d..417200dd 100644 --- a/server/docs/swagger.yaml +++ b/server/docs/swagger.yaml @@ -336,6 +336,38 @@ definitions: - voucherAndCard - balanceAndCard - voucherAndBalanceAndCard + models.AuditEvent: + properties: + action: + type: string + id: + type: integer + metadata: + type: string + role: + type: string + timestamp: + type: string + user_id: + type: string + type: object + models.AuditLog: + properties: + id: + type: integer + method: + type: string + status_code: + type: integer + success: + type: boolean + timestamp: + type: string + url: + type: string + user_id: + type: string + type: object models.Card: properties: brand: @@ -1924,6 +1956,37 @@ paths: summary: Charge user balance tags: - User + /user/event: + get: + consumes: + - application/json + description: List user's events + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.AuditEvent' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List user's events + tags: + - Audit /user/forget_password/verify_email: post: consumes: @@ -1992,6 +2055,37 @@ paths: summary: Send code to forget password email for verification tags: - User + /user/log: + get: + consumes: + - application/json + description: List user's logs + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.AuditLog' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + security: + - BearerAuth: [] + summary: List user's logs + tags: + - Audit /user/refresh_token: post: consumes: diff --git a/server/go.mod b/server/go.mod index 694342ee..c77dac94 100644 --- a/server/go.mod +++ b/server/go.mod @@ -23,6 +23,7 @@ require ( github.com/swaggo/swag v1.16.4 github.com/threefoldtech/tfgrid-sdk-go/grid-client v0.16.0 github.com/threefoldtech/tfgrid-sdk-go/grid-proxy v0.16.0 + github.com/urfave/negroni/v3 v3.1.1 golang.org/x/crypto v0.30.0 golang.org/x/text v0.21.0 gopkg.in/validator.v2 v2.0.1 diff --git a/server/go.sum b/server/go.sum index 1a3e3b89..0757d794 100644 --- a/server/go.sum +++ b/server/go.sum @@ -204,6 +204,8 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/negroni/v3 v3.1.1 h1:6MS4nG9Jk/UuCACaUlNXCbiKa0ywF9LXz5dGu09v8hw= +github.com/urfave/negroni/v3 v3.1.1/go.mod h1:jWvnX03kcSjDBl/ShB0iHvx5uOs7mAzZXW+JvJ5XYAs= github.com/vedhavyas/go-subkey v1.0.3 h1:iKR33BB/akKmcR2PMlXPBeeODjWLM90EL98OrOGs8CA= github.com/vedhavyas/go-subkey v1.0.3/go.mod h1:CloUaFQSSTdWnINfBRFjVMkWXZANW+nd8+TI5jYcl6Y= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/server/middlewares/logging.go b/server/middlewares/logging.go index 29f2fbbd..9f1829f8 100644 --- a/server/middlewares/logging.go +++ b/server/middlewares/logging.go @@ -3,14 +3,35 @@ package middlewares import ( "net/http" + "time" + "github.com/codescalers/cloud4students/models" "github.com/rs/zerolog/log" + "github.com/urfave/negroni/v3" ) -// LoggingMW logs all information of every request -func LoggingMW(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Info().Timestamp().Str("method", r.Method).Str("uri", r.RequestURI).Send() - h.ServeHTTP(w, r) - }) +func AuditLogMiddleware(db models.DB) func(http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userID := r.Context().Value(UserIDKey("UserID")).(string) + + lrw := negroni.NewResponseWriter(w) + h.ServeHTTP(lrw, r) + + statusCode := lrw.Status() + + l := models.AuditLog{ + UserID: userID, + Method: r.Method, + URL: r.URL.Path, + Timestamp: time.Now(), + Success: statusCode >= 200 && statusCode < 300, + StatusCode: statusCode, + } + + if err := db.CreateAuditLog(&l); err != nil { + log.Error().Err(err).Msg("logging audit failed") + } + }) + } } diff --git a/server/models/audit.go b/server/models/audit.go new file mode 100644 index 00000000..89c8c3ae --- /dev/null +++ b/server/models/audit.go @@ -0,0 +1,43 @@ +package models + +import ( + "time" +) + +// Struct to represent audit log details +type AuditLog struct { + ID uint `gorm:"primaryKey"` + UserID string `json:"user_id" gorm:"not null"` + Method string `json:"method"` + URL string `json:"url"` + Timestamp time.Time `json:"timestamp"` + Success bool `json:"success"` + StatusCode int `json:"status_code"` +} + +func (d *DB) CreateAuditLog(l *AuditLog) error { + return d.db.Create(&l).Error +} + +func (d *DB) GetUserLogs(userID string) ([]AuditLog, error) { + var res []AuditLog + return res, d.db.Find(&res, "user_id = ?", userID).Error +} + +type AuditEvent struct { + ID uint `gorm:"primaryKey"` + UserID string `json:"user_id" gorm:"not null"` + Action string `json:"action"` + Role string `json:"role"` + Timestamp time.Time `json:"timestamp"` + Metadata string `json:"metadata"` +} + +func (d *DB) CreateAuditEvent(e *AuditEvent) error { + return d.db.Create(&e).Error +} + +func (d *DB) GetUserEvents(userID string) ([]AuditEvent, error) { + var res []AuditEvent + return res, d.db.Find(&res, "user_id = ?", userID).Error +} diff --git a/server/models/database.go b/server/models/database.go index c0b0fc4e..d376a1bf 100644 --- a/server/models/database.go +++ b/server/models/database.go @@ -32,6 +32,7 @@ func (d *DB) Migrate() error { err := d.db.AutoMigrate( &User{}, &State{}, &Card{}, &Invoice{}, &VM{}, &K8sCluster{}, &Master{}, &Worker{}, &Voucher{}, &Maintenance{}, &Notification{}, &NextLaunch{}, &DeploymentItem{}, &PaymentDetails{}, + AuditLog{}, AuditEvent{}, ) if err != nil { return err