mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
782 lines
23 KiB
Go
782 lines
23 KiB
Go
package routes
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"github.com/go-chi/chi/v5"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
datastructure "github.com/duke-git/lancet/v2/datastructure/set"
|
|
"github.com/emvi/logbuch"
|
|
"github.com/gorilla/schema"
|
|
conf "github.com/muety/wakapi/config"
|
|
"github.com/muety/wakapi/middlewares"
|
|
"github.com/muety/wakapi/models"
|
|
"github.com/muety/wakapi/models/view"
|
|
routeutils "github.com/muety/wakapi/routes/utils"
|
|
"github.com/muety/wakapi/services"
|
|
"github.com/muety/wakapi/services/imports"
|
|
"github.com/muety/wakapi/utils"
|
|
)
|
|
|
|
const criticalError = "a critical error has occurred, sorry"
|
|
|
|
type SettingsHandler struct {
|
|
config *conf.Config
|
|
userSrvc services.IUserService
|
|
summarySrvc services.ISummaryService
|
|
heartbeatSrvc services.IHeartbeatService
|
|
aliasSrvc services.IAliasService
|
|
aggregationSrvc services.IAggregationService
|
|
languageMappingSrvc services.ILanguageMappingService
|
|
projectLabelSrvc services.IProjectLabelService
|
|
keyValueSrvc services.IKeyValueService
|
|
mailSrvc services.IMailService
|
|
httpClient *http.Client
|
|
}
|
|
|
|
var credentialsDecoder = schema.NewDecoder()
|
|
|
|
func NewSettingsHandler(
|
|
userService services.IUserService,
|
|
heartbeatService services.IHeartbeatService,
|
|
summaryService services.ISummaryService,
|
|
aliasService services.IAliasService,
|
|
aggregationService services.IAggregationService,
|
|
languageMappingService services.ILanguageMappingService,
|
|
projectLabelService services.IProjectLabelService,
|
|
keyValueService services.IKeyValueService,
|
|
mailService services.IMailService,
|
|
) *SettingsHandler {
|
|
return &SettingsHandler{
|
|
config: conf.Get(),
|
|
summarySrvc: summaryService,
|
|
aliasSrvc: aliasService,
|
|
aggregationSrvc: aggregationService,
|
|
languageMappingSrvc: languageMappingService,
|
|
projectLabelSrvc: projectLabelService,
|
|
userSrvc: userService,
|
|
heartbeatSrvc: heartbeatService,
|
|
keyValueSrvc: keyValueService,
|
|
mailSrvc: mailService,
|
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (h *SettingsHandler) RegisterRoutes(router chi.Router) {
|
|
r := chi.NewRouter()
|
|
r.Use(
|
|
middlewares.NewAuthenticateMiddleware(h.userSrvc).
|
|
WithRedirectTarget(defaultErrorRedirectTarget()).
|
|
WithRedirectErrorMessage("unauthorized").Handler,
|
|
)
|
|
r.Get("/", h.GetIndex)
|
|
r.Post("/", h.PostIndex)
|
|
|
|
router.Mount("/settings", r)
|
|
}
|
|
|
|
func (h *SettingsHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r, w))
|
|
}
|
|
|
|
func (h *SettingsHandler) PostIndex(w http.ResponseWriter, r *http.Request) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r, w).WithError("missing form values"))
|
|
return
|
|
}
|
|
|
|
action := r.PostForm.Get("action")
|
|
r.PostForm.Del("action")
|
|
|
|
actionFunc := h.dispatchAction(action)
|
|
if actionFunc == nil {
|
|
logbuch.Warn("failed to dispatch action '%s'", action)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r, w).WithError("unknown action requests"))
|
|
return
|
|
}
|
|
|
|
status, successMsg, errorMsg := actionFunc(w, r)
|
|
|
|
// action responded itself
|
|
if status == -1 {
|
|
return
|
|
}
|
|
|
|
if errorMsg != "" {
|
|
w.WriteHeader(status)
|
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r, w).WithError(errorMsg))
|
|
return
|
|
}
|
|
if successMsg != "" {
|
|
w.WriteHeader(status)
|
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r, w).WithSuccess(successMsg))
|
|
return
|
|
}
|
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r, w))
|
|
}
|
|
|
|
func (h *SettingsHandler) dispatchAction(action string) action {
|
|
switch action {
|
|
case "change_password":
|
|
return h.actionChangePassword
|
|
case "update_user":
|
|
return h.actionUpdateUser
|
|
case "reset_apikey":
|
|
return h.actionResetApiKey
|
|
case "delete_alias":
|
|
return h.actionDeleteAlias
|
|
case "add_alias":
|
|
return h.actionAddAlias
|
|
case "add_label":
|
|
return h.actionAddLabel
|
|
case "delete_label":
|
|
return h.actionDeleteLabel
|
|
case "delete_mapping":
|
|
return h.actionDeleteLanguageMapping
|
|
case "add_mapping":
|
|
return h.actionAddLanguageMapping
|
|
case "update_sharing":
|
|
return h.actionUpdateSharing
|
|
case "update_leaderboard":
|
|
return h.actionUpdateLeaderboard
|
|
case "toggle_wakatime":
|
|
return h.actionSetWakatimeApiKey
|
|
case "import_wakatime":
|
|
return h.actionImportWakatime
|
|
case "regenerate_summaries":
|
|
return h.actionRegenerateSummaries
|
|
case "clear_data":
|
|
return h.actionClearData
|
|
case "delete_account":
|
|
return h.actionDeleteUser
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *SettingsHandler) actionUpdateUser(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
|
|
var payload models.UserDataUpdate
|
|
if err := r.ParseForm(); err != nil {
|
|
return http.StatusBadRequest, "", "missing parameters"
|
|
}
|
|
if err := credentialsDecoder.Decode(&payload, r.PostForm); err != nil {
|
|
return http.StatusBadRequest, "", "missing parameters"
|
|
}
|
|
|
|
if !payload.IsValid() {
|
|
return http.StatusBadRequest, "", "invalid parameters - perhaps invalid e-mail address?"
|
|
}
|
|
|
|
if payload.Email == "" && user.HasActiveSubscription() {
|
|
return http.StatusBadRequest, "", "cannot unset email while subscription is active"
|
|
}
|
|
|
|
user.Email = payload.Email
|
|
user.Location = payload.Location
|
|
user.ReportsWeekly = payload.ReportsWeekly
|
|
user.PublicLeaderboard = payload.PublicLeaderboard
|
|
|
|
if _, err := h.userSrvc.Update(user); err != nil {
|
|
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
|
}
|
|
|
|
return http.StatusOK, "user updated successfully", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionChangePassword(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
|
|
var credentials models.CredentialsReset
|
|
if err := r.ParseForm(); err != nil {
|
|
return http.StatusBadRequest, "", "missing parameters"
|
|
}
|
|
if err := credentialsDecoder.Decode(&credentials, r.PostForm); err != nil {
|
|
return http.StatusBadRequest, "", "missing parameters"
|
|
}
|
|
|
|
if !utils.ComparePassword(user.Password, credentials.PasswordOld, h.config.Security.PasswordSalt) {
|
|
return http.StatusUnauthorized, "", "invalid credentials"
|
|
}
|
|
|
|
if !credentials.IsValid() {
|
|
return http.StatusBadRequest, "", "invalid parameters"
|
|
}
|
|
|
|
user.Password = credentials.PasswordNew
|
|
if hash, err := utils.HashPassword(user.Password, h.config.Security.PasswordSalt); err != nil {
|
|
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
|
} else {
|
|
user.Password = hash
|
|
}
|
|
|
|
if _, err := h.userSrvc.Update(user); err != nil {
|
|
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
|
}
|
|
|
|
login := &models.Login{
|
|
Username: user.ID,
|
|
Password: user.Password,
|
|
}
|
|
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login.Username)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
|
}
|
|
|
|
http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded))
|
|
return http.StatusOK, "password was updated successfully", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionResetApiKey(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
if _, err := h.userSrvc.ResetApiKey(user); err != nil {
|
|
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
|
}
|
|
|
|
msg := fmt.Sprintf("your new api key is: %s", user.ApiKey)
|
|
return http.StatusOK, msg, ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionUpdateLeaderboard(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
var err error
|
|
user := middlewares.GetPrincipal(r)
|
|
defer h.userSrvc.FlushCache()
|
|
|
|
user.PublicLeaderboard, err = strconv.ParseBool(r.PostFormValue("enable_leaderboard"))
|
|
|
|
if err != nil {
|
|
return http.StatusBadRequest, "", "invalid input"
|
|
}
|
|
if _, err := h.userSrvc.Update(user); err != nil {
|
|
return http.StatusInternalServerError, "", "internal sever error"
|
|
}
|
|
return http.StatusOK, "settings updated", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionUpdateSharing(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
var err error
|
|
user := middlewares.GetPrincipal(r)
|
|
|
|
defer h.userSrvc.FlushUserCache(user.ID)
|
|
|
|
user.ShareProjects, err = strconv.ParseBool(r.PostFormValue("share_projects"))
|
|
user.ShareLanguages, err = strconv.ParseBool(r.PostFormValue("share_languages"))
|
|
user.ShareEditors, err = strconv.ParseBool(r.PostFormValue("share_editors"))
|
|
user.ShareOSs, err = strconv.ParseBool(r.PostFormValue("share_oss"))
|
|
user.ShareMachines, err = strconv.ParseBool(r.PostFormValue("share_machines"))
|
|
user.ShareLabels, err = strconv.ParseBool(r.PostFormValue("share_labels"))
|
|
user.ShareDataMaxDays, err = strconv.Atoi(r.PostFormValue("max_days"))
|
|
|
|
if err != nil {
|
|
return http.StatusBadRequest, "", "invalid input"
|
|
}
|
|
|
|
if _, err := h.userSrvc.Update(user); err != nil {
|
|
return http.StatusInternalServerError, "", "internal sever error"
|
|
}
|
|
|
|
return http.StatusOK, "settings updated", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionDeleteAlias(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
aliasKey := r.PostFormValue("key")
|
|
aliasType, err := strconv.Atoi(r.PostFormValue("type"))
|
|
if err != nil {
|
|
aliasType = 99 // nothing will be found later on
|
|
}
|
|
|
|
if aliases, err := h.aliasSrvc.GetByUserAndKeyAndType(user.ID, aliasKey, uint8(aliasType)); err != nil {
|
|
return http.StatusNotFound, "", "aliases not found"
|
|
} else if err := h.aliasSrvc.DeleteMulti(aliases); err != nil {
|
|
return http.StatusInternalServerError, "", "could not delete aliases"
|
|
}
|
|
|
|
return http.StatusOK, "aliases deleted successfully", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionAddAlias(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
user := middlewares.GetPrincipal(r)
|
|
aliasKey := r.PostFormValue("key")
|
|
aliasValue := r.PostFormValue("value")
|
|
aliasType, err := strconv.Atoi(r.PostFormValue("type"))
|
|
if err != nil {
|
|
aliasType = 99 // Alias.IsValid() will return false later on
|
|
}
|
|
|
|
alias := &models.Alias{
|
|
UserID: user.ID,
|
|
Key: aliasKey,
|
|
Value: aliasValue,
|
|
Type: uint8(aliasType),
|
|
}
|
|
|
|
if _, err := h.aliasSrvc.Create(alias); err != nil {
|
|
// TODO: distinguish between bad request, conflict and server error
|
|
return http.StatusBadRequest, "", "invalid input"
|
|
}
|
|
|
|
return http.StatusOK, "alias added successfully", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionAddLabel(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
user := middlewares.GetPrincipal(r)
|
|
|
|
var labels []*models.ProjectLabel
|
|
|
|
for _, key := range r.Form["key"] {
|
|
label := &models.ProjectLabel{
|
|
UserID: user.ID,
|
|
ProjectKey: key,
|
|
Label: r.PostFormValue("value"),
|
|
}
|
|
labels = append(labels, label)
|
|
}
|
|
|
|
for _, label := range labels {
|
|
msg := "invalid input for project: " + label.ProjectKey
|
|
if !label.IsValid() {
|
|
return http.StatusBadRequest, "", msg
|
|
}
|
|
if _, err := h.projectLabelSrvc.Create(label); err != nil {
|
|
// TODO: distinguish between bad request, conflict and server error
|
|
return http.StatusBadRequest, "", msg
|
|
}
|
|
}
|
|
return http.StatusOK, "label added to project successfully", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionDeleteLabel(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
labelKey := r.PostFormValue("key") // label key
|
|
labelValue := r.PostFormValue("value") // project key
|
|
|
|
labels, err := h.projectLabelSrvc.GetByUser(user.ID)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, "", "could not delete label"
|
|
}
|
|
|
|
for _, l := range labels {
|
|
if l.Label == labelKey && l.ProjectKey == labelValue {
|
|
if err := h.projectLabelSrvc.Delete(l); err != nil {
|
|
return http.StatusInternalServerError, "", "could not delete label"
|
|
}
|
|
return http.StatusOK, "label deleted successfully", ""
|
|
}
|
|
}
|
|
return http.StatusNotFound, "", "label not found"
|
|
}
|
|
|
|
func (h *SettingsHandler) actionDeleteLanguageMapping(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
id, err := strconv.Atoi(r.PostFormValue("mapping_id"))
|
|
if err != nil {
|
|
return http.StatusInternalServerError, "", "could not delete mapping"
|
|
}
|
|
|
|
mapping, err := h.languageMappingSrvc.GetById(uint(id))
|
|
if err != nil || mapping == nil {
|
|
return http.StatusNotFound, "", "mapping not found"
|
|
} else if mapping.UserID != user.ID {
|
|
return http.StatusForbidden, "", "not allowed to delete mapping"
|
|
}
|
|
|
|
if err := h.languageMappingSrvc.Delete(mapping); err != nil {
|
|
return http.StatusInternalServerError, "", "could not delete mapping"
|
|
}
|
|
|
|
return http.StatusOK, "mapping deleted successfully", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionAddLanguageMapping(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
user := middlewares.GetPrincipal(r)
|
|
extension := r.PostFormValue("extension")
|
|
language := r.PostFormValue("language")
|
|
|
|
if extension[0] == '.' {
|
|
extension = extension[1:]
|
|
}
|
|
|
|
mapping := &models.LanguageMapping{
|
|
UserID: user.ID,
|
|
Extension: extension,
|
|
Language: language,
|
|
}
|
|
|
|
if _, err := h.languageMappingSrvc.Create(mapping); err != nil {
|
|
return http.StatusConflict, "", "mapping already exists"
|
|
}
|
|
|
|
return http.StatusOK, "mapping added successfully", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionSetWakatimeApiKey(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
apiKey := r.PostFormValue("api_key")
|
|
apiUrl := r.PostFormValue("api_url")
|
|
if apiUrl == conf.WakatimeApiUrl || apiKey == "" {
|
|
apiUrl = ""
|
|
}
|
|
|
|
// Healthcheck, if a new API key is set, i.e. the feature is activated
|
|
if (user.WakatimeApiKey == "" && apiKey != "") && !h.validateWakatimeKey(apiKey, apiUrl) {
|
|
return http.StatusBadRequest, "", "failed to connect to WakaTime, API key or endpoint URL invalid?"
|
|
}
|
|
|
|
if _, err := h.userSrvc.SetWakatimeApiCredentials(user, apiKey, apiUrl); err != nil {
|
|
return http.StatusInternalServerError, "", conf.ErrInternalServerError
|
|
}
|
|
|
|
return http.StatusOK, "Wakatime API Key updated successfully", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionImportWakatime(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
if user.WakatimeApiKey == "" {
|
|
return http.StatusForbidden, "", "not connected to wakatime"
|
|
}
|
|
|
|
kvKey := fmt.Sprintf("%s_%s", conf.KeyLastImportImport, user.ID)
|
|
|
|
if !h.config.IsDev() {
|
|
lastImportKv := h.keyValueSrvc.MustGetString(kvKey)
|
|
lastImport, _ := time.Parse(time.RFC822, lastImportKv.Value)
|
|
if time.Now().Sub(lastImport) < time.Duration(h.config.App.ImportBackoffMin)*time.Minute {
|
|
return http.StatusTooManyRequests,
|
|
"",
|
|
fmt.Sprintf("Too many data imports. You are only allowed to request an import every %d minutes.", h.config.App.ImportBackoffMin)
|
|
}
|
|
}
|
|
|
|
go func(user *models.User) {
|
|
start := time.Now()
|
|
importer := imports.NewWakatimeImporter(user.WakatimeApiKey)
|
|
|
|
countBefore, _ := h.heartbeatSrvc.CountByUser(user)
|
|
|
|
var (
|
|
stream <-chan *models.Heartbeat
|
|
importError error
|
|
)
|
|
if latest, err := h.heartbeatSrvc.GetLatestByOriginAndUser(imports.OriginWakatime, user); latest == nil || err != nil {
|
|
stream, importError = importer.ImportAll(user)
|
|
} else {
|
|
// if an import has happened before, only import heartbeats newer than the latest of the last import
|
|
stream, importError = importer.Import(user, latest.Time.T(), time.Now())
|
|
}
|
|
if importError != nil {
|
|
conf.Log().Error("wakatime import for user '%s' failed - %v", user.ID, importError)
|
|
return
|
|
}
|
|
|
|
count := 0
|
|
batch := make([]*models.Heartbeat, 0, h.config.App.ImportBatchSize)
|
|
|
|
insert := func(batch []*models.Heartbeat) {
|
|
if err := h.heartbeatSrvc.InsertBatch(batch); err != nil {
|
|
logbuch.Warn("failed to insert imported heartbeat, already existing? - %v", err)
|
|
}
|
|
}
|
|
|
|
for hb := range stream {
|
|
count++
|
|
batch = append(batch, hb)
|
|
|
|
if len(batch) == h.config.App.ImportBatchSize {
|
|
insert(batch)
|
|
batch = make([]*models.Heartbeat, 0, h.config.App.ImportBatchSize)
|
|
}
|
|
}
|
|
if len(batch) > 0 {
|
|
insert(batch)
|
|
}
|
|
|
|
countAfter, _ := h.heartbeatSrvc.CountByUser(user)
|
|
logbuch.Info("downloaded %d heartbeats for user '%s' (%d actually imported)", count, user.ID, countAfter-countBefore)
|
|
|
|
h.regenerateSummaries(user)
|
|
|
|
if !user.HasData {
|
|
user.HasData = true
|
|
if _, err := h.userSrvc.Update(user); err != nil {
|
|
conf.Log().Request(r).Error("failed to set 'has_data' flag for user %s - %v", user.ID, err)
|
|
}
|
|
}
|
|
|
|
if user.Email != "" {
|
|
if err := h.mailSrvc.SendImportNotification(user, time.Now().Sub(start), int(countAfter-countBefore)); err != nil {
|
|
conf.Log().Request(r).Error("failed to send import notification mail to %s - %v", user.ID, err)
|
|
} else {
|
|
logbuch.Info("sent import notification mail to %s", user.ID)
|
|
}
|
|
}
|
|
}(user)
|
|
|
|
h.keyValueSrvc.PutString(&models.KeyStringValue{
|
|
Key: kvKey,
|
|
Value: time.Now().Format(time.RFC822),
|
|
})
|
|
|
|
return http.StatusAccepted, "Import started. This will take several minutes. Please check back later.", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionRegenerateSummaries(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
go func(user *models.User) {
|
|
if err := h.regenerateSummaries(user); err != nil {
|
|
conf.Log().Request(r).Error("failed to regenerate summaries for user '%s' - %v", user.ID, err)
|
|
}
|
|
}(middlewares.GetPrincipal(r))
|
|
|
|
return http.StatusAccepted, "summaries are being regenerated - this may take a up to a couple of minutes, please come back later", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionClearData(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
logbuch.Info("user '%s' requested to delete all data", user.ID)
|
|
|
|
go func(user *models.User) {
|
|
logbuch.Info("deleting summaries for user '%s'", user.ID)
|
|
if err := h.summarySrvc.DeleteByUser(user.ID); err != nil {
|
|
logbuch.Error("failed to clear summaries: %v", err)
|
|
}
|
|
|
|
logbuch.Info("deleting heartbeats for user '%s'", user.ID)
|
|
if err := h.heartbeatSrvc.DeleteByUser(user); err != nil {
|
|
logbuch.Error("failed to clear heartbeats: %v", err)
|
|
}
|
|
}(user)
|
|
|
|
return http.StatusAccepted, "deletion in progress, this may take a couple of seconds", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) actionDeleteUser(w http.ResponseWriter, r *http.Request) (int, string, string) {
|
|
if h.config.IsDev() {
|
|
loadTemplates()
|
|
}
|
|
|
|
user := middlewares.GetPrincipal(r)
|
|
go func(user *models.User) {
|
|
logbuch.Info("deleting user '%s' shortly", user.ID)
|
|
time.Sleep(5 * time.Minute)
|
|
if err := h.userSrvc.Delete(user); err != nil {
|
|
conf.Log().Request(r).Error("failed to delete user '%s' - %v", user.ID, err)
|
|
} else {
|
|
logbuch.Info("successfully deleted user '%s'", user.ID)
|
|
}
|
|
}(user)
|
|
|
|
routeutils.SetSuccess(r, w, "Your account will be deleted in a few minutes. Sorry to you go.")
|
|
http.SetCookie(w, h.config.GetClearCookie(models.AuthCookieKey))
|
|
http.Redirect(w, r, h.config.Server.BasePath, http.StatusFound)
|
|
return -1, "", ""
|
|
}
|
|
|
|
func (h *SettingsHandler) validateWakatimeKey(apiKey string, baseUrl string) bool {
|
|
if baseUrl == "" {
|
|
baseUrl = conf.WakatimeApiUrl
|
|
}
|
|
|
|
headers := http.Header{
|
|
"Accept": []string{"application/json"},
|
|
"Authorization": []string{
|
|
fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(apiKey))),
|
|
},
|
|
}
|
|
|
|
request, err := http.NewRequest(
|
|
http.MethodGet,
|
|
baseUrl+conf.WakatimeApiUserUrl,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
request.Header = headers
|
|
|
|
response, err := h.httpClient.Do(request)
|
|
if err != nil || response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (h *SettingsHandler) regenerateSummaries(user *models.User) error {
|
|
logbuch.Info("clearing summaries for user '%s'", user.ID)
|
|
if err := h.summarySrvc.DeleteByUser(user.ID); err != nil {
|
|
conf.Log().Error("failed to clear summaries: %v", err)
|
|
return err
|
|
}
|
|
|
|
if err := h.aggregationSrvc.AggregateSummaries(datastructure.NewSet(user.ID)); err != nil {
|
|
conf.Log().Error("failed to regenerate summaries: %v", err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *SettingsHandler) buildViewModel(r *http.Request, w http.ResponseWriter) *view.SettingsViewModel {
|
|
user := middlewares.GetPrincipal(r)
|
|
|
|
// mappings
|
|
mappings, _ := h.languageMappingSrvc.GetByUser(user.ID)
|
|
|
|
// aliases
|
|
aliases, err := h.aliasSrvc.GetByUser(user.ID)
|
|
if err != nil {
|
|
conf.Log().Request(r).Error("error while building alias map - %v", err)
|
|
return &view.SettingsViewModel{Messages: view.Messages{Error: criticalError}}
|
|
}
|
|
aliasMap := make(map[string][]*models.Alias)
|
|
for _, a := range aliases {
|
|
k := fmt.Sprintf("%s_%d", a.Key, a.Type)
|
|
if _, ok := aliasMap[k]; !ok {
|
|
aliasMap[k] = []*models.Alias{a}
|
|
} else {
|
|
aliasMap[k] = append(aliasMap[k], a)
|
|
}
|
|
}
|
|
|
|
combinedAliases := make([]*view.SettingsVMCombinedAlias, 0)
|
|
for _, l := range aliasMap {
|
|
ca := &view.SettingsVMCombinedAlias{
|
|
Key: l[0].Key,
|
|
Type: l[0].Type,
|
|
Values: make([]string, len(l)),
|
|
}
|
|
for i, a := range l {
|
|
ca.Values[i] = a.Value
|
|
}
|
|
combinedAliases = append(combinedAliases, ca)
|
|
}
|
|
|
|
// labels
|
|
labelMap, err := h.projectLabelSrvc.GetByUserGroupedInverted(user.ID)
|
|
if err != nil {
|
|
conf.Log().Request(r).Error("error while building settings project label map - %v", err)
|
|
return &view.SettingsViewModel{Messages: view.Messages{Error: criticalError}}
|
|
}
|
|
|
|
combinedLabels := make([]*view.SettingsVMCombinedLabel, 0)
|
|
for _, l := range labelMap {
|
|
cl := &view.SettingsVMCombinedLabel{
|
|
Key: l[0].Label,
|
|
Values: make([]string, len(l)),
|
|
}
|
|
for i, l1 := range l {
|
|
cl.Values[i] = l1.ProjectKey
|
|
}
|
|
combinedLabels = append(combinedLabels, cl)
|
|
}
|
|
sort.Slice(combinedLabels, func(i, j int) bool {
|
|
return strings.Compare(combinedLabels[i].Key, combinedLabels[j].Key) < 0
|
|
})
|
|
|
|
// projects
|
|
projects, err := routeutils.GetEffectiveProjectsList(user, h.heartbeatSrvc, h.aliasSrvc)
|
|
if err != nil {
|
|
conf.Log().Request(r).Error("error while fetching projects - %v", err)
|
|
return &view.SettingsViewModel{Messages: view.Messages{Error: criticalError}}
|
|
}
|
|
|
|
// subscriptions
|
|
var subscriptionPrice string
|
|
if h.config.Subscriptions.Enabled {
|
|
subscriptionPrice = h.config.Subscriptions.StandardPrice
|
|
}
|
|
|
|
// user first data
|
|
var firstData time.Time
|
|
firstDataKv := h.keyValueSrvc.MustGetString(fmt.Sprintf("%s_%s", conf.KeyFirstHeartbeat, user.ID))
|
|
if firstDataKv.Value != "" {
|
|
firstData, _ = time.Parse(time.RFC822Z, firstDataKv.Value)
|
|
}
|
|
|
|
vm := &view.SettingsViewModel{
|
|
User: user,
|
|
LanguageMappings: mappings,
|
|
Aliases: combinedAliases,
|
|
Labels: combinedLabels,
|
|
Projects: projects,
|
|
ApiKey: user.ApiKey,
|
|
UserFirstData: firstData,
|
|
SubscriptionPrice: subscriptionPrice,
|
|
SupportContact: h.config.App.SupportContact,
|
|
DataRetentionMonths: h.config.App.DataRetentionMonths,
|
|
}
|
|
return routeutils.WithSessionMessages(vm, r, w)
|
|
}
|