// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. // All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package repository import ( "encoding/json" "sync" "time" "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/config" "github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/lrucache" "github.com/jmoiron/sqlx" ) var ( userCfgRepoOnce sync.Once userCfgRepoInstance *UserCfgRepo ) type UserCfgRepo struct { DB *sqlx.DB Lookup *sqlx.Stmt lock sync.RWMutex uiDefaults map[string]interface{} cache *lrucache.Cache } func GetUserCfgRepo() *UserCfgRepo { userCfgRepoOnce.Do(func() { db := GetConnection() lookupConfigStmt, err := db.DB.Preparex(`SELECT confkey, value FROM configuration WHERE configuration.username = ?`) if err != nil { log.Fatalf("db.DB.Preparex() error: %v", err) } userCfgRepoInstance = &UserCfgRepo{ DB: db.DB, Lookup: lookupConfigStmt, uiDefaults: config.Keys.UiDefaults, cache: lrucache.New(1024), } }) return userCfgRepoInstance } // Return the personalised UI config for the currently authenticated // user or return the plain default config. func (uCfg *UserCfgRepo) GetUIConfig(user *auth.User) (map[string]interface{}, error) { if user == nil { uCfg.lock.RLock() copy := make(map[string]interface{}, len(uCfg.uiDefaults)) for k, v := range uCfg.uiDefaults { copy[k] = v } uCfg.lock.RUnlock() return copy, nil } data := uCfg.cache.Get(user.Username, func() (interface{}, time.Duration, int) { uiconfig := make(map[string]interface{}, len(uCfg.uiDefaults)) for k, v := range uCfg.uiDefaults { uiconfig[k] = v } rows, err := uCfg.Lookup.Query(user.Username) if err != nil { log.Warnf("Error while looking up user uiconfig for user '%v'", user.Username) return err, 0, 0 } size := 0 defer rows.Close() for rows.Next() { var key, rawval string if err := rows.Scan(&key, &rawval); err != nil { log.Warn("Error while scanning user uiconfig values") return err, 0, 0 } var val interface{} if err := json.Unmarshal([]byte(rawval), &val); err != nil { log.Warn("Error while unmarshaling raw user uiconfig json") return err, 0, 0 } size += len(key) size += len(rawval) uiconfig[key] = val } // Add global ShortRunningJobsDuration setting as plot_list_hideShortRunningJobs uiconfig["plot_list_hideShortRunningJobs"] = config.Keys.ShortRunningJobsDuration return uiconfig, 24 * time.Hour, size }) if err, ok := data.(error); ok { log.Error("Error in returned dataset") return nil, err } return data.(map[string]interface{}), nil } // If the context does not have a user, update the global ui configuration // without persisting it! If there is a (authenticated) user, update only his // configuration. func (uCfg *UserCfgRepo) UpdateConfig( key, value string, user *auth.User) error { if user == nil { var val interface{} if err := json.Unmarshal([]byte(value), &val); err != nil { log.Warn("Error while unmarshaling raw user config json") return err } uCfg.lock.Lock() defer uCfg.lock.Unlock() uCfg.uiDefaults[key] = val return nil } if _, err := uCfg.DB.Exec(`REPLACE INTO configuration (username, confkey, value) VALUES (?, ?, ?)`, user, key, value); err != nil { log.Warnf("Error while replacing user config in DB for user '%v'", user) return err } uCfg.cache.Del(user.Username) return nil }