mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
8ddd9904a0 | |||
78874566a4 | |||
e269b37b0e | |||
e6a04cc76d | |||
cb8f68df82 | |||
b4d2ee7d16 | |||
1224024913 |
@ -22,6 +22,10 @@ const (
|
|||||||
defaultConfigPath = "config.yml"
|
defaultConfigPath = "config.yml"
|
||||||
defaultConfigPathLegacy = "config.ini"
|
defaultConfigPathLegacy = "config.ini"
|
||||||
defaultEnvConfigPathLegacy = ".env"
|
defaultEnvConfigPathLegacy = ".env"
|
||||||
|
|
||||||
|
SQLDialectMysql = "mysql"
|
||||||
|
SQLDialectPostgres = "postgres"
|
||||||
|
SQLDialectSqlite = "sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -81,13 +85,12 @@ func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
|
|||||||
switch dbDialect {
|
switch dbDialect {
|
||||||
default:
|
default:
|
||||||
return func(db *gorm.DB) error {
|
return func(db *gorm.DB) error {
|
||||||
|
db.AutoMigrate(&models.User{})
|
||||||
|
db.AutoMigrate(&models.KeyStringValue{})
|
||||||
db.AutoMigrate(&models.Alias{})
|
db.AutoMigrate(&models.Alias{})
|
||||||
|
db.AutoMigrate(&models.Heartbeat{})
|
||||||
db.AutoMigrate(&models.Summary{})
|
db.AutoMigrate(&models.Summary{})
|
||||||
db.AutoMigrate(&models.SummaryItem{})
|
db.AutoMigrate(&models.SummaryItem{})
|
||||||
db.AutoMigrate(&models.User{})
|
|
||||||
db.AutoMigrate(&models.Heartbeat{})
|
|
||||||
db.AutoMigrate(&models.SummaryItem{})
|
|
||||||
db.AutoMigrate(&models.KeyStringValue{})
|
|
||||||
db.AutoMigrate(&models.LanguageMapping{})
|
db.AutoMigrate(&models.LanguageMapping{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -114,17 +117,16 @@ func (c *Config) GetFixturesFunc(dbDialect string) models.MigrationFunc {
|
|||||||
|
|
||||||
func (c *dbConfig) GetDialector() gorm.Dialector {
|
func (c *dbConfig) GetDialector() gorm.Dialector {
|
||||||
switch c.Dialect {
|
switch c.Dialect {
|
||||||
case "mysql":
|
case SQLDialectMysql:
|
||||||
return mysql.New(mysql.Config{
|
return mysql.New(mysql.Config{
|
||||||
DriverName: c.Dialect,
|
DriverName: c.Dialect,
|
||||||
DSN: mysqlConnectionString(c),
|
DSN: mysqlConnectionString(c),
|
||||||
})
|
})
|
||||||
case "postgres":
|
case SQLDialectPostgres:
|
||||||
return postgres.New(postgres.Config{
|
return postgres.New(postgres.Config{
|
||||||
DriverName: c.Dialect,
|
DSN: postgresConnectionString(c),
|
||||||
DSN: mysqlConnectionString(c),
|
|
||||||
})
|
})
|
||||||
case "sqlite3":
|
case SQLDialectSqlite:
|
||||||
return sqlite.Open(sqliteConnectionString(c))
|
return sqlite.Open(sqliteConnectionString(c))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -227,7 +229,6 @@ func Load() *Config {
|
|||||||
|
|
||||||
config.Version = readVersion()
|
config.Version = readVersion()
|
||||||
config.App.LanguageColors = readLanguageColors()
|
config.App.LanguageColors = readLanguageColors()
|
||||||
// TODO: Read keys from env, so that users are not logged out every time the server is restarted
|
|
||||||
config.Security.SecureCookie = securecookie.New(
|
config.Security.SecureCookie = securecookie.New(
|
||||||
securecookie.GenerateRandomKey(64),
|
securecookie.GenerateRandomKey(64),
|
||||||
securecookie.GenerateRandomKey(32),
|
securecookie.GenerateRandomKey(32),
|
||||||
|
@ -59,7 +59,7 @@ func migrateLegacyConfig() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dbType == "" {
|
if dbType == "" {
|
||||||
dbType = "sqlite3"
|
dbType = SQLDialectSqlite
|
||||||
}
|
}
|
||||||
|
|
||||||
dbMaxConn := cfg.Section("database").Key("max_connections").MustUint(2)
|
dbMaxConn := cfg.Section("database").Key("max_connections").MustUint(2)
|
||||||
@ -76,8 +76,6 @@ func migrateLegacyConfig() error {
|
|||||||
basePath = basePathEnv
|
basePath = basePathEnv
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanUp := cfg.Section("app").Key("cleanup").MustBool(false)
|
|
||||||
|
|
||||||
// Read custom languages
|
// Read custom languages
|
||||||
customLangs := make(map[string]string)
|
customLangs := make(map[string]string)
|
||||||
languageKeys := cfg.Section("languages").Keys()
|
languageKeys := cfg.Section("languages").Keys()
|
||||||
@ -89,7 +87,6 @@ func migrateLegacyConfig() error {
|
|||||||
config := &Config{
|
config := &Config{
|
||||||
Env: env,
|
Env: env,
|
||||||
App: appConfig{
|
App: appConfig{
|
||||||
CleanUp: cleanUp,
|
|
||||||
CustomLanguages: customLangs,
|
CustomLanguages: customLangs,
|
||||||
},
|
},
|
||||||
Security: securityConfig{
|
Security: securityConfig{
|
||||||
|
40
main.go
40
main.go
@ -56,11 +56,6 @@ func main() {
|
|||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show data loss warning
|
|
||||||
if config.App.CleanUp {
|
|
||||||
promptAbort("`CLEANUP` is set to `true`, which may cause data loss. Are you sure to continue?", 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to database
|
// Connect to database
|
||||||
var err error
|
var err error
|
||||||
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{})
|
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{})
|
||||||
@ -68,6 +63,9 @@ func main() {
|
|||||||
db.Raw("PRAGMA foreign_keys = ON;")
|
db.Raw("PRAGMA foreign_keys = ON;")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.IsDev() {
|
||||||
|
db = db.Debug()
|
||||||
|
}
|
||||||
sqlDb, _ := db.DB()
|
sqlDb, _ := db.DB()
|
||||||
sqlDb.SetMaxIdleConns(int(config.Db.MaxConn))
|
sqlDb.SetMaxIdleConns(int(config.Db.MaxConn))
|
||||||
sqlDb.SetMaxOpenConns(int(config.Db.MaxConn))
|
sqlDb.SetMaxOpenConns(int(config.Db.MaxConn))
|
||||||
@ -102,18 +100,15 @@ func main() {
|
|||||||
// Aggregate heartbeats to summaries and persist them
|
// Aggregate heartbeats to summaries and persist them
|
||||||
go aggregationService.Schedule()
|
go aggregationService.Schedule()
|
||||||
|
|
||||||
if config.App.CleanUp {
|
|
||||||
go heartbeatService.ScheduleCleanUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move endpoint registration to the respective routes files
|
// TODO: move endpoint registration to the respective routes files
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
summaryHandler := routes.NewSummaryHandler(summaryService)
|
summaryHandler := routes.NewSummaryHandler(summaryService)
|
||||||
healthHandler := routes.NewHealthHandler(db)
|
healthHandler := routes.NewHealthHandler(db)
|
||||||
heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService, languageMappingService)
|
heartbeatHandler := routes.NewHeartbeatHandler(heartbeatService, languageMappingService)
|
||||||
settingsHandler := routes.NewSettingsHandler(userService, languageMappingService)
|
settingsHandler := routes.NewSettingsHandler(userService, summaryService, aggregationService, languageMappingService)
|
||||||
publicHandler := routes.NewIndexHandler(userService, keyValueService)
|
homeHandler := routes.NewHomeHandler(userService)
|
||||||
|
imprintHandler := routes.NewImprintHandler(keyValueService)
|
||||||
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService)
|
wakatimeV1AllHandler := wtV1Routes.NewAllTimeHandler(summaryService)
|
||||||
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService)
|
wakatimeV1SummariesHandler := wtV1Routes.NewSummariesHandler(summaryService)
|
||||||
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
|
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
|
||||||
@ -144,12 +139,12 @@ func main() {
|
|||||||
apiRouter.Use(corsMiddleware, authenticateMiddleware)
|
apiRouter.Use(corsMiddleware, authenticateMiddleware)
|
||||||
|
|
||||||
// Public Routes
|
// Public Routes
|
||||||
publicRouter.Path("/").Methods(http.MethodGet).HandlerFunc(publicHandler.GetIndex)
|
publicRouter.Path("/").Methods(http.MethodGet).HandlerFunc(homeHandler.GetIndex)
|
||||||
publicRouter.Path("/login").Methods(http.MethodPost).HandlerFunc(publicHandler.PostLogin)
|
publicRouter.Path("/login").Methods(http.MethodPost).HandlerFunc(homeHandler.PostLogin)
|
||||||
publicRouter.Path("/logout").Methods(http.MethodPost).HandlerFunc(publicHandler.PostLogout)
|
publicRouter.Path("/logout").Methods(http.MethodPost).HandlerFunc(homeHandler.PostLogout)
|
||||||
publicRouter.Path("/signup").Methods(http.MethodGet).HandlerFunc(publicHandler.GetSignup)
|
publicRouter.Path("/signup").Methods(http.MethodGet).HandlerFunc(homeHandler.GetSignup)
|
||||||
publicRouter.Path("/signup").Methods(http.MethodPost).HandlerFunc(publicHandler.PostSignup)
|
publicRouter.Path("/signup").Methods(http.MethodPost).HandlerFunc(homeHandler.PostSignup)
|
||||||
publicRouter.Path("/imprint").Methods(http.MethodGet).HandlerFunc(publicHandler.GetImprint)
|
publicRouter.Path("/imprint").Methods(http.MethodGet).HandlerFunc(imprintHandler.GetImprint)
|
||||||
|
|
||||||
// Summary Routes
|
// Summary Routes
|
||||||
summaryRouter.Methods(http.MethodGet).HandlerFunc(summaryHandler.GetIndex)
|
summaryRouter.Methods(http.MethodGet).HandlerFunc(summaryHandler.GetIndex)
|
||||||
@ -157,10 +152,11 @@ func main() {
|
|||||||
// Settings Routes
|
// Settings Routes
|
||||||
settingsRouter.Methods(http.MethodGet).HandlerFunc(settingsHandler.GetIndex)
|
settingsRouter.Methods(http.MethodGet).HandlerFunc(settingsHandler.GetIndex)
|
||||||
settingsRouter.Path("/credentials").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCredentials)
|
settingsRouter.Path("/credentials").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCredentials)
|
||||||
settingsRouter.Path("/language_mappings").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostCreateLanguageMapping)
|
settingsRouter.Path("/language_mappings").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostLanguageMapping)
|
||||||
settingsRouter.Path("/language_mappings/delete").Methods(http.MethodPost).HandlerFunc(settingsHandler.DeleteLanguageMapping)
|
settingsRouter.Path("/language_mappings/delete").Methods(http.MethodPost).HandlerFunc(settingsHandler.DeleteLanguageMapping)
|
||||||
settingsRouter.Path("/reset").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostResetApiKey)
|
settingsRouter.Path("/reset").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostResetApiKey)
|
||||||
settingsRouter.Path("/badges").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostToggleBadges)
|
settingsRouter.Path("/badges").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostToggleBadges)
|
||||||
|
settingsRouter.Path("/regenerate").Methods(http.MethodPost).HandlerFunc(settingsHandler.PostRegenerateSummaries)
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.ApiPost)
|
apiRouter.Path("/heartbeat").Methods(http.MethodPost).HandlerFunc(heartbeatHandler.ApiPost)
|
||||||
@ -194,11 +190,3 @@ func runDatabaseMigrations() {
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptAbort(message string, timeoutSec int) {
|
|
||||||
log.Printf("[WARNING] %s.\nTo abort server startup, press Ctrl+C.\n", message)
|
|
||||||
for i := timeoutSec; i > 0; i-- {
|
|
||||||
log.Printf("Starting in %d seconds ...\n", i)
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -13,13 +13,88 @@ func init() {
|
|||||||
customPreMigrations = []migrationFunc{
|
customPreMigrations = []migrationFunc{
|
||||||
{
|
{
|
||||||
f: func(db *gorm.DB, cfg *config.Config) error {
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
if db.Migrator().HasTable("custom_rules") {
|
migrator := db.Migrator()
|
||||||
return db.Migrator().RenameTable("custom_rules", &models.LanguageMapping{})
|
oldTableName, newTableName := "custom_rules", "language_mappings"
|
||||||
|
oldIndexName, newIndexName := "idx_customrule_user", "idx_language_mapping_user"
|
||||||
|
|
||||||
|
if migrator.HasTable(oldTableName) {
|
||||||
|
log.Printf("renaming '%s' table to '%s'\n", oldTableName, newTableName)
|
||||||
|
if err := migrator.RenameTable(oldTableName, &models.LanguageMapping{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("renaming '%s' index to '%s'\n", oldIndexName, newIndexName)
|
||||||
|
return migrator.RenameIndex(&models.LanguageMapping{}, oldIndexName, newIndexName)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
name: "rename language mappings table",
|
name: "rename language mappings table",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
f: func(db *gorm.DB, cfg *config.Config) error {
|
||||||
|
// drop all already existing foreign key constraints
|
||||||
|
// afterwards let them be re-created by auto migrate with the newly introduced cascade settings,
|
||||||
|
|
||||||
|
migrator := db.Migrator()
|
||||||
|
const lookupKey = "20201106-migration_cascade_constraints"
|
||||||
|
|
||||||
|
if cfg.Db.Dialect == config.SQLDialectSqlite {
|
||||||
|
// https://stackoverflow.com/a/1884893/3112139
|
||||||
|
// unfortunately, we can't migrate existing sqlite databases to the newly introduced cascade settings
|
||||||
|
// things like deleting all summaries won't work in those cases unless an entirely new db is created
|
||||||
|
log.Println("not attempting to drop and regenerate constraints on sqlite")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !migrator.HasTable(&models.KeyStringValue{}) {
|
||||||
|
log.Println("key-value table not yet existing")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
condition := "key = ?"
|
||||||
|
if cfg.Db.Dialect == config.SQLDialectMysql {
|
||||||
|
condition = "`key` = ?"
|
||||||
|
}
|
||||||
|
lookupResult := db.Where(condition, lookupKey).First(&models.KeyStringValue{})
|
||||||
|
if lookupResult.Error == nil && lookupResult.RowsAffected > 0 {
|
||||||
|
log.Println("no need to migrate cascade constraints")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SELECT * FROM INFORMATION_SCHEMA.table_constraints;
|
||||||
|
constraints := map[string]interface{}{
|
||||||
|
"fk_summaries_editors": &models.SummaryItem{},
|
||||||
|
"fk_summaries_languages": &models.SummaryItem{},
|
||||||
|
"fk_summaries_machines": &models.SummaryItem{},
|
||||||
|
"fk_summaries_operating_systems": &models.SummaryItem{},
|
||||||
|
"fk_summaries_projects": &models.SummaryItem{},
|
||||||
|
"fk_summary_items_summary": &models.SummaryItem{},
|
||||||
|
"fk_summaries_user": &models.Summary{},
|
||||||
|
"fk_language_mappings_user": &models.LanguageMapping{},
|
||||||
|
"fk_heartbeats_user": &models.Heartbeat{},
|
||||||
|
"fk_aliases_user": &models.Alias{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, table := range constraints {
|
||||||
|
if migrator.HasConstraint(table, name) {
|
||||||
|
log.Printf("dropping constraint '%s'", name)
|
||||||
|
if err := migrator.DropConstraint(table, name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Create(&models.KeyStringValue{
|
||||||
|
Key: lookupKey,
|
||||||
|
Value: "done",
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
name: "add cascade constraints",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ package models
|
|||||||
|
|
||||||
type Alias struct {
|
type Alias struct {
|
||||||
ID uint `gorm:"primary_key"`
|
ID uint `gorm:"primary_key"`
|
||||||
Type uint8 `gorm:"not null; index:idx_alias_type_key"`
|
Type uint8 `gorm:"not null; index:idx_alias_type_key; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
|
User *User `json:"-" gorm:"not null"`
|
||||||
UserID string `gorm:"not null; index:idx_alias_user"`
|
UserID string `gorm:"not null; index:idx_alias_user"`
|
||||||
Key string `gorm:"not null; index:idx_alias_type_key"`
|
Key string `gorm:"not null; index:idx_alias_type_key"`
|
||||||
Value string `gorm:"not null"`
|
Value string `gorm:"not null"`
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
type Heartbeat struct {
|
type Heartbeat struct {
|
||||||
ID uint `gorm:"primary_key"`
|
ID uint `gorm:"primary_key"`
|
||||||
User *User `json:"-" gorm:"not null"`
|
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
|
||||||
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
|
UserID string `json:"-" gorm:"not null; index:idx_time_user"`
|
||||||
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
|
Entity string `json:"entity" gorm:"not null; index:idx_entity"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
@ -2,16 +2,20 @@ package models
|
|||||||
|
|
||||||
type LanguageMapping struct {
|
type LanguageMapping struct {
|
||||||
ID uint `json:"id" gorm:"primary_key"`
|
ID uint `json:"id" gorm:"primary_key"`
|
||||||
User *User `json:"-" gorm:"not null"`
|
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
UserID string `json:"-" gorm:"not null; index:idx_language_mapping_user; uniqueIndex:idx_language_mapping_composite"`
|
UserID string `json:"-" gorm:"not null; index:idx_language_mapping_user; uniqueIndex:idx_language_mapping_composite"`
|
||||||
Extension string `json:"extension" gorm:"uniqueIndex:idx_language_mapping_composite"`
|
Extension string `json:"extension" gorm:"uniqueIndex:idx_language_mapping_composite; type:varchar(16)"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language" gorm:"type:varchar(64)"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateLanguage(language string) bool {
|
func (m *LanguageMapping) IsValid() bool {
|
||||||
return len(language) >= 1
|
return m.validateLanguage() && m.validateExtension()
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateExtension(extension string) bool {
|
func (m *LanguageMapping) validateLanguage() bool {
|
||||||
return len(extension) >= 1
|
return len(m.Language) >= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LanguageMapping) validateExtension() bool {
|
||||||
|
return len(m.Extension) >= 1
|
||||||
}
|
}
|
||||||
|
@ -35,18 +35,20 @@ const UnknownSummaryKey = "unknown"
|
|||||||
|
|
||||||
type Summary struct {
|
type Summary struct {
|
||||||
ID uint `json:"-" gorm:"primary_key"`
|
ID uint `json:"-" gorm:"primary_key"`
|
||||||
|
User *User `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
UserID string `json:"user_id" gorm:"not null; index:idx_time_summary_user"`
|
UserID string `json:"user_id" gorm:"not null; index:idx_time_summary_user"`
|
||||||
FromTime CustomTime `json:"from" gorm:"not null; type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time_summary_user"`
|
FromTime CustomTime `json:"from" gorm:"not null; type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time_summary_user"`
|
||||||
ToTime CustomTime `json:"to" gorm:"not null; type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time_summary_user"`
|
ToTime CustomTime `json:"to" gorm:"not null; type:timestamp; default:CURRENT_TIMESTAMP; index:idx_time_summary_user"`
|
||||||
Projects []*SummaryItem `json:"projects"`
|
Projects []*SummaryItem `json:"projects" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
Languages []*SummaryItem `json:"languages"`
|
Languages []*SummaryItem `json:"languages" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
Editors []*SummaryItem `json:"editors"`
|
Editors []*SummaryItem `json:"editors" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
OperatingSystems []*SummaryItem `json:"operating_systems"`
|
OperatingSystems []*SummaryItem `json:"operating_systems" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
Machines []*SummaryItem `json:"machines"`
|
Machines []*SummaryItem `json:"machines" gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SummaryItem struct {
|
type SummaryItem struct {
|
||||||
ID uint `json:"-" gorm:"primary_key"`
|
ID uint `json:"-" gorm:"primary_key"`
|
||||||
|
Summary *Summary `json:"-" gorm:"not null; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
SummaryID uint `json:"-"`
|
SummaryID uint `json:"-"`
|
||||||
Type uint8 `json:"-"`
|
Type uint8 `json:"-"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
|
@ -6,7 +6,7 @@ type User struct {
|
|||||||
Password string `json:"-"`
|
Password string `json:"-"`
|
||||||
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"`
|
CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"`
|
||||||
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"`
|
LastLoggedInAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP"`
|
||||||
BadgesEnabled bool `json:"-" gorm:"not null; default:false; type: bool"`
|
BadgesEnabled bool `json:"-" gorm:"default:false; type:bool"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Login struct {
|
type Login struct {
|
||||||
|
16
models/view/home.go
Normal file
16
models/view/home.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
type HomeViewModel struct {
|
||||||
|
Success string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HomeViewModel) WithSuccess(m string) *HomeViewModel {
|
||||||
|
s.Success = m
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *HomeViewModel) WithError(m string) *HomeViewModel {
|
||||||
|
s.Error = m
|
||||||
|
return s
|
||||||
|
}
|
22
models/view/imprint.go
Normal file
22
models/view/imprint.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
type ImprintViewModel struct {
|
||||||
|
HtmlText string
|
||||||
|
Success string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ImprintViewModel) WithSuccess(m string) *ImprintViewModel {
|
||||||
|
s.Success = m
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ImprintViewModel) WithError(m string) *ImprintViewModel {
|
||||||
|
s.Error = m
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ImprintViewModel) WithHtmlText(t string) *ImprintViewModel {
|
||||||
|
s.HtmlText = t
|
||||||
|
return s
|
||||||
|
}
|
20
models/view/settings.go
Normal file
20
models/view/settings.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
import "github.com/muety/wakapi/models"
|
||||||
|
|
||||||
|
type SettingsViewModel struct {
|
||||||
|
User *models.User
|
||||||
|
LanguageMappings []*models.LanguageMapping
|
||||||
|
Success string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsViewModel) WithSuccess(m string) *SettingsViewModel {
|
||||||
|
s.Success = m
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsViewModel) WithError(m string) *SettingsViewModel {
|
||||||
|
s.Error = m
|
||||||
|
return s
|
||||||
|
}
|
16
models/view/summary.go
Normal file
16
models/view/summary.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package view
|
||||||
|
|
||||||
|
type SummaryViewModel struct {
|
||||||
|
Success string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SummaryViewModel) WithSuccess(m string) *SummaryViewModel {
|
||||||
|
s.Success = m
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SummaryViewModel) WithError(m string) *SummaryViewModel {
|
||||||
|
s.Error = m
|
||||||
|
return s
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"github.com/muety/wakapi/config"
|
"github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -34,6 +35,9 @@ func (r *LanguageMappingRepository) GetByUser(userId string) ([]*models.Language
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *LanguageMappingRepository) Insert(mapping *models.LanguageMapping) (*models.LanguageMapping, error) {
|
func (r *LanguageMappingRepository) Insert(mapping *models.LanguageMapping) (*models.LanguageMapping, error) {
|
||||||
|
if !mapping.IsValid() {
|
||||||
|
return nil, errors.New("invalid mapping")
|
||||||
|
}
|
||||||
result := r.db.Create(mapping)
|
result := r.db.Create(mapping)
|
||||||
if err := result.Error; err != nil {
|
if err := result.Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -51,3 +51,12 @@ func (r *SummaryRepository) GetLatestByUser() ([]*models.Summary, error) {
|
|||||||
}
|
}
|
||||||
return summaries, nil
|
return summaries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SummaryRepository) DeleteByUser(userId string) error {
|
||||||
|
if err := r.db.
|
||||||
|
Where("user_id = ?", userId).
|
||||||
|
Delete(models.Summary{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -54,7 +54,7 @@ func (r *UserRepository) InsertOrGet(user *models.User) (*models.User, bool, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
func (r *UserRepository) Update(user *models.User) (*models.User, error) {
|
||||||
result := r.db.Model(&models.User{}).Updates(user)
|
result := r.db.Model(user).Updates(user)
|
||||||
if err := result.Error; err != nil {
|
if err := result.Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -43,13 +43,6 @@ func (h *HeartbeatHandler) ApiPost(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/*languageMappings, err := h.languageMappingSrvc.ResolveByUser(user.ID)
|
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}*/
|
|
||||||
|
|
||||||
for _, hb := range heartbeats {
|
for _, hb := range heartbeats {
|
||||||
hb.OperatingSystem = opSys
|
hb.OperatingSystem = opSys
|
||||||
hb.Editor = editor
|
hb.Editor = editor
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/middlewares"
|
"github.com/muety/wakapi/middlewares"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/models/view"
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -13,24 +14,22 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IndexHandler struct {
|
type HomeHandler struct {
|
||||||
config *conf.Config
|
config *conf.Config
|
||||||
userSrvc *services.UserService
|
userSrvc *services.UserService
|
||||||
keyValueSrvc *services.KeyValueService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginDecoder = schema.NewDecoder()
|
var loginDecoder = schema.NewDecoder()
|
||||||
var signupDecoder = schema.NewDecoder()
|
var signupDecoder = schema.NewDecoder()
|
||||||
|
|
||||||
func NewIndexHandler(userService *services.UserService, keyValueService *services.KeyValueService) *IndexHandler {
|
func NewHomeHandler(userService *services.UserService) *HomeHandler {
|
||||||
return &IndexHandler{
|
return &HomeHandler{
|
||||||
config: conf.Get(),
|
config: conf.Get(),
|
||||||
userSrvc: userService,
|
userSrvc: userService,
|
||||||
keyValueSrvc: keyValueService,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
func (h *HomeHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -40,29 +39,10 @@ func (h *IndexHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if handleAlerts(w, r, "") {
|
templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
templates[conf.IndexTemplate].Execute(w, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) GetImprint(w http.ResponseWriter, r *http.Request) {
|
func (h *HomeHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
|
||||||
loadTemplates()
|
|
||||||
}
|
|
||||||
|
|
||||||
text := "failed to load content"
|
|
||||||
if data, err := h.keyValueSrvc.GetString(models.ImprintKey); err == nil {
|
|
||||||
text = data.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
templates[conf.ImprintTemplate].Execute(w, &struct {
|
|
||||||
HtmlText string
|
|
||||||
}{HtmlText: text})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *IndexHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -74,29 +54,34 @@ func (h *IndexHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var login models.Login
|
var login models.Login
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
respondAlert(w, "missing parameters", "", "", http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := loginDecoder.Decode(&login, r.PostForm); err != nil {
|
if err := loginDecoder.Decode(&login, r.PostForm); err != nil {
|
||||||
respondAlert(w, "missing parameters", "", "", http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.userSrvc.GetUserById(login.Username)
|
user, err := h.userSrvc.GetUserById(login.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondAlert(w, "resource not found", "", "", http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("resource not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: depending on middleware package here is a hack
|
// TODO: depending on middleware package here is a hack
|
||||||
if !middlewares.CheckAndMigratePassword(user, &login, h.config.Security.PasswordSalt, h.userSrvc) {
|
if !middlewares.CheckAndMigratePassword(user, &login, h.config.Security.PasswordSalt, h.userSrvc) {
|
||||||
respondAlert(w, "invalid credentials", "", "", http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("invalid credentials"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login)
|
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondAlert(w, "internal server error", "", "", http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.IndexTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +99,7 @@ func (h *IndexHandler) PostLogin(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("%s/summary", h.config.Server.BasePath), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) PostLogout(w http.ResponseWriter, r *http.Request) {
|
func (h *HomeHandler) PostLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -123,7 +108,7 @@ func (h *IndexHandler) PostLogout(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, fmt.Sprintf("%s/", h.config.Server.BasePath), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("%s/", h.config.Server.BasePath), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) GetSignup(w http.ResponseWriter, r *http.Request) {
|
func (h *HomeHandler) GetSignup(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -133,14 +118,10 @@ func (h *IndexHandler) GetSignup(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if handleAlerts(w, r, conf.SignupTemplate) {
|
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
templates[conf.SignupTemplate].Execute(w, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *IndexHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
func (h *HomeHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -152,29 +133,41 @@ func (h *IndexHandler) PostSignup(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var signup models.Signup
|
var signup models.Signup
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
respondAlert(w, "missing parameters", "", conf.SignupTemplate, http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := signupDecoder.Decode(&signup, r.PostForm); err != nil {
|
if err := signupDecoder.Decode(&signup, r.PostForm); err != nil {
|
||||||
respondAlert(w, "missing parameters", "", conf.SignupTemplate, http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !signup.IsValid() {
|
if !signup.IsValid() {
|
||||||
respondAlert(w, "invalid parameters", "", conf.SignupTemplate, http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, created, err := h.userSrvc.CreateOrGet(&signup)
|
_, created, err := h.userSrvc.CreateOrGet(&signup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondAlert(w, "failed to create new user", "", conf.SignupTemplate, http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("failed to create new user"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !created {
|
if !created {
|
||||||
respondAlert(w, "user already existing", "", conf.SignupTemplate, http.StatusConflict)
|
w.WriteHeader(http.StatusConflict)
|
||||||
|
templates[conf.SignupTemplate].Execute(w, h.buildViewModel(r).WithError("user already existing"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := url.QueryEscape("account created successfully")
|
msg := url.QueryEscape("account created successfully")
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, msg), http.StatusFound)
|
http.Redirect(w, r, fmt.Sprintf("%s/?success=%s", h.config.Server.BasePath, msg), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HomeHandler) buildViewModel(r *http.Request) *view.HomeViewModel {
|
||||||
|
return &view.HomeViewModel{
|
||||||
|
Success: r.URL.Query().Get("success"),
|
||||||
|
Error: r.URL.Query().Get("error"),
|
||||||
|
}
|
||||||
|
}
|
41
routes/imprint.go
Normal file
41
routes/imprint.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
conf "github.com/muety/wakapi/config"
|
||||||
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/models/view"
|
||||||
|
"github.com/muety/wakapi/services"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImprintHandler struct {
|
||||||
|
config *conf.Config
|
||||||
|
keyValueSrvc *services.KeyValueService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewImprintHandler(keyValueService *services.KeyValueService) *ImprintHandler {
|
||||||
|
return &ImprintHandler{
|
||||||
|
config: conf.Get(),
|
||||||
|
keyValueSrvc: keyValueService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ImprintHandler) GetImprint(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.config.IsDev() {
|
||||||
|
loadTemplates()
|
||||||
|
}
|
||||||
|
|
||||||
|
text := "failed to load content"
|
||||||
|
if data, err := h.keyValueSrvc.GetString(models.ImprintKey); err == nil {
|
||||||
|
text = data.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
templates[conf.ImprintTemplate].Execute(w, h.buildViewModel(r).WithHtmlText(text))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ImprintHandler) buildViewModel(r *http.Request) *view.ImprintViewModel {
|
||||||
|
return &view.ImprintViewModel{
|
||||||
|
Success: r.URL.Query().Get("success"),
|
||||||
|
Error: r.URL.Query().Get("error"),
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@ -58,33 +57,3 @@ func loadTemplates() {
|
|||||||
templates[tplName] = tpl
|
templates[tplName] = tpl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func respondAlert(w http.ResponseWriter, error, success, tplName string, status int) {
|
|
||||||
w.WriteHeader(status)
|
|
||||||
if tplName == "" {
|
|
||||||
tplName = config.IndexTemplate
|
|
||||||
}
|
|
||||||
templates[tplName].Execute(w, struct {
|
|
||||||
Error string
|
|
||||||
Success string
|
|
||||||
}{Error: error, Success: success})
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: do better
|
|
||||||
func handleAlerts(w http.ResponseWriter, r *http.Request, tplName string) bool {
|
|
||||||
if err := r.URL.Query().Get("error"); err != "" {
|
|
||||||
if err == "unauthorized" {
|
|
||||||
respondAlert(w, err, "", tplName, http.StatusUnauthorized)
|
|
||||||
} else {
|
|
||||||
respondAlert(w, err, "", tplName, http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if success := r.URL.Query().Get("success"); success != "" {
|
|
||||||
respondAlert(w, "", success, tplName, http.StatusOK)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
@ -5,8 +5,10 @@ import (
|
|||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/models/view"
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -15,14 +17,18 @@ import (
|
|||||||
type SettingsHandler struct {
|
type SettingsHandler struct {
|
||||||
config *conf.Config
|
config *conf.Config
|
||||||
userSrvc *services.UserService
|
userSrvc *services.UserService
|
||||||
|
summarySrvc *services.SummaryService
|
||||||
|
aggregationSrvc *services.AggregationService
|
||||||
languageMappingSrvc *services.LanguageMappingService
|
languageMappingSrvc *services.LanguageMappingService
|
||||||
}
|
}
|
||||||
|
|
||||||
var credentialsDecoder = schema.NewDecoder()
|
var credentialsDecoder = schema.NewDecoder()
|
||||||
|
|
||||||
func NewSettingsHandler(userService *services.UserService, languageMappingService *services.LanguageMappingService) *SettingsHandler {
|
func NewSettingsHandler(userService *services.UserService, summaryService *services.SummaryService, aggregationService *services.AggregationService, languageMappingService *services.LanguageMappingService) *SettingsHandler {
|
||||||
return &SettingsHandler{
|
return &SettingsHandler{
|
||||||
config: conf.Get(),
|
config: conf.Get(),
|
||||||
|
summarySrvc: summaryService,
|
||||||
|
aggregationSrvc: aggregationService,
|
||||||
languageMappingSrvc: languageMappingService,
|
languageMappingSrvc: languageMappingService,
|
||||||
userSrvc: userService,
|
userSrvc: userService,
|
||||||
}
|
}
|
||||||
@ -33,16 +39,7 @@ func (h *SettingsHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
|
|
||||||
user := r.Context().Value(models.UserKey).(*models.User)
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r))
|
||||||
mappings, _ := h.languageMappingSrvc.GetByUser(user.ID)
|
|
||||||
data := map[string]interface{}{
|
|
||||||
"User": user,
|
|
||||||
"LanguageMappings": mappings,
|
|
||||||
"Success": r.FormValue("success"),
|
|
||||||
"Error": r.FormValue("error"),
|
|
||||||
}
|
|
||||||
|
|
||||||
templates[conf.SettingsTemplate].Execute(w, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SettingsHandler) PostCredentials(w http.ResponseWriter, r *http.Request) {
|
func (h *SettingsHandler) PostCredentials(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -54,34 +51,40 @@ func (h *SettingsHandler) PostCredentials(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
var credentials models.CredentialsReset
|
var credentials models.CredentialsReset
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("missing parameters")), http.StatusFound)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := credentialsDecoder.Decode(&credentials, r.PostForm); err != nil {
|
if err := credentialsDecoder.Decode(&credentials, r.PostForm); err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("missing parameters")), http.StatusFound)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("missing parameters"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !utils.CompareBcrypt(user.Password, credentials.PasswordOld, h.config.Security.PasswordSalt) {
|
if !utils.CompareBcrypt(user.Password, credentials.PasswordOld, h.config.Security.PasswordSalt) {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("invalid credentials")), http.StatusFound)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("invalid credentials"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !credentials.IsValid() {
|
if !credentials.IsValid() {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("invalid parameters")), http.StatusFound)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("invalid parameters"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Password = credentials.PasswordNew
|
user.Password = credentials.PasswordNew
|
||||||
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
if hash, err := utils.HashBcrypt(user.Password, h.config.Security.PasswordSalt); err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("internal server error")), http.StatusFound)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
user.Password = hash
|
user.Password = hash
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.userSrvc.Update(user); err != nil {
|
if _, err := h.userSrvc.Update(user); err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("internal server error")), http.StatusFound)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +94,8 @@ func (h *SettingsHandler) PostCredentials(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login)
|
encoded, err := h.config.Security.SecureCookie.Encode(models.AuthCookieKey, login)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("internal server error")), http.StatusFound)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +108,7 @@ func (h *SettingsHandler) PostCredentials(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.Server.BasePath, url.QueryEscape("password was updated successfully")), http.StatusFound)
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("password was updated successfully"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SettingsHandler) DeleteLanguageMapping(w http.ResponseWriter, r *http.Request) {
|
func (h *SettingsHandler) DeleteLanguageMapping(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -115,7 +119,8 @@ func (h *SettingsHandler) DeleteLanguageMapping(w http.ResponseWriter, r *http.R
|
|||||||
user := r.Context().Value(models.UserKey).(*models.User)
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
id, err := strconv.Atoi(r.PostFormValue("mapping_id"))
|
id, err := strconv.Atoi(r.PostFormValue("mapping_id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("could not delete mapping")), http.StatusFound)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("could not delete mapping"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,14 +131,15 @@ func (h *SettingsHandler) DeleteLanguageMapping(w http.ResponseWriter, r *http.R
|
|||||||
|
|
||||||
err = h.languageMappingSrvc.Delete(mapping)
|
err = h.languageMappingSrvc.Delete(mapping)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("could not delete mapping")), http.StatusFound)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("could not delete mapping"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.Server.BasePath, url.QueryEscape("mapping deleted successfully")), http.StatusFound)
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("mapping deleted successfully"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SettingsHandler) PostCreateLanguageMapping(w http.ResponseWriter, r *http.Request) {
|
func (h *SettingsHandler) PostLanguageMapping(w http.ResponseWriter, r *http.Request) {
|
||||||
if h.config.IsDev() {
|
if h.config.IsDev() {
|
||||||
loadTemplates()
|
loadTemplates()
|
||||||
}
|
}
|
||||||
@ -152,11 +158,12 @@ func (h *SettingsHandler) PostCreateLanguageMapping(w http.ResponseWriter, r *ht
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := h.languageMappingSrvc.Create(mapping); err != nil {
|
if _, err := h.languageMappingSrvc.Create(mapping); err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("mapping already exists")), http.StatusFound)
|
w.WriteHeader(http.StatusConflict)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("mapping already exists"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.Server.BasePath, url.QueryEscape("mapping added successfully")), http.StatusFound)
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess("mapping added successfully"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request) {
|
func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -166,12 +173,13 @@ func (h *SettingsHandler) PostResetApiKey(w http.ResponseWriter, r *http.Request
|
|||||||
|
|
||||||
user := r.Context().Value(models.UserKey).(*models.User)
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
if _, err := h.userSrvc.ResetApiKey(user); err != nil {
|
if _, err := h.userSrvc.ResetApiKey(user); err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("internal server error")), http.StatusFound)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := url.QueryEscape(fmt.Sprintf("your new api key is: %s", user.ApiKey))
|
msg := url.QueryEscape(fmt.Sprintf("your new api key is: %s", user.ApiKey))
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?success=%s", h.config.Server.BasePath, msg), http.StatusFound)
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithSuccess(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *SettingsHandler) PostToggleBadges(w http.ResponseWriter, r *http.Request) {
|
func (h *SettingsHandler) PostToggleBadges(w http.ResponseWriter, r *http.Request) {
|
||||||
@ -180,11 +188,47 @@ func (h *SettingsHandler) PostToggleBadges(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
user := r.Context().Value(models.UserKey).(*models.User)
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
|
|
||||||
if _, err := h.userSrvc.ToggleBadges(user); err != nil {
|
if _, err := h.userSrvc.ToggleBadges(user); err != nil {
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings?error=%s", h.config.Server.BasePath, url.QueryEscape("internal server error")), http.StatusFound)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
templates[conf.SettingsTemplate].Execute(w, h.buildViewModel(r).WithError("internal server error"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("%s/settings", h.config.Server.BasePath), http.StatusFound)
|
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)
|
||||||
|
|
||||||
|
log.Printf("clearing summaries for user '%s'\n", user.ID)
|
||||||
|
if err := h.summarySrvc.DeleteByUser(user.ID); err != nil {
|
||||||
|
log.Printf("failed to clear summaries: %v\n", 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 {
|
||||||
|
log.Printf("failed to regenerate summaries: %v\n", 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) buildViewModel(r *http.Request) *view.SettingsViewModel {
|
||||||
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
|
mappings, _ := h.languageMappingSrvc.GetByUser(user.ID)
|
||||||
|
return &view.SettingsViewModel{
|
||||||
|
User: user,
|
||||||
|
LanguageMappings: mappings,
|
||||||
|
Success: r.URL.Query().Get("success"),
|
||||||
|
Error: r.URL.Query().Get("error"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package routes
|
|||||||
import (
|
import (
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
|
"github.com/muety/wakapi/models/view"
|
||||||
"github.com/muety/wakapi/services"
|
"github.com/muety/wakapi/services"
|
||||||
"github.com/muety/wakapi/utils"
|
"github.com/muety/wakapi/utils"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -44,13 +45,15 @@ func (h *SummaryHandler) GetIndex(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
summary, err, status := h.loadUserSummary(r)
|
summary, err, status := h.loadUserSummary(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
respondAlert(w, err.Error(), "", conf.SummaryTemplate, status)
|
w.WriteHeader(status)
|
||||||
|
templates[conf.SummaryTemplate].Execute(w, h.buildViewModel(r).WithError(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := r.Context().Value(models.UserKey).(*models.User)
|
user := r.Context().Value(models.UserKey).(*models.User)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
respondAlert(w, "unauthorized", "", conf.SummaryTemplate, http.StatusUnauthorized)
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
templates[conf.SummaryTemplate].Execute(w, h.buildViewModel(r).WithError("unauthorized"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,3 +81,10 @@ func (h *SummaryHandler) loadUserSummary(r *http.Request) (*models.Summary, erro
|
|||||||
|
|
||||||
return summary, nil, http.StatusOK
|
return summary, nil, http.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SummaryHandler) buildViewModel(r *http.Request) *view.SummaryViewModel {
|
||||||
|
return &view.SummaryViewModel{
|
||||||
|
Success: r.URL.Query().Get("success"),
|
||||||
|
Error: r.URL.Query().Get("error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3
scripts/docker_mysql.sh
Normal file
3
scripts/docker_mysql.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secretpassword -e MYSQL_DATABASE=wakapi_local -e MYSQL_USER=wakapi_user -e MYSQL_PASSWORD=wakapi --name wakapi-mysql mysql:5
|
3
scripts/docker_postgres.sh
Normal file
3
scripts/docker_postgres.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
docker run -d -p 5432:5432 -e POSTGRES_DATABASE=wakapi_local -e POSTGRES_USER=wakapi_user -e POSTGRES_PASSWORD=wakapi --name wakapi-postgres postgres
|
@ -38,10 +38,18 @@ type AggregationJob struct {
|
|||||||
|
|
||||||
// Schedule a job to (re-)generate summaries every day shortly after midnight
|
// Schedule a job to (re-)generate summaries every day shortly after midnight
|
||||||
func (srv *AggregationService) Schedule() {
|
func (srv *AggregationService) Schedule() {
|
||||||
|
// Run once initially
|
||||||
|
if err := srv.Run(nil); err != nil {
|
||||||
|
log.Fatalf("failed to run aggregation jobs: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gocron.Every(1).Day().At(srv.config.App.AggregationTime).Do(srv.Run, nil)
|
||||||
|
<-gocron.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *AggregationService) Run(userIds map[string]bool) error {
|
||||||
jobs := make(chan *AggregationJob)
|
jobs := make(chan *AggregationJob)
|
||||||
summaries := make(chan *models.Summary)
|
summaries := make(chan *models.Summary)
|
||||||
defer close(jobs)
|
|
||||||
defer close(summaries)
|
|
||||||
|
|
||||||
for i := 0; i < runtime.NumCPU(); i++ {
|
for i := 0; i < runtime.NumCPU(); i++ {
|
||||||
go srv.summaryWorker(jobs, summaries)
|
go srv.summaryWorker(jobs, summaries)
|
||||||
@ -51,11 +59,7 @@ func (srv *AggregationService) Schedule() {
|
|||||||
go srv.persistWorker(summaries)
|
go srv.persistWorker(summaries)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run once initially
|
return srv.trigger(jobs, userIds)
|
||||||
srv.trigger(jobs)
|
|
||||||
|
|
||||||
gocron.Every(1).Day().At(srv.config.App.AggregationTime).Do(srv.trigger, jobs)
|
|
||||||
<-gocron.Start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) {
|
func (srv *AggregationService) summaryWorker(jobs <-chan *AggregationJob, summaries chan<- *models.Summary) {
|
||||||
@ -77,13 +81,22 @@ func (srv *AggregationService) persistWorker(summaries <-chan *models.Summary) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *AggregationService) trigger(jobs chan<- *AggregationJob) error {
|
func (srv *AggregationService) trigger(jobs chan<- *AggregationJob, userIds map[string]bool) error {
|
||||||
log.Println("Generating summaries.")
|
log.Println("Generating summaries.")
|
||||||
|
|
||||||
users, err := srv.userService.GetAll()
|
var users []*models.User
|
||||||
if err != nil {
|
if allUsers, err := srv.userService.GetAll(); err != nil {
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
return err
|
return err
|
||||||
|
} else if userIds != nil && len(userIds) > 0 {
|
||||||
|
users = make([]*models.User, len(userIds))
|
||||||
|
for i, u := range allUsers {
|
||||||
|
if yes, ok := userIds[u.ID]; yes && ok {
|
||||||
|
users[i] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
users = allUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
latestSummaries, err := srv.summaryService.GetLatestByUser()
|
latestSummaries, err := srv.summaryService.GetLatestByUser()
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/jasonlvhit/gocron"
|
|
||||||
"github.com/muety/wakapi/config"
|
"github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/repositories"
|
"github.com/muety/wakapi/repositories"
|
||||||
"github.com/muety/wakapi/utils"
|
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
@ -49,23 +46,6 @@ func (srv *HeartbeatService) DeleteBefore(t time.Time) error {
|
|||||||
return srv.repository.DeleteBefore(t)
|
return srv.repository.DeleteBefore(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *HeartbeatService) CleanUp() error {
|
|
||||||
refTime := utils.StartOfToday().Add(-cleanUpInterval)
|
|
||||||
if err := srv.DeleteBefore(refTime); err != nil {
|
|
||||||
log.Printf("Failed to clean up heartbeats older than %v – %v\n", refTime, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Printf("Successfully cleaned up heartbeats older than %v\n", refTime)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *HeartbeatService) ScheduleCleanUp() {
|
|
||||||
srv.CleanUp()
|
|
||||||
|
|
||||||
gocron.Every(1).Day().At("02:30").Do(srv.CleanUp)
|
|
||||||
<-gocron.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *HeartbeatService) augmented(heartbeats []*models.Heartbeat, userId string) ([]*models.Heartbeat, error) {
|
func (srv *HeartbeatService) augmented(heartbeats []*models.Heartbeat, userId string) ([]*models.Heartbeat, error) {
|
||||||
languageMapping, err := srv.languageMappingSrvc.ResolveByUser(userId)
|
languageMapping, err := srv.languageMappingSrvc.ResolveByUser(userId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -225,6 +225,10 @@ func (srv *SummaryService) GetLatestByUser() ([]*models.Summary, error) {
|
|||||||
return srv.repository.GetLatestByUser()
|
return srv.repository.GetLatestByUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (srv *SummaryService) DeleteByUser(userId string) error {
|
||||||
|
return srv.repository.DeleteByUser(userId)
|
||||||
|
}
|
||||||
|
|
||||||
func (srv *SummaryService) aggregateBy(heartbeats []*models.Heartbeat, summaryType uint8, user *models.User, c chan models.SummaryItemContainer) {
|
func (srv *SummaryService) aggregateBy(heartbeats []*models.Heartbeat, summaryType uint8, user *models.User, c chan models.SummaryItemContainer) {
|
||||||
durations := make(map[string]time.Duration)
|
durations := make(map[string]time.Duration)
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
1.14.0
|
1.15.1
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full mt-4 mb-8 pb-8">
|
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
||||||
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
||||||
Reset API Key
|
Reset API Key
|
||||||
</div>
|
</div>
|
||||||
@ -66,7 +66,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full mt-4 mb-8 pb-8">
|
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
||||||
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
||||||
Language Mappings
|
Language Mappings
|
||||||
</div>
|
</div>
|
||||||
@ -119,7 +119,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full mt-4 mb-8 pb-8">
|
<div class="w-full mt-4 mb-8 pb-8 border-b border-gray-700">
|
||||||
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
||||||
Badges
|
Badges
|
||||||
</div>
|
</div>
|
||||||
@ -169,6 +169,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full mt-4 mb-8 pb-8">
|
||||||
|
<div class="font-semibold text-lg text-white m-0 border-b-2 border-green-700 inline-block">
|
||||||
|
⚠️ Danger Zone
|
||||||
|
</div>
|
||||||
|
<div class="mt-10 text-gray-300 text-sm">
|
||||||
|
<h3 class="font-semibold text-md mb-4 border-b border-green-700 inline-block">
|
||||||
|
Regenerate summaries
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Wakapi improves its efficiency and speed by automatically aggregating individual heartbeats to summaries on a per-day basis.
|
||||||
|
That is, historic summaries, i.e. such from past days, are generated once and only fetched from the database in a static fashion afterwards, unless you pass <span class="font-mono font-normal bg-gray-900 p-1 rounded whitespace-no-wrap">&recompute=true</span> with your request.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
If, for some reason, these aggregated summaries are faulty or preconditions have change (e.g. you modified language mappings retrospectively), you may want to re-generate them from raw heartbeats.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
<strong>Note:</strong> Only run this action if you know what you are doing. Data might be lost is case heartbeats were deleted after the respective summaries had been generated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-10 flex justify-center">
|
||||||
|
<form action="settings/regenerate" method="post" id="form-regenerate-summaries">
|
||||||
|
<button type="button" class="py-1 px-3 rounded bg-red-500 hover:bg-red-600 text-white text-sm" id="btn-regenerate-summaries">
|
||||||
|
Clear & Regenerate
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -182,6 +210,14 @@
|
|||||||
e.innerHTML = e.innerHTML.replace('%s', baseUrl)
|
e.innerHTML = e.innerHTML.replace('%s', baseUrl)
|
||||||
e.classList.remove('hidden')
|
e.classList.remove('hidden')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const btnRegenerate = document.querySelector("#btn-regenerate-summaries")
|
||||||
|
const formRegenerate = document.querySelector('#form-regenerate-summaries')
|
||||||
|
btnRegenerate.addEventListener('click', () => {
|
||||||
|
if (confirm('Are you sure?')) {
|
||||||
|
formRegenerate.submit()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{ template "footer.tpl.html" . }}
|
{{ template "footer.tpl.html" . }}
|
||||||
|
Reference in New Issue
Block a user