2020-06-07 20:28:32 +03:00
package routes
import (
2021-01-30 12:54:54 +03:00
"encoding/base64"
2020-06-07 20:28:32 +03:00
"fmt"
2021-01-30 13:17:37 +03:00
"github.com/emvi/logbuch"
2021-01-30 12:34:52 +03:00
"github.com/gorilla/mux"
2020-06-07 20:28:32 +03:00
"github.com/gorilla/schema"
2020-10-09 22:37:16 +03:00
conf "github.com/muety/wakapi/config"
2021-02-03 23:28:02 +03:00
"github.com/muety/wakapi/middlewares"
2020-06-07 20:28:32 +03:00
"github.com/muety/wakapi/models"
2020-11-06 23:19:54 +03:00
"github.com/muety/wakapi/models/view"
2020-06-07 20:28:32 +03:00
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/utils"
"net/http"
2020-10-27 00:34:50 +03:00
"strconv"
2021-01-30 12:54:54 +03:00
"time"
2020-06-07 20:28:32 +03:00
)
type SettingsHandler struct {
2020-11-01 22:14:10 +03:00
config * conf . Config
2020-11-08 12:12:49 +03:00
userSrvc services . IUserService
summarySrvc services . ISummaryService
2021-01-21 02:26:52 +03:00
aliasSrvc services . IAliasService
2020-11-08 12:12:49 +03:00
aggregationSrvc services . IAggregationService
languageMappingSrvc services . ILanguageMappingService
2021-01-30 12:54:54 +03:00
httpClient * http . Client
2020-06-07 20:28:32 +03:00
}
var credentialsDecoder = schema . NewDecoder ( )
2021-01-21 02:26:52 +03:00
func NewSettingsHandler ( userService services . IUserService , summaryService services . ISummaryService , aliasService services . IAliasService , aggregationService services . IAggregationService , languageMappingService services . ILanguageMappingService ) * SettingsHandler {
2020-06-07 20:28:32 +03:00
return & SettingsHandler {
2020-11-01 22:14:10 +03:00
config : conf . Get ( ) ,
2020-11-06 19:09:41 +03:00
summarySrvc : summaryService ,
2021-01-21 02:26:52 +03:00
aliasSrvc : aliasService ,
2020-11-06 19:09:41 +03:00
aggregationSrvc : aggregationService ,
2020-11-01 22:14:10 +03:00
languageMappingSrvc : languageMappingService ,
userSrvc : userService ,
2021-01-30 12:54:54 +03:00
httpClient : & http . Client { Timeout : 10 * time . Second } ,
2020-06-07 20:28:32 +03:00
}
}
2021-01-30 12:34:52 +03:00
func ( h * SettingsHandler ) RegisterRoutes ( router * mux . Router ) {
2021-02-03 23:28:02 +03:00
r := router . PathPrefix ( "/settings" ) . Subrouter ( )
r . Use (
middlewares . NewAuthenticateMiddleware ( h . userSrvc , [ ] string { } ) . Handler ,
)
r . Methods ( http . MethodGet ) . HandlerFunc ( h . GetIndex )
r . Methods ( http . MethodPost ) . HandlerFunc ( h . PostIndex )
2021-01-30 12:34:52 +03:00
}
2020-06-07 20:28:32 +03:00
func ( h * SettingsHandler ) GetIndex ( w http . ResponseWriter , r * http . Request ) {
if h . config . IsDev ( ) {
loadTemplates ( )
}
2020-11-06 23:19:54 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) )
2020-06-07 20:28:32 +03:00
}
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) PostIndex ( w http . ResponseWriter , r * http . Request ) {
2020-06-07 20:28:32 +03:00
if h . config . IsDev ( ) {
loadTemplates ( )
}
if err := r . ParseForm ( ) ; err != nil {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusBadRequest )
2021-02-03 22:53:27 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "missing form values" ) )
2020-06-07 20:28:32 +03:00
return
}
2021-02-03 22:53:27 +03:00
action := r . PostForm . Get ( "action" )
r . PostForm . Del ( "action" )
actionFunc := h . dispatchAction ( action )
if actionFunc == nil {
logbuch . Warn ( "failed to dispatch action '%s'" , action )
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusBadRequest )
2021-02-03 22:53:27 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "unknown action requests" ) )
2020-06-07 20:28:32 +03:00
return
}
2021-02-03 22:53:27 +03:00
status , successMsg , errorMsg := actionFunc ( w , r )
// action responded itself
if status == - 1 {
2020-06-07 20:28:32 +03:00
return
}
2021-02-03 22:53:27 +03:00
if errorMsg != "" {
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( errorMsg ) )
2020-06-07 20:28:32 +03:00
return
}
2021-02-03 22:53:27 +03:00
if successMsg != "" {
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithSuccess ( successMsg ) )
return
}
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) )
}
func ( h * SettingsHandler ) dispatchAction ( action string ) action {
switch action {
case "change_password" :
return h . actionChangePassword
case "reset_apikey" :
return h . actionResetApiKey
case "delete_alias" :
return h . actionDeleteAlias
case "add_alias" :
return h . actionAddAlias
case "delete_mapping" :
return h . actionDeleteLanguageMapping
case "add_mapping" :
return h . actionAddLanguageMapping
case "toggle_badges" :
return h . actionToggleBadges
case "toggle_wakatime" :
return h . actionSetWakatimeApiKey
case "regenerate_summaries" :
return h . actionRegenerateSummaries
case "delete_account" :
return h . actionDeleteUser
}
return nil
}
func ( h * SettingsHandler ) actionChangePassword ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
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 . CompareBcrypt ( user . Password , credentials . PasswordOld , h . config . Security . PasswordSalt ) {
return http . StatusUnauthorized , "" , "invalid credentials"
}
if ! credentials . IsValid ( ) {
return http . StatusBadRequest , "" , "invalid parameters"
}
2020-06-07 20:28:32 +03:00
user . Password = credentials . PasswordNew
2020-10-16 17:11:14 +03:00
if hash , err := utils . HashBcrypt ( user . Password , h . config . Security . PasswordSalt ) ; err != nil {
2021-02-03 22:53:27 +03:00
return http . StatusInternalServerError , "" , "internal server error"
2020-10-16 17:11:14 +03:00
} else {
user . Password = hash
2020-06-07 20:28:32 +03:00
}
if _ , err := h . userSrvc . Update ( user ) ; err != nil {
2021-02-03 22:53:27 +03:00
return http . StatusInternalServerError , "" , "internal server error"
2020-06-07 20:28:32 +03:00
}
login := & models . Login {
Username : user . ID ,
Password : user . Password ,
}
2021-01-31 16:34:54 +03:00
encoded , err := h . config . Security . SecureCookie . Encode ( models . AuthCookieKey , login . Username )
2020-06-07 20:28:32 +03:00
if err != nil {
2021-02-03 22:53:27 +03:00
return http . StatusInternalServerError , "" , "internal server error"
2020-06-07 20:28:32 +03:00
}
2020-11-22 00:30:56 +03:00
http . SetCookie ( w , h . config . CreateCookie ( models . AuthCookieKey , encoded , "/" ) )
2021-02-03 22:53:27 +03:00
return http . StatusOK , "password was updated successfully" , ""
2020-10-25 09:22:10 +03:00
}
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) actionResetApiKey ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
2020-10-25 09:22:10 +03:00
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
2021-02-03 22:53:27 +03:00
if _ , err := h . userSrvc . ResetApiKey ( user ) ; err != nil {
return http . StatusInternalServerError , "" , "internal server error"
2020-10-25 09:22:10 +03:00
}
2021-02-03 22:53:27 +03:00
msg := fmt . Sprintf ( "your new api key is: %s" , user . ApiKey )
return http . StatusOK , msg , ""
2020-06-07 20:28:32 +03:00
}
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) actionDeleteAlias ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
2021-01-21 02:26:52 +03:00
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 {
2021-02-03 22:53:27 +03:00
return http . StatusNotFound , "" , "aliases not found"
2021-01-21 02:26:52 +03:00
} else if err := h . aliasSrvc . DeleteMulti ( aliases ) ; err != nil {
2021-02-03 22:53:27 +03:00
return http . StatusInternalServerError , "" , "could not delete aliases"
2021-01-21 02:26:52 +03:00
}
2021-02-03 22:53:27 +03:00
return http . StatusOK , "aliases deleted successfully" , ""
2021-01-21 02:26:52 +03:00
}
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) actionAddAlias ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
2021-01-21 02:26:52 +03:00
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 {
// TODO: distinguish between bad request, conflict and server error
2021-02-03 22:53:27 +03:00
return http . StatusBadRequest , "" , "invalid input"
2021-01-21 02:26:52 +03:00
}
2021-02-03 22:53:27 +03:00
return http . StatusOK , "alias added successfully" , ""
2021-01-21 02:26:52 +03:00
}
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) actionDeleteLanguageMapping ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
2020-06-07 20:28:32 +03:00
if h . config . IsDev ( ) {
loadTemplates ( )
}
2020-06-07 20:58:06 +03:00
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
2021-02-03 22:53:27 +03:00
id , err := strconv . Atoi ( r . PostFormValue ( "mapping_id" ) )
if err != nil {
return http . StatusInternalServerError , "" , "could not delete mapping"
2020-06-07 20:58:06 +03:00
}
2021-02-03 22:53:27 +03:00
if mapping , err := h . languageMappingSrvc . GetById ( uint ( id ) ) ; 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 ( & models . LanguageMapping { ID : uint ( id ) } ) ; err != nil {
return http . StatusInternalServerError , "" , "could not delete mapping"
}
return http . StatusOK , "mapping deleted successfully" , ""
2020-06-07 20:28:32 +03:00
}
2020-09-12 17:09:23 +03:00
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) actionAddLanguageMapping ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
2021-01-22 01:26:50 +03:00
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
2021-02-03 22:53:27 +03:00
extension := r . PostFormValue ( "extension" )
language := r . PostFormValue ( "language" )
2021-01-30 12:54:54 +03:00
2021-02-03 22:53:27 +03:00
if extension [ 0 ] == '.' {
extension = extension [ 1 : ]
2021-01-30 12:54:54 +03:00
}
2021-02-03 22:53:27 +03:00
mapping := & models . LanguageMapping {
UserID : user . ID ,
Extension : extension ,
Language : language ,
2021-01-22 01:26:50 +03:00
}
2021-02-03 22:53:27 +03:00
if _ , err := h . languageMappingSrvc . Create ( mapping ) ; err != nil {
return http . StatusConflict , "" , "mapping already exists"
}
return http . StatusOK , "mapping added successfully" , ""
2021-01-22 01:26:50 +03:00
}
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) actionToggleBadges ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
2020-09-12 17:09:23 +03:00
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
if _ , err := h . userSrvc . ToggleBadges ( user ) ; err != nil {
2021-02-03 22:53:27 +03:00
return http . StatusInternalServerError , "" , "internal server error"
2020-09-12 17:09:23 +03:00
}
2021-02-03 22:53:27 +03:00
return http . StatusOK , "" , ""
2020-09-12 17:09:23 +03:00
}
2020-11-06 19:09:41 +03:00
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) actionSetWakatimeApiKey ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
2021-02-03 00:54:22 +03:00
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
2021-02-03 22:53:27 +03:00
apiKey := r . PostFormValue ( "api_key" )
2021-02-03 00:54:22 +03:00
2021-02-03 22:53:27 +03:00
// Healthcheck, if a new API key is set, i.e. the feature is activated
if ( user . WakatimeApiKey == "" && apiKey != "" ) && ! h . validateWakatimeKey ( apiKey ) {
return http . StatusBadRequest , "" , "failed to connect to WakaTime, API key invalid?"
}
if _ , err := h . userSrvc . SetWakatimeApiKey ( user , apiKey ) ; err != nil {
return http . StatusInternalServerError , "" , "internal server error"
}
return http . StatusOK , "Wakatime API Key updated successfully" , ""
2021-02-03 00:54:22 +03:00
}
2021-02-03 22:53:27 +03:00
func ( h * SettingsHandler ) actionRegenerateSummaries ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
2020-11-06 19:09:41 +03:00
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
2021-01-30 13:17:37 +03:00
logbuch . Info ( "clearing summaries for user '%s'" , user . ID )
2020-11-06 19:09:41 +03:00
if err := h . summarySrvc . DeleteByUser ( user . ID ) ; err != nil {
2021-01-30 13:17:37 +03:00
logbuch . Error ( "failed to clear summaries: %v" , err )
2021-02-03 22:53:27 +03:00
return http . StatusInternalServerError , "" , "failed to delete old summaries"
2020-11-06 19:09:41 +03:00
}
if err := h . aggregationSrvc . Run ( map [ string ] bool { user . ID : true } ) ; err != nil {
2021-01-30 13:17:37 +03:00
logbuch . Error ( "failed to regenerate summaries: %v" , err )
2021-02-03 22:53:27 +03:00
return http . StatusInternalServerError , "" , "failed to generate aggregations"
2020-11-06 19:09:41 +03:00
}
2021-02-03 22:53:27 +03:00
return http . StatusOK , "summaries are being regenerated – this may take a few seconds" , ""
}
func ( h * SettingsHandler ) actionDeleteUser ( w http . ResponseWriter , r * http . Request ) ( int , string , string ) {
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
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 {
logbuch . Error ( "failed to delete user '%s' – %v" , user . ID , err )
} else {
logbuch . Info ( "successfully deleted user '%s'" , user . ID )
}
} ( user )
http . SetCookie ( w , h . config . GetClearCookie ( models . AuthCookieKey , "/" ) )
http . Redirect ( w , r , fmt . Sprintf ( "%s/?success=%s" , h . config . Server . BasePath , "Your account will be deleted in a few minutes. Sorry to you go." ) , http . StatusFound )
return - 1 , "" , ""
2020-11-06 23:19:54 +03:00
}
2021-01-30 12:54:54 +03:00
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
}
2020-11-06 23:19:54 +03:00
func ( h * SettingsHandler ) buildViewModel ( r * http . Request ) * view . SettingsViewModel {
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
mappings , _ := h . languageMappingSrvc . GetByUser ( user . ID )
2021-01-21 02:26:52 +03:00
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 )
}
2020-11-06 23:19:54 +03:00
return & view . SettingsViewModel {
User : user ,
LanguageMappings : mappings ,
2021-01-21 02:26:52 +03:00
Aliases : combinedAliases ,
2020-11-06 23:19:54 +03:00
Success : r . URL . Query ( ) . Get ( "success" ) ,
Error : r . URL . Query ( ) . Get ( "error" ) ,
}
2020-11-06 19:09:41 +03:00
}