2020-09-29 19:55:07 +03:00
package config
2019-05-05 23:36:49 +03:00
2020-05-24 18:32:26 +03:00
import (
"encoding/json"
2020-10-04 11:37:38 +03:00
"flag"
2020-11-01 22:14:10 +03:00
"fmt"
2022-01-21 14:35:05 +03:00
uuid "github.com/satori/go.uuid"
2021-04-16 13:24:19 +03:00
"io/ioutil"
2021-04-11 13:42:43 +03:00
"net/http"
"os"
"strings"
2021-04-30 15:07:14 +03:00
"time"
2021-04-11 13:42:43 +03:00
2021-01-30 13:17:37 +03:00
"github.com/emvi/logbuch"
2020-05-24 18:32:26 +03:00
"github.com/gorilla/securecookie"
2020-10-04 11:37:38 +03:00
"github.com/jinzhu/configor"
2021-04-11 13:42:43 +03:00
"github.com/muety/wakapi/data"
2020-09-29 19:55:07 +03:00
"github.com/muety/wakapi/models"
2020-11-01 22:14:10 +03:00
"gorm.io/gorm"
2020-05-24 18:32:26 +03:00
)
2020-10-04 12:14:44 +03:00
const (
2021-02-03 00:52:13 +03:00
defaultConfigPath = "config.yml"
2020-11-06 19:20:26 +03:00
SQLDialectMysql = "mysql"
SQLDialectPostgres = "postgres"
SQLDialectSqlite = "sqlite3"
2021-01-17 11:24:09 +03:00
KeyLatestTotalTime = "latest_total_time"
KeyLatestTotalUsers = "latest_total_users"
2021-02-05 20:47:28 +03:00
KeyLastImportImport = "last_import"
2021-02-12 21:25:59 +03:00
SimpleDateFormat = "2006-01-02"
SimpleDateTimeFormat = "2006-01-02 15:04:05"
2021-02-13 13:23:58 +03:00
ErrUnauthorized = "401 unauthorized"
2021-08-07 11:16:50 +03:00
ErrBadRequest = "400 bad request"
2021-02-13 01:06:48 +03:00
ErrInternalServerError = "500 internal server error"
2020-10-04 12:14:44 +03:00
)
2021-01-30 12:54:54 +03:00
const (
2021-02-05 20:47:28 +03:00
WakatimeApiUrl = "https://wakatime.com/api/v1"
WakatimeApiUserUrl = "/users/current"
WakatimeApiAllTimeUrl = "/users/current/all_time_since_today"
WakatimeApiHeartbeatsUrl = "/users/current/heartbeats"
WakatimeApiHeartbeatsBulkUrl = "/users/current/heartbeats.bulk"
WakatimeApiUserAgentsUrl = "/users/current/user_agents"
2021-02-11 00:08:00 +03:00
WakatimeApiMachineNamesUrl = "/users/current/machine_names"
2021-01-30 12:54:54 +03:00
)
2021-04-05 23:57:57 +03:00
const (
2021-04-10 01:07:13 +03:00
MailProviderSmtp = "smtp"
2021-04-05 23:57:57 +03:00
MailProviderMailWhale = "mailwhale"
)
var emailProviders = [ ] string {
2021-04-10 01:07:13 +03:00
MailProviderSmtp ,
2021-04-05 23:57:57 +03:00
MailProviderMailWhale ,
}
2021-04-05 17:25:13 +03:00
2020-11-08 12:12:49 +03:00
var cfg * Config
var cFlag = flag . String ( "config" , defaultConfigPath , "config file location" )
2021-04-16 13:24:19 +03:00
var env string
2020-05-24 14:41:19 +03:00
2020-10-04 12:14:44 +03:00
type appConfig struct {
2021-10-14 13:01:06 +03:00
AggregationTime string ` yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME" `
ReportTimeWeekly string ` yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY" `
ImportBackoffMin int ` yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN" `
ImportBatchSize int ` yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE" `
InactiveDays int ` yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS" `
2022-03-17 13:35:20 +03:00
HeartbeatMaxAge string ` yaml:"heartbeat_max_age" default:"4320h" env:"WAKAPI_HEARTBEAT_MAX_AGE" `
2021-10-14 13:01:06 +03:00
CountCacheTTLMin int ` yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN" `
2022-02-17 11:53:37 +03:00
AvatarURLTemplate string ` yaml:"avatar_url_template" default:"api/avatar/ { username_hash}.svg" `
2021-10-14 13:01:06 +03:00
CustomLanguages map [ string ] string ` yaml:"custom_languages" `
Colors map [ string ] map [ string ] string ` yaml:"-" `
2020-10-04 12:14:44 +03:00
}
type securityConfig struct {
2021-02-12 20:37:30 +03:00
AllowSignup bool ` yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP" `
ExposeMetrics bool ` yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS" `
2021-10-11 12:00:50 +03:00
EnableProxy bool ` yaml:"enable_proxy" default:"false" env:"WAKAPI_ENABLE_PROXY" ` // only intended for production instance at wakapi.dev
2020-10-04 12:14:44 +03:00
// this is actually a pepper (https://en.wikipedia.org/wiki/Pepper_(cryptography))
PasswordSalt string ` yaml:"password_salt" default:"" env:"WAKAPI_PASSWORD_SALT" `
InsecureCookies bool ` yaml:"insecure_cookies" default:"false" env:"WAKAPI_INSECURE_COOKIES" `
2020-11-22 00:30:56 +03:00
CookieMaxAgeSec int ` yaml:"cookie_max_age" default:"172800" env:"WAKAPI_COOKIE_MAX_AGE" `
2020-10-04 12:14:44 +03:00
SecureCookie * securecookie . SecureCookie ` yaml:"-" `
}
type dbConfig struct {
2021-04-18 12:03:54 +03:00
Host string ` env:"WAKAPI_DB_HOST" `
Port uint ` env:"WAKAPI_DB_PORT" `
User string ` env:"WAKAPI_DB_USER" `
Password string ` env:"WAKAPI_DB_PASSWORD" `
Name string ` default:"wakapi_db.db" env:"WAKAPI_DB_NAME" `
Dialect string ` yaml:"-" `
Charset string ` default:"utf8mb4" env:"WAKAPI_DB_CHARSET" `
Type string ` yaml:"dialect" default:"sqlite3" env:"WAKAPI_DB_TYPE" `
MaxConn uint ` yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS" `
Ssl bool ` default:"false" env:"WAKAPI_DB_SSL" `
AutoMigrateFailSilently bool ` yaml:"automigrate_fail_silently" default:"false" env:"WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY" `
2020-10-04 12:14:44 +03:00
}
type serverConfig struct {
2021-06-23 18:22:51 +03:00
Port int ` default:"3000" env:"WAKAPI_PORT" `
ListenIpV4 string ` yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4" `
ListenIpV6 string ` yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6" `
ListenSocket string ` yaml:"listen_socket" default:"" env:"WAKAPI_LISTEN_SOCKET" `
2021-06-24 22:56:47 +03:00
TimeoutSec int ` yaml:"timeout_sec" default:"30" env:"WAKAPI_TIMEOUT_SEC" `
2021-06-23 18:22:51 +03:00
BasePath string ` yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH" `
PublicUrl string ` yaml:"public_url" default:"http://localhost:3000" env:"WAKAPI_PUBLIC_URL" `
TlsCertPath string ` yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH" `
TlsKeyPath string ` yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH" `
2020-10-04 12:14:44 +03:00
}
2021-03-24 00:12:15 +03:00
type sentryConfig struct {
Dsn string ` env:"WAKAPI_SENTRY_DSN" `
EnableTracing bool ` yaml:"enable_tracing" env:"WAKAPI_SENTRY_TRACING" `
SampleRate float32 ` yaml:"sample_rate" default:"0.75" env:"WAKAPI_SENTRY_SAMPLE_RATE" `
SampleRateHeartbeats float32 ` yaml:"sample_rate_heartbeats" default:"0.1" env:"WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS" `
}
2021-04-05 17:25:13 +03:00
type mailConfig struct {
2021-04-12 23:57:52 +03:00
Enabled bool ` env:"WAKAPI_MAIL_ENABLED" default:"true" `
Provider string ` env:"WAKAPI_MAIL_PROVIDER" default:"smtp" `
MailWhale MailwhaleMailConfig ` yaml:"mailwhale" `
Smtp SMTPMailConfig ` yaml:"smtp" `
2021-04-30 16:14:29 +03:00
Sender string ` env:"WAKAPI_MAIL_SENDER" yaml:"sender" `
2021-04-05 17:25:13 +03:00
}
type MailwhaleMailConfig struct {
Url string ` env:"WAKAPI_MAIL_MAILWHALE_URL" `
ClientId string ` yaml:"client_id" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_ID" `
ClientSecret string ` yaml:"client_secret" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET" `
}
2021-04-10 01:07:13 +03:00
type SMTPMailConfig struct {
Host string ` env:"WAKAPI_MAIL_SMTP_HOST" `
Port uint ` env:"WAKAPI_MAIL_SMTP_PORT" `
Username string ` env:"WAKAPI_MAIL_SMTP_USER" `
Password string ` env:"WAKAPI_MAIL_SMTP_PASS" `
TLS bool ` env:"WAKAPI_MAIL_SMTP_TLS" `
}
2019-05-05 23:36:49 +03:00
type Config struct {
2021-12-26 15:21:20 +03:00
Env string ` default:"dev" env:"ENVIRONMENT" `
Version string ` yaml:"-" `
QuickStart bool ` yaml:"quick_start" env:"WAKAPI_QUICK_START" `
2022-01-21 14:35:05 +03:00
InstanceId string ` yaml:"-" ` // only temporary, changes between runs
2021-12-26 15:21:20 +03:00
App appConfig
Security securityConfig
Db dbConfig
Server serverConfig
Sentry sentryConfig
Mail mailConfig
2019-05-05 23:36:49 +03:00
}
2020-02-20 17:39:56 +03:00
2022-01-17 10:25:29 +03:00
func ( c * Config ) CreateCookie ( name , value string ) * http . Cookie {
return c . createCookie ( name , value , c . Server . BasePath , c . Security . CookieMaxAgeSec )
2020-11-22 00:30:56 +03:00
}
2022-01-17 10:25:29 +03:00
func ( c * Config ) GetClearCookie ( name string ) * http . Cookie {
return c . createCookie ( name , "" , c . Server . BasePath , - 1 )
2020-11-22 00:30:56 +03:00
}
func ( c * Config ) createCookie ( name , value , path string , maxAge int ) * http . Cookie {
return & http . Cookie {
Name : name ,
Value : value ,
Path : path ,
MaxAge : maxAge ,
Secure : ! c . Security . InsecureCookies ,
HttpOnly : true ,
SameSite : http . SameSiteStrictMode ,
}
}
2020-02-20 17:39:56 +03:00
func ( c * Config ) IsDev ( ) bool {
2020-05-30 13:11:21 +03:00
return IsDev ( c . Env )
}
2020-12-13 00:07:00 +03:00
func ( c * Config ) UseTLS ( ) bool {
return c . Server . TlsCertPath != "" && c . Server . TlsKeyPath != ""
}
2020-09-29 19:55:07 +03:00
func ( c * Config ) GetMigrationFunc ( dbDialect string ) models . MigrationFunc {
2020-05-30 21:41:27 +03:00
switch dbDialect {
default :
return func ( db * gorm . DB ) error {
2021-04-18 12:03:54 +03:00
if err := db . AutoMigrate ( & models . User { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
2021-04-14 00:23:57 +03:00
return err
}
2021-04-18 12:03:54 +03:00
if err := db . AutoMigrate ( & models . KeyStringValue { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
2021-04-14 00:23:57 +03:00
return err
}
2021-04-18 12:03:54 +03:00
if err := db . AutoMigrate ( & models . Alias { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
2021-04-14 00:23:57 +03:00
return err
}
2021-04-18 12:03:54 +03:00
if err := db . AutoMigrate ( & models . Heartbeat { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
2021-04-14 00:23:57 +03:00
return err
}
2021-04-18 12:03:54 +03:00
if err := db . AutoMigrate ( & models . Summary { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
2021-04-14 00:23:57 +03:00
return err
}
2021-04-18 12:03:54 +03:00
if err := db . AutoMigrate ( & models . SummaryItem { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
2021-04-14 00:23:57 +03:00
return err
}
2021-04-18 12:03:54 +03:00
if err := db . AutoMigrate ( & models . LanguageMapping { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
2021-04-14 00:23:57 +03:00
return err
}
2021-06-11 21:59:34 +03:00
if err := db . AutoMigrate ( & models . ProjectLabel { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
return err
}
2021-08-07 11:16:50 +03:00
if err := db . AutoMigrate ( & models . Diagnostics { } ) ; err != nil && ! c . Db . AutoMigrateFailSilently {
return err
}
2020-05-30 21:41:27 +03:00
return nil
}
}
}
2021-01-07 12:52:49 +03:00
func ( c * appConfig ) GetCustomLanguages ( ) map [ string ] string {
2021-01-30 11:51:36 +03:00
return cloneStringMap ( c . CustomLanguages , false )
2021-01-07 12:52:49 +03:00
}
func ( c * appConfig ) GetLanguageColors ( ) map [ string ] string {
2021-01-30 11:51:36 +03:00
return cloneStringMap ( c . Colors [ "languages" ] , true )
}
func ( c * appConfig ) GetEditorColors ( ) map [ string ] string {
return cloneStringMap ( c . Colors [ "editors" ] , true )
}
func ( c * appConfig ) GetOSColors ( ) map [ string ] string {
return cloneStringMap ( c . Colors [ "operating_systems" ] , true )
2021-01-07 12:52:49 +03:00
}
2021-04-30 15:07:14 +03:00
func ( c * appConfig ) GetWeeklyReportDay ( ) time . Weekday {
s := strings . Split ( c . ReportTimeWeekly , "," ) [ 0 ]
return parseWeekday ( s )
}
func ( c * appConfig ) GetWeeklyReportTime ( ) string {
return strings . Split ( c . ReportTimeWeekly , "," ) [ 1 ]
}
2022-03-17 13:35:20 +03:00
func ( c * appConfig ) HeartbeatsMaxAge ( ) time . Duration {
d , _ := time . ParseDuration ( c . HeartbeatMaxAge )
return d
}
2021-12-14 04:17:59 +03:00
func ( c * dbConfig ) IsSQLite ( ) bool {
return c . Dialect == "sqlite3"
}
2021-12-15 12:50:16 +03:00
func ( c * dbConfig ) IsMySQL ( ) bool {
return c . Dialect == "mysql"
}
func ( c * dbConfig ) IsPostgres ( ) bool {
return c . Dialect == "postgres"
}
2021-04-05 17:25:13 +03:00
func ( c * serverConfig ) GetPublicUrl ( ) string {
return strings . TrimSuffix ( c . PublicUrl , "/" )
}
2021-04-10 01:07:13 +03:00
func ( c * SMTPMailConfig ) ConnStr ( ) string {
return fmt . Sprintf ( "%s:%d" , c . Host , c . Port )
}
2020-05-30 13:11:21 +03:00
func IsDev ( env string ) bool {
return env == "dev" || env == "development"
2020-02-20 17:39:56 +03:00
}
2020-05-24 18:32:26 +03:00
2021-01-30 11:51:36 +03:00
func readColors ( ) map [ string ] map [ string ] string {
2020-05-24 18:32:26 +03:00
// Read language colors
2021-01-30 11:51:36 +03:00
// Source:
2022-02-17 14:20:22 +03:00
// - https://raw.githubusercontent.com/ozh/github-colors/master/colors.json
// - https://wakatime.com/colors/operating_systems
2021-01-30 11:51:36 +03:00
// - https://wakatime.com/colors/editors
// Extracted from Wakatime website with XPath (see below) and did a bit of regex magic after.
2022-02-17 14:20:22 +03:00
// - $x('//span[@class="editor-icon tip"]/@data-original-title').map(e => e.nodeValue)
// - $x('//span[@class="editor-icon tip"]/div[1]/text()').map(e => e.nodeValue)
2021-04-16 13:24:19 +03:00
raw := data . ColorsFile
if IsDev ( env ) {
raw , _ = ioutil . ReadFile ( "data/colors.json" )
}
2021-01-30 11:51:36 +03:00
var colors = make ( map [ string ] map [ string ] string )
2021-04-16 13:24:19 +03:00
if err := json . Unmarshal ( raw , & colors ) ; err != nil {
2021-01-30 13:17:37 +03:00
logbuch . Fatal ( err . Error ( ) )
2020-05-24 18:32:26 +03:00
}
2020-10-04 11:37:38 +03:00
return colors
}
func mustReadConfigLocation ( ) string {
if _ , err := os . Stat ( * cFlag ) ; err != nil {
2021-01-30 13:17:37 +03:00
logbuch . Fatal ( "failed to find config file at '%s'" , * cFlag )
2020-10-04 11:37:38 +03:00
}
return * cFlag
}
2021-01-18 23:34:08 +03:00
func resolveDbDialect ( dbType string ) string {
if dbType == "cockroach" {
return "postgres"
}
2021-12-15 12:50:16 +03:00
if dbType == "sqlite" {
return "sqlite3"
}
if dbType == "mariadb" {
return "mysql"
}
2021-01-18 23:34:08 +03:00
return dbType
}
2021-04-05 17:25:13 +03:00
func findString ( needle string , haystack [ ] string , defaultVal string ) string {
for _ , s := range haystack {
if s == needle {
return s
}
}
return defaultVal
}
2021-04-30 15:07:14 +03:00
func parseWeekday ( s string ) time . Weekday {
switch strings . ToLower ( s ) {
case "mon" , strings . ToLower ( time . Monday . String ( ) ) :
return time . Monday
case "tue" , strings . ToLower ( time . Tuesday . String ( ) ) :
return time . Tuesday
case "wed" , strings . ToLower ( time . Wednesday . String ( ) ) :
return time . Wednesday
case "thu" , strings . ToLower ( time . Thursday . String ( ) ) :
return time . Thursday
case "fri" , strings . ToLower ( time . Friday . String ( ) ) :
return time . Friday
case "sat" , strings . ToLower ( time . Saturday . String ( ) ) :
return time . Saturday
case "sun" , strings . ToLower ( time . Sunday . String ( ) ) :
return time . Sunday
}
return time . Monday
}
2020-10-04 11:37:38 +03:00
func Set ( config * Config ) {
cfg = config
}
func Get ( ) * Config {
return cfg
}
2021-04-11 13:42:43 +03:00
func Load ( version string ) * Config {
2020-10-04 11:37:38 +03:00
config := & Config { }
2020-11-08 12:12:49 +03:00
flag . Parse ( )
2020-10-04 11:37:38 +03:00
if err := configor . New ( & configor . Config { } ) . Load ( config , mustReadConfigLocation ( ) ) ; err != nil {
2021-01-30 13:17:37 +03:00
logbuch . Fatal ( "failed to read config: %v" , err )
2020-10-04 11:37:38 +03:00
}
2021-04-16 13:24:19 +03:00
env = config . Env
2021-04-11 13:42:43 +03:00
config . Version = strings . TrimSpace ( version )
2022-01-21 14:35:05 +03:00
config . InstanceId = uuid . NewV4 ( ) . String ( )
2021-01-30 11:51:36 +03:00
config . App . Colors = readColors ( )
2021-01-18 23:34:08 +03:00
config . Db . Dialect = resolveDbDialect ( config . Db . Type )
2020-10-04 11:37:38 +03:00
config . Security . SecureCookie = securecookie . New (
2020-05-24 18:32:26 +03:00
securecookie . GenerateRandomKey ( 64 ) ,
securecookie . GenerateRandomKey ( 32 ) ,
)
2020-10-04 11:37:38 +03:00
if strings . HasSuffix ( config . Server . BasePath , "/" ) {
config . Server . BasePath = config . Server . BasePath [ : len ( config . Server . BasePath ) - 1 ]
}
for k , v := range config . App . CustomLanguages {
if v == "" {
config . App . CustomLanguages [ k ] = "unknown"
}
}
2020-09-29 19:55:07 +03:00
2021-05-04 22:04:11 +03:00
if config . Sentry . Dsn != "" {
logbuch . Info ( "enabling sentry integration" )
initSentry ( config . Sentry , config . IsDev ( ) )
}
// some validation checks
2021-06-23 18:22:51 +03:00
if config . Server . ListenIpV4 == "" && config . Server . ListenIpV6 == "" && config . Server . ListenSocket == "" {
logbuch . Fatal ( "either of listen_ipv4 or listen_ipv6 or listen_socket must be set" )
2020-12-13 00:07:00 +03:00
}
2021-02-03 00:52:13 +03:00
if config . Db . MaxConn <= 0 {
logbuch . Fatal ( "you must allow at least one database connection" )
}
2021-12-14 04:17:59 +03:00
if config . Db . MaxConn > 1 && config . Db . IsSQLite ( ) {
logbuch . Warn ( "with sqlite, only a single connection is supported" ) // otherwise 'PRAGMA foreign_keys=ON' would somehow have to be set for every connection in the pool
config . Db . MaxConn = 1
}
2021-04-05 17:25:13 +03:00
if config . Mail . Provider != "" && findString ( config . Mail . Provider , emailProviders , "" ) == "" {
logbuch . Fatal ( "unknown mail provider '%s'" , config . Mail . Provider )
}
2021-05-04 22:04:11 +03:00
if _ , err := time . Parse ( "15:04" , config . App . GetWeeklyReportTime ( ) ) ; err != nil {
logbuch . Fatal ( "invalid interval set for report_time_weekly" )
}
if _ , err := time . Parse ( "15:04" , config . App . AggregationTime ) ; err != nil {
logbuch . Fatal ( "invalid interval set for aggregation_time" )
}
2022-03-17 13:35:20 +03:00
if _ , err := time . ParseDuration ( config . App . HeartbeatMaxAge ) ; err != nil {
logbuch . Fatal ( "invalid duration set for heartbeat_max_age" )
}
2021-04-05 17:25:13 +03:00
2020-10-04 11:37:38 +03:00
Set ( config )
2020-09-29 19:55:07 +03:00
return Get ( )
2020-05-24 18:32:26 +03:00
}