1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00
wakapi/routes/settings.go
Ferdinand Mütsch 979549448c chore: remove legacy support for md5 hashed passwords
chore: remove password from encoded cookie content as not used anyway
2021-01-31 14:34:54 +01:00

381 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package routes
import (
"encoding/base64"
"fmt"
"github.com/emvi/logbuch"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/models/view"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"net/http"
"strconv"
"time"
)
type SettingsHandler struct {
config *conf.Config
userSrvc services.IUserService
summarySrvc services.ISummaryService
aliasSrvc services.IAliasService
aggregationSrvc services.IAggregationService
languageMappingSrvc services.ILanguageMappingService
httpClient *http.Client
}
var credentialsDecoder = schema.NewDecoder()
func NewSettingsHandler(userService services.IUserService, summaryService services.ISummaryService, aliasService services.IAliasService, aggregationService services.IAggregationService, languageMappingService services.ILanguageMappingService) *SettingsHandler {
return &SettingsHandler{
config: conf.Get(),
summarySrvc: summaryService,
aliasSrvc: aliasService,
aggregationSrvc: aggregationService,
languageMappingSrvc: languageMappingService,
userSrvc: userService,
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
func (h *SettingsHandler) RegisterRoutes(router *mux.Router) {
router.Methods(http.MethodGet).HandlerFunc(h.GetIndex)
router.Path("/credentials").Methods(http.MethodPost).HandlerFunc(h.PostCredentials)
router.Path("/aliases").Methods(http.MethodPost).HandlerFunc(h.PostAlias)
router.Path("/aliases/delete").Methods(http.MethodPost).HandlerFunc(h.DeleteAlias)
router.Path("/language_mappings").Methods(http.MethodPost).HandlerFunc(h.PostLanguageMapping)
router.Path("/language_mappings/delete").Methods(http.MethodPost).HandlerFunc(h.DeleteLanguageMapping)
router.Path("/reset").Methods(http.MethodPost).HandlerFunc(h.PostResetApiKey)
router.Path("/badges").Methods(http.MethodPost).HandlerFunc(h.PostToggleBadges)
router.Path("/wakatime_integration").Methods(http.MethodPost).HandlerFunc(h.PostSetWakatimeApiKey)
router.Path("/regenerate").Methods(http.MethodPost).HandlerFunc(h.PostRegenerateSummaries)
}
func (h *SettingsHandler) RegisterAPIRoutes(router *mux.Router) {}
func (h *SettingsHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r))
}
func (h *SettingsHandler) PostCredentials(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
var credentials models.CredentialsReset
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
return
}
if err := credentialsDecoder.Decode(&credentials, r.PostForm); err != nil {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
return
}
if !utils.CompareBcrypt(user.Password, credentials.PasswordOld, h.config.Security.PasswordSalt) {
w.WriteHeader(http.StatusUnauthorized)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("invalid credentials"))
return
}
if !credentials.IsValid() {
w.WriteHeader(http.StatusBadRequest)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters"))
return
}
user.Password = credentials.PasswordNew
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
return
} else {
user.Password = hash
}
if _, err := h.userSrvc.Update(user); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
return
}
login := &models.Login{
Username: user.ID,
Password: user.Password,
}
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login.Username)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
return
}
http.SetCookie(w, h.config.CreateCookie(models.AuthCookieKey, encoded, "/"))
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("password was updated successfully"))
}
func (h *SettingsHandler) DeleteLanguageMapping(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
id, err := strconv.Atoi(r.PostFormValue("mapping_id"))
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("could not delete mapping"))
return
}
if mapping, err := h.languageMappingSrvc.GetById(uint(id)); err != nil || mapping == nil {
w.WriteHeader(http.StatusNotFound)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("mapping not found"))
return
} else if mapping.UserID != user.ID {
w.WriteHeader(http.StatusForbidden)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("not allowed to delete mapping"))
return
}
if err := h.languageMappingSrvc.Delete(&models.LanguageMapping{ID: uint(id)}); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("could not delete mapping"))
return
}
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("mapping deleted successfully"))
}
func (h *SettingsHandler) PostLanguageMapping(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
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 {
w.WriteHeader(http.StatusConflict)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("mapping already exists"))
return
}
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("mapping added successfully"))
}
func (h *SettingsHandler) DeleteAlias(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
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 {
w.WriteHeader(http.StatusNotFound)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("aliases not found"))
return
} else if err := h.aliasSrvc.DeleteMulti(aliases); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("could not delete aliases"))
return
}
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("aliases deleted successfully"))
}
func (h *SettingsHandler) PostAlias(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
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 {
w.WriteHeader(http.StatusBadRequest)
// TODO: distinguish between bad request, conflict and server error
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("invalid input"))
return
}
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("alias added successfully"))
}
func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
if _, err := h.userSrvc.ResetApiKey(user); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
return
}
msg := fmt.Sprintf("your new api key is: %s", user.ApiKey)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess(msg))
}
func (h *SettingsHandler) PostSetWakatimeApiKey(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
apiKey := r.PostFormValue("api_key")
// Healthcheck, if a new API key is set, i.e. the feature is activated
if (user.WakatimeApiKey == "" && apiKey != "") && !h.validateWakatimeKey(apiKey) {
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("failed to connect to WakaTime, API key invalid?"))
return
}
if _, err := h.userSrvc.SetWakatimeApiKey(user, apiKey); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
return
}
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("Wakatime API Key updated successfully"))
}
func (h *SettingsHandler) PostToggleBadges(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
if _, err := h.userSrvc.ToggleBadges(user); err != nil {
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
return
}
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r))
}
func (h *SettingsHandler) PostRegenerateSummaries(w http.ResponseWriter, r *http.Request) {
if h.config.IsDev() {
loadTemplates()
}
user := r.Context().Value(models.UserKey).(*models.User)
logbuch.Info("clearing summaries for user '%s'", user.ID)
if err := h.summarySrvc.DeleteByUser(user.ID); err != nil {
logbuch.Error("failed to clear summaries: %v", err)
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("failed to delete old summaries"))
return
}
if err := h.aggregationSrvc.Run(map[string]bool{user.ID: true}); err != nil {
logbuch.Error("failed to regenerate summaries: %v", err)
w.WriteHeader(http.StatusInternalServerError)
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("failed to generate aggregations"))
return
}
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("summaries are being regenerated this may take a few seconds"))
}
func (h *SettingsHandler) validateWakatimeKey(apiKey string) bool {
headers := http.Header{
"Accept": []string{"application/json"},
"Authorization": []string{
fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(apiKey))),
},
}
request, err := http.NewRequest(
http.MethodGet,
conf.WakatimeApiUrl+conf.WakatimeApiUserEndpoint,
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) buildViewModel(r *http.Request) *view.SettingsViewModel {
user := r.Context().Value(models.UserKey).(*models.User)
mappings, _ := h.languageMappingSrvc.GetByUser(user.ID)
aliases, _ := h.aliasSrvc.GetByUser(user.ID)
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)
}
return &view.SettingsViewModel{
User: user,
LanguageMappings: mappings,
Aliases: combinedAliases,
Success: r.URL.Query().Get("success"),
Error: r.URL.Query().Get("error"),
}
}