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"
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 ) {
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 )
2021-02-03 00:54:22 +03:00
router . Path ( "/user/delete" ) . Methods ( http . MethodPost ) . HandlerFunc ( h . DeleteUser )
2021-01-30 12:34:52 +03:00
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 ) { }
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
}
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 {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusBadRequest )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "missing parameters" ) )
2020-06-07 20:28:32 +03:00
return
}
if err := credentialsDecoder . Decode ( & credentials , r . PostForm ) ; err != nil {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusBadRequest )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "missing parameters" ) )
2020-06-07 20:28:32 +03:00
return
}
2020-10-16 17:11:14 +03:00
if ! utils . CompareBcrypt ( user . Password , credentials . PasswordOld , h . config . Security . PasswordSalt ) {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusUnauthorized )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "invalid credentials" ) )
2020-06-07 20:28:32 +03:00
return
}
if ! credentials . IsValid ( ) {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusBadRequest )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "invalid parameters" ) )
2020-06-07 20:28:32 +03:00
return
}
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 {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "internal server error" ) )
2020-06-07 20:28:32 +03:00
return
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 {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "internal server error" ) )
2020-06-07 20:28:32 +03:00
return
}
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 {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "internal server error" ) )
2020-06-07 20:28:32 +03:00
return
}
2020-11-22 00:30:56 +03:00
http . SetCookie ( w , h . config . CreateCookie ( models . AuthCookieKey , encoded , "/" ) )
2020-11-06 23:19:54 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithSuccess ( "password was updated successfully" ) )
2020-10-25 09:22:10 +03:00
}
2020-11-01 22:14:10 +03:00
func ( h * SettingsHandler ) DeleteLanguageMapping ( w http . ResponseWriter , r * http . Request ) {
2020-10-25 09:22:10 +03:00
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
2020-11-01 22:14:10 +03:00
id , err := strconv . Atoi ( r . PostFormValue ( "mapping_id" ) )
2020-10-25 09:22:10 +03:00
if err != nil {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "could not delete mapping" ) )
2020-10-25 09:22:10 +03:00
return
}
2021-01-20 22:49:27 +03:00
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
2020-10-27 00:34:50 +03:00
}
2020-11-01 18:56:36 +03:00
2021-01-20 22:49:27 +03:00
if err := h . languageMappingSrvc . Delete ( & models . LanguageMapping { ID : uint ( id ) } ) ; err != nil {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "could not delete mapping" ) )
2020-11-01 18:56:36 +03:00
return
}
2020-11-06 23:19:54 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithSuccess ( "mapping deleted successfully" ) )
2020-10-25 09:22:10 +03:00
}
2020-11-06 19:09:41 +03:00
func ( h * SettingsHandler ) PostLanguageMapping ( w http . ResponseWriter , r * http . Request ) {
2020-10-25 09:22:10 +03:00
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 : ]
}
2020-11-01 22:14:10 +03:00
mapping := & models . LanguageMapping {
2020-10-27 00:34:50 +03:00
UserID : user . ID ,
2020-10-25 09:22:10 +03:00
Extension : extension ,
2020-10-27 00:34:50 +03:00
Language : language ,
}
2020-10-25 09:22:10 +03:00
2020-11-01 22:14:10 +03:00
if _ , err := h . languageMappingSrvc . Create ( mapping ) ; err != nil {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusConflict )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "mapping already exists" ) )
2020-10-25 09:22:10 +03:00
return
}
2020-11-06 23:19:54 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithSuccess ( "mapping added successfully" ) )
2020-06-07 20:28:32 +03:00
}
2021-01-21 02:26:52 +03:00
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" ) )
}
2020-06-07 20:28:32 +03:00
func ( h * SettingsHandler ) PostResetApiKey ( w http . ResponseWriter , r * http . Request ) {
if h . config . IsDev ( ) {
loadTemplates ( )
}
2020-06-07 20:58:06 +03:00
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
if _ , err := h . userSrvc . ResetApiKey ( user ) ; err != nil {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "internal server error" ) )
2020-06-07 20:58:06 +03:00
return
}
2020-11-08 12:12:49 +03:00
msg := fmt . Sprintf ( "your new api key is: %s" , user . ApiKey )
2020-11-06 23:19:54 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithSuccess ( msg ) )
2020-06-07 20:28:32 +03:00
}
2020-09-12 17:09:23 +03:00
2021-01-22 01:26:50 +03:00
func ( h * SettingsHandler ) PostSetWakatimeApiKey ( w http . ResponseWriter , r * http . Request ) {
if h . config . IsDev ( ) {
loadTemplates ( )
}
user := r . Context ( ) . Value ( models . UserKey ) . ( * models . User )
2021-01-30 12:54:54 +03:00
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 {
2021-01-22 01:26:50 +03:00
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" ) )
}
2020-09-12 17:09:23 +03:00
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 {
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "internal server error" ) )
2020-09-12 17:09:23 +03:00
return
}
2020-11-06 23:19:54 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) )
2020-09-12 17:09:23 +03:00
}
2020-11-06 19:09:41 +03:00
2021-02-03 00:54:22 +03:00
func ( h * SettingsHandler ) DeleteUser ( w http . ResponseWriter , r * http . Request ) {
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 )
}
2020-11-06 19:09:41 +03:00
func ( h * SettingsHandler ) PostRegenerateSummaries ( w http . ResponseWriter , r * http . Request ) {
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 )
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "failed to delete old summaries" ) )
2020-11-06 19:09:41 +03:00
return
}
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 )
2020-11-06 23:19:54 +03:00
w . WriteHeader ( http . StatusInternalServerError )
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithError ( "failed to generate aggregations" ) )
2020-11-06 19:09:41 +03:00
return
}
2020-11-06 23:19:54 +03:00
templates [ conf . SettingsTemplate ] . Execute ( w , h . buildViewModel ( r ) . WithSuccess ( "summaries are being regenerated – this may take a few seconds" ) )
}
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
}